diff --git a/AGENTS.md b/AGENTS.md
index a639b6fe6..e3d529006 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.1` (`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.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**.
### Which layer am I touching?
@@ -211,7 +211,9 @@ contract.
> underneath us. It did: dotnet/runtime#130566 (.NET 11 preview 7) dropped `Convert.ToDecimal`'s 15-significant-digit
> rounding, which came from OA's own `VarDecFromR8` and had been stable since the 1990s. That turned
> `SUM(ROUND(UnitPrice, 2))` into `58.600000000000001421085471520`. The fix was to own it:
-> `src/EFCore.Jet.Data/JetDecimalConverter.cs`. **When a long-stable conversion suddenly misbehaves with no code
+> `src/Shared/Data/JetDecimalConverter.cs` (compiled into both EFCore.Jet.Data and LibRed.Core; every LibRed
+> assembly uses the Core copy, and any double/float that can reach a decimal goes through its `ToDecimal`, never
+> `Convert.ToDecimal` or a cast). **When a long-stable conversion suddenly misbehaves with no code
> change on our side, suspect the runtime's OA-era compatibility behaviour before suspecting the provider.**
## LibRed — Native Managed Engine
diff --git a/CLAUDE.md b/CLAUDE.md
index ae2304379..bebf1608d 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.1` (`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.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**.
### Which layer am I touching?
@@ -223,7 +223,9 @@ contract.
> underneath us. It did: dotnet/runtime#130566 (.NET 11 preview 7) dropped `Convert.ToDecimal`'s 15-significant-digit
> rounding, which came from OA's own `VarDecFromR8` and had been stable since the 1990s. That turned
> `SUM(ROUND(UnitPrice, 2))` into `58.600000000000001421085471520`. The fix was to own it:
-> `src/EFCore.Jet.Data/JetDecimalConverter.cs`. **When a long-stable conversion suddenly misbehaves with no code
+> `src/Shared/Data/JetDecimalConverter.cs` (compiled into both EFCore.Jet.Data and LibRed.Core; every LibRed
+> assembly uses the Core copy, and any double/float that can reach a decimal goes through its `ToDecimal`, never
+> `Convert.ToDecimal` or a cast). **When a long-stable conversion suddenly misbehaves with no code
> change on our side, suspect the runtime's OA-era compatibility behaviour before suspecting the provider.**
## LibRed — Native Managed Engine
diff --git a/Version.props b/Version.props
index 18997bbd6..f7528b9b9 100644
--- a/Version.props
+++ b/Version.props
@@ -17,7 +17,7 @@
-->
11.0.0alpha
- 2
+ 3
+
+
+
diff --git a/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs
index 1e2e09774..3bbcd0187 100644
--- a/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs
+++ b/src/LibRed/LibRed.Core/Pages/DatabaseDefinitionPage.cs
@@ -47,6 +47,14 @@ public sealed class DatabaseDefinitionPage : Page
/// pointer at . 2 in every observed file.
public int CatalogRootPage { 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.
+ 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.
+ public (int Row, int Page) ReleasedPagesMap { get; internal set; }
+
public DateTime DatabaseCreationDate { get; internal set; }
public override void Read(PageBuffer buffer, Formats.JetFormatBase format)
@@ -67,6 +75,8 @@ public override void Read(PageBuffer buffer, Formats.JetFormatBase format)
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);
// 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
@@ -79,6 +89,18 @@ public override void Read(PageBuffer buffer, Formats.JetFormatBase format)
DatabaseCreationDate = DateTime.FromOADate(days);
}
+ /// 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)
+ {
+ 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));
+ }
+
/// XOR-de-obfuscates the page-0 header region into , whose length
/// equals the mask length; clear[i] corresponds to page offset
/// + i.
diff --git a/src/LibRed/LibRed.Core/Pages/PageType.cs b/src/LibRed/LibRed.Core/Pages/PageType.cs
index a7e4f6446..9cf9d9fee 100644
--- a/src/LibRed/LibRed.Core/Pages/PageType.cs
+++ b/src/LibRed/LibRed.Core/Pages/PageType.cs
@@ -12,4 +12,19 @@ public enum PageType : byte
IntermediateIndexPage = 0x03,
LeafIndexPage = 0x04,
PageUsageBitmap = 0x05,
+
+ /// 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
+ /// 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,
+
+ /// A long-value page released because the last value sharing it was deleted. Several small
+ /// (single-page form) values pack onto one LVAL page; each delete retires its row to a 0-length
+ /// deleted+overflow tombstone, and when none are left the page is stamped with this type and freed.
+ /// A chained value owns its pages outright and they go back at instead,
+ /// which is why only the packed form produces this. Nothing needs to handle it on read: allocation
+ /// selects on the free map, not on this byte. See docs/format/page-09-released-long-value.md.
+ ReleasedLongValuePage = 0x09,
}
diff --git a/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs b/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs
index 9df637367..b6954bb84 100644
--- a/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs
+++ b/src/LibRed/LibRed.Core/Pages/TdefChainReader.cs
@@ -28,11 +28,15 @@ internal static (PageBuffer Buffer, IReadOnlyList ContinuationPages) Read(
$"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 continuationCount = definitionLength <= pageSize
+ int stored = definitionLength + JetFormatBase.TdefContinuationHeaderSize;
+ int continuationCount = stored <= pageSize
? 0
- : (definitionLength - pageSize + bodySize - 1) / bodySize;
+ : (stored - pageSize + bodySize - 1) / bodySize;
int next = first.ReadInt32(format.TdefNextPageOffset);
var continuationPages = new List(continuationCount);
diff --git a/src/LibRed/LibRed.Core/Storage/Calculated/CalculatedEvaluator.cs b/src/LibRed/LibRed.Core/Storage/Calculated/CalculatedEvaluator.cs
index 0e80a2c48..9c6475e80 100644
--- a/src/LibRed/LibRed.Core/Storage/Calculated/CalculatedEvaluator.cs
+++ b/src/LibRed/LibRed.Core/Storage/Calculated/CalculatedEvaluator.cs
@@ -1,4 +1,5 @@
using System.Globalization;
+using EntityFrameworkCore.Jet.Data;
using LibRed.Catalog;
namespace LibRed.Storage.Calculated;
@@ -574,7 +575,7 @@ private static bool Like(string text, string pattern)
JetDataType.Int64 => (long)Math.Round(ToNumber(value), MidpointRounding.ToEven),
JetDataType.Single => (float)ToNumber(value),
JetDataType.Double => ToNumber(value),
- JetDataType.Currency => Math.Round((decimal)ToNumber(value), 4, MidpointRounding.ToEven),
+ JetDataType.Currency => Math.Round(JetDecimalConverter.FromDouble(ToNumber(value)), 4, MidpointRounding.ToEven),
JetDataType.DateTime => ToDate(value),
JetDataType.Text or JetDataType.Memo => ToText(value),
_ => throw new CalculatedExpressionException($"Cannot store a calculated {type} value."),
diff --git a/src/LibRed/LibRed.Core/Storage/Calculated/CalculatedExpression.cs b/src/LibRed/LibRed.Core/Storage/Calculated/CalculatedExpression.cs
index f240eeb61..a7d1dbf4d 100644
--- a/src/LibRed/LibRed.Core/Storage/Calculated/CalculatedExpression.cs
+++ b/src/LibRed/LibRed.Core/Storage/Calculated/CalculatedExpression.cs
@@ -458,16 +458,18 @@ private static List Tokenise(string text)
tokens.Add(new Token(TokenKind.Bracketed, text[(i + 1)..end]));
i = end + 1;
}
- else if (c == '"')
+ else if (c is '"' or '\'')
{
+ // Text in either quote, a doubled quote standing for itself (verified vs ACE through DAO:
+ // [A] & 'it''s' & [B] is accepted, stored as written, and gives the same values as "it's").
var value = new System.Text.StringBuilder();
i++;
while (true)
{
if (i >= text.Length) throw new CalculatedExpressionException("Unterminated string in expression.");
- if (text[i] == '"')
+ if (text[i] == c)
{
- if (i + 1 < text.Length && text[i + 1] == '"') { value.Append('"'); i += 2; continue; }
+ if (i + 1 < text.Length && text[i + 1] == c) { value.Append(c); i += 2; continue; }
i++;
break;
}
diff --git a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs
index 5512f832f..faa0fba3f 100644
--- a/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs
+++ b/src/LibRed/LibRed.Core/Storage/DatabaseCreator.cs
@@ -39,14 +39,15 @@ public static byte[] BuildDefinitionPage(
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[0x15] = (byte)(version == 0x03 ? 0x01 : 0x00); // 2010-format minor byte
+ 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];
- BinaryPrimitives.WriteInt32LittleEndian(clear[(0x18 - b)..], 0x00000100); // 0x18 fixed constant
- BinaryPrimitives.WriteInt32LittleEndian(clear[(0x1C - b)..], 0x00000101); // 0x1C fixed constant
+ // 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);
@@ -252,6 +253,18 @@ private static readonly (string Name, ColumnSpec[] Columns)[] MSysComplexTypeTab
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): the "
+ + "Access engine refuses to open a file stamped with it. Access 2013 writes the Access 2010 "
+ + $"format, so use {nameof(JetVersion.Version14_2010)} for a 2013-era database. Existing 0x04 "
+ + "files can still be opened for reading.");
+
JetFormatBase format = JetFormatBase.FromVersionByte(version);
// Jet 4 (0x01, the Access 2000 / 2002-2003 `.mdb`) and the ACCDB versions can both be created; the
diff --git a/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs b/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs
index a0cb14756..765bea224 100644
--- a/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs
+++ b/src/LibRed/LibRed.Core/Storage/IndexKeyEncoder.cs
@@ -1,6 +1,7 @@
using System.Buffers.Binary;
using System.Globalization;
using System.Runtime.InteropServices;
+using EntityFrameworkCore.Jet.Data;
using LibRed.Catalog;
using LibRed.Formats;
using LibRed.Storage.Types;
@@ -280,7 +281,7 @@ private static byte[] EncodeFixed(ColumnDef column, object value)
case JetDataType.Int32:
return EncodeInteger(Convert.ToInt32(value, c), 4);
case JetDataType.Currency:
- return EncodeInteger((long)decimal.Round(Convert.ToDecimal(value, c) * 10000m), 8);
+ 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:
@@ -290,7 +291,7 @@ private static byte[] EncodeFixed(ColumnDef column, object value)
case JetDataType.DateTime:
return EncodeFloatBits(BitConverter.DoubleToInt64Bits(Convert.ToDateTime(value, c).ToOADate()), 8);
case JetDataType.FixedPoint:
- return EncodeFixedPoint(Convert.ToDecimal(value, c), column.Scale);
+ return EncodeFixedPoint(JetDecimalConverter.ToDecimal(value, c), column.Scale);
default:
throw new NotSupportedException($"Index key type {column.Type} is not encodable.");
}
@@ -309,7 +310,9 @@ private static byte[] EncodeFixedPoint(decimal value, byte scale)
{
decimal factor = 1m;
for (int i = 0; i < scale; i++) factor *= 10m;
- decimal magnitude = decimal.Truncate(decimal.Round(Math.Abs(value) * factor, 0));
+ // 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];
diff --git a/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs b/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs
new file mode 100644
index 000000000..e01ef7036
--- /dev/null
+++ b/src/LibRed/LibRed.Core/Storage/JetTextComparer.cs
@@ -0,0 +1,22 @@
+using System.Runtime.InteropServices;
+
+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 .
+///
+public static class JetTextComparer
+{
+ /// The sign of against , or null when either holds a character
+ /// the order does not cover.
+ public static int? Compare(string a, string b)
+ {
+ 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;
+ return Math.Sign(CollectionsMarshal.AsSpan(left).SequenceCompareTo(CollectionsMarshal.AsSpan(right)));
+ }
+}
diff --git a/src/LibRed/LibRed.Core/Storage/LongValueReader.cs b/src/LibRed/LibRed.Core/Storage/LongValueReader.cs
index a38602072..621f7c44f 100644
--- a/src/LibRed/LibRed.Core/Storage/LongValueReader.cs
+++ b/src/LibRed/LibRed.Core/Storage/LongValueReader.cs
@@ -10,7 +10,8 @@ namespace LibRed.Storage;
///
///
/// 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 reserved. Flags:
+/// 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.
@@ -54,9 +55,37 @@ internal byte[] ResolveWithPages(ReadOnlySpan descriptor, out IReadOnlyLis
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];
diff --git a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs b/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs
index 4724775bb..4ec7d192b 100644
--- a/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs
+++ b/src/LibRed/LibRed.Core/Storage/LongValueWriter.cs
@@ -65,6 +65,14 @@ public LongValueResult Write(byte[] payload)
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;
@@ -77,10 +85,11 @@ public LongValueResult Write(byte[] payload)
row[2] = (byte)(nextPage >> 8);
row[3] = (byte)(nextPage >> 16);
payload.AsSpan(start, len).CopyTo(row.AsSpan(4));
- WriteChunkPage(pages[i], row);
+ WriteChunkPage(pages[i], row, i == 0 ? stamp : 0);
}
- return new LongValueResult(Descriptor(payload.Length, LongValueFormat.FlagChained, pages[0]), pages, FreePage: 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
@@ -148,14 +157,17 @@ public int WriteNewPage(byte[] 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.
- private void WriteChunkPage(int pageNumber, byte[] 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)
{
JetFormatBase format = _channel.Format;
var page = new byte[format.PageSize];
page[0] = (byte)PageType.DataPage;
page[1] = 0x01; // page flags (observed constant)
BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataOwnerOffset, 4), LongValueFormat.LvalMarker);
+ BinaryPrimitives.WriteUInt32LittleEndian(page.AsSpan(format.DataChainStampOffset, 4), stamp);
int offset = format.PageSize - row.Length;
row.CopyTo(page.AsSpan(offset));
@@ -166,8 +178,10 @@ private void WriteChunkPage(int pageNumber, byte[] row)
_channel.WritePage(pageNumber, page);
}
- /// Builds the 12-byte descriptor: 4-byte length with storage flags, row/page, reserved.
- private static byte[] Descriptor(int length, byte flag, int firstPage, int row = 0)
+ /// 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];
@@ -176,6 +190,7 @@ private static byte[] Descriptor(int length, byte flag, int firstPage, int 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;
}
}
diff --git a/src/LibRed/LibRed.Core/Storage/PageAllocator.cs b/src/LibRed/LibRed.Core/Storage/PageAllocator.cs
index 8512fcc37..a20f5a398 100644
--- a/src/LibRed/LibRed.Core/Storage/PageAllocator.cs
+++ b/src/LibRed/LibRed.Core/Storage/PageAllocator.cs
@@ -6,14 +6,21 @@
namespace LibRed.Storage;
///
-/// Allocates database pages the way Access does: through the **global free-pages map** at page 1,
-/// row 0 (an inline usage map where a set bit marks a free page). Allocation takes a free page,
-/// clears its bit (so it is no longer free), and returns it — reusing freed pages rather than
-/// always growing the file. The file is grown only when no free page is available.
+/// Allocates database pages the way Access does: through the **global free-pages map**, the usage-map record
+/// page 0 names at 0x18 (an inline or reference map where a set bit marks a free page). Allocation takes a
+/// free page, clears its bit (so it is no longer free), and returns it — reusing freed pages rather than always
+/// growing the file. The file is grown only when no free page is available.
///
+///
+/// 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).
+/// 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)
{
- private const int GlobalMapPage = 1;
private const byte InlineMapType = 0x00;
private const byte ReferenceMapType = 0x01;
/// Bytes preceding the bitmap on a dedicated usage-bitmap page (type 0x05).
@@ -25,103 +32,330 @@ public sealed class PageAllocator(PageChannel channel)
private readonly PageChannel _channel = channel;
+ /// 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)
+ {
+ public ReadOnlySpan Record => Page.AsSpan(Slot.Offset, Slot.Length);
+ public byte Type => Page[Slot.Offset];
+ }
+
public int Allocate()
{
- (byte[] page, RowSlot slot) = ReadGlobalMap();
- int mapOffset = slot.Offset;
- byte mapType = page[mapOffset];
- if (mapType == ReferenceMapType)
- return AllocateFromReferenceMap(page.AsSpan(mapOffset, slot.Length));
- if (mapType != InlineMapType)
- throw new InvalidDataException($"Global free-pages map has unknown type 0x{mapType:X2}.");
- if (slot.Length < 5)
- throw new InvalidDataException("Global inline free-pages map is shorter than its 5-byte header.");
+ (MapRecord free, MapRecord released) = ReadGlobalMaps();
+ var releasedPages = new ReleasedPages(this, released);
+ if (free.Type == ReferenceMapType)
+ 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 + slot.Length;
+ int bitmapEnd = mapOffset + free.Slot.Length;
for (int i = bitmapStart; i < bitmapEnd; i++)
{
- if (page[i] == 0) continue;
- int bit = BitOperations.TrailingZeroCount(page[i]);
- int allocated = startPage + (i - bitmapStart) * 8 + bit;
- ValidateReusablePage(allocated, "inline free bit", allowAppendBoundary: true);
- EnsurePhysicalAllocation(allocated);
- page[i] &= (byte)~(1 << bit); // no longer free
- _channel.WritePage(GlobalMapPage, page);
- return allocated;
+ 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;
+ }
}
// An unrepresented page is not safely recorded as used. Grow the global map before appending.
return GrowAndAllocate();
}
- /// Returns a page to the global free-pages map (sets its bit) so it can be reused — the inverse
- /// of . Used when dropping an index frees its B-tree pages, matching Access.
+ /// Returns a page to the global free-pages map now (sets its bit), so it can be reused — the
+ /// inverse of . ACE frees this way only the pages of a long value an UPDATE replaces;
+ /// every other freed page is held until the session closes, through .
public void Free(int page)
{
- ValidateReusablePage(page, "page being freed", allowAppendBoundary: false);
- (byte[] p, RowSlot slot) = ReadGlobalMap();
+ (MapRecord free, MapRecord released) = ReadGlobalMaps();
+ ValidateReusablePage(page, "page being freed", free, released, _channel.PageCount - 1);
- int mapOffset = slot.Offset;
- byte mapType = p[mapOffset];
- if (mapType == ReferenceMapType)
+ if (free.Type == ReferenceMapType)
{
- FreeInReferenceMap(p.AsSpan(mapOffset, slot.Length), page);
+ FreeInReferenceMap(free, released, page);
return;
}
- if (mapType != InlineMapType)
- throw new InvalidDataException($"Global free-pages map has unknown type 0x{mapType:X2}.");
- if (slot.Length < 5)
- throw new InvalidDataException("Global inline free-pages map is shorter than its 5-byte header.");
+ byte[] p = free.Page;
+ int mapOffset = free.Slot.Offset;
int startPage = BinaryPrimitives.ReadInt32LittleEndian(p.AsSpan(mapOffset + 1, 4));
int bit = page - startPage;
int byteIndex = mapOffset + 5 + bit / 8;
- if (bit < 0 || byteIndex >= mapOffset + slot.Length) return; // outside the inline window
+ if (bit < 0 || byteIndex >= mapOffset + free.Slot.Length) return; // outside the inline window
p[byteIndex] |= (byte)(1 << (bit % 8));
- _channel.WritePage(GlobalMapPage, p);
+ _channel.WritePage(free.PageNumber, p);
+ }
+
+ ///
+ /// Frees a page the way ACE frees the pages of a deleted row's long values, a dropped index and a dropped
+ /// table: it is not reusable while this handle stays open, and goes back to the global free map only when
+ /// runs at close. Inside a transaction it is released only if the
+ /// transaction commits.
+ ///
+ public void Release(int page)
+ {
+ (MapRecord free, MapRecord released) = ReadGlobalMaps();
+ ValidateReusablePage(page, "page being released", free, released, _channel.PageCount - 1);
+ _channel.ReleaseAtClose(page);
+ }
+
+ ///
+ /// What ACE does at close: every page this handle released, and every page already set in the global
+ /// released-pages map, goes back to the global free-pages map, and the released map is cleared. First an
+ /// inline released map is sized to cover the highest page released (), which
+ /// can convert it to reference form. Writes nothing when nothing was released and this handle changed
+ /// nothing.
+ ///
+ public void ReturnReleasedPages()
+ {
+ SortedSet pages = [.. _channel.PagesReleasedAtClose];
+ if (pages.Count == 0 && !_channel.HasPublishedWrites) return;
+ (_, MapRecord released) = ReadGlobalMaps();
+ pages.UnionWith(ReleasedMapPages(released));
+ if (pages.Count == 0) return;
+
+ bool ownTransaction = !_channel.InTransaction;
+ if (ownTransaction) _channel.BeginTransaction();
+ try
+ {
+ // Sized before the merge: a conversion allocates its bitmap pages from the free map as it stands,
+ // without the pages being released.
+ SizeReleasedMap(pages);
+
+ // A released page past the end of the file was never materialized; the free map already records
+ // every page past the end as free.
+ foreach (int page in pages)
+ if (page < _channel.PageCount) Free(page);
+
+ (_, released) = ReadGlobalMaps();
+ ClearReleasedMap(released);
+ _channel.ClearPagesReleasedAtClose();
+ if (ownTransaction) _channel.CommitTransaction(flush: false);
+ }
+ catch
+ {
+ if (ownTransaction) _channel.RollbackTransaction();
+ throw;
+ }
+ }
+
+ /// The pages set in the global released-pages map, inline or reference form.
+ private IEnumerable 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.
+ ///
+ private void SizeReleasedMap(SortedSet pages)
+ {
+ (_, MapRecord released) = ReadGlobalMaps();
+ if (released.Type == ReferenceMapType)
+ {
+ byte[] existing = released.Record.ToArray();
+ if (!AddReleasedBitmapPages(existing, pages)) return;
+ (_, released) = ReadGlobalMaps();
+ LayMapRecord(released, existing);
+ return;
+ }
+
+ int start = BinaryPrimitives.ReadInt32LittleEndian(released.Record.Slice(1, 4));
+ 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;
+
+ var holder = new DataPage();
+ holder.Read(new PageBuffer(released.Page, released.PageNumber), _channel.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);
+
+ if (lowest >= start && Covering(start) <= longest)
+ {
+ var grown = new byte[Covering(start)];
+ released.Record[..5].CopyTo(grown);
+ LayMapRecord(released, grown);
+ 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);
+ 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;
+ 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));
+ LayMapRecord(released, record);
+
+ var reference = new byte[1 + ReferenceMapSlots * 4];
+ reference[0] = ReferenceMapType;
+ AddReleasedBitmapPages(reference, pages);
+ (_, released) = ReadGlobalMaps();
+ LayMapRecord(released, reference);
+ }
+
+ /// Allocates an empty bitmap page, in range order, for every range of that
+ /// the reference record has no bitmap page for, and writes its pointer into the record. Returns whether any
+ /// was added.
+ private bool AddReleasedBitmapPages(byte[] reference, SortedSet pages)
+ {
+ int pagesPerBitmap = (_channel.PageSize - BitmapPageHeaderSize) * 8;
+ bool added = false;
+ foreach (int slot in pages.Select(p => p / pagesPerBitmap).Distinct())
+ {
+ if (slot >= ReferenceMapSlots)
+ 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;
+ int bitmapPage = Allocate();
+ var bitmap = new byte[_channel.PageSize];
+ bitmap[0] = (byte)PageType.PageUsageBitmap;
+ bitmap[1] = 1;
+ _channel.WritePage(bitmapPage, bitmap);
+ BinaryPrimitives.WriteInt32LittleEndian(reference.AsSpan(1 + slot * 4), 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.
+ private void LayMapRecord(MapRecord map, byte[] record)
+ {
+ 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 _)
+ ?? 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);
+ }
+
+ /// 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)
+ {
+ 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);
+ }
+ return;
+ }
+
+ released.Page.AsSpan(released.Slot.Offset + 5, released.Slot.Length - 5).Clear();
+ _channel.WritePage(released.PageNumber, released.Page);
}
+ ///
+ /// Checks that page 0's two global map pointers name distinct, well-formed usage-map records inside the
+ /// file. A pointer past the end of the file makes ACE mark the database corrupt, and one naming anything
+ /// other than a usage map fails at the first allocation, so a writable open refuses both up front.
+ ///
+ 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
/// 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.
- private int AllocateFromReferenceMap(ReadOnlySpan map)
+ private int AllocateFromReferenceMap(MapRecord free, MapRecord released, ReleasedPages releasedPages)
{
var format = _channel.Format;
- ValidateReferenceMap(map);
+ ReadOnlySpan map = free.Record;
int pagesPerBitmap = (format.PageSize - BitmapPageHeaderSize) * 8;
- var bitmapPages = new HashSet();
- for (int slot = 0; slot < ReferenceMapSlots; slot++)
- {
- int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1 + slot * 4, 4));
- if (bitmapPage == 0) continue;
- ValidateBitmapPage(bitmapPage);
- if (!bitmapPages.Add(bitmapPage))
- throw new InvalidDataException($"Global reference free map repeats bitmap page {bitmapPage}.");
- }
+ HashSet bitmapPages = ReferenceBitmapPages(free, released);
for (int slot = 0; slot < ReferenceMapSlots; slot++)
{
int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(map.Slice(1 + slot * 4, 4));
if (bitmapPage == 0) continue; // no bitmap page allocated for this range
- byte[] bitmap = ValidateBitmapPage(bitmapPage);
+ byte[] bitmap = ValidateBitmapPage(bitmapPage, free, released);
for (int i = BitmapPageHeaderSize; i < format.PageSize; i++)
{
- if (bitmap[i] == 0) continue;
- int bit = BitOperations.TrailingZeroCount(bitmap[i]);
- int allocated = slot * pagesPerBitmap + (i - BitmapPageHeaderSize) * 8 + bit;
- ValidateReusablePage(allocated, $"reference-map slot {slot} free bit", allowAppendBoundary: true);
- if (bitmapPages.Contains(allocated))
- throw new InvalidDataException($"Global free map marks bitmap page {allocated} itself as free.");
- EnsurePhysicalAllocation(allocated);
- bitmap[i] &= (byte)~(1 << bit); // no longer free
- _channel.WritePage(bitmapPage, bitmap);
- return allocated;
+ 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;
+ }
}
}
@@ -151,8 +385,9 @@ private int GrowAndAllocate()
private int GrowAndAllocateCore()
{
- (byte[] page, RowSlot slot) = ReadGlobalMap();
- byte[] record = page.AsSpan(slot.Offset, slot.Length).ToArray();
+ (MapRecord free, MapRecord released) = ReadGlobalMaps();
+ var releasedPages = new ReleasedPages(this, released);
+ byte[] record = free.Record.ToArray();
int frontier = _channel.PageCount;
if (record[0] == InlineMapType)
{
@@ -161,7 +396,7 @@ private int GrowAndAllocateCore()
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)
- return _channel.AllocatePage(); // already represented as used
+ return AppendUnreleasedPage(releasedPages); // already represented as used
var grown = new byte[5 + bitmapBytes];
// Preserve existing free bits; newly covered physical pages are already used. Only future
@@ -170,12 +405,12 @@ private int GrowAndAllocateCore()
for (int bit = frontier; bit < bitmapBytes * 8; bit++)
grown[5 + bit / 8] |= (byte)(1 << (bit % 8));
var holder = new DataPage();
- holder.Read(_channel.ReadPage(GlobalMapPage), _channel.Format);
- byte[]? rewritten = UsageMapWriter.ReplaceMapRecord(page, holder, _channel.Format, 0, grown, out _);
+ holder.Read(_channel.ReadPage(free.PageNumber), _channel.Format);
+ byte[]? rewritten = UsageMapWriter.ReplaceMapRecord(free.Page, holder, _channel.Format, free.Row, grown, out _);
if (rewritten is not null &&
BinaryPrimitives.ReadUInt16LittleEndian(rewritten.AsSpan(_channel.Format.DataFreeSpaceOffset)) >= 4)
{
- _channel.WritePage(GlobalMapPage, rewritten);
+ _channel.WritePage(free.PageNumber, rewritten);
return Allocate();
}
@@ -188,7 +423,7 @@ private int GrowAndAllocateCore()
{
if (range >= ReferenceMapSlots)
throw new NotSupportedException("Global allocation map has no remaining bitmap slots.");
- int bitmap = _channel.AllocatePage();
+ int bitmap = AppendUnreleasedPage(releasedPages);
BinaryPrimitives.WriteInt32LittleEndian(record.AsSpan(1 + range * 4), bitmap);
}
for (int range = 0; range < ReferenceMapSlots; range++)
@@ -196,25 +431,33 @@ private int GrowAndAllocateCore()
int bitmap = BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(1 + range * 4));
if (bitmap != 0) WriteNewGlobalBitmap(bitmap, range);
}
- WriteGlobalRecord(record);
+ WriteGlobalRecord(free, record);
return Allocate();
}
- ValidateReferenceMap(record);
int pagesPerBitmap = (_channel.PageSize - BitmapPageHeaderSize) * 8;
int rangeIndex = frontier / pagesPerBitmap;
if (rangeIndex >= ReferenceMapSlots)
throw new NotSupportedException("Global allocation map has no remaining bitmap slots.");
if (BinaryPrimitives.ReadInt32LittleEndian(record.AsSpan(1 + rangeIndex * 4)) != 0)
- return _channel.AllocatePage(); // represented range, bit already clear
+ return AppendUnreleasedPage(releasedPages); // represented range, bit already clear
- int newBitmap = _channel.AllocatePage();
+ int newBitmap = AppendUnreleasedPage(releasedPages);
BinaryPrimitives.WriteInt32LittleEndian(record.AsSpan(1 + rangeIndex * 4), newBitmap);
WriteNewGlobalBitmap(newBitmap, rangeIndex);
- WriteGlobalRecord(record);
+ WriteGlobalRecord(free, record);
return Allocate();
}
+ /// Appends the next page past the end of the file that is not released. Released pages at the
+ /// frontier are materialized — the file stays contiguous — but not handed out.
+ private int AppendUnreleasedPage(ReleasedPages releasedPages)
+ {
+ while (releasedPages.Contains(_channel.PageCount))
+ _channel.AllocatePage();
+ return _channel.AllocatePage();
+ }
+
private void WriteNewGlobalBitmap(int number, int range)
{
var bitmap = new byte[_channel.PageSize];
@@ -227,23 +470,23 @@ private void WriteNewGlobalBitmap(int number, int range)
_channel.WritePage(number, bitmap);
}
- private void WriteGlobalRecord(byte[] record)
+ private void WriteGlobalRecord(MapRecord free, byte[] record)
{
- byte[] page = _channel.ReadPage(GlobalMapPage).Span.ToArray();
+ byte[] page = _channel.ReadPage(free.PageNumber).Span.ToArray();
var holder = new DataPage();
- holder.Read(_channel.ReadPage(GlobalMapPage), _channel.Format);
- byte[] rewritten = UsageMapWriter.ReplaceMapRecord(page, holder, _channel.Format, 0, record, out _)
+ 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(GlobalMapPage, rewritten);
+ _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(ReadOnlySpan map, int page)
+ private void FreeInReferenceMap(MapRecord free, MapRecord released, int page)
{
var format = _channel.Format;
- ValidateReferenceMap(map);
+ ReadOnlySpan map = free.Record;
int pagesPerBitmap = (format.PageSize - BitmapPageHeaderSize) * 8;
int slot = page / pagesPerBitmap;
if (slot < 0 || slot >= ReferenceMapSlots) return; // beyond the map's ~2 GB reach
@@ -252,58 +495,173 @@ private void FreeInReferenceMap(ReadOnlySpan map, int page)
if (bitmapPage == 0) return; // range has no bitmap page — nothing to record into
int bitInRange = page - slot * pagesPerBitmap;
- byte[] bitmap = ValidateBitmapPage(bitmapPage);
+ 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));
_channel.WritePage(bitmapPage, bitmap);
}
- private (byte[] Page, RowSlot Slot) ReadGlobalMap()
+ /// Reads both global map records through page 0's pointers, and refuses pointers that are equal,
+ /// name a page outside the file, or name anything other than a usage-map record.
+ private (MapRecord Free, MapRecord Released) ReadGlobalMaps()
{
- if (_channel.PageCount <= GlobalMapPage)
- throw new InvalidDataException("Database has no global free-pages map at page 1.");
- PageBuffer buffer = _channel.ReadPage(GlobalMapPage);
+ ReadOnlySpan page0 = _channel.ReadPage(0).Span;
+ (int Row, int Page) freePointer =
+ DatabaseDefinitionPage.ReadMapPointer(page0, Formats.JetFormatBase.FreePagesMapPointerOffset);
+ (int Row, int Page) releasedPointer =
+ DatabaseDefinitionPage.ReadMapPointer(page0, Formats.JetFormatBase.ReleasedPagesMapPointerOffset);
+ if (freePointer == releasedPointer)
+ throw new InvalidDataException(
+ $"Page 0 names the same record (page {freePointer.Page}, row {freePointer.Row}) for the global " +
+ "free-pages and released-pages maps; they must be distinct.");
+
+ MapRecord free = ReadMapRecord("free-pages", freePointer);
+ MapRecord released = ReadMapRecord("released-pages", releasedPointer);
+ ReferenceBitmapPages(free, released);
+ return (free, released);
+ }
+
+ 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 (buffer.Span[0] != (byte)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 (data.RowCount < 1)
- throw new InvalidDataException("Global free-pages map page has no row 0.");
- RowSlot slot = data.Rows[0];
+ 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 free-pages map row 0 is deleted, overflowed, or empty.");
- return (buffer.Span.ToArray(), slot);
+ 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;
}
- private static void ValidateReferenceMap(ReadOnlySpan map)
+ /// 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)
{
- if (map.Length != 1 + ReferenceMapSlots * 4)
- throw new InvalidDataException(
- $"Global reference free map must be exactly {1 + ReferenceMapSlots * 4} bytes; got {map.Length}.");
+ var pages = new HashSet();
+ foreach (MapRecord map in new[] { free, released })
+ {
+ if (map.Type != ReferenceMapType) continue;
+ ReadOnlySpan record = map.Record;
+ for (int slot = 0; slot < ReferenceMapSlots; slot++)
+ {
+ int bitmapPage = BinaryPrimitives.ReadInt32LittleEndian(record.Slice(1 + slot * 4, 4));
+ if (bitmapPage == 0) continue;
+ ValidateBitmapPage(bitmapPage, free, released);
+ if (!pages.Add(bitmapPage))
+ throw new InvalidDataException($"Global reference maps repeat bitmap page {bitmapPage}.");
+ }
+ }
+ return pages;
}
- private byte[] ValidateBitmapPage(int pageNumber)
+ private byte[] ValidateBitmapPage(int pageNumber, MapRecord free, MapRecord released)
{
- ValidateReusablePage(pageNumber, "usage-map bitmap pointer", allowAppendBoundary: false);
+ 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)
- throw new InvalidDataException($"Global free-map pointer {pageNumber} does not target a valid bitmap page.");
+ throw new InvalidDataException($"Global map pointer {pageNumber} does not target a valid bitmap page.");
return page;
}
- private void ValidateReusablePage(int page, string source, bool allowAppendBoundary)
+ /// Page 0 and the pages holding the two global maps are never allocatable; nor is a page past
+ /// .
+ private static void ValidateReusablePage(int page, string source, MapRecord free, MapRecord released, int maximum)
{
- int maximum = allowAppendBoundary ? _channel.PageCount : _channel.PageCount - 1;
- if (page <= GlobalMapPage || page > maximum)
+ if (page <= 0 || page == free.PageNumber || page == released.PageNumber || page > maximum)
throw new InvalidDataException(
- $"Global {source} names page {page}, outside the reusable contiguous range 2..{maximum}.");
+ $"Global {source} names page {page}, which is page 0, a global-map holder page " +
+ $"({free.PageNumber}/{released.PageNumber}), or past the contiguous range ending at {maximum}.");
+ }
+
+ /// The highest page an allocation may name: the next page past the end of the file, pushed
+ /// further by any released pages sitting at the frontier (they are materialized, not handed out).
+ private int AppendBoundary(ReleasedPages releasedPages)
+ {
+ int boundary = _channel.PageCount;
+ while (releasedPages.Contains(boundary)) boundary++;
+ return boundary;
}
- private void EnsurePhysicalAllocation(int page)
+ private void EnsurePhysicalAllocation(int page, ReleasedPages releasedPages)
{
+ while (_channel.PageCount < page)
+ {
+ 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();
+ }
if (page < _channel.PageCount) return;
int allocated = _channel.AllocatePage();
if (allocated != page)
throw new InvalidDataException(
$"Global free map selected append page {page}, but contiguous allocation produced page {allocated}.");
}
+
+ /// Membership in the global released-pages map, inline or reference form.
+ private sealed class ReleasedPages
+ {
+ private readonly PageAllocator _owner;
+ private readonly MapRecord _map;
+ private readonly Dictionary _bitmaps = [];
+
+ public ReleasedPages(PageAllocator owner, MapRecord map)
+ {
+ _owner = owner;
+ _map = map;
+ }
+
+ public bool Contains(int page)
+ {
+ if (page < 0) return false;
+ ReadOnlySpan record = _map.Record;
+ if (_map.Type == InlineMapType)
+ {
+ 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;
+ }
+
+ int pagesPerBitmap = (_owner._channel.PageSize - BitmapPageHeaderSize) * 8;
+ int slot = page / pagesPerBitmap;
+ if (slot >= ReferenceMapSlots) 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();
+ _bitmaps[slot] = bitmap;
+ }
+ if (bitmap is null) return false;
+ int inRange = page - slot * pagesPerBitmap;
+ return (bitmap[BitmapPageHeaderSize + inRange / 8] & (1 << (inRange % 8))) != 0;
+ }
+ }
}
diff --git a/src/LibRed/LibRed.Core/Storage/RowEncoder.cs b/src/LibRed/LibRed.Core/Storage/RowEncoder.cs
index 2a18d1744..d3070b3f8 100644
--- a/src/LibRed/LibRed.Core/Storage/RowEncoder.cs
+++ b/src/LibRed/LibRed.Core/Storage/RowEncoder.cs
@@ -50,7 +50,11 @@ public sealed class RowEncoder(IReadOnlyList columns, JetFormatBase f
/// 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.
- public byte[] Encode(object?[] values, IReadOnlyDictionary? preservedCalculated)
+ /// 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));
@@ -90,7 +94,7 @@ public byte[] Encode(object?[] values, IReadOnlyDictionary? preserv
{
if (column.IsCalculated)
{
- varChunks[column.VariableIndex] = EncodeCalculated(column, values, preservedCalculated);
+ varChunks[column.VariableIndex] = EncodeCalculated(column, logicalValues ?? values, preservedCalculated);
continue;
}
object? v = values[column.Index];
@@ -145,6 +149,25 @@ private static void EnsureFitsDeclaredLength(ColumnDef column, byte[] encoded)
+ $"{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
@@ -175,14 +198,18 @@ internal static byte[] AssembleRow(int maxColumnId, ReadOnlySpan fixedRegi
for (int j = 0; j < numVar; j++) varDataLength += varChunks[j].Length;
int varSectionLen = numVar > 0 ? varDataLength + (numVar + 1) * 2 + 2 : 0;
- var row = new byte[countSize + fixedRegion.Length + varSectionLen + nullBitmapSize];
+ // 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 + fixedRegion.Length;
+ int varDataStart = countSize + fixedLen;
int pos = varDataStart;
for (int j = 0; j < numVar; j++) { varChunks[j].CopyTo(row.AsSpan(pos)); pos += varChunks[j].Length; }
@@ -199,7 +226,7 @@ internal static byte[] AssembleRow(int maxColumnId, ReadOnlySpan fixedRegi
BinaryPrimitives.WriteUInt16LittleEndian(row.AsSpan(numVarPos, 2), (ushort)numVar);
bitmapPos = numVarPos + 2;
}
- else bitmapPos = countSize + fixedRegion.Length;
+ else bitmapPos = countSize + fixedLen;
var liveIds = new HashSet();
foreach (ColumnDef column in columns)
diff --git a/src/LibRed/LibRed.Core/Storage/RowInserter.cs b/src/LibRed/LibRed.Core/Storage/RowInserter.cs
index df9951384..ad716d23a 100644
--- a/src/LibRed/LibRed.Core/Storage/RowInserter.cs
+++ b/src/LibRed/LibRed.Core/Storage/RowInserter.cs
@@ -19,6 +19,8 @@ public sealed class RowInserter(PageChannel channel, TableDef table)
private readonly UsageMapWriter _usageMaps = new(channel);
private readonly TableDef _table = table;
+ private bool HasCalculatedColumns => _table.Columns.Any(c => c.IsCalculated);
+
/// Encodes and writes (aligned to column Index) into the table.
public void Insert(object?[] values) => Insert(values, updateIndexes: true);
@@ -43,14 +45,16 @@ public void Insert(object?[] values, bool updateIndexes)
// Index keys are encoded from the *logical* values. MaterializeLongValues replaces a memo/OLE value
// with its on-disk LongValueDescriptor, and a Memo column IS indexable (its key is the collation key
// of the first 255 characters), so snapshot the values first and key the index off that snapshot.
- object?[] keyValues = updateIndexes ? (object?[])values.Clone() : values;
+ // A calculated column reads the same logical values, so the snapshot serves it too.
+ bool calculated = HasCalculatedColumns;
+ object?[] keyValues = updateIndexes || calculated ? (object?[])values.Clone() : values;
MaterializeLongValues(values);
// 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),
_table.VariableColumnCount, SpillCalculated);
- byte[] record = encoder.Encode(values);
+ byte[] record = encoder.Encode(values, null, calculated ? keyValues : null);
EnsureRecordFits(format, record);
@@ -75,7 +79,9 @@ public void Insert(object?[] values, bool updateIndexes)
_channel.WritePage(pageNumber, page);
- UpdateTdefCounters(format, values, generatedAutoNumbers);
+ // Asked before the row's own entries go in, so an index "already has" a key only through another row.
+ HashSet newKeys = updateIndexes ? IndexesGainingANewKey(keyValues) : [];
+ UpdateTdefCounters(format, values, generatedAutoNumbers, newKeys);
if (updateIndexes)
UpdateIndexes(keyValues, new RowId(pageNumber, rowCount));
}
@@ -93,6 +99,10 @@ public void Update(RowId id, object?[] values, IReadOnlySet changedColumns)
JetFormatBase format = _channel.Format;
RejectExplicitCalculatedValues(values, changedColumns);
+ // The row as its columns hold it, for a recomputed calculated column to read: the loop below swaps an
+ // unchanged memo's text for its on-disk descriptor, and MaterializeLongValues the changed ones.
+ object?[]? logicalValues = HasCalculatedColumns ? (object?[])values.Clone() : null;
+
// 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.
byte[] oldRow = ReadRowBytes(id);
@@ -103,7 +113,7 @@ public void Update(RowId id, object?[] values, IReadOnlySet changedColumns)
{
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);
+ if (changedColumns.Contains(column.Index)) FreeLongValue(column, oldDescriptor, releaseAtClose: false);
else values[column.Index] = new LongValueDescriptor(oldDescriptor);
}
@@ -131,7 +141,7 @@ public void Update(RowId id, object?[] values, IReadOnlySet changedColumns)
if (preservedCalculated?.ContainsKey(column.Index) == true) continue;
if (oldCalculated.TryGetValue(column.Index, out byte[]? stale)
&& stale.Length >= LongValueFormat.DescriptorSize)
- FreeLongValue(column, stale);
+ FreeLongValue(column, stale, releaseAtClose: false);
}
MaterializeLongValues(values);
@@ -142,7 +152,7 @@ public void Update(RowId id, object?[] values, IReadOnlySet changedColumns)
// Order Details), which without the guard would overflow `new byte[len]`.
var encoder = new RowEncoder(_table.Columns, format, InferFixedDataLength(format),
_table.VariableColumnCount, SpillCalculated);
- byte[] record = encoder.Encode(values, preservedCalculated);
+ byte[] record = encoder.Encode(values, preservedCalculated, logicalValues);
// Here as well as on the insert path, and before the in-place rewrite rather than beside the
// page-search: a row that grows past the cap but still fits its current page is rewritten where it
@@ -254,12 +264,12 @@ public void Delete(RowId id)
{
JetFormatBase format = _channel.Format;
- // Free the deleted row's chained long-value pages.
+ // Free the deleted row's chained long-value pages — held until close, as ACE holds them.
var oldDescriptors = new RowDecoder(_table.Columns, format).LongValueRaw(ReadRowBytes(id));
foreach (ColumnDef column in _table.Columns)
if ((column.Type is JetDataType.Memo or JetDataType.Ole || column.HasLongValueMap)
&& oldDescriptors.TryGetValue(column.Index, out byte[]? d))
- FreeLongValue(column, d);
+ FreeLongValue(column, d, releaseAtClose: true);
byte[] page = ArrayPool.Shared.Rent(format.PageSize);
try
@@ -350,7 +360,7 @@ private void ReclaimRelocationTarget(JetFormatBase format, byte[] sourcePage, in
/// 0FED 0FDA CFDA, with free space rising by 19 in each case.
///
///
- private static void ReclaimRow(JetFormatBase format, byte[] page, int row)
+ 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(
@@ -438,7 +448,12 @@ private byte[] ReadRowBytes(RowId id)
/// Inline (0x80) values have no pages; single-page (0x40) values share a page with others, so reclaiming
/// their row is deferred (they are left in place — a small, shared-page leak).
///
- private void FreeLongValue(ColumnDef column, byte[] descriptor)
+ ///
+ /// Measured against ACE on one connection: the pages of a value an UPDATE replaces are set in the global
+ /// free map at once, while a DELETE's are not reusable until the connection closes
+ /// (, see ).
+ ///
+ 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
@@ -452,13 +467,21 @@ private void FreeLongValue(ColumnDef column, byte[] descriptor)
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) has no pages; single-page (0x40) shares its page with other values — neither is
- // reclaimed here. Only a chained value owns pages outright.
- if (flags != LongValueFormat.FlagChained) return;
+ // Inline (0x80) keeps its payload in the row, so there is nothing to give back.
+ if (flags == LongValueFormat.FlagInline) return;
TableDefinitionPage definition = ReadDefinition();
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)
+ {
+ ReleasePackedValue(column, descriptor[5] | (descriptor[6] << 8) | (descriptor[7] << 16),
+ descriptor[4], owned, free, releaseAtClose);
+ return;
+ }
var allocator = new PageAllocator(_channel);
var reader = new LongValueReader(_channel);
_ = reader.ResolveWithPages(descriptor, out IReadOnlyList pages);
@@ -474,23 +497,86 @@ private void FreeLongValue(ColumnDef column, byte[] descriptor)
// one per statement); a direct Core caller without one gets no more than any other multi-page write.
foreach (int page in pages)
{
- allocator.Free(page);
+ 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);
}
}
- /// The raw bytes of slot on a data page (walks the packed rows).
- private static byte[] SlotBytes(byte[] page, JetFormatBase format, int slot)
+ ///
+ /// 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
+ /// and free maps, and the page returned to the global allocator.
+ ///
+ ///
+ /// Measured against ACE, deleting 4 of 12 rows whose 400-character memos shared one page, then all 12:
+ ///
+ /// start 0x01 n=5 free=72 [3296,2496,1696,896,96]
+ /// 4 gone 0x01 n=5 free=3272 [4096DO,4096DO,4096DO,4096DO,3296] the survivor slid to the top
+ /// all 0x09 n=5 free=4072 [4096DO x5]
+ ///
+ /// This is where page type 0x09 comes from — an emptied packed long-value page, which the spec had
+ /// recorded as a released page of unidentified origin (page-09). Chained values are unaffected: they own
+ /// their pages outright and are freed below, leaving them at 0x01, which is why no experiment that
+ /// used a memo large enough to chain ever produced one.
+ ///
+ private void ReleasePackedValue(ColumnDef column, int pageNumber, int row,
+ (int Row, int Page) owned, (int Row, int Page) free, bool releaseAtClose)
{
- int prevEnd = format.PageSize;
- for (int i = 0; i <= slot; i++)
+ JetFormatBase format = _channel.Format;
+ if (pageNumber <= 0 || pageNumber >= _channel.PageCount)
+ throw new InvalidDataException($"Long-value page pointer {pageNumber} is outside the file.");
+
+ byte[] page = _channel.ReadPage(pageNumber).Span.ToArray();
+ var holder = new DataPage();
+ holder.Read(new PageBuffer(page, pageNumber), format);
+ if (!holder.IsLongValuePage)
+ throw new InvalidDataException($"Long-value pointer {pageNumber}:{row} targets a non-LVAL data page.");
+ if (row < 0 || row >= holder.RowCount)
+ throw new InvalidDataException(
+ $"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;
+ var records = new byte[holder.RowCount][];
+ var flags = new ushort[holder.RowCount];
+ for (int i = 0; i < holder.RowCount; i++)
{
- int offset = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(format.DataRowDirectoryOffset + i * 2, 2)) & RowPointer.OffsetMask;
- if (i == slot) return page.AsSpan(offset, prevEnd - offset).ToArray();
- prevEnd = offset;
+ 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));
}
- throw new ArgumentOutOfRangeException(nameof(slot));
+
+ 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)));
+
+ bool emptied = records.All(r => r.Length == 0);
+ if (emptied) page[0] = (byte)PageType.ReleasedLongValuePage;
+ _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.
+ if (!emptied)
+ {
+ _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 (releaseAtClose) new PageAllocator(_channel).Release(pageNumber);
+ else new PageAllocator(_channel).Free(pageNumber);
}
/// Rejects the insert if a UNIQUE or PRIMARY index would gain a duplicate key. A row with a
@@ -530,6 +616,27 @@ private void UpdateIndexes(object?[] values, RowId rowId)
}
}
+ ///
+ /// The root pages of the non-unique indexes the row brings a key they do not hold yet — the ones whose
+ /// unique-entry count advances (verified vs ACE: a key equal to one already in the index, collation included,
+ /// adds nothing; a key whose last row was deleted counts again; a Null key counts like any other, except in an
+ /// IGNORE NULL index, which does not hold it). A unique index always gains a new key, so it is not asked.
+ ///
+ private HashSet IndexesGainingANewKey(object?[] values)
+ {
+ var gaining = new HashSet();
+ IndexWriter? writer = null;
+ foreach (IndexDef index in _table.Indexes
+ .Where(i => !i.IsUnique && i.RootPage > 0)
+ .GroupBy(i => i.RootPage).Select(g => g.First()))
+ {
+ if (index.IgnoreNulls && HasNullKey(index, values)) continue;
+ writer ??= new IndexWriter(_channel, _table);
+ if (!writer.KeyExists(index, values)) gaining.Add(index.RootPage);
+ }
+ return gaining;
+ }
+
private static bool HasNullKey(IndexDef index, object?[] values) =>
index.Columns.Any(c => values[c.Column.Index] is null or DBNull);
@@ -861,8 +968,7 @@ private IReadOnlyList MapPages(int mapRow, int mapPage)
private void AppendMapBits(List result, ReadOnlySpan bitmap, int startPage)
{
- // Bound read once, for the reason UsageMap.AppendSetBits records: outside a transaction PageCount is a
- // file-length syscall, and a per-bit test costs real time on a hot path. Nothing here writes.
+ // Bound read once, as UsageMap.AppendSetBits does: the loop runs per bit on a hot path. Nothing here writes.
int pageCount = _channel.PageCount;
for (int i = 0; i < bitmap.Length; i++)
@@ -943,14 +1049,15 @@ private static int RandomAutoNumber()
/// to pick the *next* id = this + increment; leaving it stale makes Access reissue an existing id and
/// reject the insert as a duplicate primary key.)
/// Per-index **unique-entry count** (`0x3F + ordinal×12`, `+4`) — incremented by one for
- /// each **unique** index (a unique index gets a distinct key per row). This is the cumulative
- /// count Access advances on every insert and never decrements. The sibling **total-entry count**
- /// (`+0`) is deliberately left untouched: Access does **not** maintain it live — it stays `0`
- /// through inserts and is only written (to the row count) on compact/repair (verified: a live
- /// ACE-inserted table reads total `0` while saved Northwind tables read total = row count).
+ /// each **unique** index (a unique index gets a distinct key per row) and each non-unique index in
+ /// (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: Access does **not** maintain it live — it is written only
+ /// when the index is built (to its entries, in TableCreator's back-fill) or the file compacted
+ /// (verified: an index created on an empty table reads total `0` through any number of inserts).
///
///
- private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]? generatedAutoNumbers)
+ private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]? generatedAutoNumbers, IReadOnlySet newKeys)
{
byte[] tdef = _channel.ReadPageShared(_table.DefinitionPage).Span.ToArray();
@@ -994,14 +1101,10 @@ private void UpdateTdefCounters(JetFormatBase format, object?[] values, bool[]?
column.Seed = unchecked(newHighWater + column.Increment);
}
- // TODO(non-unique-index-stats): a non-unique index's unique-entry count must advance only
- // when the inserted key is genuinely new (Access's cumulative-distinct semantics), which
- // needs a probe of the existing keys. Only unique indexes advance it today — and LibRed does
- // create non-unique ones (every FK backing index is one, and CREATE INDEX without UNIQUE),
- // so this gap applies to the majority of indexes written, not to none of them.
- foreach (IndexDef index in _table.Indexes)
+ // One count per real index, so a relationship's logical index sharing a real one does not advance it twice.
+ foreach (IndexDef index in _table.Indexes.GroupBy(i => i.RealIndexOrdinal).Select(g => g.First()))
{
- if (!index.IsUnique) continue;
+ 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));
diff --git a/src/LibRed/LibRed.Core/Storage/TableCreator.cs b/src/LibRed/LibRed.Core/Storage/TableCreator.cs
index 44c92ca13..101183457 100644
--- a/src/LibRed/LibRed.Core/Storage/TableCreator.cs
+++ b/src/LibRed/LibRed.Core/Storage/TableCreator.cs
@@ -1,9 +1,11 @@
using System.Buffers.Binary;
using System.Text;
+using EntityFrameworkCore.Jet.Data;
using LibRed.Catalog;
using LibRed.Formats;
using LibRed.IO;
using LibRed.Pages;
+using MapRetirement = ((int Row, int Page) Map, System.Collections.Generic.IReadOnlyList<(int Row, int Page)> Clear, System.Collections.Generic.IReadOnlyList Pages);
namespace LibRed.Storage;
@@ -91,6 +93,10 @@ public void Create(
// data and a 100-char index name breaks ACE's index enumeration. Only validate caller-supplied names.
if (primaryKeyName is not null) JetName.Validate(primaryKeyName, "primary key name");
foreach (RelationshipSpec r in relationships) JetName.Validate(r.Name, "foreign key name");
+ var relationshipNames = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (RelationshipSpec r in relationships)
+ if (!relationshipNames.Add(r.Name)) throw RelationshipNameTaken(r.Name);
+ else EnsureRelationshipNameFree(r.Name);
foreach (UniqueIndexSpec u in uniqueConstraints) JetName.Validate(u.Name, "unique constraint name");
foreach ((string checkName, _) in checkConstraints) JetName.Validate(checkName, "check constraint name");
@@ -105,6 +111,18 @@ public void Create(
throw new InvalidOperationException(
$"Table '{name}' has {columns.Count} columns; Jet/ACE tables are limited to {MaxColumnsPerTable}.");
+ // Before the foreign keys' type match, as ACE checks it: an OLE column referencing a LONG key gets this.
+ RejectOleIndexColumns(
+ (primaryKey ?? []).Concat(uniqueConstraints.SelectMany(u => u.Columns))
+ .Concat(relationships.SelectMany(r => r.Columns.Select(c => c.Column))),
+ n => columns.FirstOrDefault(c => string.Equals(c.Name, n, StringComparison.OrdinalIgnoreCase))?.Type);
+
+ relationships = relationships.Select(fk => ResolvePrimaryKeyReference(fk, creatingTable: name)).ToList();
+ foreach (RelationshipSpec fk in relationships)
+ EnsureSameDataTypes(fk, ColumnOf(columns), string.Equals(fk.ReferencedTable, name, StringComparison.OrdinalIgnoreCase)
+ ? ColumnOf(columns)
+ : ColumnOf(_catalog.FindTable(fk.ReferencedTable)));
+
JetFormatBase format = _channel.Format;
// Allocate the pages the table needs through the global free-pages map (so Access accounts
@@ -263,18 +281,19 @@ public void Create(
WriteInt24(tdef, format.TdefFreePagesOffset + 1, usageMapPage);
// 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], []);
+ WriteDefinition(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 (Access omits
- // it for a nullable column, and — verified — for an AutoNumber, which is implicitly required).
+ // a DEFAULT is a memo property; a NOT NULL column carries a boolean Required property, and a nullable
+ // one none. An AutoNumber follows the same rule — ACE writes Required for COUNTER NOT NULL and not for
+ // a bare COUNTER (verified by reading its property blob back).
var columnProps = new List();
foreach (ColumnSpec col in columns)
{
var def = columnDefaults.FirstOrDefault(d => string.Equals(d.Column, col.Name, StringComparison.OrdinalIgnoreCase));
if (def.DefaultSql is not null)
columnProps.Add(new PropertyBlob.Property(col.Name, PropertyBlob.DefaultValueProperty, def.DefaultSql));
- if (!col.IsNullable && !col.IsAutoNumber)
+ if (!col.IsNullable)
columnProps.Add(PropertyBlob.Bool(col.Name, PropertyBlob.RequiredProperty, true));
columnProps.AddRange(CalculatedProperties(col));
}
@@ -338,6 +357,31 @@ private readonly record struct IncomingRelationship(
int ParentPage, int Number, int ReferencedOrdinal, uint ChildBlockNumber, int ChildPage,
byte UpdateAction, byte DeleteAction);
+ ///
+ /// ACE's "same data types" rule for a relationship, measured over every pairing of the column types: each child
+ /// column must have its parent column's storage type, whatever either one's length — TEXT(5),
+ /// TEXT(20) and CHAR(10) all pair with one another, as do DECIMALs of any precision and
+ /// scale and BINARY with VARBINARY. An AutoNumber is a Long on either side. Checked before
+ /// anything is written. A column that is not found is left to the check that reports it.
+ ///
+ private static void EnsureSameDataTypes(RelationshipSpec fk,
+ Func childColumn, Func parentColumn)
+ {
+ foreach ((string column, string referenced) in fk.Columns)
+ {
+ if (childColumn(column) is not { } child || parentColumn(referenced) is not { } parent) continue;
+ if (child != parent)
+ throw new InvalidOperationException(
+ "Relationship must be on the same number of fields with the same data types. " +
+ $"'{column}' ({child}) cannot reference '{fk.ReferencedTable}.{referenced}' ({parent}).");
+ }
+ }
+
+ 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;
+
/// 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).
private static int SelfReferencedOrdinal(
@@ -384,10 +428,42 @@ private int NextLogicalIndexNumber(int tdefPage)
return maxNum + 1;
}
+ /// A relationship's parent table, or ACE's error when it does not exist.
+ private TableDef ReferencedTableOf(RelationshipSpec fk) =>
+ _catalog.FindTable(fk.ReferencedTable)
+ ?? throw new InvalidOperationException(
+ $"Cannot find table or constraint: the referenced table '{fk.ReferencedTable}' does not exist.");
+
+ ///
+ /// Resolves REFERENCES table with no column list ()
+ /// to the parent's primary key, pairing the child columns with the key's in order whatever either side's
+ /// columns are named — as ACE does. Any other relationship comes back unchanged.
+ ///
+ ///
+ /// ACE refuses it when the parent has no primary key (a unique index does not stand in for one) and when the
+ /// columns differ in number (verified). A table referencing itself in its own CREATE TABLE
+ /// () has no key in the catalog yet; the SQL parser pairs such a reference
+ /// with a key the statement declares earlier, so one still unpaired here has no key to reference.
+ ///
+ private RelationshipSpec ResolvePrimaryKeyReference(RelationshipSpec fk, string? creatingTable)
+ {
+ if (!fk.ReferencesPrimaryKey) return fk;
+ bool selfInCreate = creatingTable is not null
+ && string.Equals(fk.ReferencedTable, creatingTable, StringComparison.OrdinalIgnoreCase);
+ IndexDef primaryKey = (selfInCreate ? null : ReferencedTableOf(fk).Indexes.FirstOrDefault(i => i.IsPrimaryKey))
+ ?? throw new InvalidOperationException(
+ $"Cannot create relationship. Referenced table '{fk.ReferencedTable}' does not have a primary key.");
+ return fk with
+ {
+ Columns = RelationshipSpec.PairColumns(fk.ReferencedTable,
+ fk.Columns.Select(c => c.Column).ToList(), primaryKey.Columns.Select(c => c.Column.Name).ToList()),
+ ReferencesPrimaryKey = false,
+ };
+ }
+
private (int Page, int ReferencedOrdinal, int NextIndexNumber) ResolveParent(RelationshipSpec fk, int childPage)
{
- TableDef parent = _catalog.FindTable(fk.ReferencedTable)
- ?? throw new InvalidOperationException($"Referenced table '{fk.ReferencedTable}' was not found.");
+ TableDef parent = ReferencedTableOf(fk);
if (parent.DefinitionPage == childPage)
throw new InvalidOperationException($"Self-referencing foreign key '{fk.Name}' should have been handled inline.");
@@ -398,15 +474,28 @@ private int NextLogicalIndexNumber(int tdefPage)
return (parent.DefinitionPage, refIndex.RealIndexOrdinal, NextLogicalIndexNumber(parent.DefinitionPage));
}
+ /// ACE refuses a relationship name another relationship already has (verified); a table or query may
+ /// share it. Checked before anything is written.
+ private void EnsureRelationshipNameFree(string name)
+ {
+ if (_catalog.Relationships.Any(r => string.Equals(r.Name, name, StringComparison.OrdinalIgnoreCase)))
+ throw RelationshipNameTaken(name);
+ }
+
+ private static SchemaObjectExistsException RelationshipNameTaken(string name) =>
+ new($"There is already a relationship named '{name}' in the current database.", name);
+
///
/// Writes the MSysRelationships rows for one relationship — one row per column pair, with
/// ccolumn = the pair count, icolumn = the 0-based pair index, and grbit
- /// encoding enforce/cascade (verified against Access: an enforced no-cascade FK stores grbit 0).
+ /// encoding enforce/cascade (verified against Access: an enforced no-cascade FK stores grbit 0) — and the
+ /// relationship's own MSysObjects object, which ACE records for every relationship.
///
private void AddRelationshipRows(string childTable, RelationshipSpec fk)
{
TableDef msys = _catalog.FindTable("MSysRelationships")
?? throw new InvalidOperationException("MSysRelationships catalog table was not found.");
+ new ViewCreator(_channel, _catalog).CreateRelationshipObject(fk.Name);
int grbit = 0;
if (!fk.IsEnforced) grbit |= RelationshipFlags.DontEnforce;
@@ -452,7 +541,11 @@ public void AddIndex(string tableName, string indexName, IReadOnlyList<(string C
RejectCalculatedIndexColumns(indexName, columns.Select(c => c.Column),
n => table.Columns.FirstOrDefault(
c => c.IsCalculated && string.Equals(c.Name, n, StringComparison.OrdinalIgnoreCase))?.Name);
+ RejectOleIndexColumns(columns.Select(c => c.Column), n => table.FindColumn(n)?.Type);
var slots = ResolveSlots(table, columns.Select(c => (c.Column, Ascending: !c.Descending)));
+ // A table has one primary key, and ACE refuses a second (verified).
+ if (isPrimary && table.Indexes.Any(i => i.IsPrimaryKey))
+ 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));
@@ -476,6 +569,17 @@ private static void RejectCalculatedIndexColumns(
+ "offer one for indexing, and an index over it makes the table refuse every insert.");
}
+ /// Refuses an index over an OLE column — a key, a unique constraint, a relationship's — before anything
+ /// is written, as ACE does on every route (verified: CREATE INDEX, PRIMARY KEY and UNIQUE both in CREATE TABLE and
+ /// added, a foreign key in either place, and ALTER COLUMN of an indexed column to OLE). An OLE value has no index
+ /// key, and without this the definition is accepted on an empty table and every later insert fails.
+ private static void RejectOleIndexColumns(IEnumerable columnNames, Func typeOf)
+ {
+ foreach (string name in columnNames)
+ if (typeOf(name) == JetDataType.Ole)
+ throw new InvalidOperationException($"Invalid field definition '{name}' in definition of index or relationship.");
+ }
+
/// Resolves index column names to (columnId, ascending) slots against a table.
private static IReadOnlyList<(int Id, bool Ascending)> ResolveSlots(
TableDef table, IEnumerable<(string Column, bool Ascending)> columns)
@@ -509,13 +613,13 @@ private int InsertIndex(TableDef table, string indexName, IReadOnlyList<(int Id,
(LibRed.IO.PageBuffer buf, IReadOnlyList existingContinuations) = ReadDefinition(table.DefinitionPage);
int existingRowCount = buf.ReadInt32(format.TdefRowCountOffset);
- // A unique index over rows that already exist has to be rejected if those rows aren't unique — ACE
- // refuses the DDL too. Done *here*, before a single byte of the TDEF moves, rather than during the
- // back-fill: this path is not transactional, so a failure discovered mid-back-fill would leave the
- // index committed to the TDEF and half-populated. Scanning first means a rejected CREATE INDEX
- // leaves the file exactly as it was.
- if (unique && existingRowCount != 0)
- EnsureNoDuplicateKeys(table, indexName, slots);
+ // 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
+ // single byte of the TDEF moves, rather than during the back-fill: this path is not transactional, so a
+ // failure discovered mid-back-fill would leave the index committed to the TDEF and half-populated.
+ // Scanning first means a rejected CREATE INDEX leaves the file exactly as it was.
+ if ((unique || required) && existingRowCount != 0)
+ EnsureExistingRowsFitIndex(table, indexName, slots, unique, required);
int dataCount = buf.ReadInt32(format.TdefIndexCountOffset);
int logicalCount = buf.ReadInt32(format.TdefLogicalIndexCountOffset);
@@ -627,7 +731,7 @@ private int InsertIndex(TableDef table, string indexName, IReadOnlyList<(int Id,
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);
+ WriteDefinition(table.DefinitionPage, def, existingContinuations, rewrite: true);
_catalog.Invalidate();
// Back-fill the new (empty) index B-tree with an entry per existing row, so the index is complete.
@@ -637,12 +741,14 @@ private int InsertIndex(TableDef table, string indexName, IReadOnlyList<(int Id,
}
///
- /// Throws if the table's existing rows already hold a duplicate key for a would-be unique index. Purely a
- /// read: it encodes each row's key and looks for a repeat, touching nothing on disk, so it is safe to call
- /// before the index exists. Rows with a null in any key column are exempt — Jet's uniqueness is over the
- /// non-null keys only, so several rows may be null (verified vs ACE; see the constraints page). That skip
- /// is unconditional: a WITH IGNORE NULL index leaves null-keyed rows out of the B-tree altogether, so
- /// either way they cannot collide.
+ /// Throws if the table's existing rows cannot go into a would-be index: a duplicate key for a unique one, or
+ /// a NULL in any key column for a required one — a primary key or WITH DISALLOW NULL, which ACE refuses with
+ /// "Index or primary key cannot contain a Null value" (verified; an ADD COLUMN … PRIMARY KEY on a table that
+ /// already holds rows is the usual way to get there). Purely a read, touching nothing on disk, so it is safe
+ /// to call before the index exists. For uniqueness, rows with a null in any key column are exempt — Jet's
+ /// uniqueness is over the non-null keys only, so several rows may be null (verified vs ACE; see the
+ /// constraints page); a WITH IGNORE NULL index leaves them out of the B-tree altogether, so either way they
+ /// cannot collide.
///
///
/// Comparison is on the encoded key, not the raw values, which is deliberate: the encoding is what the
@@ -650,8 +756,8 @@ private int InsertIndex(TableDef table, string indexName, IReadOnlyList<(int Id,
/// uniqueness domain, and it keeps this agreeing with the insert- and update-time checks, which compare
/// the same way via .
///
- private void EnsureNoDuplicateKeys(
- TableDef table, string indexName, IReadOnlyList<(int Id, bool Ascending)> slots)
+ private void EnsureExistingRowsFitIndex(
+ TableDef 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))
@@ -660,8 +766,14 @@ private void EnsureNoDuplicateKeys(
var seen = new HashSet();
foreach (object?[] values in new Table(_channel, table).Rows())
{
- if (keyColumns.Any(k => values[k.Column.Index] is null)) continue;
- if (!seen.Add(Convert.ToHexString(IndexKeyEncoder.Encode(keyColumns, values))))
+ if (keyColumns.Any(k => values[k.Column.Index] is null))
+ {
+ if (required)
+ throw new InvalidOperationException(
+ $"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))))
throw new InvalidOperationException(
$"Cannot create unique index '{indexName}' on '{table.Name}': duplicate key values exist.");
}
@@ -676,15 +788,27 @@ private void EnsureNoDuplicateKeys(
/// 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
- /// is Access's uniqueness domain (see ).
+ /// 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
+ /// 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)
{
TableDef table = _catalog.FindTable(tableName)
?? throw new InvalidOperationException($"Table '{tableName}' was not found after adding the index.");
IndexDef index = table.Indexes.First(ix => string.Equals(ix.Name, indexName, StringComparison.OrdinalIgnoreCase));
- var keyColumnIds = index.Columns.Select(c => c.Column.Index).ToArray();
+ List<(byte[] Key, int Pointer, bool NullKey)> entries = IndexEntries(table, index, ignoreNulls);
+ new IndexWriter(_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)
+ {
+ 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())
{
@@ -692,8 +816,36 @@ private void BackfillIndex(string tableName, string indexName, bool ignoreNulls,
if (ignoreNulls && hasNullKey) continue;
entries.Add((IndexKeyEncoder.Encode(index.Columns, values), (id.Page << 8) | id.Row, hasNullKey));
}
+ return entries;
+ }
- new IndexWriter(_channel, table).BulkBuild(index, entries, validateUnique && index.IsUnique);
+ /// 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());
+
+ /// 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();
+ 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);
+ _channel.WritePage(table.DefinitionPage, tdef);
}
/// Appends one empty inline usage-map record (row ) to an existing
@@ -750,12 +902,17 @@ private void AppendEmptyUsageMapRow(JetFormatBase format, int pageNumber, int ne
public void AddForeignKey(string childTable, RelationshipSpec fk)
{
JetName.Validate(fk.Name, "foreign key name");
+ EnsureRelationshipNameFree(fk.Name);
TableDef 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.");
if (fk.UpdateSetNull) throw UpdateSetNullNotImplemented();
+ RejectOleIndexColumns(fk.Columns.Select(c => c.Column), n => child.FindColumn(n)?.Type);
+ fk = ResolvePrimaryKeyReference(fk, creatingTable: null);
+ EnsureSameDataTypes(fk, ColumnOf(child), ColumnOf(_catalog.FindTable(fk.ReferencedTable)));
+
byte upd = fk.CascadeUpdate ? CascadeAction : NoCascadeAction;
byte del = fk.CascadeDelete ? CascadeAction : fk.DeleteSetNull ? SetNullAction : NoCascadeAction;
var slots = ResolveSlots(child, fk.Columns.Select(c => (c.Column, Ascending: true)));
@@ -829,10 +986,17 @@ public bool DropConstraint(string childTable, string name)
WriteTdef(parent.DefinitionPage, parentParts);
}
- new PageAllocator(_channel).Free(fkIndex.RootPage);
+ new PageAllocator(_channel).Release(fkIndex.RootPage);
}
SoftDeleteRelationshipRows(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)
+ {
+ DeleteCatalogRows("MSysObjects", "Id", objectId);
+ DeleteCatalogRows("MSysACEs", "ObjectId", objectId);
+ }
_catalog.Invalidate();
return true;
}
@@ -840,8 +1004,9 @@ public bool DropConstraint(string childTable, string name)
///
/// Drops a table — DROP TABLE table. Removes the object's MSysObjects row and its
/// MSysACEs permission rows (soft-delete, as ACE does), and frees the table's pages back to the
- /// global free map so a later create reuses them (verified vs ACE): its index B-tree roots, its data
- /// pages (owned-pages usage map), and the TDEF page. Returns false if the table doesn't exist.
+ /// global free map when the database closes (verified vs ACE): every page of its indexes, its data and
+ /// long-value pages, the TDEF page and its continuation pages, a reference-form map's bitmap pages, and any
+ /// usage-map holder left with no live record. Returns false if the table doesn't exist.
///
/// A table that is the child (referencing) side of relationships can be dropped directly: ACE
/// lets you drop the referencing table while the parent stays, so each such relationship is removed first
@@ -849,9 +1014,6 @@ 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.
///
- /// Multi-page TDEFs, multi-level index trees (non-root pages), memo/OLE LVAL pages and dedicated
- /// usage-map pages are not yet freed (they leak until Compact); the catalog removal is complete regardless,
- /// so the table disappears and Access opens the file.
public bool DropTable(string tableName)
{
TableDef? table = _catalog.FindTable(tableName);
@@ -876,11 +1038,68 @@ public bool DropTable(string tableName)
int tdefPage = table.DefinitionPage;
var allocator = new PageAllocator(_channel);
+ var maps = new UsageMap(_channel, table);
+
+ // Collect before freeing anything: the pointers are read out of the TDEF, which this method frees.
+ var owned = new HashSet();
+ // 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();
+ definition.Read(_channel, tdefPage);
+
+ // The map RECORDS live as rows on owner-zero data pages, and each is retired in turn — its bits cleared
+ // first where ACE clears them, then its row reclaimed, which slides every record below it up the page.
+ // The order is ACE's and it shows on disk: a slide leaves a copy of the records it moved in the space
+ // they vacated, so retiring the same records in another order leaves different bytes behind. Measured
+ // by whole-file diff against ACE drops: each long-value column's owned then free map, bits cleared;
+ // then each index's owned map in index order, bits cleared; then the table's own owned map, bits
+ // cleared, and its free map, whose bits stay.
+ var retire = new List();
+
+ 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.Indexes.Where(i => i.RootPage > 0).GroupBy(i => i.RootPage).Select(g => g.First()))
- allocator.Free(index.RootPage);
- foreach (int dataPage in new UsageMap(_channel, table).DataPages())
- allocator.Free(dataPage);
- allocator.Free(tdefPage);
+ owned.Add(index.RootPage);
+ 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);
+
+ PageBuffer tdef = _channel.ReadPage(tdefPage);
+ (int Row, int Page) dataOwned = (tdef.ReadByte(_channel.Format.TdefOwnedPagesOffset),
+ tdef.ReadInt24(_channel.Format.TdefOwnedPagesOffset + 1));
+ retire.Add((dataOwned, [dataOwned], dataPages));
+ retire.Add(((tdef.ReadByte(_channel.Format.TdefFreePagesOffset),
+ tdef.ReadInt24(_channel.Format.TdefFreePagesOffset + 1)), [], []));
+
+ RetireMapRecords(retire, maps, owned);
+
+ // Access marks the released definition page itself: its type byte becomes 0x08 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.
+ byte[] released = _channel.ReadPage(tdefPage).Span.ToArray();
+ released[0] = (byte)PageType.ReleasedTableDefinition;
+ _channel.WritePage(tdefPage, released);
+
+ foreach (int page in owned)
+ allocator.Release(page); // reusable only after this handle closes, as ACE holds them
DeleteCatalogRows("MSysObjects", "Id", tdefPage);
DeleteCatalogRows("MSysACEs", "ObjectId", tdefPage);
@@ -888,6 +1107,84 @@ public bool DropTable(string tableName)
return true;
}
+ ///
+ /// Queues a long-value column's two usage-map records for , owned then free, and
+ /// adds the pages its owned map records to . A Memo/OLE (or calculated long) column owns
+ /// its LVAL pages through a PER-COLUMN usage map, whose pointer sits in the TDEF keyed by column id. Those pages
+ /// are not in the table's data-page map, so freeing only the data pages leaves every long value stranded — and
+ /// for a memo-heavy table that is nearly the whole table. Measured against ACE: dropping a 60-row memo table
+ /// returned 123 pages through ACE and 2 through LibRed, the missing 121 being LVAL pages. Nothing is queued for
+ /// a column with no long-value maps.
+ ///
+ private static void QueueLongValueMaps(
+ TableDefinitionPage 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);
+ if (map.Page != 0)
+ {
+ // Clear each page's bit on the way out, exactly as releasing a single value does: the record's
+ // bitmap bytes are zeroed before its row is retired. Except a page still in the free map, whose bit
+ // stays in both records — measured by whole-file diff of ACE drops: of an OLE column owning a chain,
+ // two full single-value pages and its current append page, every bit went but the append page's.
+ List pages = maps.PagesInMap(map.Row, map.Page).ToList();
+ owned.UnionWith(pages);
+ HashSet stillFree = columnFree.Page != 0 ? maps.PagesInMap(columnFree.Row, columnFree.Page).ToHashSet() : [];
+ retire.Add((map, columnFree.Page != 0 ? [map, columnFree] : [map], pages.Where(p => !stillFree.Contains(p)).ToList()));
+ }
+ if (columnFree.Page != 0) retire.Add((columnFree, [], []));
+ }
+
+ ///
+ /// 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
+ /// shared holder that is the whole fix: the page survives and must not keep records for a map that no longer
+ /// exists. Each record's Clear maps first have its Pages cleared. Adds to
+ /// the pages to release: a reference-form record's bitmap pages, and every holder left with no live row.
+ ///
+ private void RetireMapRecords(IEnumerable retire, UsageMap maps, HashSet owned)
+ {
+ var usageMaps = new UsageMapWriter(_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)
+ {
+ if (map.Page <= 1 || map.Page >= _channel.PageCount || !retired.Add(map)) continue;
+ foreach ((int Row, int Page) cleared in clear)
+ 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);
+ }
+
+ byte[] holderBytes = _channel.ReadPage(map.Page).Span.ToArray();
+ RowInserter.ReclaimRow(_channel.Format, holderBytes, map.Row);
+ _channel.WritePage(map.Page, holderBytes);
+ if (!holders.Contains(map.Page)) holders.Add(map.Page);
+ }
+
+ // ACE frees a holder once the dropped records were the only thing on it — measured: for a one-memo-column
+ // table ACE returned the long-value map's holder. A holder can carry records for several columns or tables
+ // as separate rows, so releasing one that still serves another map would hand away a live page: corruption
+ // rather than a leak. Hence the holder goes only when no live row is left on it.
+ foreach (int holderPage in holders)
+ {
+ var holder = new DataPage();
+ holder.Read(_channel.ReadPage(holderPage), _channel.Format);
+ bool live = false;
+ for (int row = 0; row < holder.RowCount && !live; row++)
+ live = !holder.Rows[row].IsDeleted && holder.Rows[row].Length > 0;
+ if (!live) owned.Add(holderPage);
+ }
+ }
+
///
/// 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
@@ -897,6 +1194,19 @@ public bool DropTable(string tableName)
/// 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;
+
+ DeleteCatalogRows("MSysObjects", "Id", objId);
+ DeleteCatalogRows("MSysQueries", "ObjectId", objId);
+ DeleteCatalogRows("MSysACEs", "ObjectId", objId);
+ _catalog.Invalidate();
+ 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.FindTable("MSysObjects")
?? throw new InvalidOperationException("MSysObjects catalog table was not found.");
@@ -904,18 +1214,11 @@ public bool DropQueryObject(string name)
int nameIdx = (mo.FindColumn("Name") ?? throw new InvalidOperationException("MSysObjects is missing 'Name'.")).Index;
int typeIdx = (mo.FindColumn("Type") ?? throw new InvalidOperationException("MSysObjects is missing 'Type'.")).Index;
- int? objId = null;
foreach (object?[] values in new Table(_channel, mo).Rows())
if (string.Equals(values[nameIdx] as string, name, StringComparison.OrdinalIgnoreCase)
- && Convert.ToInt16(values[typeIdx] ?? (short)0) == StoredQueryFormat.ObjectTypeQuery)
- { objId = Convert.ToInt32(values[idIdx]); break; }
- if (objId is null) return false;
-
- DeleteCatalogRows("MSysObjects", "Id", objId.Value);
- DeleteCatalogRows("MSysQueries", "ObjectId", objId.Value);
- DeleteCatalogRows("MSysACEs", "ObjectId", objId.Value);
- _catalog.Invalidate();
- return true;
+ && Convert.ToInt16(values[typeIdx] ?? (short)0) == type)
+ return Convert.ToInt32(values[idIdx]);
+ return null;
}
@@ -1256,7 +1559,7 @@ public bool DropIndex(string tableName, string indexName)
RemoveTdefBlocks(parts, removeDataOrdinal: index.RealIndexOrdinal,
removeLogical: b => NameOf(b.Name).Equals(indexName, StringComparison.OrdinalIgnoreCase));
WriteTdef(table.DefinitionPage, parts);
- new PageAllocator(_channel).Free(index.RootPage);
+ new PageAllocator(_channel).Release(index.RootPage);
_catalog.Invalidate();
return true;
}
@@ -1442,9 +1745,44 @@ public bool AddColumn(string tableName, ColumnSpec spec, string? defaultValue =
if (props.Count > 0) SetColumnProperties(table.DefinitionPage, spec.Name, props);
_catalog.Invalidate();
+ if (spec.IsAutoNumber) NumberExistingRows(tableName, spec);
return true;
}
+ ///
+ /// 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).
+ ///
+ ///
+ /// The existing rows are numbered 1, 2, 3 … in table order whatever the column's seed and increment. A counter
+ /// with the default seed 1 and increment 1 — however it was spelled — then continues after them: two rows take
+ /// 1 and 2, the next insert 3. Any other counter restarts at its own seed, even where that repeats a value the
+ /// rows were given: COUNTER(2, 1) over two rows goes on 2, 3, 4, and COUNTER(1, 5) 1, 6, 11. A
+ /// primary key over the new column has a value in every row either way.
+ ///
+ private void NumberExistingRows(string tableName, ColumnSpec spec)
+ {
+ TableDef table = _catalog.FindTable(tableName)
+ ?? throw new InvalidOperationException($"Table '{tableName}' was not found after adding column '{spec.Name}'.");
+ ColumnDef column = table.FindColumn(spec.Name)!;
+ int increment = spec.Increment == 0 ? 1 : spec.Increment;
+
+ var rows = new Table(_channel, table).Rows().WithIds().ToList();
+ if (rows.Count > 0)
+ {
+ var writer = new RowInserter(_channel, table);
+ var changed = new HashSet { column.Index };
+ int number = 0;
+ foreach ((RowId id, object?[] values) in rows)
+ {
+ values[column.Index] = ++number;
+ writer.Update(id, values, changed);
+ }
+ }
+
+ 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.
@@ -1465,6 +1803,22 @@ private static void AddLongValueMapEntry(TdefParts parts, int columnId, int used
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
/// column, adds the new one, and rewrites the blob (preserving every other property).
@@ -1927,6 +2281,12 @@ private void RewriteColumn(string tableName, string columnName, ColumnSpec newCo
// 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
@@ -1937,7 +2297,8 @@ private void RewriteColumn(string tableName, string columnName, ColumnSpec newCo
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)).ToList();
+ 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();
@@ -1954,7 +2315,7 @@ private void RewriteColumn(string tableName, string columnName, ColumnSpec newCo
// 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);
+ 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
@@ -1982,7 +2343,17 @@ private void RewriteColumn(string tableName, string columnName, ColumnSpec newCo
_catalog.Invalidate();
var dest = new Table(_channel, _catalog.FindTable(tableName)!);
- foreach (object?[] row in rows) dest.Insert(row);
+ 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
@@ -2013,6 +2384,22 @@ private void RewriteColumn(string tableName, string columnName, ColumnSpec newCo
_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)
@@ -2023,30 +2410,6 @@ private void RewriteColumn(string tableName, string columnName, ColumnSpec newCo
}
}
- /// The TDEF-page step of the in-place column type change: bump the 0x29 high-water and rewrite
- /// ONLY the target descriptor (type, burned id, fixed-offset appended to the end of the fixed region with the
- /// old slot left dead, length), leaving the TDEF page number and every other descriptor byte-identical to ACE.
- /// This alone is not a self-consistent change — wraps it with the row
- /// re-lay and index rebuild; this entry point exists so a byte-diff test can isolate the TDEF page.
- public void AlterColumnTypeInPlaceTdef(string tableName, string columnName, ColumnSpec newSpec, int? fixedEndOverride = null)
- {
- 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);
- JetFormatBase format = _channel.Format;
-
- TdefParts parts = ParseTdef(def.DefinitionPage);
- // The fixed-region end must include dead slots, so callers with rows pass the row-derived length.
- int fixedEnd = fixedEndOverride ?? def.Columns.Where(c => c.IsFixedLength && c.Type != JetDataType.Boolean)
- .Select(c => c.FixedOffset + c.Length).DefaultIfEmpty(0).Max();
-
- EditTargetDescriptor(parts, target, newSpec, fixedEnd, _collation, format);
- WriteTdef(def.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
@@ -2089,9 +2452,9 @@ private static int EditTargetDescriptor(TdefParts parts, ColumnDef target, Colum
return maxCols;
}
- /// Full in-place column type change, byte-for-byte like ACE (currently: an all-fixed, non-boolean
- /// table whose target stays fixed and is not indexed; falls back to otherwise).
- /// Edits the TDEF in place () and re-lays every row — the target's
+ /// 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
/// 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)
@@ -2101,6 +2464,8 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp
ColumnDef oldTarget = oldDef.FindColumn(columnName)
?? throw new InvalidOperationException($"Column '{columnName}' does not exist in '{tableName}'.");
EnsureColumnIsNotInRelationship(oldDef, oldTarget);
+ if (newSpec.Type == JetDataType.Ole && oldDef.Indexes.Any(i => i.Columns.Any(c => c.Column.ColumnId == oldTarget.ColumnId)))
+ RejectOleIndexColumns([oldTarget.Name], _ => JetDataType.Ole);
// 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.
@@ -2127,7 +2492,8 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp
var rows = new Table(_channel, oldDef).Rows().WithIds()
.Select(r => (r.Id, Raw: reader.ReadRow(r.Id), Values: (object?[])r.Values.Clone()))
.ToList();
- foreach (var r in rows) r.Values[oldTarget.Index] = ConvertValue(r.Values[oldTarget.Index], newSpec.Type);
+ foreach (var r in rows)
+ r.Values[oldTarget.Index] = ConvertValue(r.Values[oldTarget.Index], newSpec.Type, newSpec.Name);
// The fixed-region length is authoritative from the existing rows (their var-data-start), NOT the live
// column descriptors — those diverge once a high-offset column has been retyped to variable and left a
@@ -2202,7 +2568,7 @@ public void AlterColumnTypeInPlace(string tableName, string columnName, ColumnSp
foreach (var p in pending)
{
BackfillIndex(tableName, p.Name, p.IgnoreNulls, validateUnique: true);
- _allocator.Free(p.OldRoot);
+ _allocator.Release(p.OldRoot);
}
if (ownTx) _channel.CommitTransaction();
@@ -2291,7 +2657,7 @@ private int PrepareIndexRebuild(TdefParts parts, TableDef table, IndexDef index,
int newRow = RecycleOwnedMapRow(format, usageMapPage, oldUsageRow, newRoot);
// Re-point the index-data block: the target's burned id in its column slot, the new root, the new
- // usage-map row; bump the stats block (+0x00, observed 0→1 on ACE's rebuild).
+ // 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++)
{
int at = IndexBlockFormat.ColumnsOffset + slot * IndexBlockFormat.ColumnSlotSize;
@@ -2300,48 +2666,120 @@ private int PrepareIndexRebuild(TdefParts parts, TableDef table, IndexDef index,
}
block[IndexBlockFormat.UsageMapRowOffset] = (byte)newRow;
BinaryPrimitives.WriteInt32LittleEndian(block.Slice(IndexBlockFormat.RootPageOffset, 4), newRoot);
- Span stats = parts.Stats[index.RealIndexOrdinal];
- BinaryPrimitives.WriteInt32LittleEndian(stats, BinaryPrimitives.ReadInt32LittleEndian(stats) + 1);
return newRoot;
}
- /// Recycles an index's owned-pages usage-map row exactly the way ACE does on a rebuild: append a
- /// fresh row and set the new root's bit (ACE's first write, at the appended slot), then MOVE that map into
- /// the old row's freed slot and soft-delete the old row (a 0-length deleted+overflow tombstone) — leaving
- /// the appended slot's bytes stale in free space, byte-for-byte as ACE does. Returns the new row number.
+ ///
+ /// Recycles an index's owned-pages usage-map row the way ACE does on a rebuild, in the two writes whose
+ /// combined result is observable on disk: (1) append a fresh row at the bottom of the holder page
+ /// and set the new root's bit — those bytes are then abandoned and stay as a stale copy; (2) lay
+ /// the page out again with the old row's record reclaimed: its slot becomes a 0-length
+ /// deleted+overflow tombstone at the preceding record's offset, every later row keeps its number while its
+ /// record slides up, and the fresh map takes the position freed at the end of the live region under the
+ /// appended row number. Returns that number — the only pointer the caller re-points, because no other
+ /// row's number changes and each one's data travels with it.
+ ///
+ ///
+ /// Both halves are load-bearing and each was missed once. The stale copy decides a whole-file byte diff
+ /// against ACE on a single byte (the new root's bit, at offset 49 of the abandoned record) and is
+ /// invisible to free-space accounting, since it lies below the lowest live record inside the region free
+ /// space already covers. The compaction is invisible to slot offsets alone and shows up only when records
+ /// are identified by content — ACE's own page, before → after, with a long-value column's maps below the
+ /// index's:
+ ///
+ /// before row2 @3889 pages=[353] row3 @3820 pages=[] row4 @3751 pages=[]
+ /// after row2 @3958 TOMBSTONE row3 @3889 pages=[] row4 @3820 pages=[] row5 @3751 pages=[355]
+ /// stale: @3731 = 0x08 (the abandoned append, at 3682)
+ ///
+ /// The long-value maps slid up a record width and kept rows 3 and 4; only the index's pointer moved, to
+ /// the appended row 5. Writing step (1)'s record into the old row's slot instead — which produces the same
+ /// bytes whenever the recycled row happens to be the LAST one, the only case an ACE-built schema gives —
+ /// points a slot back up the page as soon as it is not, and no reader can walk that: a row's extent runs
+ /// to where the previous slot begins. AlterColumnTypeInPlace scans the table through this map
+ /// immediately afterwards, so the ALTER failed outright on any table that had gained a Memo or OLE
+ /// column after its index.
+ ///
private int RecycleOwnedMapRow(JetFormatBase format, int usageMapPage, int oldRow, int newRoot)
{
- const int MapLength = 1 + 4 + 64;
int dir = format.DataRowDirectoryOffset;
- int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(_channel.ReadPage(usageMapPage).Span.Slice(format.DataRowCountOffset, 2));
+ int rowCount = BinaryPrimitives.ReadUInt16LittleEndian(
+ _channel.ReadPage(usageMapPage).Span.Slice(format.DataRowCountOffset, 2));
int newRow = rowCount;
+ if (oldRow < 0 || oldRow >= rowCount)
+ throw new InvalidDataException(
+ $"Usage-map row {usageMapPage}:{oldRow} does not exist; the page has {rowCount} rows.");
- // ACE's first write: append a fresh row and set the new root's bit (this copy is later left stale).
+ // (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);
- // Then move that map into the old row's freed slot and turn the old row into a tombstone; the appended
- // slot's bytes are left in place (stale, in free space) — matching ACE's leftover.
+ // (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.
byte[] page = _channel.ReadPage(usageMapPage).Span.ToArray();
- int freshOffset = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(dir + newRow * 2, 2)) & RowPointer.OffsetMask;
- int oldOffset = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(dir + oldRow * 2, 2)) & RowPointer.OffsetMask;
- int aboveOffset = BinaryPrimitives.ReadUInt16LittleEndian(page.AsSpan(dir + (oldRow - 1) * 2, 2)) & RowPointer.OffsetMask;
-
- Array.Copy(page, freshOffset, page, oldOffset, MapLength); // move the map into the old slot
- BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(dir + newRow * 2, 2), (ushort)oldOffset);
- BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(dir + oldRow * 2, 2),
- (ushort)(aboveOffset | RowPointer.DeletedFlag | RowPointer.OverflowFlag)); // old row → 0-length tombstone
+ var holder = new DataPage();
+ holder.Read(_channel.ReadPage(usageMapPage), format);
+
+ var records = new byte[rowCount + 1][];
+ var flags = new ushort[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));
+ }
+
+ 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)(oldOffset - (dir + (rowCount + 1) * 2)));
+ (ushort)(offset - directoryEnd));
_channel.WritePage(usageMapPage, page);
return newRow;
}
/// Converts a stored value to the CLR type for a new column type (ALTER COLUMN). NULL stays NULL;
/// an unconvertible value throws (as ACE's rewrite would).
- private static object? ConvertValue(object? value, JetDataType type)
+ /// Throwing is the intent; the type has to be actionable. The bare Convert.To* calls leaked
+ /// //, none
+ /// naming the column and none distinguishable from a bug in the rewrite.
+ private static object? ConvertValue(object? value, JetDataType type, string columnName)
{
if (value is null) return null;
+ try
+ {
+ return ConvertCore(value, type);
+ }
+ catch (Exception ex) when (ex is InvalidCastException or FormatException or OverflowException
+ or ArgumentException)
+ {
+ throw new InvalidOperationException(
+ $"Column '{columnName}' cannot be changed to {type}: the existing value "
+ + $"'{Describe(value)}' ({value.GetType().Name}) cannot be converted to it.", ex);
+ }
+ }
+
+ /// Bounded rendering for an error message, so a memo does not paste thousands of characters into
+ /// one.
+ private static string Describe(object value) => value switch
+ {
+ byte[] bytes => $"{bytes.Length} bytes",
+ string { Length: > 40 } text => $"{text[..40]}…",
+ _ => value.ToString() ?? "",
+ };
+
+ private static object? ConvertCore(object value, JetDataType type)
+ {
var inv = System.Globalization.CultureInfo.InvariantCulture;
return type switch
{
@@ -2352,7 +2790,7 @@ private int RecycleOwnedMapRow(JetFormatBase format, int usageMapPage, int oldRo
JetDataType.Int64 => Convert.ToInt64(value, inv),
JetDataType.Single => Convert.ToSingle(value, inv),
JetDataType.Double => Convert.ToDouble(value, inv),
- JetDataType.Currency or JetDataType.FixedPoint => Convert.ToDecimal(value, inv),
+ JetDataType.Currency or JetDataType.FixedPoint => JetDecimalConverter.ToDecimal(value, inv),
JetDataType.DateTime => value is DateTime d ? d : Convert.ToDateTime(value, inv),
JetDataType.Text or JetDataType.Memo => Convert.ToString(value, inv),
JetDataType.Guid => value is Guid g ? g : Guid.Parse(value.ToString()!),
@@ -2386,8 +2824,11 @@ private static void AppendColumnToParts(TdefParts parts, int colCount, byte[] de
/// 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
/// bytes). Returns false if the column doesn't exist. Multi-page TDEFs are handled. Throws for a column
- /// that backs an index/key (drop that first) or a memo/OLE column (its long-value usage-map entry/pages
- /// aren't handled yet).
+ /// that backs an index/key (drop that first).
+ /// A memo/OLE column also owns long-value pages through its own usage maps, and ACE retires those as
+ /// DROP TABLE does (measured by whole-file diff): its §3.3.2 entry leaves the TDEF, its owned and free map
+ /// records are retired from their holder, and its owned pages go back to the global free map at close. The
+ /// pages themselves are left as they were, and the other long-value columns keep their entries and records.
///
public bool DropColumn(string tableName, string columnName)
{
@@ -2423,13 +2864,24 @@ public bool DropColumn(string tableName, string columnName)
throw new InvalidOperationException(
$"Cannot drop column '{columnName}': it is part of an index or key — drop the index/constraint first.");
- if (col.Type is JetDataType.Memo or JetDataType.Ole)
- throw new NotSupportedException(
- $"DROP COLUMN '{columnName}': dropping a memo/OLE (long-value) column is not supported yet.");
+ // A long-value column's maps, read from the TDEF before it loses them.
+ var definition = new TableDefinitionPage();
+ 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);
WriteTdef(table.DefinitionPage, parts);
+
+ RetireMapRecords(retire, maps, owned);
+ var allocator = new PageAllocator(_channel);
+ 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)
_catalog.Invalidate();
return true;
@@ -2619,9 +3071,9 @@ private void WriteTdef(int tdefPage, TdefParts parts)
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 (reusing the existing ones) — handles a
+ // 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);
+ WriteDefinition(tdefPage, def, parts.Continuations, rewrite: true);
}
/// Marks a row deleted by setting the deleted flag (0x8000) on its slot-directory entry — a
@@ -2696,62 +3148,68 @@ private static byte[] BuildOutgoingInfoBlock(int number, int dataOrdinal, byte f
/// 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.
- /// Existing continuation pages are reused before allocating new ones.
+ /// 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 reusePages)
+ 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)
{
- var only = new byte[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;
}
- // Plan the continuation chunks: each holds up to (ps - header) data; the last also leaves the reserve.
- // Each continuation carries PageSize - 8 bytes of definition; the LAST one also leaves the 8-byte
- // trailing reserve, so it can hold only PageSize - 16. A chunk sized for a middle page that then turns
- // out to be the last leaves free space of -8, written as 0xFFF8 — an 8-byte window at every page
- // boundary, reachable by ADD COLUMN growing a definition a few bytes at a time. When the remainder
- // lands in that window, stop short and let a small tail chunk take the rest.
+ // 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 maxLast = maxMiddle - JetFormatBase.TdefContinuationHeaderSize;
- var chunks = new List<(int Offset, int Length)>();
- for (int offset = ps; offset < def.Length;)
+ int stored = def.Length + JetFormatBase.TdefContinuationHeaderSize;
+ var chunks = new List<(int Offset, int Length, int Free)>();
+ for (int consumed = ps; consumed < stored;)
{
- int remaining = def.Length - offset;
- int length = remaining <= maxLast ? remaining
- : remaining > maxMiddle ? maxMiddle
- : maxLast;
- chunks.Add((offset, length));
- offset += length;
+ int placed = Math.Min(maxMiddle, stored - consumed);
+ chunks.Add((consumed, Math.Clamp(def.Length - consumed, 0, placed), maxMiddle - placed));
+ consumed += placed;
}
- int reuse = 0;
- int[] pageNumbers = chunks.Select(_ => reuse < reusePages.Count ? reusePages[reuse++] : _allocator.Allocate()).ToArray();
+ // 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, ps); // page 1 is completely full in a multi-page definition
+ 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) = chunks[i];
+ var (offset, length, free) = chunks[i];
var page = new byte[ps];
page[0] = (byte)PageType.TableDefinition;
page[1] = 0x01;
- Array.Copy(def, offset, page, JetFormatBase.TdefContinuationHeaderSize, length);
+ 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);
- int free = next != 0 ? 0 : ps - JetFormatBase.TdefContinuationHeaderSize - length - JetFormatBase.TdefContinuationHeaderSize;
BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefFreeSpaceOffset, 2), (ushort)free);
_channel.WritePage(pageNumbers[i], page);
}
@@ -2803,15 +3261,13 @@ 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). Single
- /// definition page only (throws if the definition spans continuation pages).
+ /// 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.
///
private void AddIncomingRelationshipBlock(IncomingRelationship inc)
{
JetFormatBase format = _channel.Format;
- var buf = _channel.ReadPage(inc.ParentPage);
- if (buf.ReadInt32(format.TdefNextPageOffset) != 0)
- throw new NotSupportedException("Adding a relationship to a multi-page table definition is not supported yet.");
+ (LibRed.IO.PageBuffer buf, IReadOnlyList existingContinuations) = ReadDefinition(inc.ParentPage);
int dataCount = buf.ReadInt32(format.TdefIndexCountOffset); // 0x33 real data blocks
int logicalCount = buf.ReadInt32(format.TdefLogicalIndexCountOffset); // 0x2F logical blocks
@@ -2853,20 +3309,17 @@ private void AddIncomingRelationshipBlock(IncomingRelationship inc)
nameBytes.Insert(k, EncodeName(newName));
int newDefEnd = infoStart + blocks.Count * IndexBlockFormat.InfoBlockSize + nameBytes.Sum(n => n.Length) + lvalRegion.Length;
- if (newDefEnd > format.PageSize - JetFormatBase.TdefContinuationHeaderSize)
- throw new NotSupportedException("No room in the table definition for another relationship (needs a continuation page).");
- var page = buf.Span.ToArray();
+ var def = new byte[newDefEnd];
+ buf.Span[..infoStart].CopyTo(def);
int w = infoStart;
- foreach (byte[] b in blocks) { b.CopyTo(page.AsSpan(w)); w += b.Length; }
- foreach (byte[] n in nameBytes) { n.CopyTo(page.AsSpan(w)); w += n.Length; }
- lvalRegion.CopyTo(page.AsSpan(w));
-
- BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefLogicalIndexCountOffset, 4), logicalCount + 1);
- BinaryPrimitives.WriteInt32LittleEndian(page.AsSpan(format.TdefLengthOffset, 4), newDefEnd);
- BinaryPrimitives.WriteUInt16LittleEndian(page.AsSpan(format.TdefFreeSpaceOffset, 2),
- (ushort)(format.PageSize - newDefEnd - JetFormatBase.TdefContinuationHeaderSize));
- _channel.WritePage(inc.ParentPage, page);
+ 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);
}
private byte[] BuildIncomingInfoBlock(IncomingRelationship inc)
diff --git a/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs b/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs
index f9f9467c8..52b14d888 100644
--- a/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs
+++ b/src/LibRed/LibRed.Core/Storage/Types/JetTypeCodec.cs
@@ -1,5 +1,6 @@
using System.Buffers.Binary;
using System.Text;
+using EntityFrameworkCore.Jet.Data;
using LibRed.Catalog;
using LibRed.Formats;
@@ -256,7 +257,7 @@ 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(Convert.ToDecimal(value, c) * 10000m)));
+ return Bytes(8, b => BinaryPrimitives.WriteInt64LittleEndian(b, (long)decimal.Round(JetDecimalConverter.ToDecimal(value, c) * 10000m)));
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
@@ -272,9 +273,9 @@ string s when Guid.TryParse(s, out Guid parsed) => parsed,
case JetDataType.Text:
return EncodeText(column, AsText(value, c));
case JetDataType.Binary:
- return EncodeBinary(column, AsBinary(value));
+ return EncodeBinary(column, AsBinary(column, value));
case JetDataType.FixedPoint:
- return EncodeNumeric(Convert.ToDecimal(value, c), column.Scale);
+ 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
@@ -289,7 +290,7 @@ string s when Guid.TryParse(s, out Guid parsed) => parsed,
TryCompressText(column, memo, requireCapableFlag: false) ?? Encoding.Unicode.GetBytes(memo));
}
case JetDataType.Ole:
- return EncodeInlineLongValue(AsBinary(value));
+ return EncodeInlineLongValue(AsBinary(column, value));
default:
throw new NotSupportedException($"Encoding {column.Type} is not supported yet.");
@@ -407,6 +408,15 @@ private static byte[] EncodeBinary(ColumnDef column, byte[] value)
return padded;
}
+ /// 10^, for exponents up to
+ /// (10^28 is inside decimal's range; 10^29 is not).
+ private static decimal Pow10(byte exponent)
+ {
+ decimal result = 1m;
+ for (int i = 0; i < exponent; i++) result *= 10m;
+ return result;
+ }
+
private static byte[] Bytes(int length, Action> write)
{
var b = new byte[length];
@@ -427,11 +437,16 @@ private static byte[] Bytes(int length, Action> write)
/// literal: Access has no digitless 0x (it rejects it), which is why
/// JetByteArrayTypeMapping emits '' for an empty array.
///
- private static byte[] AsBinary(object value) => value switch
+ /// The fallback names the column rather than hard-casting: a cast raised
+ /// , which names nothing and is indistinguishable from a bug in the
+ /// writer. Same fix as the GUID case above.
+ private static byte[] AsBinary(ColumnDef column, object value) => value switch
{
byte[] bytes => bytes,
string text => Encoding.Unicode.GetBytes(text),
- _ => (byte[])value,
+ _ => throw new NotSupportedException(
+ $"Cannot store {value.GetType().Name} in {column.Type} column '{column.Name}': a binary column "
+ + "takes a byte array, or a string (stored as its UTF-16LE bytes)."),
};
/// The OLE-automation epoch (1899-12-30), which is also Jet's zero date and the base for
@@ -469,11 +484,32 @@ internal static byte[] EncodeInlineLongValue(ReadOnlySpan payload)
}
/// Inverse of : 17 bytes, sign + 128-bit magnitude (top word 0).
- private static byte[] EncodeNumeric(decimal value, byte scale)
+ /// The largest precision Jet/ACE accepts on a NUMERIC/DECIMAL column.
+ internal const byte MaxNumericPrecision = 28;
+
+ private static byte[] EncodeNumeric(ColumnDef column, decimal value)
{
+ byte scale = column.Scale;
+
+ // ACE refuses a value wider than the declared precision, on every write path including an ALTER that
+ // narrows the column. The 17-byte payload cannot enforce it — a 20-digit value fits DECIMAL(18,4) as
+ // comfortably as a 2-digit one. Tested before scaling, which also stops an out-of-range input
+ // overflowing the multiply below and reporting OverflowException instead of this.
+ if (column.Precision is >= 1 and <= MaxNumericPrecision && scale <= column.Precision
+ && Math.Abs(value) >= Pow10((byte)(column.Precision - scale)))
+ throw new InvalidOperationException(
+ $"Value {value} does not fit column '{column.Name}', declared "
+ + $"DECIMAL({column.Precision},{column.Scale}): it holds at most {column.Precision - scale} "
+ + $"digits before the decimal point. Access refuses such a value rather than storing it.");
+
decimal factor = 1m;
for (int i = 0; i < scale; i++) factor *= 10m;
- decimal magnitude = decimal.Truncate(decimal.Round(Math.Abs(value) * factor, 0));
+
+ // 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.
+ 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];
diff --git a/src/LibRed/LibRed.Core/Storage/UsageMap.cs b/src/LibRed/LibRed.Core/Storage/UsageMap.cs
index 19190ce10..adb0a40b8 100644
--- a/src/LibRed/LibRed.Core/Storage/UsageMap.cs
+++ b/src/LibRed/LibRed.Core/Storage/UsageMap.cs
@@ -37,6 +37,38 @@ 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 0x05) 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.
///
/// Scans the bitmap backwards rather than enumerating and taking the maximum:
@@ -106,11 +138,14 @@ private byte[] ReadMapRecord(int pointerOffset)
/// the TDEF. Both maps share the same pointer shape and record format.
private IEnumerable PagesAt(int pointerOffset)
{
- JetFormatBase format = _channel.Format;
-
PageBuffer tdef = _channel.ReadPage(_table.DefinitionPage);
- int mapRow = tdef.ReadByte(pointerOffset);
- int mapPage = tdef.ReadInt24(pointerOffset + 1);
+ return ReadMapAt(tdef.ReadByte(pointerOffset), tdef.ReadInt24(pointerOffset + 1));
+ }
+
+ /// 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)
+ {
// 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.
@@ -119,7 +154,9 @@ private IEnumerable PagesAt(int pointerOffset)
$"Usage-map pointer names page {mapPage}, outside the file's 2..{_channel.PageCount - 1} range.");
var holder = new DataPage();
- holder.Read(_channel.ReadPage(mapPage), format);
+ 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)
@@ -190,10 +227,10 @@ private ReadOnlySpan ReadBitmapPage(int pageNumber)
/// in RowInserter already range-checks; this is the same check.
private void AppendSetBits(List pages, ReadOnlySpan bitmap, int basePage)
{
- // Read the bound ONCE. Outside a transaction PageChannel.PageCount is a file-length syscall (inside one
- // it is a cached field), and this loop runs per set bit on every insert — so testing it per bit made a
- // non-transactional insert ~1.9x slower while leaving the transactional path untouched, which is what
- // made the cost so easy to miss. Nothing in the loop writes, so the count cannot move under it.
+ // Read the bound ONCE: this loop runs per set bit on every insert. PageChannel.PageCount used to be a
+ // file-length syscall outside a transaction, which made a per-bit test cost a non-transactional insert
+ // ~1.9x; it is a cached field now, but one read is still all the loop needs. Nothing in the loop writes,
+ // so the count cannot move under it.
int pageCount = _channel.PageCount;
for (int i = 0; i < bitmap.Length; i++)
diff --git a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs
index afa72a386..be6b09454 100644
--- a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs
+++ b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs
@@ -25,13 +25,17 @@ public sealed class ViewCreator(PageChannel channel, JetCatalog catalog)
private const int QueryOwnerAcm = 0xF00FE; // 983294
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;
public void Create(string name, ViewSpec spec)
{
- int objectId = AllocateObject(name, ViewFlags);
+ int objectId = AllocateQueryObject(name, ViewFlags);
AddQueryRows(objectId, spec);
}
@@ -39,45 +43,66 @@ public void Create(string name, ViewSpec spec)
public void CreateAction(string name, ActionQuerySpec spec)
{
int flags = spec.Kind == ActionQueryKind.DataDefinition ? DataDefinitionFlags : AppendFlags;
- int objectId = AllocateObject(name, flags);
+ int objectId = AllocateQueryObject(name, flags);
AddActionRows(objectId, spec);
}
- /// Reserves the next free query object id, checks the name is unique, and writes the MSysObjects
- /// row with the given (which distinguish view / append / data-definition).
- private int AllocateObject(string name, int flags)
+ ///
+ /// 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,
+ RelationshipOwnerAcm, RelationshipAdminAcm);
+
+ private int AllocateQueryObject(string name, int flags) =>
+ AllocateObject(name, StoredQueryFormat.ObjectTypeQuery, CatalogFormat.ObjectContainerParentId, flags,
+ QueryOwnerAcm, QueryAdminAcm);
+
+ /// 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)
{
TableDef msysObjects = _catalog.FindTable("MSysObjects")
?? throw new InvalidOperationException("MSysObjects catalog table was not found.");
int idIndex = ColumnIndex(msysObjects, "Id");
int nameIndex = ColumnIndex(msysObjects, "Name");
+ int parentIndex = ColumnIndex(msysObjects, "ParentId");
- // A query's name must be unique among all objects (it also cannot equal an existing table name);
- // find the next free negative id (queries increment from 0x80000000) in one scan.
+ // 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.
+ bool relationship = parentId == CatalogFormat.RelationshipContainerParentId;
int nextId = unchecked((int)0x80000000);
foreach (object?[] row in new Table(_channel, msysObjects).Rows())
{
- if (string.Equals(row[nameIndex] as string, name, StringComparison.OrdinalIgnoreCase))
- throw new SchemaObjectExistsException($"An object named '{name}' already exists.", name);
+ if ((!relationship || 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);
if (row[idIndex] is int id && id < 0 && id >= nextId) nextId = id + 1;
}
- AddObjectRow(msysObjects, name, nextId, flags);
- AddPermissionRows(nextId);
+ AddObjectRow(msysObjects, name, nextId, type, parentId, flags);
+ AddPermissionRows(nextId, ownerAcm, adminAcm);
return nextId;
}
///
- /// Adds the two MSysACEs permission rows Access writes for a new query/view object — owner (0x690C) at
- /// ACM 0xF00FE and admin/users (0x680C) at ACM 0xFFEFF — maintaining the ObjectId index so Access's
- /// security check finds them. Without these Access warns about permissions when opening the query.
+ /// Adds the two MSysACEs permission rows Access writes for a new query/view or relationship object — owner
+ /// (0x690C) and admin/users (0x680C) — maintaining the ObjectId index so Access's security check finds them.
+ /// Without these Access warns about permissions when opening a query.
///
- private void AddPermissionRows(int objectId)
+ private void AddPermissionRows(int objectId, int ownerAcm, int adminAcm)
{
TableDef msysAces = _catalog.FindTable("MSysACEs")
?? throw new InvalidOperationException("MSysACEs catalog table was not found.");
- foreach ((byte[] sid, int acm) in new[] { (DefaultOwner, QueryOwnerAcm), (AdminSid, QueryAdminAcm) })
+ foreach ((byte[] sid, int acm) in new[] { (DefaultOwner, ownerAcm), (AdminSid, adminAcm) })
{
var values = new object?[msysAces.Columns.Count];
SetByName(msysAces, values, "ACM", acm);
@@ -88,13 +113,13 @@ private void AddPermissionRows(int objectId)
}
}
- private void AddObjectRow(TableDef msysObjects, string name, int objectId, int flags)
+ private void AddObjectRow(TableDef msysObjects, string name, int objectId, short type, int parentId, int flags)
{
DateTime now = DateTime.Now;
var values = new object?[msysObjects.Columns.Count];
SetByName(msysObjects, values, "Id", objectId);
- SetByName(msysObjects, values, "ParentId", CatalogFormat.ObjectContainerParentId);
- SetByName(msysObjects, values, "Type", StoredQueryFormat.ObjectTypeQuery);
+ SetByName(msysObjects, values, "ParentId", parentId);
+ SetByName(msysObjects, values, "Type", type);
SetByName(msysObjects, values, "Name", name);
SetByName(msysObjects, values, "Flags", flags);
SetByName(msysObjects, values, "Owner", DefaultOwner);
diff --git a/src/LibRed/LibRed.EFCore/Extensions/LibRedServiceCollectionExtensions.cs b/src/LibRed/LibRed.EFCore/Extensions/LibRedServiceCollectionExtensions.cs
index 8744b6e37..c84eb3a26 100644
--- a/src/LibRed/LibRed.EFCore/Extensions/LibRedServiceCollectionExtensions.cs
+++ b/src/LibRed/LibRed.EFCore/Extensions/LibRedServiceCollectionExtensions.cs
@@ -51,11 +51,11 @@ public static IServiceCollection AddEntityFrameworkLibRed(this IServiceCollectio
.TryAdd()
.TryAdd(p => p.GetRequiredService())
.TryAdd()
- .TryAdd()
+ .TryAdd()
.TryAdd()
.TryAdd()
.TryAdd()
- .TryAdd()
+ .TryAdd()
.TryAdd()
.TryAdd()
.TryAdd()
diff --git a/src/LibRed/LibRed.EFCore/Query/Internal/LibRedMathTranslator.cs b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedMathTranslator.cs
new file mode 100644
index 000000000..6c2e50776
--- /dev/null
+++ b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedMathTranslator.cs
@@ -0,0 +1,197 @@
+using EntityFrameworkCore.Jet.Query;
+using EntityFrameworkCore.Jet.Utilities;
+using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions;
+
+namespace EntityFrameworkCore.LibRed.Query.Internal;
+
+///
+/// This API supports the Entity Framework Core infrastructure and is not intended to be used
+/// directly from your code. This API may change or be removed in future releases.
+///
+public class LibRedMathTranslator(ISqlExpressionFactory sqlExpressionFactory) : IMethodCallTranslator
+{
+ private readonly JetSqlExpressionFactory _sqlExpressionFactory = (JetSqlExpressionFactory)sqlExpressionFactory;
+
+ private static readonly Dictionary _supportedMethodTranslationsDirect = new()
+ {
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(decimal)])!, "ABS"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(double)])!, "ABS"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(float)])!, "ABS"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(int)]) !, "ABS"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(long)]) !, "ABS"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(sbyte)]) !, "ABS"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Abs), [typeof(short)]) !, "ABS"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Pow), [typeof(double), typeof(double)]) !, "POWER"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Exp), [typeof(double)]) !, "EXP"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Log), [typeof(double)])!, "LN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Log10), [typeof(double)])!, "LOG10"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sqrt), [typeof(double)])!, "SQRT"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Floor), [typeof(decimal)])!, "FLOOR"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Floor), [typeof(double)])!, "FLOOR"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Ceiling), [typeof(decimal)])!, "CEILING"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Ceiling), [typeof(double)])!, "CEILING"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Acos), [typeof(double)])!, "ACOS"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Asin), [typeof(double)])!, "ASIN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Atan), [typeof(double)])!, "ATAN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Atan2), [typeof(double), typeof(double)])!, "ATAN2"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Cos), [typeof(double)])!, "COS"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sin), [typeof(double)])!, "SIN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Tan), [typeof(double)])!, "TAN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Cosh), [typeof(double)])!, "COSH"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sinh), [typeof(double)])!, "SINH"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Tanh), [typeof(double)])!, "TANH"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(decimal)])!, "SIGN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(double)])!, "SIGN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(float)])!, "SIGN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(int)])!, "SIGN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(long)])!, "SIGN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(sbyte)])!, "SIGN"},
+ {typeof(Math).GetRuntimeMethod(nameof(Math.Sign), [typeof(short)])!, "SIGN"},
+ {typeof(double).GetRuntimeMethod(nameof(double.DegreesToRadians), [typeof(double)])!, "RADIANS"},
+ {typeof(double).GetRuntimeMethod(nameof(double.RadiansToDegrees), [typeof(double)])!, "DEGREES"},
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Abs), [typeof(float)])!, "ABS" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Pow), [typeof(float), typeof(float)])!, "POWER" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Exp), [typeof(float)])!, "EXP" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Log), [typeof(float)])!, "LN" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Log10), [typeof(float)])!, "LOG10" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Sqrt), [typeof(float)])!, "SQRT" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Floor), [typeof(float)])!, "FLOOR" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Ceiling), [typeof(float)])!, "CEILING" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Acos), [typeof(float)])!, "ACOS" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Asin), [typeof(float)])!, "ASIN" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Atan), [typeof(float)])!, "ATAN" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Atan2), [typeof(float), typeof(float)])!, "ATAN2" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Cos), [typeof(float)])!, "COS" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Sin), [typeof(float)])!, "SIN" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Tan), [typeof(float)])!, "TAN" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Cosh), [typeof(float)])!, "COSH" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Sinh), [typeof(float)])!, "SINH" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Tanh), [typeof(float)])!, "TANH" },
+ { typeof(MathF).GetRuntimeMethod(nameof(MathF.Sign), [typeof(float)])!, "SIGN" },
+ { typeof(float).GetRuntimeMethod(nameof(float.DegreesToRadians), [typeof(float)])!, "RADIANS" },
+ { typeof(float).GetRuntimeMethod(nameof(float.RadiansToDegrees), [typeof(float)])!, "DEGREES" }
+ };
+
+ private static readonly IEnumerable _logMethodInfos =
+ [
+ typeof(Math).GetRuntimeMethod(nameof(Math.Log), [typeof(double), typeof(double)])!,
+ typeof(MathF).GetRuntimeMethod(nameof(MathF.Log), [typeof(float), typeof(float)])!
+ ];
+
+ private static readonly IEnumerable _truncateMethodInfos =
+ [
+ typeof(Math).GetRuntimeMethod(nameof(Math.Truncate), [typeof(decimal)])!,
+ typeof(Math).GetRuntimeMethod(nameof(Math.Truncate), [typeof(double)])!,
+ typeof(MathF).GetRuntimeMethod(nameof(MathF.Truncate), [typeof(float)])!
+ ];
+
+ private static readonly IEnumerable _roundMethodInfos =
+ [
+ typeof(Math).GetRuntimeMethod(nameof(Math.Round), [typeof(decimal)])!,
+ typeof(Math).GetRuntimeMethod(nameof(Math.Round), [typeof(double)])!,
+ typeof(Math).GetRuntimeMethod(nameof(Math.Round), [typeof(decimal), typeof(int)])!,
+ typeof(Math).GetRuntimeMethod(nameof(Math.Round), [typeof(double), typeof(int)])!,
+ typeof(MathF).GetRuntimeMethod(nameof(MathF.Round), [typeof(float)])!,
+ typeof(MathF).GetRuntimeMethod(nameof(MathF.Round), [typeof(float), typeof(int)])!
+ ];
+
+ public SqlExpression? Translate(SqlExpression? instance, MethodInfo method, IReadOnlyList arguments, IDiagnosticsLogger logger)
+ {
+ Check.NotNull(method, nameof(method));
+
+ if (_supportedMethodTranslationsDirect.TryGetValue(method, out var sqlFunctionName))
+ {
+ var typeMapping = arguments.Count == 1
+ ? ExpressionExtensions.InferTypeMapping(arguments[0])
+ : ExpressionExtensions.InferTypeMapping(arguments[0], arguments[1]);
+
+ var newArguments = new SqlExpression[arguments.Count];
+ newArguments[0] = _sqlExpressionFactory.ApplyTypeMapping(arguments[0], typeMapping);
+
+ if (arguments.Count == 2)
+ {
+ newArguments[1] = _sqlExpressionFactory.ApplyTypeMapping(arguments[1], typeMapping);
+ }
+
+ return _sqlExpressionFactory.Function(
+ sqlFunctionName,
+ newArguments,
+ nullable: true,
+ argumentsPropagateNullability: newArguments.Select(_ => true).ToArray(),
+ method.ReturnType,
+ sqlFunctionName == "SIGN" ? null : typeMapping);
+ }
+
+ if (_logMethodInfos.Contains(method))
+ {
+ // Math.Log(x, n) is LOG(n, x): the engine's LOG takes the base first
+ var typeMapping = ExpressionExtensions.InferTypeMapping(arguments[0], arguments[1]);
+
+ return _sqlExpressionFactory.Function(
+ "LOG",
+ [
+ _sqlExpressionFactory.ApplyTypeMapping(arguments[1], typeMapping),
+ _sqlExpressionFactory.ApplyTypeMapping(arguments[0], typeMapping)
+ ],
+ nullable: true,
+ argumentsPropagateNullability: [true, true],
+ method.ReturnType,
+ typeMapping);
+ }
+
+ if (_truncateMethodInfos.Contains(method))
+ {
+ var argument = arguments[0];
+ // C# has Round over decimal/double/float only so our argument will be one of those types (compiler puts convert node)
+ // In database result will be same type except for float which returns double which we need to cast back to float.
+ var resultType = argument.Type;
+ if (resultType == typeof(float))
+ {
+ resultType = typeof(double);
+ }
+ var result = (SqlExpression)_sqlExpressionFactory.Function(
+ "FIX",
+ [argument],
+ nullable: true,
+ argumentsPropagateNullability: [true],
+ resultType);
+
+ if (argument.Type == typeof(float))
+ {
+ result = _sqlExpressionFactory.Convert(result, typeof(float));
+ }
+
+ return _sqlExpressionFactory.ApplyTypeMapping(result, argument.TypeMapping);
+ }
+
+ if (_roundMethodInfos.Contains(method))
+ {
+ var argument = arguments[0];
+ var digits = arguments.Count == 2 ? arguments[1] : _sqlExpressionFactory.Constant(0);
+ // C# has Round over decimal/double/float only so our argument will be one of those types (compiler puts convert node)
+ // In database result will be same type except for float which returns double which we need to cast back to float.
+ var resultType = argument.Type;
+ if (resultType == typeof(float))
+ {
+ resultType = typeof(double);
+ }
+
+ var result = (SqlExpression)_sqlExpressionFactory.Function(
+ "ROUND",
+ [argument, digits],
+ nullable: false,
+ argumentsPropagateNullability: [true, false],
+ resultType);
+
+ if (argument.Type == typeof(float))
+ {
+ result = _sqlExpressionFactory.Convert(result, typeof(float));
+ }
+
+ return _sqlExpressionFactory.ApplyTypeMapping(result, argument.TypeMapping);
+ }
+
+ return null;
+ }
+}
diff --git a/src/LibRed/LibRed.EFCore/Query/Internal/LibRedMethodCallTranslatorProvider.cs b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedMethodCallTranslatorProvider.cs
new file mode 100644
index 000000000..cf244d496
--- /dev/null
+++ b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedMethodCallTranslatorProvider.cs
@@ -0,0 +1,45 @@
+using EntityFrameworkCore.Jet.Query;
+using EntityFrameworkCore.Jet.Query.ExpressionTranslators.Internal;
+using EntityFrameworkCore.LibRed.Infrastructure;
+using EntityFrameworkCore.LibRed.Infrastructure.Internal;
+
+namespace EntityFrameworkCore.LibRed.Query.Internal;
+
+///
+/// This API supports the Entity Framework Core infrastructure and is not intended to be used
+/// directly from your code. This API may change or be removed in future releases.
+///
+public class LibRedMethodCallTranslatorProvider : RelationalMethodCallTranslatorProvider
+{
+ ///
+ /// This API supports the Entity Framework Core infrastructure and is not intended to be used
+ /// directly from your code. This API may change or be removed in future releases.
+ ///
+ public LibRedMethodCallTranslatorProvider(
+ RelationalMethodCallTranslatorProviderDependencies dependencies,
+ ILibRedOptions options)
+ : base(dependencies)
+ {
+ var sqlExpressionFactory = (JetSqlExpressionFactory)dependencies.SqlExpressionFactory;
+
+ // ReSharper disable once VirtualMemberCallInConstructor
+ AddTranslators(
+ [
+ new JetByteArrayMethodTranslator(sqlExpressionFactory),
+ new JetConvertTranslator(sqlExpressionFactory),
+ new JetDateDiffFunctionsTranslator(sqlExpressionFactory),
+ new JetDateOnlyMethodTranslator(sqlExpressionFactory),
+ new JetDateTimeMethodTranslator(sqlExpressionFactory),
+ new JetIsDateFunctionTranslator(sqlExpressionFactory),
+ options.SqlMode == LibRedSqlMode.Compatible
+ ? new JetMathTranslator(sqlExpressionFactory)
+ : new LibRedMathTranslator(sqlExpressionFactory),
+ new JetNewGuidTranslator(sqlExpressionFactory),
+ new JetObjectToStringTranslator(sqlExpressionFactory),
+ new JetParseTranslator(sqlExpressionFactory),
+ new JetStringMethodTranslator(sqlExpressionFactory),
+ new JetRandomTranslator(sqlExpressionFactory),
+ new JetTimeOnlyMethodTranslator(sqlExpressionFactory)
+ ]);
+ }
+}
diff --git a/src/LibRed/LibRed.EFCore/Query/Internal/LibRedSqlTranslatingExpressionVisitor.cs b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedSqlTranslatingExpressionVisitor.cs
new file mode 100644
index 000000000..120ee5c50
--- /dev/null
+++ b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedSqlTranslatingExpressionVisitor.cs
@@ -0,0 +1,617 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
+using System.Text;
+using EntityFrameworkCore.Jet;
+using EntityFrameworkCore.Jet.Internal;
+using ExpressionExtensions = Microsoft.EntityFrameworkCore.Query.ExpressionExtensions;
+
+namespace EntityFrameworkCore.LibRed.Query.Internal;
+
+///
+/// LibRed's own SQL translating expression visitor, used in
+/// mode. It is a copy of the shared JetSqlTranslatingExpressionVisitor — which the compatible mode keeps
+/// using, because ACE has to run what that mode generates — and differs where LibRed's engine goes beyond ACE:
+/// Math.Max/Math.Min, and a Max()/Min() over an inline collection, become the
+/// standard GREATEST/LEAST rather than a nest of CASE comparisons.
+///
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+public class LibRedSqlTranslatingExpressionVisitor(
+ RelationalSqlTranslatingExpressionVisitorDependencies dependencies,
+ QueryCompilationContext queryCompilationContext,
+ QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor) : RelationalSqlTranslatingExpressionVisitor(dependencies, queryCompilationContext, queryableMethodTranslatingExpressionVisitor)
+{
+ private readonly QueryCompilationContext _queryCompilationContext = queryCompilationContext;
+ private readonly ISqlExpressionFactory _sqlExpressionFactory = dependencies.SqlExpressionFactory;
+
+ private static readonly HashSet DateTimeDataTypes
+ =
+ [
+ "time",
+ "date",
+ "datetime",
+ "datetime2",
+ "datetimeoffset"
+ ];
+
+ private static readonly HashSet DateTimeClrTypes
+ =
+ [
+ typeof(TimeOnly),
+ typeof(DateOnly),
+ typeof(TimeSpan),
+ typeof(DateTime),
+ typeof(DateTimeOffset)
+ ];
+
+ private static readonly HashSet ArithmeticOperatorTypes
+ =
+ [
+ ExpressionType.Add,
+ ExpressionType.Subtract,
+ ExpressionType.Multiply,
+ ExpressionType.Divide,
+ ExpressionType.Modulo
+ ];
+
+ private static readonly MethodInfo StringStartsWithMethodInfoString
+ = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), [typeof(string)])!;
+
+ private static readonly MethodInfo StringStartsWithMethodInfoChar
+ = typeof(string).GetRuntimeMethod(nameof(string.StartsWith), [typeof(char)])!;
+
+ private static readonly MethodInfo StringEndsWithMethodInfoString
+ = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), [typeof(string)])!;
+
+ private static readonly MethodInfo StringEndsWithMethodInfoChar
+ = typeof(string).GetRuntimeMethod(nameof(string.EndsWith), [typeof(char)])!;
+
+ private static readonly MethodInfo StringContainsMethodInfoString
+ = typeof(string).GetRuntimeMethod(nameof(string.Contains), [typeof(string)])!;
+
+ private static readonly MethodInfo StringContainsMethodInfoChar
+ = typeof(string).GetRuntimeMethod(nameof(string.Contains), [typeof(char)])!;
+
+ private static readonly MethodInfo EscapeLikePatternParameterMethod =
+ typeof(LibRedSqlTranslatingExpressionVisitor).GetTypeInfo().GetDeclaredMethod(nameof(ConstructLikePatternParameter))!;
+
+ private static readonly MethodInfo StringJoinMethodInfo
+ = typeof(string).GetRuntimeMethod(nameof(string.Join), [typeof(string), typeof(string[])])!;
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitBinary(BinaryExpression binaryExpression)
+ {
+ if (binaryExpression.NodeType == ExpressionType.ArrayIndex
+ && binaryExpression.Left.Type == typeof(byte[]))
+ {
+ return TranslateByteArrayElementAccess(
+ binaryExpression.Left,
+ binaryExpression.Right,
+ binaryExpression.Type);
+ }
+
+ var visitedExpression = base.VisitBinary(binaryExpression);
+
+ if (visitedExpression is SqlBinaryExpression be)
+ {
+ if (be.OperatorType is ExpressionType.And
+ or ExpressionType.Or
+ or ExpressionType.ExclusiveOr)
+ {
+ var left = CoerceIfNeeded(be.Left);
+ var right = CoerceIfNeeded(be.Right);
+
+ if (!ReferenceEquals(left, be.Left) || !ReferenceEquals(right, be.Right))
+ {
+ visitedExpression = new SqlBinaryExpression(
+ be.OperatorType,
+ left,
+ right,
+ be.Type,
+ be.TypeMapping);
+ }
+ }
+ }
+
+ if (visitedExpression is SqlBinaryExpression sqlBinaryExpression
+ && ArithmeticOperatorTypes.Contains(sqlBinaryExpression.OperatorType))
+ {
+ var inferredProviderType = GetProviderType(sqlBinaryExpression.Left) ?? GetProviderType(sqlBinaryExpression.Right);
+ if (inferredProviderType != null)
+ {
+ if (DateTimeDataTypes.Contains(inferredProviderType))
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+ }
+ else
+ {
+ var leftType = sqlBinaryExpression.Left.Type;
+ var rightType = sqlBinaryExpression.Right.Type;
+ if (DateTimeClrTypes.Contains(leftType)
+ || DateTimeClrTypes.Contains(rightType))
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+ }
+ }
+
+ return visitedExpression;
+ }
+
+ private SqlExpression CoerceIfNeeded(SqlExpression operand)
+ {
+ // Skip if already a LONG (Jet) or if already a CLNG(...) call
+ var storeType = operand.TypeMapping?.StoreType;
+ if (storeType is "long" or "integer" or "int") return operand;
+ var clr = operand.Type;
+ if (clr == typeof(byte) || clr == typeof(short))
+ {
+ return _sqlExpressionFactory.Function(
+ "CLNG",
+ [operand],
+ true,
+ argumentsPropagateNullability: [true],
+ typeof(long),
+ IntTypeMapping.Default);
+ }
+ return operand;
+ }
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ protected override Expression VisitUnary(UnaryExpression unaryExpression)
+ {
+ if (unaryExpression.NodeType == ExpressionType.ArrayLength
+ && unaryExpression.Operand.Type == typeof(byte[]))
+ {
+ if (!(base.Visit(unaryExpression.Operand) is SqlExpression sqlExpression))
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+ throw new InvalidOperationException(JetStrings.ByteArrayLength);
+ }
+
+ return base.VisitUnary(unaryExpression);
+ }
+
+ protected override Expression VisitExtension(Expression extensionExpression)
+ {
+ var result = base.VisitExtension(extensionExpression);
+ if (extensionExpression is ShapedQueryExpression shapedQueryExpression)
+ {
+ var shaperExpression = shapedQueryExpression.ShaperExpression;
+ if (shapedQueryExpression.ResultCardinality == ResultCardinality.SingleOrDefault
+ && !shaperExpression.Type.IsNullableType() && result is SqlFunctionExpression { Name: "COALESCE"} sqlFunctionExpression)
+ {
+ if (sqlFunctionExpression.Arguments?[1] is SqlConstantExpression { Value: DateTime { Ticks: 0 } })
+ {
+ var newconst = new SqlConstantExpression(new DateTime(100, 1, 1), sqlFunctionExpression.Arguments[1].TypeMapping);
+ return _sqlExpressionFactory.Coalesce(sqlFunctionExpression.Arguments[0],
+ (SqlExpression)Visit(newconst));
+ }
+ }
+ }
+ return result;
+ }
+
+ protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
+ {
+ var method = methodCallExpression.Method;
+
+ if (method.IsGenericMethod
+ && method.GetGenericMethodDefinition() == EnumerableMethods.ElementAt
+ && methodCallExpression.Arguments[0].Type == typeof(byte[]))
+ {
+ return TranslateByteArrayElementAccess(
+ methodCallExpression.Arguments[0],
+ methodCallExpression.Arguments[1],
+ methodCallExpression.Type);
+ }
+
+ if ((method == StringStartsWithMethodInfoString || method == StringStartsWithMethodInfoChar)
+ && TryTranslateStartsEndsWithContains(
+ methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.StartsWith, out var translation1))
+ {
+ return translation1;
+ }
+
+ if ((method == StringEndsWithMethodInfoString || method == StringEndsWithMethodInfoChar)
+ && TryTranslateStartsEndsWithContains(
+ methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.EndsWith, out var translation2))
+ {
+ return translation2;
+ }
+
+ if ((method == StringContainsMethodInfoString || method == StringContainsMethodInfoChar)
+ && TryTranslateStartsEndsWithContains(
+ methodCallExpression.Object!, methodCallExpression.Arguments[0], StartsEndsWithContains.Contains, out var translation3))
+ {
+ return translation3;
+ }
+
+ if (method == StringJoinMethodInfo
+ && methodCallExpression.Arguments[1] is NewArrayExpression newArrayExpression
+ && Visit(methodCallExpression.Arguments[0]) is SqlExpression separator)
+ {
+ var stringTypeMapping = separator.TypeMapping;
+
+ SqlExpression? result = null;
+
+ foreach (var expression in newArrayExpression.Expressions)
+ {
+ if (Visit(expression) is not SqlExpression translated)
+ {
+ return QueryCompilationContext.NotTranslatedExpression;
+ }
+
+ translated = _sqlExpressionFactory.Coalesce(
+ _sqlExpressionFactory.ApplyTypeMapping(translated, stringTypeMapping),
+ _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping));
+
+ result = result is null
+ ? translated
+ : _sqlExpressionFactory.Add(
+ _sqlExpressionFactory.Add(result, separator),
+ translated);
+ }
+
+ return result ?? _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping);
+ }
+
+ return base.VisitMethodCall(methodCallExpression);
+
+ bool TryTranslateStartsEndsWithContains(
+ Expression instance,
+ Expression pattern,
+ StartsEndsWithContains methodType,
+ [NotNullWhen(true)] out SqlExpression? translation)
+ {
+ if (Visit(instance) is not SqlExpression translatedInstance
+ || Visit(pattern) is not SqlExpression translatedPattern)
+ {
+ translation = null;
+ return false;
+ }
+
+ var stringTypeMapping = ExpressionExtensions.InferTypeMapping(translatedInstance, translatedPattern);
+
+ translatedInstance = _sqlExpressionFactory.ApplyTypeMapping(translatedInstance, stringTypeMapping);
+ translatedPattern = _sqlExpressionFactory.ApplyTypeMapping(translatedPattern, stringTypeMapping);
+
+ switch (translatedPattern)
+ {
+ case SqlConstantExpression patternConstant:
+ {
+ // The pattern is constant. Aside from null and empty string, we bracket every wildcard character (see
+ // IsLikeWildChar) and send a simple LIKE
+ translation = patternConstant.Value switch
+ {
+ null => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant(null,typeof(string), stringTypeMapping)),
+
+ // In .NET, all strings start with/end with/contain the empty string, but SQL LIKE return false for empty patterns.
+ // Return % which always matches instead.
+ // Note that we don't just return a true constant, since null strings shouldn't match even an empty string
+ // (but SqlNullabilityProcess will convert this to a true constant if the instance is non-nullable)
+ "" => _sqlExpressionFactory.Like(translatedInstance, _sqlExpressionFactory.Constant("%")),
+
+ string s => s.Any(IsLikeWildChar)
+ ? _sqlExpressionFactory.Like(
+ translatedInstance,
+ _sqlExpressionFactory.Constant(
+ methodType switch
+ {
+ StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%',
+ StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s),
+ StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%",
+
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ }))
+ : _sqlExpressionFactory.Like(
+ translatedInstance,
+ _sqlExpressionFactory.Constant(
+ methodType switch
+ {
+ StartsEndsWithContains.StartsWith => s + '%',
+ StartsEndsWithContains.EndsWith => '%' + s,
+ StartsEndsWithContains.Contains => $"%{s}%",
+
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ })),
+
+ char s when !IsLikeWildChar(s)
+ => _sqlExpressionFactory.Like(
+ translatedInstance,
+ _sqlExpressionFactory.Constant(
+ methodType switch
+ {
+ StartsEndsWithContains.StartsWith => s + "%",
+ StartsEndsWithContains.EndsWith => "%" + s,
+ StartsEndsWithContains.Contains => $"%{s}%",
+
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ })),
+
+ // LibRed, like Jet, has no ESCAPE clause, so a wildcard character is bracketed as in a string pattern.
+ char s => _sqlExpressionFactory.Like(
+ translatedInstance,
+ _sqlExpressionFactory.Constant(
+ methodType switch
+ {
+ StartsEndsWithContains.StartsWith => EscapeLikePattern(s.ToString()) + "%",
+ StartsEndsWithContains.EndsWith => "%" + EscapeLikePattern(s.ToString()),
+ StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s.ToString())}%",
+
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ })),
+
+ _ => throw new UnreachableException()
+ };
+
+ return true;
+ }
+
+ case SqlParameterExpression patternParameter:
+ {
+ // The pattern is a parameter, register a runtime parameter that will contain the rewritten LIKE pattern, where
+ // all special characters have been escaped.
+ var lambda = Expression.Lambda(
+ Expression.Call(
+ EscapeLikePatternParameterMethod,
+ QueryCompilationContext.QueryContextParameter,
+ Expression.Constant(patternParameter.Name),
+ Expression.Constant(methodType)),
+ QueryCompilationContext.QueryContextParameter);
+
+ var escapedPatternParameter =
+ _queryCompilationContext.RegisterRuntimeParameter(
+ $"{patternParameter.Name}_{methodType.ToString().ToLower(CultureInfo.InvariantCulture)}", lambda);
+
+ translation = _sqlExpressionFactory.Like(
+ translatedInstance,
+ new SqlParameterExpression(escapedPatternParameter.Name!, escapedPatternParameter.Type, stringTypeMapping));
+
+ return true;
+ }
+
+ default:
+ // The pattern is a column or a complex expression; the possible special characters in the pattern cannot be escaped,
+ // preventing us from translating to LIKE.
+ translation = methodType switch
+ {
+ // For StartsWith/EndsWith, use LEFT or RIGHT instead to extract substring and compare:
+ // WHERE instance IS NOT NULL AND pattern IS NOT NULL AND LEFT(instance, LEN(pattern)) = pattern
+ // This is less efficient than LIKE (i.e. StartsWith does an index scan instead of seek), but we have no choice.
+ // Note that we compensate for the case where both the instance and the pattern are null (null.StartsWith(null)); a
+ // simple equality would yield true in that case, but we want false. We technically
+ StartsEndsWithContains.StartsWith or StartsEndsWithContains.EndsWith
+ => _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedInstance),
+ _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedPattern),
+ _sqlExpressionFactory.Equal(
+ _sqlExpressionFactory.Function(
+ methodType is StartsEndsWithContains.StartsWith ? "LEFT" : "RIGHT",
+ [
+ translatedInstance,
+ _sqlExpressionFactory.Coalesce(
+ _sqlExpressionFactory.Function(
+ "LEN",
+ [translatedPattern],
+ nullable: true,
+ argumentsPropagateNullability: [false],
+ typeof(int)),
+ _sqlExpressionFactory.Constant(0)
+ )
+ ],
+ nullable: true,
+ argumentsPropagateNullability: [true, false],
+ typeof(string),
+ stringTypeMapping),
+ translatedPattern))),
+
+ // For Contains, just use INSTR and check if the result is greater than 0.
+ // Add a check to return null when the pattern is an empty string (and the string isn't null)
+ StartsEndsWithContains.Contains
+ => _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedInstance),
+ _sqlExpressionFactory.AndAlso(
+ _sqlExpressionFactory.IsNotNull(translatedPattern),
+ _sqlExpressionFactory.OrElse(
+ _sqlExpressionFactory.GreaterThan(
+ _sqlExpressionFactory.Function(
+ "INSTR",
+ [_sqlExpressionFactory.Constant(1), translatedInstance, translatedPattern, _sqlExpressionFactory.Constant(1)],
+ nullable: true,
+ argumentsPropagateNullability: [false, true, true, false],
+ typeof(int)),
+ _sqlExpressionFactory.Constant(0)),
+ _sqlExpressionFactory.Like(
+ translatedPattern,
+ _sqlExpressionFactory.Constant(string.Empty, stringTypeMapping))))),
+
+ _ => throw new UnreachableException()
+ };
+
+ return true;
+ }
+ }
+ }
+
+ public static string? ConstructLikePatternParameter(
+ QueryContext queryContext,
+ string baseParameterName,
+ StartsEndsWithContains methodType)
+ => queryContext.Parameters[baseParameterName] switch
+ {
+ null => null,
+
+ // In .NET, all strings start/end with the empty string, but SQL LIKE return false for empty patterns.
+ // Return % which always matches instead.
+ "" => "%",
+
+ string s => methodType switch
+ {
+ StartsEndsWithContains.StartsWith => EscapeLikePattern(s) + '%',
+ StartsEndsWithContains.EndsWith => '%' + EscapeLikePattern(s),
+ StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s)}%",
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ },
+
+ char s when !IsLikeWildChar(s) => methodType switch
+ {
+ StartsEndsWithContains.StartsWith => s + "%",
+ StartsEndsWithContains.EndsWith => "%" + s,
+ StartsEndsWithContains.Contains => $"%{s}%",
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ },
+
+ char s => methodType switch
+ {
+ StartsEndsWithContains.StartsWith => EscapeLikePattern(s.ToString()) + "%",
+ StartsEndsWithContains.EndsWith => "%" + EscapeLikePattern(s.ToString()),
+ StartsEndsWithContains.Contains => $"%{EscapeLikePattern(s.ToString())}%",
+ _ => throw new ArgumentOutOfRangeException(nameof(methodType), methodType, null)
+ },
+
+ _ => throw new UnreachableException()
+ };
+
+ public enum StartsEndsWithContains
+ {
+ StartsWith,
+ EndsWith,
+ Contains
+ }
+
+ //Extra resources
+ // https://support.microsoft.com/en-us/office/like-operator-b2f7ef03-9085-4ffb-9829-eef18358e931
+ // https://support.microsoft.com/en-us/office/access-wildcard-character-reference-af00c501-7972-40ee-8889-e18abaad12d1
+ // https://support.microsoft.com/en-us/office/use-wildcards-in-queries-and-parameters-in-access-ec057a45-78b1-4d16-8c20-242cde582e0b
+ //These are the characters to escape in LIKE pattern
+ private static bool IsLikeWildChar(char c)
+ => c == '%' || c == '_' || c == '[' || c == '^' || c == '?' || c == '#' || c == '*';
+
+ private static string EscapeLikePattern(string pattern)
+ {
+ var builder = new StringBuilder();
+ foreach (var c in pattern)
+ {
+ if (IsLikeWildChar(c))
+ {
+ builder.Append('[');
+ builder.Append(c);
+ builder.Append(']');
+ }
+ else
+ {
+ builder.Append(c);
+ }
+ }
+
+ return builder.ToString();
+ }
+
+ private Expression TranslateByteArrayElementAccess(Expression array, Expression index, Type resultType)
+ {
+ var visitedArray = Visit(array);
+ var visitedIndex = Visit(index);
+
+ return visitedArray is SqlExpression sqlArray
+ && visitedIndex is SqlExpression sqlIndex
+ ? Dependencies.SqlExpressionFactory.Function(
+ "ASCB",
+ [ Dependencies.SqlExpressionFactory.Function(
+ "MIDB",
+ [
+ sqlArray,
+ Dependencies.SqlExpressionFactory.Add(
+ Dependencies.SqlExpressionFactory.ApplyDefaultTypeMapping(sqlIndex),
+ Dependencies.SqlExpressionFactory.Constant(1)),
+ Dependencies.SqlExpressionFactory.Constant(1) ],
+ nullable: true,
+ argumentsPropagateNullability: [true, true, true],
+ typeof(byte[])) ],
+ nullable: true,
+ argumentsPropagateNullability: [true],
+ typeof(int))
+
+ : QueryCompilationContext.NotTranslatedExpression;
+ }
+
+ ///
+ /// GREATEST(a, b, …), which LibRed's engine evaluates as SQL Server and PostgreSQL do: NULL arguments
+ /// are ignored, and the result is NULL only when every argument is.
+ ///
+ public override SqlExpression? GenerateGreatest(IReadOnlyList expressions, Type resultType)
+ => GenerateGreatestOrLeast("GREATEST", expressions, resultType);
+
+ ///
+ /// LEAST(a, b, …), which LibRed's engine evaluates as SQL Server and PostgreSQL do: NULL arguments are
+ /// ignored, and the result is NULL only when every argument is.
+ ///
+ public override SqlExpression? GenerateLeast(IReadOnlyList expressions, Type resultType)
+ => GenerateGreatestOrLeast("LEAST", expressions, resultType);
+
+ private SqlExpression? GenerateGreatestOrLeast(string name, IReadOnlyList expressions, Type resultType)
+ {
+ if (expressions.Count == 0)
+ {
+ return null;
+ }
+
+ IReadOnlyList mappedExpressions = ApplyMinMaxResultTypeMapping(expressions, resultType);
+
+ // A NULL argument is skipped rather than propagated, so no argument's nullability carries into the result
+ // on its own; the result is still nullable, for when every argument is NULL.
+ return _sqlExpressionFactory.Function(
+ name,
+ mappedExpressions,
+ nullable: true,
+ argumentsPropagateNullability: Enumerable.Repeat(false, mappedExpressions.Count),
+ resultType,
+ ExpressionExtensions.InferTypeMapping(mappedExpressions.ToArray()));
+ }
+
+ ///
+ /// Math.Min/Max operands can arrive with different store mappings even though their CLR expression has one
+ /// declared result type. Prefer an operand mapping for that result type and apply it to every argument;
+ /// otherwise the first argument can incorrectly force (for example) an Int16 mapping onto an Int32 constant
+ /// in an outer nested Min/Max.
+ ///
+ private IReadOnlyList ApplyMinMaxResultTypeMapping(
+ IReadOnlyList expressions, Type resultType)
+ {
+ RelationalTypeMapping? resultMapping = expressions
+ .Select(e => e.TypeMapping)
+ .FirstOrDefault(m => m?.ClrType == resultType);
+
+ if (resultMapping is null)
+ {
+ return expressions;
+ }
+
+ return expressions
+ .Select(e => Dependencies.SqlExpressionFactory.ApplyTypeMapping(e, resultMapping))
+ .ToArray();
+ }
+
+
+ private static string? GetProviderType(SqlExpression expression)
+ => expression.TypeMapping?.StoreType;
+
+}
diff --git a/src/LibRed/LibRed.EFCore/Query/Internal/LibRedSqlTranslatingExpressionVisitorFactory.cs b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedSqlTranslatingExpressionVisitorFactory.cs
new file mode 100644
index 000000000..d76e2bbc7
--- /dev/null
+++ b/src/LibRed/LibRed.EFCore/Query/Internal/LibRedSqlTranslatingExpressionVisitorFactory.cs
@@ -0,0 +1,35 @@
+using EntityFrameworkCore.Jet.Query.Internal;
+using EntityFrameworkCore.LibRed.Infrastructure;
+using EntityFrameworkCore.LibRed.Infrastructure.Internal;
+
+namespace EntityFrameworkCore.LibRed.Query.Internal
+{
+ ///
+ /// Creates the SQL translating expression visitor for the configured SQL mode: the shared
+ /// in mode, whose
+ /// SQL ACE also has to run, and LibRed's own in
+ /// mode.
+ ///
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public class LibRedSqlTranslatingExpressionVisitorFactory(
+ RelationalSqlTranslatingExpressionVisitorDependencies dependencies,
+ ILibRedOptions options) : IRelationalSqlTranslatingExpressionVisitorFactory
+ {
+ ///
+ /// Relational provider-specific dependencies for this service.
+ ///
+ protected virtual RelationalSqlTranslatingExpressionVisitorDependencies Dependencies { get; } = dependencies;
+
+ public virtual RelationalSqlTranslatingExpressionVisitor Create(
+ QueryCompilationContext queryCompilationContext,
+ QueryableMethodTranslatingExpressionVisitor queryableMethodTranslatingExpressionVisitor)
+ => options.SqlMode == LibRedSqlMode.Compatible
+ ? new JetSqlTranslatingExpressionVisitor(Dependencies, queryCompilationContext, queryableMethodTranslatingExpressionVisitor)
+ : new LibRedSqlTranslatingExpressionVisitor(Dependencies, queryCompilationContext, queryableMethodTranslatingExpressionVisitor);
+ }
+}
diff --git a/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs b/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs
index 5e789aca7..a4b1dcb98 100644
--- a/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs
+++ b/src/LibRed/LibRed.Engine/Execution/AccessTypeMapper.cs
@@ -15,7 +15,7 @@ internal static class AccessTypeMapper
{
public static ColumnSpec ToColumnSpec(ColumnDefinition column, JetVersion version)
{
- ColumnSpec spec = MapType(column, version) with
+ ColumnSpec spec = WithIdentity(MapType(column, version), column.Identity) with
{
IsNullable = !column.NotNull,
// WITH COMPRESSION is only meaningful on the two types ACE accepts it for; Access rejects it
@@ -55,6 +55,19 @@ public static ColumnSpec ToColumnSpec(ColumnDefinition column, JetVersion versio
return ColumnSpec.Calculated(spec.Name, spec.Type, expression);
}
+ ///
+ /// Applies a trailing IDENTITY [(seed [, increment])] the way ACE does: a Long column becomes an
+ /// AutoNumber counting from the attribute's seed by its increment, each 1 when omitted. Those replace whatever
+ /// the type declared — COUNTER(5, 2) IDENTITY(9, 3) counts 9, 12, 15 and COUNTER(5, 2) IDENTITY
+ /// counts 1, 2, 3. Every other type is left exactly as declared: SHORT, BIGINT, TEXT(10),
+ /// GUID and the rest accept the word and ignore it (all verified).
+ ///
+ internal static ColumnSpec WithIdentity(ColumnSpec spec, IdentityAttribute? identity)
+ {
+ if (identity is null || spec.Type != JetDataType.Int32) return spec;
+ return spec with { IsAutoNumber = true, Seed = identity.Seed ?? 1, Increment = identity.Increment ?? 1 };
+ }
+
/// Whether WITH COMPRESSION applies to this column's type. Mapped at the HIGHEST version so the
/// version gate inside cannot fire here: asking at Version4 made
/// BIGINT … WITH COMPRESSION on an ACE 16 file — which had already passed the real gate — report
@@ -99,9 +112,9 @@ private static ColumnSpec MapType(ColumnDefinition column, JetVersion version)
return t switch
{
// AutoNumber. COUNTER(seed, increment) parses seed/increment as the (size, scale) pair; a plain
- // COUNTER defaults to 1/1. INTEGER IDENTITY(seed, increment) is the ANSI-style spelling.
+ // COUNTER defaults to 1/1. IDENTITY after another type — INTEGER IDENTITY(seed, increment) — is a
+ // column attribute rather than part of the type name, applied by WithIdentity.
"COUNTER" or "AUTOINCREMENT" or "IDENTITY"
- or "INTEGER IDENTITY" or "INT IDENTITY" or "LONG IDENTITY"
=> new ColumnSpec(column.Name, JetDataType.Int32, 4, IsFixedLength: true, IsAutoNumber: true,
Seed: column.Size ?? 1, Increment: column.Scale ?? 1),
"INTEGER" or "INT" or "LONG" or "INTEGER4"
diff --git a/src/LibRed/LibRed.Engine/Execution/EvalScope.cs b/src/LibRed/LibRed.Engine/Execution/EvalScope.cs
index e86e37a37..239ee7413 100644
--- a/src/LibRed/LibRed.Engine/Execution/EvalScope.cs
+++ b/src/LibRed/LibRed.Engine/Execution/EvalScope.cs
@@ -64,6 +64,13 @@ public bool TryResolve(ColumnReference reference, out object? value)
internal static string Describe(ColumnReference r) => r.Table is null ? r.Column : $"{r.Table}.{r.Column}";
+ /// The current row's value at a 1-based column position.
+ public object? At(int position) =>
+ position >= 1 && position <= row.Length
+ ? row[position - 1]
+ : throw new InvalidOperationException(
+ $"'{position}' is not a valid field name or expression: ORDER BY {position} names no output column.");
+
/// Every table alias visible in this scope and its enclosing scopes — so index selection on a
/// correlated subquery knows which column references belong to the outer query (and are seekable constants).
public IEnumerable VisibleAliases()
diff --git a/src/LibRed/LibRed.Engine/Execution/ExistsSemiJoin.cs b/src/LibRed/LibRed.Engine/Execution/ExistsSemiJoin.cs
index 65e8c6d2c..7c9365fd9 100644
--- a/src/LibRed/LibRed.Engine/Execution/ExistsSemiJoin.cs
+++ b/src/LibRed/LibRed.Engine/Execution/ExistsSemiJoin.cs
@@ -1,3 +1,4 @@
+using EntityFrameworkCore.Jet.Data;
using LibRed.Catalog;
using LibRed.Engine.Plan;
using LibRed.Engine.Planning;
@@ -198,7 +199,7 @@ private static bool TopCannotChangeExistence(SelectStatement subquery)
return !subquery.TopPercent
&& subquery.Top is LiteralExpression { Value: { } value }
&& IsNumeric(value)
- && Convert.ToDecimal(value, System.Globalization.CultureInfo.InvariantCulture) >= 1m;
+ && JetDecimalConverter.ToDecimal(value, System.Globalization.CultureInfo.InvariantCulture) >= 1m;
}
private static bool IsNumeric(object v)
diff --git a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.Format.cs b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.Format.cs
new file mode 100644
index 000000000..039363896
--- /dev/null
+++ b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.Format.cs
@@ -0,0 +1,840 @@
+using System.Globalization;
+using System.Text;
+using LibRed.Sql.Ast;
+
+namespace LibRed.Engine.Execution;
+
+///
+/// Access Format, FormatNumber, FormatCurrency, FormatPercent and FormatDateTime
+/// (verified vs ACE). Output follows the current culture's separators, names and patterns, as ACE follows the
+/// system's regional settings.
+///
+internal sealed partial class ExpressionEvaluator
+{
+ private enum FormatKind
+ {
+ Number,
+ Date,
+ Text,
+ }
+
+ private enum NumberStyle
+ {
+ Number,
+ Currency,
+ Percent,
+ }
+
+ ///
+ /// Access Format(value, [format], [firstdayofweek], [firstweekofyear]). A named format is matched whole and
+ /// without regard to case. Otherwise the format is a text format when it has @ & < > !, a number
+ /// format when it has a 0 or #, a date format when it has a date or time symbol, and otherwise a
+ /// number format of literals only. Text that is neither a number nor a date is returned unchanged by number and
+ /// date formats. A Null value is empty text, or the format's Null section. A Null format or setting gives Null
+ /// where ACE raises an error.
+ ///
+ private object? FormatValue(FunctionCall f)
+ {
+ object? value = Evaluate(f.Arguments[0]);
+ string format = "";
+ if (f.Arguments.Count > 1)
+ {
+ if (Evaluate(f.Arguments[1]) is not { } formatValue)
+ return null;
+ format = ConcatText(formatValue);
+ }
+ if (FirstDayOfWeek(f, 2) is not { } first || FirstWeekOfYear(f, 3) is not { } rule)
+ return null;
+ return FormatText(value, format, first, rule);
+ }
+
+ /// The date formats of FormatDateTime's 0 to 4 — the general date, long date, short date, long time and
+ /// short time — which the named formats of the same names share.
+ private static readonly string[] DateTimeFormats = ["c", "dddddd", "ddddd", "ttttt", "hh:nn"];
+
+ private static string FormatText(object? value, string format, DayOfWeek first, CalendarWeekRule rule)
+ {
+ if (format.Length == 0)
+ return value is null ? "" : GeneralText(value);
+
+ switch (format.ToLowerInvariant())
+ {
+ case "general number": return NamedNumber(value, number => ConcatText(number));
+ case "currency": return NamedNumber(value, number => StyledNumber(DigitsOf(number), NumberStyle.Currency, -1, -2, -2, -2));
+ case "fixed": return FormatNumberSections(value, ["0.00"]);
+ case "standard": return FormatNumberSections(value, ["#,##0.00"]);
+ case "percent": return FormatNumberSections(value, ["0.00%"]);
+ case "scientific": return FormatNumberSections(value, ["0.00E+00"]);
+ case "yes/no": return NamedNumber(value, number => DigitsOf(number).IsZero ? "No" : "Yes");
+ case "true/false": return NamedNumber(value, number => DigitsOf(number).IsZero ? "False" : "True");
+ case "on/off": return NamedNumber(value, number => DigitsOf(number).IsZero ? "Off" : "On");
+ case "general date": return FormatDate(value, DateTimeFormats[0], first, rule);
+ case "long date": return FormatDate(value, DateTimeFormats[1], first, rule);
+ case "medium date": return FormatDate(value, "dd-mmm-yy", first, rule);
+ case "short date": return FormatDate(value, DateTimeFormats[2], first, rule);
+ case "long time": return FormatDate(value, DateTimeFormats[3], first, rule);
+ case "medium time": return FormatDate(value, "hh:nn AM/PM", first, rule);
+ case "short time": return FormatDate(value, DateTimeFormats[4], first, rule);
+ }
+
+ List sections = FormatSections(format);
+ return FormatKindOf(format) switch
+ {
+ FormatKind.Text => FormatTextSections(value, sections),
+ FormatKind.Date => FormatDate(value, sections[0], first, rule),
+ _ => FormatNumberSections(value, sections),
+ };
+ }
+
+ /// A value as Format writes it without a format: as CStr does, but a date with its year padded and
+ /// rounded to the second.
+ private static string GeneralText(object value) =>
+ value is DateTime date ? DateText(RoundToSecond(date), padYear: true) : ConcatText(value);
+
+ private static string NamedNumber(object? value, Func
private object? ByteMid(FunctionCall f)
{
object? sv = Evaluate(f.Arguments[0]);
- if (sv is null) return null;
+ if (sv is null || CountArgument(f, 1, least: 1) is not { } first
+ || CountArgument(f, 2, absent: int.MaxValue) is not { } length)
+ return null;
byte[] b = ToBytes(sv);
- int start = Math.Max(0, Convert.ToInt32(Evaluate(f.Arguments[1]), CultureInfo.InvariantCulture) - 1);
+ int start = first - 1;
if (start >= b.Length) return ByteResult(sv, []);
- int len = f.Arguments.Count > 2
- ? Convert.ToInt32(Evaluate(f.Arguments[2]), CultureInfo.InvariantCulture)
- : b.Length - start;
- len = Math.Clamp(len, 0, b.Length - start);
+ int len = Math.Min(length, b.Length - start);
return ByteResult(sv, b[start..(start + len)]);
}
@@ -869,9 +1066,9 @@ private static string FromUnicodeBytes(string s)
int argc = f.Arguments.Count;
object? s1v = Evaluate(f.Arguments[argc >= 3 ? 1 : 0]);
object? s2v = Evaluate(f.Arguments[argc >= 3 ? 2 : 1]);
- if (s1v is null || s2v is null) return null;
- int start = argc >= 3 ? Math.Max(0, Convert.ToInt32(Evaluate(f.Arguments[0]), CultureInfo.InvariantCulture) - 1) : 0;
- int idx = IndexOfBytes(ToBytes(s1v), ToBytes(s2v), start);
+ if (s1v is null || s2v is null || (argc >= 3 ? CountArgument(f, 0, least: 1) : 1) is not { } start)
+ return null;
+ int idx = IndexOfBytes(ToBytes(s1v), ToBytes(s2v), start - 1);
return idx < 0 ? 0 : idx + 1;
}
@@ -893,139 +1090,249 @@ private static int IndexOfBytes(byte[] hay, byte[] needle, int start)
/// column as a UTF-16 string — LenB of a 3-byte value is 4).
private static byte[] ToBytes(object v)
{
- if (v is not byte[] bytes) return System.Text.Encoding.Unicode.GetBytes(v.ToString()!);
+ if (v is not byte[] bytes) return Encoding.Unicode.GetBytes(ConcatText(v));
if (bytes.Length % 2 == 0) return bytes;
var padded = new byte[bytes.Length + 1];
Array.Copy(bytes, padded, bytes.Length);
return padded;
}
- /// Decodes a byte slice back to a string, dropping a trailing incomplete (odd) byte — matching ACE
- /// (MidB(x, 1, 3) yields one character from three bytes).
- private static string FromBytes(byte[] b) =>
- System.Text.Encoding.Unicode.GetString(b, 0, b.Length - (b.Length % 2));
-
- /// The text a string function operates on: a binary value reinterpreted as a UTF-16LE string (odd
- /// byte zero-padded), or a normal string.
- private static string ToText(object v) =>
- v is byte[] ? System.Text.Encoding.Unicode.GetString(ToBytes(v)) : v.ToString()!;
-
- // --- Financial / formatting / colour functions (JES surface) ---
-
- private double FinArg(FunctionCall f, int i) => Convert.ToDouble(Evaluate(f.Arguments[i]), CultureInfo.InvariantCulture);
- private double FinArgOr(FunctionCall f, int i, double def)
- => f.Arguments.Count > i && Evaluate(f.Arguments[i]) is { } v ? Convert.ToDouble(v, CultureInfo.InvariantCulture) : def;
- /// Optional decimal-count arg for the FormatX functions — default 2 (also for the VBA "-1" default).
- private int FmtDigits(FunctionCall f, int i)
- => f.Arguments.Count > i && Evaluate(f.Arguments[i]) is { } v && Convert.ToInt32(v, CultureInfo.InvariantCulture) >= 0
- ? Convert.ToInt32(v, CultureInfo.InvariantCulture) : 2;
+ ///
+ /// SQL Server's DATALENGTH, a LibRed extension (Access has none): the bytes a value takes as Access stores
+ /// it. Text is two per character — Access text is UTF-16, as nvarchar is — with trailing spaces counted and
+ /// no account of the on-disk Unicode compression; a binary value is its length, unpadded (LenB pads an odd one).
+ /// A Byte is 1, an Integer 2, a Long and a Single 4, a Double, a Currency, a date and a BIGINT 8, a GUID 16 and a
+ /// Decimal 17 — its sign byte and 16-byte magnitude. A Boolean is stored as a bit of the row's null bitmap and so
+ /// takes no byte of its own; it counts 1, as SQL Server counts a bit.
+ ///
+ private static int DataLength(object value, bool currency) => value switch
+ {
+ string s => checked(s.Length * 2),
+ char => 2,
+ byte[] bytes => bytes.Length,
+ bool or byte or sbyte => 1,
+ short or ushort => 2,
+ int or uint or float => 4,
+ long or ulong or double or DateTime => 8,
+ decimal => currency ? 8 : 17,
+ Guid => 16,
+ _ => checked(ConcatText(value).Length * 2),
+ };
- private object FormatDateTimeFn(FunctionCall f)
+ /// Successive byte pairs as UTF-16 code units (low byte first), dropping a trailing odd byte — matching
+ /// ACE (MidB(x, 1, 3) yields one character from three bytes). A lone surrogate is kept as it is.
+ private static string FromBytes(byte[] bytes)
{
- var dt = (DateTime)ToDate(Evaluate(f.Arguments[0])!);
- int mode = f.Arguments.Count > 1 ? Convert.ToInt32(Evaluate(f.Arguments[1]), CultureInfo.InvariantCulture) : 0;
- CultureInfo c = CultureInfo.CurrentCulture;
- return mode switch
- {
- 1 => dt.ToString("D", c), // Long Date
- 2 => dt.ToString("d", c), // Short Date
- 3 => dt.ToString("T", c), // Long Time
- 4 => dt.ToString("t", c), // Short Time
- _ => dt.TimeOfDay == TimeSpan.Zero ? dt.ToString("d", c) : dt.ToString("g", c), // General Date
- };
+ var chars = new char[bytes.Length / 2];
+ for (int i = 0; i < chars.Length; i++)
+ chars[i] = (char)(bytes[2 * i] | (bytes[2 * i + 1] << 8));
+ return new string(chars);
}
+ // --- Colour functions ---
+
// QBColor maps 0..15 to fixed BGR Long values (verified vs ACE: QBColor(4) = 128).
private static readonly int[] QbColors =
[
0x000000, 0x800000, 0x008000, 0x808000, 0x000080, 0x800080, 0x008080, 0xC0C0C0,
0x808080, 0xFF0000, 0x00FF00, 0xFFFF00, 0x0000FF, 0xFF00FF, 0x00FFFF, 0xFFFFFF,
];
- private static int QbColor(int n) => QbColors[((n % 16) + 16) % 16];
-
- private static double Pow1(double rate, double nper) => Math.Pow(1 + rate, nper);
- private static double AnnuityFactor(double rate, double nper, double type) => (1 + rate * type) * (Pow1(rate, nper) - 1) / rate;
- /// VBA Pmt(rate, nper, pv, [fv=0], [type=0]): the constant payment for an annuity.
- private object Pmt(FunctionCall f)
+ ///
+ /// Access RGB(red, green, blue): the colour as a Long, red in the low byte. NULL-propagating. Each part is a
+ /// setting () where above 255 is taken as 255 (verified vs ACE: RGB(256, 0, 0) is 255, and
+ /// RGB(-0.6, 0, 0) fails).
+ ///
+ private object? Rgb(FunctionCall f)
{
- double rate = FinArg(f, 0), nper = FinArg(f, 1), pv = FinArg(f, 2), fv = FinArgOr(f, 3, 0), type = FinArgOr(f, 4, 0);
- return rate == 0 ? -(pv + fv) / nper : -(pv * Pow1(rate, nper) + fv) / AnnuityFactor(rate, nper, type);
+ if (Evaluate(f.Arguments[0]) is not { } red || Evaluate(f.Arguments[1]) is not { } green
+ || Evaluate(f.Arguments[2]) is not { } blue)
+ return null;
+ return Part(red) | Part(green) << 8 | Part(blue) << 16;
+
+ static int Part(object v) => Math.Min(Setting(v, 0, short.MaxValue), 255);
}
- /// VBA FV(rate, nper, pmt, [pv=0], [type=0]): the future value of an annuity.
- private object Fv(FunctionCall f)
+ // --- Financial functions ---
+
+ ///
+ /// A financial function (verified vs ACE, to the last bit). The arguments are read as the conversion functions read
+ /// them (text as a number, a date as its serial, True as -1); an omitted one is 0, except Rate's guess and DDB's
+ /// factor. A "type" argument means payment at the start of each period whenever it is not 0. Arguments out of range
+ /// are an invalid procedure call; a result past a Double comes back as infinity or NaN, as ACE returns it. A Null
+ /// argument gives Null where ACE raises an error.
+ /// The algorithms and the order of their arithmetic are the VBA runtime's, as Microsoft.VisualBasic's
+ /// Financial module carries them; the order decides the last digit.
+ ///
+ private object? Financial(FunctionCall f, Func compute)
{
- double rate = FinArg(f, 0), nper = FinArg(f, 1), pmt = FinArg(f, 2), pv = FinArgOr(f, 3, 0), type = FinArgOr(f, 4, 0);
- return rate == 0 ? -(pv + pmt * nper) : -(pv * Pow1(rate, nper) + pmt * AnnuityFactor(rate, nper, type));
+ var arguments = new double[6];
+ for (int i = 0; i < f.Arguments.Count; i++)
+ {
+ if (Evaluate(f.Arguments[i]) is not { } value)
+ return null;
+ arguments[i] = Dbl(ConversionNumber(value));
+ }
+ return compute(arguments);
}
- /// VBA PV(rate, nper, pmt, [fv=0], [type=0]): the present value of an annuity.
- private object Pv(FunctionCall f)
+ /// A "type" argument: payment at the start of each period when it rounds (half to even) to anything but 0
+ /// (verified vs ACE: 0.5 is the end, 1.5, 2 and -1 the start).
+ private static bool Due(double type) => Math.Round(type, MidpointRounding.ToEven) != 0;
+
+ private static ArgumentException OutOfRange(string argument) =>
+ new($"Invalid procedure call: {argument} is out of range.");
+
+ /// Access Pmt(rate, nper, pv, [fv], [type]): the payment each period. An nper of 0 is out of range.
+ private static double Pmt(double rate, double nper, double pv, double fv, bool due)
{
- double rate = FinArg(f, 0), nper = FinArg(f, 1), pmt = FinArg(f, 2), fv = FinArgOr(f, 3, 0), type = FinArgOr(f, 4, 0);
- return rate == 0 ? -(fv + pmt * nper) : -(fv + pmt * AnnuityFactor(rate, nper, type)) / Pow1(rate, nper);
+ if (nper == 0)
+ throw OutOfRange("NPer");
+ if (rate == 0)
+ return (-fv - pv) / nper;
+ double start = due ? 1 + rate : 1;
+ double growth = Math.Pow(rate + 1, nper);
+ return (-fv - pv * growth) / (start * (growth - 1)) * rate;
}
- /// VBA NPer(rate, pmt, pv, [fv=0], [type=0]): the number of periods for an annuity.
- private object NPer(FunctionCall f)
+ /// Access FV(rate, nper, pmt, [pv], [type]): the value after the last payment.
+ private static double Fv(double rate, double nper, double pmt, double pv, bool due)
{
- double rate = FinArg(f, 0), pmt = FinArg(f, 1), pv = FinArg(f, 2), fv = FinArgOr(f, 3, 0), type = FinArgOr(f, 4, 0);
- if (rate == 0) return -(pv + fv) / pmt;
- double a = pmt * (1 + rate * type);
- return Math.Log((a - fv * rate) / (a + pv * rate)) / Math.Log(1 + rate);
+ if (rate == 0)
+ return -pv - pmt * nper;
+ double start = due ? 1 + rate : 1;
+ double growth = Math.Pow(1 + rate, nper);
+ return -(pv * growth + pmt * start * (growth - 1) / rate);
}
- /// VBA IPmt(rate, per, nper, pv, [fv=0], [type=0]): the interest portion of payment per.
- private object IPmt(FunctionCall f)
+ /// Access PV(rate, nper, pmt, [fv], [type]): the value before the first payment.
+ private static double Pv(double rate, double nper, double pmt, double fv, bool due)
{
- double rate = FinArg(f, 0), per = FinArg(f, 1), nper = FinArg(f, 2), pv = FinArg(f, 3), fv = FinArgOr(f, 4, 0), type = FinArgOr(f, 5, 0);
- double pmt = rate == 0 ? -(pv + fv) / nper : -(pv * Pow1(rate, nper) + fv) / AnnuityFactor(rate, nper, type);
- if (type == 1 && per == 1) return 0.0;
- double balance = pv * Pow1(rate, per - 1) + pmt * (rate == 0 ? per - 1 : AnnuityFactor(rate, per - 1, type));
- double interest = -balance * rate; // interest paid is a cash outflow (negative), like Pmt
- return type == 1 ? interest / (1 + rate) : interest;
+ if (rate == 0)
+ return -fv - pmt * nper;
+ double start = due ? 1 + rate : 1;
+ double growth = Math.Pow(1 + rate, nper);
+ return -(fv + pmt * start * (growth - 1) / rate) / growth;
+ }
+
+ /// Access NPer(rate, pmt, pv, [fv], [type]): the number of payments. A rate of -1 or less, no
+ /// payment at a rate of 0, or payments that never reach the future value are out of range.
+ private static double NPer(double rate, double pmt, double pv, double fv, bool due)
+ {
+ if (rate <= -1)
+ throw OutOfRange("Rate");
+ if (rate == 0)
+ return pmt == 0 ? throw OutOfRange("Pmt") : -(pv + fv) / pmt;
+ double payment = due ? pmt * (1 + rate) / rate : pmt / rate;
+ double future = -fv + payment, present = pv + payment;
+ if (future < 0 && present < 0)
+ (future, present) = (-future, -present);
+ else if (future <= 0 || present <= 0)
+ throw OutOfRange("Pmt");
+ return (Math.Log(future) - Math.Log(present)) / Math.Log(rate + 1);
}
- /// VBA PPmt(...): the principal portion of a payment (= Pmt − IPmt).
- private object PPmt(FunctionCall f)
+ /// Access IPmt(rate, per, nper, pv, [fv], [type]): the interest in payment per, which must be
+ /// above 0 and below nper + 1.
+ private static double IPmt(double rate, double per, double nper, double pv, double fv, bool due)
+ {
+ if (per <= 0 || per >= nper + 1)
+ throw OutOfRange("Per");
+ if (due && per == 1)
+ return 0;
+ double pmt = Pmt(rate, nper, pv, fv, due);
+ if (due)
+ pv += pmt;
+ return Fv(rate, per - (due ? 2 : 1), pmt, pv, false) * rate;
+ }
+
+ /// Access PPmt(rate, per, nper, pv, [fv], [type]): the principal in payment per.
+ private static double PPmt(double rate, double per, double nper, double pv, double fv, bool due)
{
- double rate = FinArg(f, 0), nper = FinArg(f, 2), pv = FinArg(f, 3), fv = FinArgOr(f, 4, 0), type = FinArgOr(f, 5, 0);
- double pmt = rate == 0 ? -(pv + fv) / nper : -(pv * Pow1(rate, nper) + fv) / AnnuityFactor(rate, nper, type);
- return pmt - Convert.ToDouble(IPmt(f), CultureInfo.InvariantCulture);
+ if (per <= 0 || per >= nper + 1)
+ throw OutOfRange("Per");
+ return Pmt(rate, nper, pv, fv, due) - IPmt(rate, per, nper, pv, fv, due);
}
- /// VBA DDB(cost, salvage, life, period, [factor=2]): double-declining-balance depreciation.
- private object Ddb(FunctionCall f)
+ ///
+ /// Access Rate(nper, pmt, pv, [fv], [type], [guess]): the rate per period, found by the secant method from the
+ /// guess (0.1 when omitted). An nper of 0 or less, or no answer within 40 steps, is out of range.
+ ///
+ private static double Rate(double nper, double pmt, double pv, double fv, bool due, double guess)
{
- double cost = FinArg(f, 0), salvage = FinArg(f, 1), life = FinArg(f, 2), period = FinArg(f, 3), factor = FinArgOr(f, 4, 2);
- double rate = factor / life;
- double bookStart = cost * Math.Pow(1 - rate, period - 1);
- double dep = bookStart * rate;
- if (bookStart - dep < salvage) dep = Math.Max(0, bookStart - salvage);
- return dep;
+ const double step = 0.00001, epsilon = 0.0000001;
+ if (nper <= 0)
+ throw OutOfRange("NPer");
+ double Error(double rate)
+ {
+ if (rate == 0)
+ return pv + pmt * nper + fv;
+ double growth = Math.Pow(rate + 1, nper);
+ double start = due ? 1 + rate : 1;
+ return pv * growth + pmt * start * (growth - 1) / rate + fv;
+ }
+
+ double rate0 = guess, error0 = Error(rate0);
+ double rate1 = error0 > 0 ? rate0 / 2 : rate0 * 2, error1 = Error(rate1);
+ for (int i = 0; i < 40; i++)
+ {
+ if (error1 == error0)
+ {
+ rate0 = rate1 > rate0 ? rate0 - step : rate0 + step;
+ error0 = Error(rate0);
+ if (error1 == error0)
+ throw OutOfRange("Rate");
+ }
+ rate0 = rate1 - (rate1 - rate0) * error1 / (error1 - error0);
+ error0 = Error(rate0);
+ if (Math.Abs(error0) < epsilon)
+ return rate0;
+ (error0, error1) = (error1, error0);
+ (rate0, rate1) = (rate1, rate0);
+ }
+ throw OutOfRange("Rate");
}
- /// VBA Rate(nper, pmt, pv, [fv=0], [type=0], [guess=0.1]): the per-period rate, solved by
- /// Newton–Raphson on the annuity equation.
- private object Rate(FunctionCall f)
+ /// Access SLN(cost, salvage, life): straight-line depreciation. A life of 0 is out of range.
+ private static double Sln(double cost, double salvage, double life) =>
+ life == 0 ? throw OutOfRange("Life") : (cost - salvage) / life;
+
+ /// Access SYD(cost, salvage, life, period): sum-of-years'-digits depreciation. A negative salvage, or a
+ /// period of 0 or less or past the life, is out of range.
+ private static double Syd(double cost, double salvage, double life, double period)
{
- double nper = FinArg(f, 0), pmt = FinArg(f, 1), pv = FinArg(f, 2), fv = FinArgOr(f, 3, 0), type = FinArgOr(f, 4, 0);
- double r = FinArgOr(f, 5, 0.1);
- for (int iter = 0; iter < 100; iter++)
+ if (salvage < 0 || period > life || period <= 0)
+ throw OutOfRange("Period");
+ return (cost - salvage) * (life - period + 1) / (life * (life + 1) / 2);
+ }
+
+ ///
+ /// Access DDB(cost, salvage, life, period, [factor]): declining-balance depreciation at factor (2 when
+ /// omitted) over the life. A factor of 0 or less, a negative salvage, or a period of 0 or less or past the life is
+ /// out of range. A cost of 0 or less depreciates nothing, and a period's depreciation never takes the value below
+ /// the salvage.
+ ///
+ private static double Ddb(double cost, double salvage, double life, double period, double factor)
+ {
+ if (factor <= 0 || salvage < 0 || period <= 0 || period > life)
+ throw OutOfRange("Period");
+ if (cost <= 0)
+ return 0;
+ if (life < 2)
+ return cost - salvage;
+ if (life == 2)
+ return period > 1 ? 0 : cost - salvage;
+ if (period <= 1)
{
- double v = r == 0 ? pv + pmt * nper + fv
- : pv * Pow1(r, nper) + pmt * (1 + r * type) * (Pow1(r, nper) - 1) / r + fv;
- const double h = 1e-6;
- double vh = (r + h) == 0 ? pv + pmt * nper + fv
- : pv * Pow1(r + h, nper) + pmt * (1 + (r + h) * type) * (Pow1(r + h, nper) - 1) / (r + h) + fv;
- double deriv = (vh - v) / h;
- if (Math.Abs(deriv) < 1e-12) break;
- double next = r - v / deriv;
- if (Math.Abs(next - r) < 1e-10) return next;
- r = next;
+ double first = cost * factor / life, most = cost - salvage;
+ return first > most ? most : first;
}
- return r;
+ double remaining = (life - factor) / life;
+ double depreciation = factor * cost / life * Math.Pow(remaining, period - 1);
+ double excess = cost * (1 - Math.Pow(remaining, period)) - cost + salvage;
+ if (excess > 0)
+ depreciation -= excess;
+ return depreciation >= 0 ? depreciation : 0;
}
private uint _localRandSeed = 0x50000; // used when no connection-scoped SessionState is available
@@ -1035,11 +1342,13 @@ private object Rate(FunctionCall f)
/// number = 0 repeats the last value; number < 0 reseeds deterministically from the
/// argument's Single bit pattern. The seed is connection-scoped (via ) since the
/// JES has no Randomize. Result is a Single widened to Double, matching ACE.
- private object Rnd(FunctionCall f)
+ private object? Rnd(FunctionCall f)
{
uint seed = session?.RandSeed ?? _localRandSeed;
- double arg = f.Arguments.Count > 0 && Evaluate(f.Arguments[0]) is { } a
- ? Convert.ToDouble(a, CultureInfo.InvariantCulture) : 1.0;
+ // The argument is read as a Single, as conversion functions read it; Null gives Null (ACE raises
+ // "Invalid use of Null").
+ if (Optional(f, 0, 1.0, a => Finite(Sng(ConversionNumber(a)))) is not { } arg)
+ return null;
if (arg != 0)
{
if (arg < 0)
@@ -1061,23 +1370,50 @@ private static int RandomLong()
return value;
}
- /// Access ROUND(number[, digits]): banker's rounding, preserving the operand's type (a double
- /// rounds to a double, a decimal to a decimal — the EF contract). NULL-propagating.
+ ///
+ /// Access Round(number[, places]) (verified vs ACE): half to even on the number's decimal form — a Double or
+ /// Single as OLE Automation writes it, so Round(2.675, 2) is 2.68 and Round('2.55', 1) 2.6 — and the
+ /// number unchanged when nothing is cut. The operand's type is kept (the EF contract); text and dates give a
+ /// Double and True is -1; a number written with a decimal point rounds as written. Places are read as a number and
+ /// rounded; Null places give Null, fewer than 0 are an invalid procedure call, and more than a Decimal holds leave
+ /// the number as it is.
+ ///
private object? Round(FunctionCall f)
{
- object? value = Evaluate(f.Arguments[0]);
- if (value is null) return null;
- int digits = f.Arguments.Count > 1
- ? Convert.ToInt32(Evaluate(f.Arguments[1]), CultureInfo.InvariantCulture)
- : 0;
+ if (Evaluate(f.Arguments[0]) is not { } value || Optional(f, 1, 0, v => Count(v)) is not { } places)
+ return null;
+ places = Math.Min(places, 28);
+
return value switch
{
- decimal m => Math.Round(m, digits, MidpointRounding.ToEven),
- double d => Math.Round(d, digits, MidpointRounding.ToEven),
- float s => (float)Math.Round((double)s, digits, MidpointRounding.ToEven),
- long l => (long)Math.Round((decimal)l, digits, MidpointRounding.ToEven),
- _ => (int)Math.Round(Convert.ToDecimal(value, CultureInfo.InvariantCulture), digits, MidpointRounding.ToEven),
+ decimal m => decimal.Round(m, places, MidpointRounding.ToEven),
+ double d when WrittenValue(f.Arguments[0]) is decimal written =>
+ RoundWritten(d, places, () => written, r => (double)r),
+ double d => RoundDouble(d, places),
+ float s => RoundWritten(s, places, () => JetDecimalConverter.FromSingle(s), r => (float)r),
+ long or ulong => Lng(value),
+ int or short or byte or bool => Int(value),
+ _ => RoundDouble(Dbl(Serial(NumericOperand(value)!)), places),
};
+
+ static object RoundDouble(double d, int places) =>
+ RoundWritten(d, places, () => JetDecimalConverter.FromDouble(d), r => (double)r);
+
+ // A number past a Decimal has no places left to cut.
+ static object RoundWritten(T original, int places, Func written, Func back) where T : notnull
+ {
+ decimal exact;
+ try
+ {
+ exact = written();
+ }
+ catch (OverflowException)
+ {
+ return original;
+ }
+ decimal rounded = decimal.Round(exact, places, MidpointRounding.ToEven);
+ return rounded == exact ? original : back(rounded);
+ }
}
/// Applies a conversion to a single argument, propagating NULL.
@@ -1087,29 +1423,62 @@ private static int RandomLong()
return value is null ? null : convert(value);
}
- /// Access CDate: a date passes through, a string is parsed, a number is an OLE Automation date
- /// (days since 1899-12-30).
- private static object ToDate(object v) => v switch
+ ///
+ /// Access CDate (verified vs ACE): a date passes through; text is read as a date and time the way OLE
+ /// Automation reads it (), and otherwise as a number; a number is the date at that
+ /// serial (True is -1, 1899-12-29).
+ ///
+ private static DateTime ToDate(object v) => v switch
{
DateTime d => d,
- string s => DateTime.Parse(s, CultureInfo.InvariantCulture),
- _ => DateTime.FromOADate(Convert.ToDouble(v, CultureInfo.InvariantCulture)),
+ _ when TryDateText(v, out DateTime parsed) => parsed,
+ _ => OaDate(Dbl(ConversionNumber(v))),
};
- /// Access IsDate: true only for a date value or a string that parses as a date/time. A number,
- /// NULL, or an unrecognisable string is false (verified vs ACE — unlike CDate, a bare number is not a
- /// date here).
- private static bool IsDateValue(object? v) => v switch
+ /// A conversion function's argument as a number: text read as one (), a date
+ /// as its serial and a Boolean as -1 or 0; a GUID or binary value is a type mismatch.
+ internal static object ConversionNumber(object v) => Numeric(Serial(NumericOperand(v)!));
+
+ ///
+ /// A CCur or CDec argument as a Decimal, exactly where it can be: a number written with a decimal
+ /// point as written, and text that reads as a number within a Decimal's range as it reads (verified vs ACE:
+ /// CCur('12345678901234.5678') and CCur(12345678901234.5678) keep every place). Anything else is read as the
+ /// conversion functions read it. Null for a Null argument.
+ ///
+ private object? DecimalArgument(FunctionCall f, Func convert)
{
- null => false,
- DateTime => true,
- string s => DateTime.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.None, out _),
- _ => false,
- };
+ if (WrittenValue(f.Arguments[0]) is decimal written)
+ return convert(written);
+ return Evaluate(f.Arguments[0]) switch
+ {
+ null => null,
+ (string or char) and var text when TextAsDecimal(text.ToString()!) is decimal exact => convert(exact),
+ var value => convert(ArithmeticDecimal(ConversionNumber(value))),
+ };
+ }
- /// A (string, int) → string function (LEFT/RIGHT), propagating NULL on the string argument.
- // Left/Right: NULL string propagates; a NULL length raises "Data type mismatch" and a negative length
- // "Invalid procedure call" — matching ACE (which errors rather than clamping). A zero length yields "".
+ /// Access CCur: the value to four places, half to even; past a Currency is an overflow.
+ private static decimal ToCurrency(decimal number)
+ {
+ decimal value = decimal.Round(number, 4, MidpointRounding.ToEven);
+ return Math.Abs(value) <= 922337203685477.5807m || value == -922337203685477.5808m
+ ? value
+ : throw new OverflowException($"Overflow: {value} is outside the range of a Currency.");
+ }
+
+ /// A date, or text that reads as one (); a number or Null is not.
+ private static bool TryDateText(object? v, out DateTime value)
+ {
+ value = v as DateTime? ?? default;
+ return v is DateTime || v is string text && VbaDateText.TryParse(text, CultureInfo.CurrentCulture, out value);
+ }
+
+ /// A DateValue or TimeValue argument: a date or text that reads as one, else a type mismatch.
+ private static DateTime DateValueArgument(object v) =>
+ TryDateText(v, out DateTime value) ? value : throw new InvalidCastException($"Type mismatch: '{ConcatText(v)}' is not a date.");
+
+ /// Left and Right: the text's first or last characters. A Null length raises "Data type mismatch" as
+ /// ACE does, and a negative one is an invalid procedure call.
private object? StringInt(FunctionCall f, Func op)
{
object? s = Evaluate(f.Arguments[0]);
@@ -1117,59 +1486,48 @@ private static int RandomLong()
object? nv = Evaluate(f.Arguments[1]);
if (nv is null)
throw new InvalidOperationException("Data type mismatch in criteria expression: length argument is null.");
- int n = Convert.ToInt32(nv, CultureInfo.InvariantCulture);
- if (n < 0)
- throw new InvalidOperationException("Invalid procedure call: length cannot be negative.");
- return op(s.ToString()!, n);
+ return op(ConcatText(s), Count(nv));
}
- /// Access MID(string, start[, length]) — a 1-based substring; length omitted means to the end.
+ /// Access MID(string, start[, length]) — a 1-based substring; length omitted means to the end. A start
+ /// below 1 or a negative length is an invalid procedure call.
private object? Mid(FunctionCall f)
{
object? sv = Evaluate(f.Arguments[0]);
- if (sv is null) return null; // Mid propagates NULL on the string argument
- string s = sv.ToString()!;
- int start = Convert.ToInt32(Evaluate(f.Arguments[1]), CultureInfo.InvariantCulture);
- if (start < 1) // ACE errors rather than clamping
- throw new InvalidOperationException("Invalid procedure call: Mid() start must be >= 1.");
+ if (sv is null || CountArgument(f, 1, least: 1) is not { } start
+ || CountArgument(f, 2, absent: int.MaxValue) is not { } length)
+ return null;
+ string s = ConcatText(sv);
int from = start - 1;
if (from >= s.Length) return "";
- int avail = s.Length - from;
- int len = avail;
- if (f.Arguments.Count > 2 && Evaluate(f.Arguments[2]) is { } lenVal)
- {
- int requested = Convert.ToInt32(lenVal, CultureInfo.InvariantCulture);
- if (requested < 0) // ACE errors on a negative length
- throw new InvalidOperationException("Invalid procedure call: Mid() length cannot be negative.");
- len = Math.Min(requested, avail);
- }
- return s.Substring(from, len);
+ return s.Substring(from, Math.Min(length, s.Length - from));
}
- /// Access INSTR([start,] string1, string2[, compare]) — the 1-based position of string2 in
- /// string1 (0 if not found). start defaults to 1; compare 0 = binary (case-sensitive), else text.
+ /// Access INSTR([start,] string1, string2[, compare]) — the 1-based position of string2 in string1, 0 if
+ /// it is not there. An empty string2 is found at start, wherever that is, unless string1 is empty; a start below 1
+ /// is an invalid procedure call.
private object? Instr(FunctionCall f)
{
int argc = f.Arguments.Count;
// 2 args: (s1, s2); 3+: (start, s1, s2[, compare]).
- int start = argc >= 3 ? Convert.ToInt32(Evaluate(f.Arguments[0]), CultureInfo.InvariantCulture) : 1;
+ if ((argc >= 3 ? CountArgument(f, 0, least: 1) : 1) is not { } start)
+ return null;
object? s1v = Evaluate(f.Arguments[argc >= 3 ? 1 : 0]);
object? s2v = Evaluate(f.Arguments[argc >= 3 ? 2 : 1]);
- if (s1v is null || s2v is null) return null;
- StringComparison cmp = argc >= 4 && Convert.ToInt32(Evaluate(f.Arguments[3]), CultureInfo.InvariantCulture) == 0
- ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
+ if (s1v is null || s2v is null || BinaryCompare(f, 3) is not { } binary)
+ return null;
// A byte[] argument (e.g. a 0xNN hex literal needle, or a binary haystack) coerces to a string one
// char per byte, matching Jet — not "System.Byte[]".
string s1 = ToJetString(s1v), s2 = ToJetString(s2v);
- if (start < 1) start = 1;
+ if (s1.Length == 0) return 0;
+ if (s2.Length == 0) return start;
if (start > s1.Length) return 0;
- int idx = s1.IndexOf(s2, start - 1, cmp);
- return idx < 0 ? 0 : idx + 1;
+ return FindText(s1, s2, start - 1, binary).Index + 1;
}
- /// Access REPLACE(string, find, replace[, start[, count[, compare]]]) — replaces occurrences of
- /// find (from the 1-based start, at most count times, case-insensitive by default).
+ /// Access REPLACE(string, find, replace[, start[, count[, compare]]]) — the text from start on, with
+ /// find replaced at most count times (all when count is -1). A start below 1 is an invalid procedure call.
private object? Replace(FunctionCall f)
{
object? sv = Evaluate(f.Arguments[0]), findv = Evaluate(f.Arguments[1]), replv = Evaluate(f.Arguments[2]);
@@ -1179,14 +1537,12 @@ private static int RandomLong()
// changes, only ones that error start returning NULL.
if (sv is null || findv is null || replv is null)
return null;
- string s = sv.ToString()!, find = findv.ToString()!, repl = replv.ToString()!;
+ string s = ConcatText(sv), find = ConcatText(findv), repl = ConcatText(replv);
- int start = f.Arguments.Count > 3 ? Convert.ToInt32(Evaluate(f.Arguments[3]), CultureInfo.InvariantCulture) : 1;
- if (start < 1) // ACE errors rather than clamping
- throw new InvalidOperationException("Invalid procedure call: Replace() start must be >= 1.");
- int count = f.Arguments.Count > 4 ? Convert.ToInt32(Evaluate(f.Arguments[4]), CultureInfo.InvariantCulture) : -1;
- StringComparison cmp = f.Arguments.Count > 5 && Convert.ToInt32(Evaluate(f.Arguments[5]), CultureInfo.InvariantCulture) == 0
- ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
+ if (CountArgument(f, 3, least: 1, absent: 1) is not { } start
+ || CountArgument(f, 4, least: -1, absent: -1) is not { } count
+ || BinaryCompare(f, 5) is not { } binary)
+ return null;
if (start > s.Length) return "";
s = s[(start - 1)..];
@@ -1196,114 +1552,321 @@ private static int RandomLong()
int pos = 0, replaced = 0;
while (true)
{
- int j = (count >= 0 && replaced >= count) ? -1 : s.IndexOf(find, pos, cmp);
+ (int j, int length) = count >= 0 && replaced >= count ? (-1, 0) : FindText(s, find, pos, binary);
if (j < 0) { sb.Append(s.AsSpan(pos)); break; }
sb.Append(s, pos, j - pos).Append(repl);
- pos = j + find.Length;
+ pos = j + length;
replaced++;
}
return sb.ToString();
}
- /// Applies a numeric transform to a single argument, propagating NULL.
- /// A numeric transform (Fix/Int/Abs) that **preserves the operand's type** (double→double,
- /// single→single, decimal→decimal, int→int, long→long) so it matches EF's Math.* return type. Integer
- /// types use the exact decimal op (no floating round-trip). NULL-propagating.
- private object? Numeric1(FunctionCall f, Func dOp, Func mOp) =>
+ ///
+ /// Fix, Int and Abs, keeping the operand's type (double→double, single→single, decimal→decimal, long→long, and
+ /// the narrower integers and Booleans→int) so it matches EF's Math.* return type; NULL-propagating. Text is read
+ /// as a number and gives a Double, True is -1, and an Integer result past a Long widens to a Double (verified vs
+ /// ACE: Abs(-2147483648) is 2147483648). A date is its serial: Int and Fix give a date back
+ /// (), Abs a Double.
+ ///
+ private object? Numeric1(FunctionCall f, Func dOp, Func mOp, bool keepsDate) =>
Evaluate(f.Arguments[0]) switch
{
null => null,
decimal m => mOp(m),
double d => dOp(d),
float s => (float)dOp(s),
- long l => (long)mOp(l),
- var v => (int)mOp(Convert.ToDecimal(v!, CultureInfo.InvariantCulture)),
+ long l => checked((long)mOp(l)),
+ DateTime d => keepsDate ? OaDate(dOp(d.ToOADate())) : dOp(d.ToOADate()),
+ var v when v is string or char or Guid or byte[] => dOp(Dbl(NumericOperand(v)!)),
+ var v => mOp(Dec(v)) is var r && r >= int.MinValue && r <= int.MaxValue ? (object)(int)r : (double)r,
};
- /// Applies a double-precision transform to a single argument, propagating NULL. Used for
- /// the trig/exp/log/sqrt VBA functions, which are inherently floating-point.
+ ///
+ /// Sqr, Exp, Log, Sin, Cos, Tan and Atn, on a Double read as the conversion functions read it; NULL-propagating.
+ /// A result that is not a number — the square root or log of a number below its domain, or a trigonometric
+ /// function of a number too large to reduce — is an invalid procedure call, and a result past a Double an
+ /// overflow (verified vs ACE).
+ ///
private object? UnaryDouble(FunctionCall f, Func op)
{
- object? value = Evaluate(f.Arguments[0]);
- return value is null ? null : op(Convert.ToDouble(value, CultureInfo.InvariantCulture));
+ if (Evaluate(f.Arguments[0]) is not { } value)
+ return null;
+ double x = Dbl(ConversionNumber(value));
+ double result = op(x);
+ return double.IsNaN(result) || double.IsNegativeInfinity(result) && x == 0
+ ? throw new ArgumentException($"Invalid procedure call: the function is not defined at {x}.")
+ : Finite(result);
}
- /// Access DATEPART(interval, date): extracts a component of a date as an int.
- private static object? DatePart(object? interval, object? date)
+ ///
+ /// A function of two Doubles, read as reads one; NULL-propagating. A result that is not
+ /// a number is an invalid procedure call, and a result past a Double an overflow.
+ ///
+ private object? BinaryDouble(FunctionCall f, Func op)
{
- if (date is null) return null;
- var d = Convert.ToDateTime(date, CultureInfo.InvariantCulture);
- return (interval?.ToString() ?? "").ToLowerInvariant() switch
+ if (Evaluate(f.Arguments[0]) is not { } left || Evaluate(f.Arguments[1]) is not { } right)
+ return null;
+ double x = Dbl(ConversionNumber(left)), y = Dbl(ConversionNumber(right));
+ double result = op(x, y);
+ return double.IsNaN(result)
+ ? throw new ArgumentException($"Invalid procedure call: {f.Name} is not defined at {x}, {y}.")
+ : Finite(result);
+ }
+
+ /// A trigonometric function, which ACE refuses for an argument of 9.223372E18 or more (verified).
+ private static Func Trigonometric(Func op) =>
+ x => Math.Abs(x) >= 9.223372E18 ? double.NaN : op(x);
+
+ /// Access DatePart(interval, date, [firstdayofweek], [firstweekofyear]): a component of a date.
+ /// "ms", "mcs" and "ns" are LibRed extensions.
+ private object? DatePart(FunctionCall f)
+ {
+ if (Evaluate(f.Arguments[0]) is not { } interval || Evaluate(f.Arguments[1]) is not { } date
+ || FirstDayOfWeek(f, 2) is not { } first || FirstWeekOfYear(f, 3) is not { } rule)
+ return null;
+ DateTime d = ToDate(date);
+ return ConcatText(interval).ToLowerInvariant() switch
{
"yyyy" => d.Year,
"q" => (d.Month + 2) / 3,
"m" => d.Month,
"y" => d.DayOfYear,
"d" => d.Day,
- "w" => (int)d.DayOfWeek + 1,
- "ww" => CultureInfo.InvariantCulture.Calendar.GetWeekOfYear(d, CalendarWeekRule.FirstDay, DayOfWeek.Sunday),
+ "w" => DaysIntoWeek(d, first) + 1,
+ "ww" => WeekOfYear(d, first, rule),
"h" => d.Hour,
"n" => d.Minute,
"s" => d.Second,
"ms" => d.Millisecond,
"mcs" => d.Microsecond,
"ns" => d.Nanosecond,
- _ => throw new NotSupportedException($"DATEPART interval '{interval}' is not supported."),
+ _ => throw UnknownInterval(interval),
};
}
- /// A bitwise op (Access BAND/BOR/BXOR) over integer operands; the result
- /// keeps the operand's int type (Int32, or Int64 if either operand is long).
- private static object BitwiseOp(object a, object b, Func op) =>
- a is long or ulong || b is long or ulong ? (object)op(Lng(a), Lng(b)) : (int)op(Int(a), Int(b));
+ private static ArgumentException UnknownInterval(object interval) =>
+ new($"Invalid procedure call: '{interval}' is not a date interval.");
+
+ /// Access Weekday(date, [firstdayofweek]): the day's 1-based position in the week.
+ private object? Weekday(FunctionCall f) =>
+ Evaluate(f.Arguments[0]) is { } date && FirstDayOfWeek(f, 1) is { } first
+ ? DaysIntoWeek(ToDate(date), first) + 1
+ : null;
+
+ ///
+ /// A firstdayofweek argument: 1 (Sunday) to 7 (Saturday), or 0 for the system's first day; anything else is an
+ /// invalid procedure call (verified vs ACE: 0 is Monday under en-AU, and 8 and -1 are refused). Null when the
+ /// argument is Null.
+ ///
+ private DayOfWeek? FirstDayOfWeek(FunctionCall f, int index, int absent = 1) =>
+ Optional(f, index, absent, v => Setting(v, 0, 7)) switch
+ {
+ null => null,
+ 0 => CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek,
+ var day => (DayOfWeek)(day - 1),
+ };
+
+ /// A firstweekofyear argument: 1 (the week of January 1), 2 (the first week with four days), 3 (the first
+ /// full week), or 0 for the system's rule; anything else is an invalid procedure call. Null when it is Null.
+ private CalendarWeekRule? FirstWeekOfYear(FunctionCall f, int index) =>
+ Optional(f, index, 1, v => Setting(v, 0, 3)) switch
+ {
+ null => null,
+ 0 => CultureInfo.CurrentCulture.DateTimeFormat.CalendarWeekRule,
+ 1 => CalendarWeekRule.FirstDay,
+ 2 => CalendarWeekRule.FirstFourDayWeek,
+ _ => CalendarWeekRule.FirstFullWeek,
+ };
+
+ /// A setting read as CInt reads it (half to even; verified vs ACE: '2' and 1.5 are 2); outside
+ /// - is an invalid procedure call.
+ private static int Setting(object v, int lowest, int highest) => InRange(AsInteger(v), lowest, highest);
- /// A function of a single date argument (Year/Month/Day/…), NULL-propagating.
- private object? DatePartOf(FunctionCall f, Func part)
+ /// How many days of the week come before the date's day, for a week starting on .
+ private static int DaysIntoWeek(DateTime date, DayOfWeek first) => ((int)date.DayOfWeek - (int)first + 7) % 7;
+
+ ///
+ /// The date's week number (verified vs ACE). Days before the year's first week belong to the last week of the year
+ /// before, and under the four-day rule a week with four days in the next year is that year's week 1.
+ ///
+ private static int WeekOfYear(DateTime date, DayOfWeek first, CalendarWeekRule rule)
{
- object? v = Evaluate(f.Arguments[0]);
- return v is null ? null : part(Convert.ToDateTime(v, CultureInfo.InvariantCulture));
+ date = date.Date;
+ if (date < WeekOne(date.Year))
+ return (date - WeekOne(date.Year - 1)).Days / 7 + 1;
+ if (rule == CalendarWeekRule.FirstFourDayWeek && date.Year < 9999 && date >= WeekOne(date.Year + 1))
+ return 1;
+ return (date - WeekOne(date.Year)).Days / 7 + 1;
+
+ DateTime WeekOne(int year)
+ {
+ var january1 = new DateTime(year, 1, 1);
+ int before = DaysIntoWeek(january1, first);
+ return january1.AddDays(rule switch
+ {
+ CalendarWeekRule.FirstDay => -before,
+ CalendarWeekRule.FirstFourDayWeek => before <= 3 ? -before : 7 - before,
+ _ => before == 0 ? 0 : 7 - before,
+ });
+ }
+ }
+
+ ///
+ /// BAND, BOR and BXOR on the operands' bits. When either operand is 16 bits, only the low 16
+ /// bits are combined and the rest are the left operand's: its own upper bits, or the sign of the 16-bit result when
+ /// it is 16 bits itself (verified vs ACE: 1 BOR TRUE is 65535, CLNG(-1) BAND CINT(1) is -65535, CINT(1) BOR 70000
+ /// is 4465, CINT(-2) BOR 1 is -1). Otherwise the result has the wider operand's width. The result is an Integer
+ /// only when both operands are 16 bits, and otherwise a Long or an Int64 (verified vs ACE: TRUE BAND CINT(-2) is
+ /// an Integer, CINT(-2) BAND 70000 and a Byte BAND a Byte are Longs).
+ ///
+ private static object BitwiseOp(object a, object b, Func op)
+ {
+ (long left, int leftWidth) = BitOperand(a);
+ (long right, int rightWidth) = BitOperand(b);
+ if (leftWidth != 16 && rightWidth != 16)
+ return Signed(op(left, right), Math.Max(leftWidth, rightWidth));
+
+ long low = op(left, right) & 0xFFFF;
+ return leftWidth != 16 ? Signed(left & ~0xFFFFL | low, leftWidth)
+ : rightWidth == 16 ? Signed(low, 16)
+ : Signed((short)low, 32);
+ }
+
+ ///
+ /// Unary minus, keeping the operand's type as C# does (an Integer, a Byte or a Boolean gives a Long). Text is read as
+ /// a number first, and a date negates its serial and stays a date (verified vs ACE: -#2020-01-02# is 1779-12-27). A
+ /// result the operand's type cannot hold is an overflow (verified vs ACE: -CINT(-32768) and -CLNG(-2147483648)).
+ ///
+ private static object Negate(object v) => NumericOperand(v)! switch
+ {
+ decimal d => -d,
+ double d => -d,
+ float f => -f,
+ long l => checked(-l),
+ ulong u => checked(-(long)u),
+ DateTime d => OaDate(-d.ToOADate()),
+ short.MinValue => throw new OverflowException("Overflow: an Integer cannot hold 32768."),
+ var n => checked(-Int(n)),
+ };
+
+ /// BNOT: every bit of the operand flipped, an Integer for a Boolean or an Integer (verified vs ACE:
+ /// BNOT of a Yes/No column is an Integer, of a Byte a Long).
+ private static object BitNot(object v)
+ {
+ (long bits, int width) = BitOperand(v);
+ return Signed(~bits, width);
}
- /// DateSerial(y,m,d) / TimeSerial(h,m,s): build a date/time from three integer parts (parts may
- /// be out of range and roll over, matching Access). NULL-propagating.
+ ///
+ /// An operand's bits, unsigned, and how many there are: 16 for a Boolean (True is all 16 set) or an Integer, 32 for a
+ /// Byte or a Long, 64 for an Int64. Anything else is first read as a whole number, as \ reads it: text as a
+ /// number, a date as its serial, then rounded half to even, and it must fit a Long.
+ ///
+ private static (long Bits, int Width) BitOperand(object v) => v switch
+ {
+ bool b => (b ? 0xFFFF : 0, 16),
+ short s => ((ushort)s, 16),
+ byte b => (b, 32),
+ int i => ((uint)i, 32),
+ long l => (l, 64),
+ ulong u => ((long)u, 64),
+ _ => ((uint)Int(Serial(NumericOperand(v)!)), 32),
+ };
+
+ /// The low bits read as a signed number: an Int16 for 16 bits, an Int32 for 32,
+ /// an Int64 for 64.
+ private static object Signed(long bits, int width) => width switch
+ {
+ // Each arm boxed on its own, or the switch would widen them all to Int64.
+ 16 => (object)(short)bits,
+ 32 => (object)(int)bits,
+ _ => (object)bits,
+ };
+
+ /// DateSerial and TimeSerial: a date or time from three Integer parts, read as CInt reads them (verified vs
+ /// ACE: 32768 is an overflow). Parts out of their range carry into the next.
private object? DateParts(FunctionCall f, Func build)
{
- object? a = Evaluate(f.Arguments[0]), b = Evaluate(f.Arguments[1]), c = Evaluate(f.Arguments[2]);
- if (a is null || b is null || c is null) return null;
- return build(Int(a), Int(b), Int(c));
+ if (Evaluate(f.Arguments[0]) is not { } a || Evaluate(f.Arguments[1]) is not { } b || Evaluate(f.Arguments[2]) is not { } c)
+ return null;
+ return build(AsInteger(a), AsInteger(b), AsInteger(c));
}
- /// Access DateAdd(interval, number, date): add number intervals to a date.
+ ///
+ /// Access DateSerial (verified vs ACE): the month carries into the year first, then a year below 100 takes
+ /// its century from the calendar's two-digit year window (so 49 is 2049, 50 is 1950, -1 is 1999 and
+ /// DateSerial(100, 0, 1) is 1999-12-01), then the day carries. A year outside 100-9999 is an invalid procedure call.
+ ///
+ private static DateTime DateSerial(int year, int month, int day)
+ {
+ int months = year * 12 + month - 1;
+ year = (int)Math.Floor(months / 12.0);
+ month = months - year * 12 + 1;
+ if (year < 100)
+ {
+ int latest = CultureInfo.CurrentCulture.Calendar.TwoDigitYearMax;
+ int century = latest - latest % 100;
+ year += year <= latest % 100 ? century : century - 100;
+ }
+ return InDateRange(() => new DateTime(year, month, 1).AddDays(day - 1));
+ }
+
+ /// A computed date, or an invalid procedure call when it falls outside 100-01-01 … 9999-12-31.
+ private static DateTime InDateRange(Func compute)
+ {
+ try
+ {
+ DateTime date = compute();
+ if (date.Year >= 100)
+ return date;
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ throw new ArgumentException("Invalid procedure call: the date is out of range.");
+ }
+
+ ///
+ /// Access DateAdd(interval, number, date) (verified vs ACE): the number is truncated (True is -1), a month
+ /// step keeps the day where the month has it and takes the month's last day otherwise, and a result outside
+ /// 100-9999 is an invalid procedure call. "ms" is a LibRed extension.
+ ///
private object? DateAdd(FunctionCall f)
{
- object? intervalV = Evaluate(f.Arguments[0]), numberV = Evaluate(f.Arguments[1]), dateV = Evaluate(f.Arguments[2]);
- if (dateV is null || numberV is null) return null;
- int n = (int)Math.Truncate(Convert.ToDouble(numberV, CultureInfo.InvariantCulture)); // Access truncates
- var d = Convert.ToDateTime(dateV, CultureInfo.InvariantCulture);
- return (intervalV?.ToString() ?? "").ToLowerInvariant() switch
- {
- "yyyy" => d.AddYears(n),
- "q" => d.AddMonths(n * 3),
- "m" => d.AddMonths(n),
- "y" or "d" or "w" => d.AddDays(n),
- "ww" => d.AddDays(n * 7),
- "h" => d.AddHours(n),
- "n" => d.AddMinutes(n),
- "s" => d.AddSeconds(n),
- "ms" => d.AddMilliseconds(n),
- _ => throw new NotSupportedException($"DATEADD interval '{intervalV}' is not supported."),
+ if (Evaluate(f.Arguments[0]) is not { } interval || Evaluate(f.Arguments[1]) is not { } number
+ || Evaluate(f.Arguments[2]) is not { } date)
+ return null;
+ double n = Math.Truncate(Dbl(ConversionNumber(number)));
+ DateTime d = ToDate(date);
+ Func add = ConcatText(interval).ToLowerInvariant() switch
+ {
+ "yyyy" => () => d.AddYears(Int(n)),
+ "q" => () => d.AddMonths(checked(Int(n) * 3)),
+ "m" => () => d.AddMonths(Int(n)),
+ "y" or "d" or "w" => () => d.AddDays(n),
+ "ww" => () => d.AddDays(n * 7),
+ "h" => () => d.AddHours(n),
+ "n" => () => d.AddMinutes(n),
+ "s" => () => d.AddSeconds(n),
+ "ms" => () => d.AddMilliseconds(n),
+ _ => throw UnknownInterval(interval),
};
+ return InDateRange(add);
}
- /// Access DateDiff(interval, date1, date2): the number of interval boundaries from date1 to
- /// date2 (a Long Integer). NULL-propagating.
+ ///
+ /// Access DateDiff(interval, date1, date2, [firstdayofweek], [firstweekofyear]) (verified vs ACE): the
+ /// number of interval boundaries from date1 to date2, as a Long Integer. "w" is whole weeks of days, "ww" counts
+ /// the weeks' first days, and "h", "n" and "s" count hour, minute and second boundaries; a count past a Long
+ /// Integer is an overflow. The first day of the week matters only to "ww", and the first week of the year to none.
+ ///
private object? DateDiff(FunctionCall f)
{
- object? intervalV = Evaluate(f.Arguments[0]), d1V = Evaluate(f.Arguments[1]), d2V = Evaluate(f.Arguments[2]);
- if (d1V is null || d2V is null) return null;
- var d1 = Convert.ToDateTime(d1V, CultureInfo.InvariantCulture);
- var d2 = Convert.ToDateTime(d2V, CultureInfo.InvariantCulture);
- string interval = (intervalV?.ToString() ?? "").ToLowerInvariant();
+ if (Evaluate(f.Arguments[0]) is not { } intervalV || Evaluate(f.Arguments[1]) is not { } d1V
+ || Evaluate(f.Arguments[2]) is not { } d2V)
+ return null;
+ DateTime d1 = ToDate(d1V), d2 = ToDate(d2V);
+ string interval = ConcatText(intervalV).ToLowerInvariant();
// "ms" is a LibRed extension — ACE's interval list stops at "s". It is available because LibRed stores
// the full OA double rather than truncating to whole seconds as ACE does, and it is exact: .NET's OA
@@ -1318,19 +1881,28 @@ private static object BitwiseOp(object a, object b, Func op) =
return (long)(d2 - d1).TotalMilliseconds;
}
+ if (interval == "ww")
+ {
+ if (FirstDayOfWeek(f, 3) is not { } first)
+ return null;
+ return (d2.Date.AddDays(-DaysIntoWeek(d2, first)) - d1.Date.AddDays(-DaysIntoWeek(d1, first))).Days / 7;
+ }
+
return interval switch
{
"yyyy" => d2.Year - d1.Year,
"q" => (d2.Year - d1.Year) * 4 + (d2.Month - 1) / 3 - (d1.Month - 1) / 3,
"m" => (d2.Year - d1.Year) * 12 + d2.Month - d1.Month,
- "y" or "d" or "w" => (int)(d2.Date - d1.Date).TotalDays,
- "ww" => (int)((d2.Date - d1.Date).TotalDays / 7),
- "h" => (int)(d2 - d1).TotalHours,
- "n" => (int)(d2 - d1).TotalMinutes,
- "s" => (int)(d2 - d1).TotalSeconds,
+ "y" or "d" => (d2.Date - d1.Date).Days,
+ "w" => (d2.Date - d1.Date).Days / 7,
+ "h" => Boundaries(TimeSpan.TicksPerHour),
+ "n" => Boundaries(TimeSpan.TicksPerMinute),
+ "s" => Boundaries(TimeSpan.TicksPerSecond),
// "ms" is handled above, as Int64.
- _ => throw new NotSupportedException($"DATEDIFF interval '{intervalV}' is not supported."),
+ _ => throw UnknownInterval(intervalV),
};
+
+ int Boundaries(long unit) => checked((int)(d2.Ticks / unit - d1.Ticks / unit));
}
// Access truthiness: a filter/logical context treats any non-zero number as true (so a boolean stored
@@ -1343,16 +1915,8 @@ private static object BitwiseOp(object a, object b, Func op) =
return u.Operator switch
{
UnaryOperator.Not => AsBool(v) is bool b ? !b : null, // coerce a -1/0 integer boolean too
- UnaryOperator.Negate => v switch // preserve the operand's numeric type (EF contract), like C# unary minus
- {
- null => null,
- decimal d => -d,
- double db => -db,
- float f => -f,
- long or ulong => -Lng(v),
- _ => -Int(v), // int/short/byte → int
- },
- UnaryOperator.BitNot => v is null ? null : v is long or ulong ? (object)~Lng(v) : ~Int(v),
+ UnaryOperator.Negate => v is null ? null : Negate(v),
+ UnaryOperator.BitNot => v is null ? null : BitNot(v),
UnaryOperator.IsNull => v is null,
UnaryOperator.IsNotNull => v is not null,
_ => throw new NotSupportedException($"Unary operator {u.Operator}."),
@@ -1374,36 +1938,104 @@ private static object BitwiseOp(object a, object b, Func op) =
bool? l = AsBool(Evaluate(b.Left));
return l == true ? true : l | AsBool(Evaluate(b.Right));
}
+ // XOR and EQV are Null when either side is; IMP is True whenever its left side is False or its right side
+ // is True, and Null otherwise when a side is Null (the VBA truth tables; verified vs ACE).
+ if (b.Operator is BinaryOperator.Xor or BinaryOperator.Eqv or BinaryOperator.Imp)
+ {
+ bool? l = AsBool(Evaluate(b.Left)), r = AsBool(Evaluate(b.Right));
+ return b.Operator switch
+ {
+ BinaryOperator.Xor => l ^ r,
+ BinaryOperator.Eqv => l is null || r is null ? null : l == r,
+ _ => l == false || r == true ? true : l is null || r is null ? null : false,
+ };
+ }
- object? left = Evaluate(b.Left);
- object? right = Evaluate(b.Right);
+ // A date plus or less a span (IsSpan) is the date moved by it. Which side is the span is settled before
+ // either is evaluated, so neither side is worked out twice; beside anything but a date, the span side is
+ // evaluated as it always is — as the time on the epoch.
+ object? left, right;
+ if (SpanSide(b) is { } spanOnRight)
+ {
+ object? other = Evaluate(spanOnRight ? b.Left : b.Right);
+ if (other is DateTime date)
+ return SpanOf(spanOnRight ? b.Right : b.Left) is { } span
+ ? Shift(date, spanOnRight && b.Operator == BinaryOperator.Subtract ? -span : span)
+ : null;
+ (left, right) = spanOnRight ? (other, Evaluate(b.Right)) : (Evaluate(b.Left), other);
+ }
+ else
+ {
+ left = Evaluate(b.Left);
+ right = Evaluate(b.Right);
+ }
+ // '&' treats a single Null as "" but is Null when both sides are (verified vs ACE).
if (b.Operator == BinaryOperator.Concat)
- return (left is null ? "" : ToText(left)) + (right is null ? "" : ToText(right));
+ return left is null && right is null
+ ? null
+ : (left is null ? "" : ConcatText(left)) + (right is null ? "" : ConcatText(right));
+
+ // The arithmetic operators other than '+' read text as a number even when the other side is Null, so text
+ // that is not a number, a GUID or a binary value is a type mismatch before Null propagates (verified vs
+ // ACE: 'abc' * NULL fails, '1' * NULL is Null).
+ if (b.Operator is BinaryOperator.Subtract or BinaryOperator.Multiply or BinaryOperator.Divide
+ or BinaryOperator.IntDivide or BinaryOperator.Modulo or BinaryOperator.Power)
+ {
+ left = NumericOperand(left);
+ right = NumericOperand(right);
+ }
+
+ // A number written with a decimal point meets a Decimal as the value written, not as the Double it was
+ // parsed to (verified vs ACE: a DECIMAL(18,4) 4.5 * 334.90 is 1507.05, not 1507.04999…).
+ if (b.Operator is BinaryOperator.Add or BinaryOperator.Subtract or BinaryOperator.Multiply or BinaryOperator.Divide)
+ {
+ if (right is decimal && WrittenValue(b.Left) is decimal leftWritten) left = leftWritten;
+ if (left is decimal && WrittenValue(b.Right) is decimal rightWritten) right = rightWritten;
+ }
+
+ // A pattern written as just '%' is False for Null rather than Null, so it reads as IS NOT NULL (verified vs ACE;
+ // '%%', or a '%' the pattern is computed to, stays Null).
+ if (left is null && b.Operator == BinaryOperator.Like && b.Right is LiteralExpression { Value: "%" })
+ return false;
if (left is null || right is null)
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;
+
+ // 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
+ // 1.2345 * 1.2345 is 1.524, 0.0003 * 1.2345 is 0.0004, and the largest Currency + 1 or * 3 is an
+ // overflow). A Currency with a written decimal or a Double is not one, so those are left as they are.
+ if (b.Operator is BinaryOperator.Add or BinaryOperator.Subtract or BinaryOperator.Multiply)
+ {
+ object result = b.Operator switch
+ {
+ BinaryOperator.Add => Add(left, right),
+ BinaryOperator.Subtract => Arithmetic(left, right, '-'),
+ _ => Arithmetic(left, right, '*'),
+ };
+ return result is decimal money && IsCurrency(b) ? ToCurrency(money) : result;
+ }
+
return b.Operator switch
{
- BinaryOperator.Equal => Compare(left, right) == 0,
- BinaryOperator.NotEqual => Compare(left, right) != 0,
- BinaryOperator.LessThan => Compare(left, right) < 0,
- BinaryOperator.LessThanOrEqual => Compare(left, right) <= 0,
- BinaryOperator.GreaterThan => Compare(left, right) > 0,
- BinaryOperator.GreaterThanOrEqual => Compare(left, right) >= 0,
- // LIKE reads a binary value as text, so it is CASE-INSENSITIVE over a binary column even though
- // '=' on the same column is byte-wise and case-sensitive. Verified vs ACE: `B LIKE 'A%'` matches
- // both 0x4100 ('A') and 0x6100 ('a'), while `B = 0x4100` matches only the first.
- BinaryOperator.Like => Like(ToText(left), ToText(right)),
- // Access '+' concatenates when either operand is text (but, unlike '&', null already propagated above).
- BinaryOperator.Add => left is string || right is string ? ToText(left) + ToText(right) : Arithmetic(left, right, '+'),
- BinaryOperator.Subtract => Arithmetic(left, right, '-'),
- BinaryOperator.Multiply => Arithmetic(left, right, '*'),
+ BinaryOperator.Equal => CompareAsKinds(left, right) == 0,
+ BinaryOperator.NotEqual => CompareAsKinds(left, right) != 0,
+ BinaryOperator.LessThan => CompareAsKinds(left, right) < 0,
+ BinaryOperator.LessThanOrEqual => CompareAsKinds(left, right) <= 0,
+ BinaryOperator.GreaterThan => CompareAsKinds(left, right) > 0,
+ BinaryOperator.GreaterThanOrEqual => CompareAsKinds(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').
+ BinaryOperator.Like => LikeMatcher.IsMatch(ConcatText(left), ConcatText(right)),
BinaryOperator.Divide => Divide(left, right), // Access '/' is floating division
BinaryOperator.Modulo => IntegerOp(left, right, '%'),
BinaryOperator.IntDivide => IntegerOp(left, right, '\\'),
- BinaryOperator.Power => Math.Pow(Convert.ToDouble(left, CultureInfo.InvariantCulture), Convert.ToDouble(right, CultureInfo.InvariantCulture)),
+ BinaryOperator.Power => Power(left, right),
BinaryOperator.BitAnd => BitwiseOp(left, right, (x, y) => x & y),
BinaryOperator.BitOr => BitwiseOp(left, right, (x, y) => x | y),
BinaryOperator.BitXor => BitwiseOp(left, right, (x, y) => x ^ y),
@@ -1411,60 +2043,203 @@ private static object BitwiseOp(object a, object b, Func op) =
};
}
- /// SQL LIKE: '%'/'*' match any run, '_'/'?' match one char; case-insensitive.
- private static bool Like(string value, string pattern)
+ ///
+ /// Whether is a span — known before any row is read, as a Currency result is.
+ /// A span starts from a TimeSpan or TimeOnly parameter (); only a parameter can say it
+ /// is one, since a time column or a time written into the SQL is a date on the epoch, as Jet stores it. A span
+ /// stays one under negation, added to or less another span, times or divided by something that is not a span,
+ /// and chosen by IIF, COALESCE or CASE from spans and Nulls. A date plus or less a span is the date moved by it
+ /// () — a date, where a date less a date is a day count.
+ ///
+ internal static bool IsSpan(Expression expression, Func duration)
{
- // Access/Jet LIKE wildcards: * or % = any run, ? or _ = any single char, # = any single DIGIT, and
- // [charlist] / [!charlist] = a (negated) single-char class. A literal special char is escaped by
- // bracketing it — e.g. EF's Contains("C#") emits `%C[#]%`, where [#] matches a literal '#'. Without
- // bracket-class support that pattern would look for the literal text "C[#]" and match nothing.
- var sb = new StringBuilder("^");
- int i = 0;
- while (i < pattern.Length)
+ bool Span(Expression e) => IsSpan(e, duration);
+ bool SpanOrNull(Expression e) => e is LiteralExpression { Value: null } || Span(e);
+ bool Choice(IEnumerable results) => results.All(SpanOrNull) && results.Any(Span);
+
+ return expression switch
{
- char ch = pattern[i];
- if (ch == '[')
+ ParameterExpression parameter => duration(parameter.Name) is not null,
+ UnaryExpression { Operator: UnaryOperator.Negate } negation => Span(negation.Operand),
+ BinaryExpression { Operator: BinaryOperator.Add or BinaryOperator.Subtract } sum => Span(sum.Left) && Span(sum.Right),
+ BinaryExpression { Operator: BinaryOperator.Multiply } product => Span(product.Left) != Span(product.Right),
+ BinaryExpression { Operator: BinaryOperator.Divide } quotient => Span(quotient.Left) && !Span(quotient.Right),
+ FunctionCall call when call.Name.Equals("IIF", StringComparison.OrdinalIgnoreCase) && call.Arguments.Count >= 2 =>
+ Choice(call.Arguments.Skip(1)),
+ FunctionCall call when call.Name.Equals("COALESCE", StringComparison.OrdinalIgnoreCase) => Choice(call.Arguments),
+ CaseExpression @case => Choice(@case.WhenClauses.Select(w => w.Result)
+ .Concat(@case.ElseResult is { } otherwise ? [otherwise] : [])),
+ _ => false,
+ };
+ }
+
+ /// Which side of a + or - is a span that a date beside it would move by: true for the right,
+ /// false for the left (only for +, since a span less a date is no date), null when neither.
+ private bool? SpanSide(BinaryExpression b)
+ {
+ if (parameters is null || b.Operator is not (BinaryOperator.Add or BinaryOperator.Subtract))
+ return null;
+ if (IsSpan(b.Right, parameters.Duration))
+ return true;
+ return b.Operator == BinaryOperator.Add && IsSpan(b.Left, parameters.Duration) ? false : null;
+ }
+
+ /// The value of a span (), to the tick; null when it is Null. A factor or divisor is read
+ /// as a number as the arithmetic operators read one.
+ private TimeSpan? SpanOf(Expression expression)
+ {
+ switch (expression)
+ {
+ case ParameterExpression parameter:
+ return parameters!.Duration(parameter.Name);
+ case LiteralExpression { Value: null }:
+ return null;
+ case UnaryExpression negation:
+ return -SpanOf(negation.Operand);
+ case BinaryExpression { Operator: BinaryOperator.Add or BinaryOperator.Subtract } sum:
+ return SpanOf(sum.Left) is { } a && SpanOf(sum.Right) is { } b
+ ? checked(sum.Operator == BinaryOperator.Add ? a + b : a - b)
+ : null;
+ case BinaryExpression product:
{
- int close = pattern.IndexOf(']', i + 1);
- if (close > i)
- {
- sb.Append(TranslateLikeClass(pattern.Substring(i + 1, close - i - 1)));
- i = close + 1;
- continue;
- }
- // No closing ']' → a literal '['.
+ bool spanOnLeft = product.Operator == BinaryOperator.Divide || IsSpan(product.Left, parameters!.Duration);
+ if (SpanOf(spanOnLeft ? product.Left : product.Right) is not { } span
+ || Evaluate(spanOnLeft ? product.Right : product.Left) is not { } by)
+ return null;
+ double factor = Dbl(ConversionNumber(by));
+ if (product.Operator == BinaryOperator.Divide)
+ factor = factor != 0 ? 1 / factor : throw new DivideByZeroException("Division by zero.");
+ return TimeSpan.FromTicks(checked((long)Math.Round(span.Ticks * factor)));
}
+ case FunctionCall call when call.Name.Equals("IIF", StringComparison.OrdinalIgnoreCase):
+ return Evaluate(call.Arguments[0]) is { } condition && IifCondition(condition)
+ ? SpanOf(call.Arguments[1])
+ : call.Arguments.Count == 3 ? SpanOf(call.Arguments[2]) : null;
+ case FunctionCall coalesce:
+ foreach (Expression argument in coalesce.Arguments)
+ if (SpanOf(argument) is { } first)
+ return first;
+ return null;
+ case CaseExpression @case:
+ foreach (CaseWhen when in @case.WhenClauses)
+ if (IsTrue(when.Condition))
+ return SpanOf(when.Result);
+ return @case.ElseResult is { } otherwise ? SpanOf(otherwise) : null;
+ default:
+ throw new InvalidOperationException($"{expression.GetType().Name} is not a span.");
+ }
+ }
- sb.Append(ch switch
- {
- '%' or '*' => ".*",
- '_' or '?' => ".",
- '#' => "[0-9]",
- _ => Regex.Escape(ch.ToString()),
- });
- i++;
+ /// A date moved by a span, to the tick; outside the dates Jet holds, an overflow.
+ private static DateTime Shift(DateTime date, TimeSpan by)
+ {
+ long ticks = date.Ticks + by.Ticks;
+ return ticks >= new DateTime(100, 1, 1).Ticks && ticks <= DateTime.MaxValue.Ticks
+ ? new DateTime(ticks)
+ : throw new OverflowException($"Overflow: {date} moved by {by} is outside the range of a date.");
+ }
+
+ ///
+ /// = or <> against the literal True or False tests the other side's truth
+ /// rather than comparing it (verified vs ACE): a value that reads as 0 is False and anything else is True, so
+ /// 2 = True, 'abc' = True and '' = True are all True, while '0' = False is True.
+ /// Null for any other comparison.
+ ///
+ private static bool? TruthTest(BinaryExpression b, object left, object right) =>
+ b.Right is LiteralExpression { Value: bool rightLiteral } ? IsTruthy(left) == rightLiteral
+ : b.Left is LiteralExpression { Value: bool leftLiteral } ? IsTruthy(right) == leftLiteral
+ : null;
+
+ ///
+ /// IIf's condition, which unlike the others is rounded to a whole number first, half to even (verified vs ACE:
+ /// 0.4, 0.5, '0.4' and a date at 06:00 on 1899-12-30 are False, 0.6 and 1E+20 are True). Text that is not a
+ /// number is True, as has it.
+ ///
+ private static bool IifCondition(object value) => value switch
+ {
+ bool b => b,
+ string or char => !(TryTextAsNumber(value.ToString()!) is { } number && Math.Round(number, MidpointRounding.ToEven) == 0),
+ DateTime d => Math.Round(d.ToOADate(), MidpointRounding.ToEven) != 0,
+ Guid or byte[] => true,
+ _ => !IsNumeric(value) || Math.Round(Dbl(value), MidpointRounding.ToEven) != 0,
+ };
+
+ /// Text read as reads it, or null when it is not a number or is past a Double.
+ private static double? TryTextAsNumber(string text)
+ {
+ try
+ {
+ return TextAsNumber(text);
+ }
+ catch (Exception e) when (e is InvalidCastException or OverflowException)
+ {
+ return null;
}
- sb.Append('$');
- return Regex.IsMatch(value, sb.ToString(), RegexOptions.IgnoreCase | RegexOptions.Singleline);
}
- /// Translates an Access LIKE bracket list (the text between [ and ]) to a regex
- /// character class: a leading ! is negation (^), ranges (a-z) carry over, and the
- /// regex-special \ ] ^ are escaped so a bracketed literal like [#]/[[] matches itself.
- private static string TranslateLikeClass(string inner)
+ private static bool IsTruthy(object value) => value switch
{
- var sb = new StringBuilder("[");
- if (inner.StartsWith('!')) { sb.Append('^'); inner = inner[1..]; }
- foreach (char c in inner)
+ bool b => b,
+ string s => !ReadsAsZero(s),
+ char c => !ReadsAsZero(c.ToString()),
+ DateTime d => d != OaEpoch,
+ Guid or byte[] => true,
+ _ => !IsNumeric(value) || Dbl(value) != 0,
+ };
+
+ private static bool ReadsAsZero(string text)
+ {
+ try
{
- if (c is '\\' or ']' or '^') sb.Append('\\');
- sb.Append(c);
+ return TextAsDecimal(text) == 0m;
+ }
+ catch (InvalidCastException)
+ {
+ return false; // not a number, so not 0
}
- sb.Append(']');
- return sb.ToString();
}
- private static bool? AsBool(object? v) => v switch { bool b => b, null => null, _ => Convert.ToBoolean(v) };
+ ///
+ /// The two sides of a comparison, brought to a kind they compare as (verified vs ACE, except as noted).
+ ///
+ /// Text against a number, Boolean or date: the text reads as a number (), a type
+ /// mismatch when it is not one. ACE does this for text a function returns; for a text literal or column it
+ /// raises a type mismatch instead, which LibRed does not, as SQL Server compares them numerically too.
+ /// A date against a number: its serial.
+ /// A GUID or binary value against text, or against the other of the two: both as text (a GUID braced upper
+ /// case, binary as UTF-16). Against a number or date: a type mismatch.
+ ///
+ ///
+ private static (object Left, object Right) Comparable(object left, object right)
+ {
+ if (left is char leftChar) left = leftChar.ToString();
+ if (right is char rightChar) right = rightChar.ToString();
+ bool leftText = left is string, rightText = right is string;
+ bool leftBlob = left is Guid or byte[], rightBlob = right is Guid or byte[];
+ if ((leftText && rightBlob) || (leftBlob && rightText) || (leftBlob && rightBlob && left.GetType() != right.GetType()))
+ return (ConcatText(left), ConcatText(right));
+ if (leftBlob || rightBlob)
+ return leftBlob && rightBlob
+ ? (left, right)
+ : throw new InvalidCastException("Type mismatch: a GUID or binary value cannot be compared with a number or date.");
+ if (leftText == rightText)
+ return left is DateTime ^ right is DateTime ? (Serial(left), Serial(right)) : (left, right);
+ return (leftText ? TextAsNumber((string)left) : Serial(left), rightText ? TextAsNumber((string)right) : Serial(right));
+ }
+
+ /// The order of two values once has brought them to a common kind.
+ private static int CompareAsKinds(object left, object right)
+ {
+ (object l, object r) = Comparable(left, right);
+ return Compare(l, r);
+ }
+
+ private static object Serial(object value) => value is DateTime d ? d.ToOADate() : value;
+
+ /// A value as a condition — for NOT, AND, OR, XOR, EQV, IMP and a
+ /// WHERE: False when it reads as 0, True otherwise, Null when Null (verified vs ACE: '0' is False,
+ /// 'abc', '', a date, a GUID and a binary value are True).
+ private static bool? AsBool(object? v) => v is null ? null : IsTruthy(v);
/// + - * with C# widest-operand type promotion, so the result CLR type matches what EF
/// expects (int+int→int, …): decimal > double > single > long > int. (Contract: like
@@ -1482,63 +2257,519 @@ private static object Arithmetic(object left, object right, char op)
return op switch
{
'-' when bothDates => a - b, // date − date → number of days
- '+' => RoundToSecond(DateTime.FromOADate(a + b)),
- '-' => RoundToSecond(DateTime.FromOADate(a - b)),
- _ => a * b, // date × n has no date meaning → numeric
+ '+' => OaDate(a + b),
+ '-' => OaDate(a - b),
+ _ => Finite(a * b), // date × n has no date meaning → numeric
};
}
- if (left is decimal || right is decimal) { decimal a = Dec(left), b = Dec(right); return op == '+' ? a + b : op == '-' ? a - b : a * b; }
- if (left is double || right is double) { double a = Dbl(left), b = Dbl(right); return op == '+' ? a + b : op == '-' ? a - b : a * b; }
- if (left is float || right is float) { float a = (float)Dbl(left), b = (float)Dbl(right); return op == '+' ? a + b : op == '-' ? a - b : a * b; }
- if (left is long or ulong || right is long or ulong) { long a = Lng(left), b = Lng(right); return op == '+' ? a + b : op == '-' ? a - b : a * b; }
- int x = Int(left), y = Int(right); return op == '+' ? x + y : op == '-' ? x - y : x * y;
+ // A result past its type is an error, not a wrapped or infinite value (verified vs ACE: 2147483647 + 1,
+ // 2147483647 * 2, -2147483647 - 2 and 1E300 * 1E300 all fail).
+ if (left is decimal || right is decimal) { decimal a = ArithmeticDecimal(left), b = ArithmeticDecimal(right); return op == '+' ? a + b : op == '-' ? a - b : a * b; }
+ if (left is double || right is double) { double a = Dbl(left), b = Dbl(right); return op == '+' ? a + b : Finite(op == '-' ? a - b : a * b); }
+ if (left is float || right is float) { float a = (float)Dbl(left), b = (float)Dbl(right); return op == '+' ? a + b : Finite(op == '-' ? a - b : a * b); }
+ if (left is long or ulong || right is long or ulong) { long a = Lng(left), b = Lng(right); return checked(op == '+' ? a + b : op == '-' ? a - b : a * b); }
+ int x = Int(left), y = Int(right); return checked(op == '+' ? x + y : op == '-' ? x - y : x * y);
}
- /// Access / is floating division — Decimal when either operand is Decimal/Currency,
- /// otherwise Double (never integer division; that is \).
- private static object Divide(object left, object right) =>
- left is decimal || right is decimal ? Dec(left) / Dec(right) : Dbl(left) / Dbl(right);
+ /// The exact value of a number written with a decimal point (negated as written), or null.
+ private static decimal? WrittenValue(Expression expression)
+ {
+ bool negate = false;
+ while (expression is UnaryExpression { Operator: UnaryOperator.Negate } negation)
+ {
+ negate = !negate;
+ expression = negation.Operand;
+ }
+ return expression is LiteralExpression { Written: decimal written } ? (negate ? -written : written) : null;
+ }
- /// Access integer operators \ (int division) and MOD: operands round to an
- /// integer, and the result keeps the operand's integer type (int, or long if either is Int64) — so
- /// int \ int is Int32, matching the EF contract.
- private static object IntegerOp(object left, object right, char op)
+ ///
+ /// The type ACE gives a result column before reading any row, as far as its places go (verified vs ACE). ACE
+ /// works the expression out in full and cuts the value only as it goes into a Decimal result column — so
+ /// Pmt(0.05 / 12, …) uses the whole rate while SELECT 1 / 1.5 is 0.6.
+ ///
+ /// A number written with a decimal point is a Decimal of the places written less trailing zeros
+ /// (334.90 has one; 3.0 is a whole number), and so is a whole number too big for a Long, with
+ /// none; a Decimal column has its scale; Currency counts as four places.
+ /// * and /: a Decimal with whole numbers, dates, Booleans or text stays that Decimal
+ /// (DECIMAL(18,4) 4.5 / 7 is 0.6428; 1.5 / '2.5' is 0.6); two Decimals of the same places stay it (1.5 * 1.5 is
+ /// 2.2); two of different places, a Double or Single, or a Currency divided give a Double (1.5 * 1.25 is
+ /// 1.875; 1.5 / DECIMAL 4.5 is 0.333…).
+ /// + and - keep the same rule, except that Currency with a Decimal is a Decimal of the
+ /// larger places (1.5 - Currency 3.25 is -1.7500).
+ /// A function other than CCur and Sum/Min/Max/First/Last, a
+ /// parameter or anything else is not a Decimal; a function returning text or a date counts as text or a date
+ /// (1.5 / Left('12', 2) is 0.1), as says.
+ ///
+ ///
+ internal static NumberType NumberTypeOf(
+ Expression expression, IReadOnlyList columns, Func declaredType)
{
- if (left is long or ulong || right is long or ulong)
- { long a = Lng(left), b = Lng(right); return op == '%' ? a % b : a / b; }
- int x = Int(left), y = Int(right); return op == '%' ? x % y : x / y;
+ NumberType Of(Expression e) => NumberTypeOf(e, columns, declaredType);
+
+ switch (expression)
+ {
+ case LiteralExpression { Written: decimal written }:
+ {
+ int places = (written / 1.0000000000000000000000000000m).Scale; // trailing zeros dropped
+ return places == 0 ? new(NumberClass.Whole) : new(NumberClass.Decimal, places);
+ }
+ case LiteralExpression literal:
+ return literal.Value switch
+ {
+ // A whole number too big for a Long is a Decimal to ACE, so Currency * 864000000000 is a Double
+ // there rather than an overflowing Currency (verified vs ACE); LibRed keeps the value an Int64.
+ long => new(NumberClass.Decimal, 0),
+ int or short or byte or bool => new(NumberClass.Whole),
+ double or float => new(NumberClass.Double),
+ string => new(NumberClass.Text),
+ DateTime => new(NumberClass.Date),
+ _ => new(NumberClass.Other),
+ };
+ case ColumnReference reference:
+ if (OutputColumn.Find(columns, reference) is not { } column)
+ return new(NumberClass.Other);
+ if (column.Currency)
+ return new(NumberClass.Currency);
+ if (column.Scale is int scale)
+ return new(NumberClass.Decimal, scale);
+ Type? type = column.ClrType;
+ return type == typeof(double) || type == typeof(float) ? new(NumberClass.Double)
+ : type == typeof(int) || type == typeof(long) || type == typeof(short) || type == typeof(byte)
+ || type == typeof(bool) ? new(NumberClass.Whole)
+ : type == typeof(string) ? new(NumberClass.Text)
+ : type == typeof(DateTime) ? new(NumberClass.Date)
+ : new(NumberClass.Other);
+ case UnaryExpression { Operator: UnaryOperator.Negate } negation:
+ return Of(negation.Operand);
+ case BinaryExpression { Operator: BinaryOperator.Multiply or BinaryOperator.Divide } product:
+ return Product(Of(product.Left), Of(product.Right), product.Operator == BinaryOperator.Divide);
+ case BinaryExpression { Operator: BinaryOperator.Add or BinaryOperator.Subtract } sum:
+ return Sum(Of(sum.Left), Of(sum.Right), sum.Operator == BinaryOperator.Add);
+ case BinaryExpression { Operator: BinaryOperator.IntDivide or BinaryOperator.Modulo }:
+ return new(NumberClass.Whole);
+ case BinaryExpression { Operator: BinaryOperator.Power }:
+ return new(NumberClass.Double);
+ case FunctionCall function when function.Name.Equals("CCUR", StringComparison.OrdinalIgnoreCase):
+ return new(NumberClass.Currency);
+ case FunctionCall { Arguments: [var argument] } function
+ when function.Name.ToUpperInvariant() is "SUM" or "MIN" or "MAX" or "FIRST" or "LAST":
+ return Of(argument);
+ case FunctionCall { WithinGroup: not null, Arguments: [_, var key] } function
+ when function.Name.Equals("PERCENTILE_DISC", StringComparison.OrdinalIgnoreCase):
+ return Of(key);
+ case FunctionCall function:
+ Type? returns = declaredType(function);
+ return returns == typeof(string) ? new(NumberClass.Text)
+ : returns == typeof(DateTime) ? new(NumberClass.Date)
+ : new(NumberClass.Other);
+ default:
+ return new(NumberClass.Other);
+ }
}
- /// VBA CStr. A Double renders at 15 significant digits and a Single at 7 — the OA/VB
- /// convention, not .NET Core 3.0+'s shortest-round-trippable form, which would turn 0.1+0.2 into
- /// "0.30000000000000004" (verified vs ACE: "0.3", and CStr(CSng(1/3)) is "0.3333333"). A Boolean
- /// renders as its VARIANT_BOOL number, "-1" — note that is the Jet Expression Service's behaviour and
- /// differs from the VBA runtime proper, which renders "True".
- private static string VbaString(object v) => v switch
+ private static bool IsWhole(NumberType type) => type.Class is NumberClass.Whole or NumberClass.Text or NumberClass.Date;
+
+ private static NumberType Product(NumberType left, NumberType right, bool divide)
+ {
+ if (left.Class == NumberClass.Other || right.Class == NumberClass.Other)
+ return new(NumberClass.Other);
+ if (left.Class == NumberClass.Double || right.Class == NumberClass.Double
+ || divide && (left.Class == NumberClass.Currency || right.Class == NumberClass.Currency))
+ return new(NumberClass.Double);
+ if (IsWhole(left) && IsWhole(right))
+ return divide ? new(NumberClass.Double) : new(NumberClass.Whole);
+ if (IsWhole(left)) return right;
+ if (IsWhole(right)) return left;
+ if (left.Class == NumberClass.Currency && right.Class == NumberClass.Currency)
+ return new(NumberClass.Currency);
+ int leftPlaces = left.Class == NumberClass.Currency ? 4 : left.Places;
+ int rightPlaces = right.Class == NumberClass.Currency ? 4 : right.Places;
+ return leftPlaces == rightPlaces ? new(NumberClass.Decimal, leftPlaces) : new(NumberClass.Double);
+ }
+
+ private static NumberType Sum(NumberType left, NumberType right, bool add)
+ {
+ if (left.Class is NumberClass.Other or NumberClass.Date || right.Class is NumberClass.Other or NumberClass.Date
+ || add && left.Class == NumberClass.Text && right.Class == NumberClass.Text)
+ return new(NumberClass.Other);
+ if (left.Class == NumberClass.Double || right.Class == NumberClass.Double)
+ return new(NumberClass.Double);
+ if (IsWhole(left) && IsWhole(right))
+ return new(NumberClass.Whole);
+ if (IsWhole(left)) return right;
+ if (IsWhole(right)) return left;
+ if (left.Class == NumberClass.Currency && right.Class == NumberClass.Currency)
+ return new(NumberClass.Currency);
+ if (left.Class == NumberClass.Currency || right.Class == NumberClass.Currency)
+ return new(NumberClass.Decimal, Math.Max(4, Math.Max(left.Places, right.Places)));
+ return left.Places == right.Places ? left : new(NumberClass.Double);
+ }
+
+ ///
+ /// A value as it goes into a result column of : a Decimal column keeps only its places,
+ /// the rest cut off. A Double or Single — LibRed's type for a written decimal literal — becomes a Decimal the
+ /// way OLE Automation converts it (), so binary rounding (0.6 held as
+ /// 0.5999…) cannot drop a digit.
+ ///
+ internal static object? ToResultPlaces(object? value, NumberType type) =>
+ type.Class != NumberClass.Decimal ? value : value switch
+ {
+ decimal d => decimal.Round(d, type.Places, MidpointRounding.ToZero),
+ double or float => CutFloating(value, type.Places),
+ _ => value,
+ };
+
+ ///
+ /// A value converted to the type its result column declares, when it has another: a number to a wider number
+ /// (a Boolean as -1 or 0, a Double into a Decimal the OLE Automation way), anything to text as & writes
+ /// it, and anything to binary as its bytes (). Null, or no , leaves
+ /// the value as it is.
+ ///
+ internal static object? AsColumnType(object? value, Type? type, bool currency)
+ {
+ if (value is null || type is null || value.GetType() == type)
+ return value;
+ if (type == typeof(string)) return ConcatText(value);
+ if (type == typeof(byte[])) return ColumnBytes(value, currency);
+ if (type == typeof(decimal)) return Dec(value);
+ if (type == typeof(double)) return Dbl(value);
+ return Convert.ChangeType(Numeric(value), type, CultureInfo.InvariantCulture);
+ }
+
+ ///
+ /// A value in a binary result column (verified vs ACE, which writes it so where a UNION mixes binary or GUID
+ /// values with others): text as UTF-16, a GUID as its 16 bytes, a Boolean as a 16-bit -1 or 0, a Byte or a Long
+ /// as 4 bytes, an Integer as 2, a Single as 4 and a Double or a date's serial as 8, Currency as its 8-byte scaled
+ /// integer, and a Decimal or a Large Number as its text. ACE cuts a Large Number's text to 8 bytes, which LibRed
+ /// does not.
+ ///
+ private static byte[] ColumnBytes(object value, bool currency) => value switch
+ {
+ byte[] bytes => bytes,
+ string text => Encoding.Unicode.GetBytes(text),
+ Guid guid => guid.ToByteArray(),
+ bool b => BitConverter.GetBytes((short)(b ? -1 : 0)),
+ byte b => BitConverter.GetBytes((int)b),
+ short s => BitConverter.GetBytes(s),
+ int i => BitConverter.GetBytes(i),
+ float f => BitConverter.GetBytes(f),
+ double d => BitConverter.GetBytes(d),
+ DateTime date => BitConverter.GetBytes(date.ToOADate()),
+ decimal m when currency => BitConverter.GetBytes(decimal.ToOACurrency(m)),
+ _ => Encoding.Unicode.GetBytes(ConcatText(value)),
+ };
+
+ private static object CutFloating(object value, int places)
{
- bool b => b ? "-1" : "0",
- double d => d.ToString("G15", CultureInfo.InvariantCulture),
- float f => f.ToString("G7", CultureInfo.InvariantCulture),
- _ => Convert.ToString(v, CultureInfo.InvariantCulture)!,
+ try
+ {
+ decimal exact = value is float f ? JetDecimalConverter.FromSingle(f) : JetDecimalConverter.FromDouble((double)value);
+ decimal cut = decimal.Round(exact, places, MidpointRounding.ToZero);
+ return value is float ? (float)cut : (double)cut;
+ }
+ catch (OverflowException)
+ {
+ return value; // past a Decimal: ACE could not have held it as one either
+ }
+ }
+
+ /// A number as a Decimal for arithmetic with a Decimal: a Double or Single converted the OLE Automation
+ /// way (), a date as its serial.
+ private static decimal ArithmeticDecimal(object v) => v is DateTime date ? SerialDecimal(date) : Dec(v);
+
+ private static double Finite(double value) =>
+ double.IsFinite(value) ? value : throw new OverflowException("Overflow: the result is too large for a number.");
+
+ private static float Finite(float value) =>
+ float.IsFinite(value) ? value : throw new OverflowException("Overflow: the result is too large for a number.");
+
+ /// The date at an OLE Automation serial; a serial outside 100-01-01 … 9999-12-31 is an overflow.
+ private static DateTime OaDate(double serial)
+ {
+ try
+ {
+ return DateTime.FromOADate(serial);
+ }
+ catch (ArgumentException)
+ {
+ throw new OverflowException($"Overflow: {serial} is outside the range of a date.");
+ }
+ }
+
+ /// A date's day number as a Decimal, built from its parts rather than converted from the Double serial
+ /// (before the epoch the time fraction still counts away from zero, as in the OLE Automation serial).
+ private static decimal SerialDecimal(DateTime d)
+ {
+ decimal days = (d.Date - OaEpoch).Days;
+ decimal time = d.TimeOfDay.Ticks / (decimal)TimeSpan.TicksPerDay;
+ return days >= 0 ? days + time : days - time;
+ }
+
+
+ ///
+ /// Access ^ (verified vs ACE): Double, a date read as its serial. A negative base with a fractional
+ /// exponent is an invalid procedure call, zero to a negative power a division by zero, and a result past a
+ /// Double an overflow.
+ ///
+ private static double Power(object left, object right)
+ {
+ double x = Oa(left), y = Oa(right);
+ double result = Math.Pow(x, y);
+ if (double.IsNaN(result))
+ throw new ArgumentException($"Invalid procedure call: {x} cannot be raised to the power {y}.");
+ if (double.IsInfinity(result))
+ throw x == 0 ? new DivideByZeroException("Division by zero: zero raised to a negative power.") : new OverflowException("Overflow: the result is too large for a number.");
+ return result;
+ }
+
+ ///
+ /// Access + (verified vs ACE). Two text operands concatenate — a GUID or binary value counts as text.
+ /// Otherwise the operands add, text read as a number (); so '1' + 1 is 2 and
+ /// '1' + #2020-01-02# is the next day. A GUID or binary value with anything but text is a type
+ /// mismatch. Null has already propagated.
+ ///
+ private static object Add(object left, object right) =>
+ IsConcatText(left) && IsConcatText(right)
+ ? ConcatText(left) + ConcatText(right)
+ : Arithmetic(NumericOperand(left)!, NumericOperand(right)!, '+');
+
+ private static bool IsConcatText(object v) => v is string or char or Guid or byte[];
+
+ /// An arithmetic operand: text read as a number (); a GUID or binary value
+ /// is a type mismatch; anything else, Null included, as it is. A (a parameter can carry one)
+ /// is one character of text.
+ private static object? NumericOperand(object? v) => v switch
+ {
+ 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."),
+ _ => v,
};
- /// VBA CBool: any non-zero number is True (so 0.5 is True), and a string may hold a number
- /// ("-1") as well as "True"/"False". rejects the numeric-string form
- /// with a FormatException, so ACE accepts input LibRed used to refuse (verified vs ACE).
- private static bool VbaBool(object v) => v switch
+ ///
+ /// A value as & (and + between two texts) writes it (verified vs ACE). A Boolean is its
+ /// VARIANT_BOOL number; a Double has 15 significant digits and a Single 7; a Decimal drops trailing zeros; a
+ /// date is written in the regional short date and long time, without the time at midnight and without the
+ /// date on 1899-12-30; a GUID is braced upper case; a binary value is read as UTF-16 text.
+ ///
+ internal static string ConcatText(object v) => v switch
{
- bool b => b,
- string s => bool.TryParse(s, out var parsed)
- ? parsed
- : double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var n)
- ? n != 0
- : throw new InvalidOperationException($"Type mismatch: '{s}' cannot be converted to Boolean."),
- _ => Dbl(v) != 0,
+ string s => s,
+ bool b => b ? "-1" : "0",
+ double d => FloatingText(d, 15),
+ float f => FloatingText(f, 7),
+ decimal m => m.ToString("0.############################", CultureInfo.CurrentCulture),
+ DateTime d => DateText(d),
+ Guid g => g.ToString("B").ToUpperInvariant(),
+ byte[] => Encoding.Unicode.GetString(ToBytes(v)),
+ _ => Convert.ToString(v, CultureInfo.CurrentCulture)!,
};
+ ///
+ /// A floating value rounded to significant digits, trailing zeros dropped. Written
+ /// in E notation when its exponent is at least , or when fixed notation would need
+ /// more than decimals (verified vs ACE: 1/3 is 0.333333333333333, a Single
+ /// 1E7 is 1E+07, a Single 1E-5 is 0.00001, 1E300 is 1E+300).
+ ///
+ private static string FloatingText(double value, int digits, NumberFormatInfo? format = null)
+ {
+ if (value == 0) return "0";
+ if (!double.IsFinite(value)) return value.ToString(CultureInfo.InvariantCulture);
+
+ format ??= CultureInfo.CurrentCulture.NumberFormat;
+ string scientific = value.ToString("E" + (digits - 1), CultureInfo.InvariantCulture); // -d.dddE+ddd
+ int mark = scientific.IndexOf('E');
+ int exponent = int.Parse(scientific[(mark + 1)..], CultureInfo.InvariantCulture);
+ bool negative = scientific[0] == '-';
+ string significant = scientific[(negative ? 1 : 0)..mark].Replace(".", "").TrimEnd('0');
+ string sign = negative ? format.NegativeSign : "";
+ string point = format.NumberDecimalSeparator;
+
+ int decimals = significant.Length - 1 - exponent;
+ if (exponent >= digits || decimals > digits)
+ {
+ string mantissa = significant.Length == 1 ? significant : significant[0] + point + significant[1..];
+ return $"{sign}{mantissa}E{(exponent < 0 ? "-" : "+")}{Math.Abs(exponent):00}";
+ }
+ if (exponent < 0)
+ return sign + "0" + point + new string('0', -exponent - 1) + significant;
+ if (significant.Length <= exponent + 1)
+ return sign + significant + new string('0', exponent + 1 - significant.Length);
+ return sign + significant[..(exponent + 1)] + point + significant[(exponent + 1)..];
+ }
+
+ private static readonly DateTime OaEpoch = new(1899, 12, 30);
+
+ /// The culture's long time pattern, with the narrow no-break space newer ICU data puts before AM/PM
+ /// (U+202F, on Linux and macOS) written as the plain space of Windows' regional settings, and so of ACE. A date
+ /// then writes the same text on every platform.
+ private static string LongTimePattern(DateTimeFormatInfo format) => format.LongTimePattern.Replace(' ', ' ');
+
+ /// A date as writes it; the year is not zero-padded (year 100 is "100") unless
+ /// asks for it, as Format does.
+ private static string DateText(DateTime d, bool padYear = false)
+ {
+ DateTimeFormatInfo format = CultureInfo.CurrentCulture.DateTimeFormat;
+ string time = d.ToString(LongTimePattern(format), CultureInfo.CurrentCulture);
+ if (d.Date == OaEpoch) return time;
+ string datePattern = padYear
+ ? format.ShortDatePattern
+ : format.ShortDatePattern.Replace("yyyy", "'" + d.Year.ToString(CultureInfo.InvariantCulture) + "'");
+ string date = d.ToString(datePattern, CultureInfo.CurrentCulture);
+ return d.TimeOfDay == TimeSpan.Zero ? date : date + " " + time;
+ }
+
+ ///
+ /// Text as + reads it as a number, in the regional separators and currency symbol (verified vs ACE).
+ /// Surrounding whitespace is skipped. One sign may come before or after the number, spaced from it; brackets
+ /// make it negative but cannot be combined with a sign; one currency symbol may come on either side. Group
+ /// separators are ignored once a digit has been read, even after the decimal point. An e or d
+ /// exponent needs at least one digit. &H/&O text is a whole number, read as a Long when
+ /// it fits 32 bits (&HFFFFFFFF is -1). Anything else is a type mismatch, and a value past a Double
+ /// is an overflow.
+ ///
+ private static double TextAsNumber(string text)
+ {
+ double value = double.Parse(NumberText(text), NumberStyles.Float, CultureInfo.InvariantCulture);
+ if (double.IsInfinity(value))
+ throw new OverflowException($"Overflow: '{text}' is too large for a number.");
+ return value;
+ }
+
+ /// Text read as reads it, but exactly; null when it is past a Decimal.
+ private static decimal? TextAsDecimal(string text) =>
+ decimal.TryParse(NumberText(text), NumberStyles.Float, CultureInfo.InvariantCulture, out decimal value) ? value : null;
+
+ /// The number reads, written out in invariant form.
+ private static string NumberText(string text)
+ {
+ NumberFormatInfo format = CultureInfo.CurrentCulture.NumberFormat;
+ string s = text.Trim();
+ if (s.Length > 1 && s[0] == '&' && s[1] is 'H' or 'h' or 'O' or 'o')
+ return RadixNumber(text, s[2..], s[1] is 'H' or 'h' ? 16 : 8).ToString(CultureInfo.InvariantCulture);
+
+ int i = 0;
+ bool negative = false, signed = false, currency = false, open = false, closed = false;
+ while (i < s.Length)
+ {
+ if (char.IsWhiteSpace(s[i])) i++;
+ else if (s[i] is '+' or '-' && !signed) { signed = true; negative = s[i] == '-'; i++; }
+ else if (s[i] == '(' && !open) { open = true; i++; }
+ else if (!currency && At(s, i, format.CurrencySymbol)) { currency = true; i += format.CurrencySymbol.Length; }
+ else break;
+ }
+
+ var number = new StringBuilder();
+ bool digits = false, point = false;
+ while (i < s.Length)
+ {
+ if (s[i] is >= '0' and <= '9') { number.Append(s[i]); digits = true; i++; }
+ else if (!point && At(s, i, format.NumberDecimalSeparator)) { number.Append('.'); point = true; i += format.NumberDecimalSeparator.Length; }
+ else if (digits && At(s, i, format.NumberGroupSeparator)) i += format.NumberGroupSeparator.Length;
+ else break;
+ }
+ if (!digits) throw NotANumber(text);
+ if (point && number[^1] == '.') number.Append('0');
+
+ if (i < s.Length && s[i] is 'e' or 'E' or 'd' or 'D')
+ {
+ number.Append('E');
+ i++;
+ if (i < s.Length && s[i] is '+' or '-') number.Append(s[i++]);
+ int start = i;
+ while (i < s.Length && s[i] is >= '0' and <= '9') number.Append(s[i++]);
+ if (i == start) throw NotANumber(text);
+ }
+
+ while (i < s.Length)
+ {
+ if (char.IsWhiteSpace(s[i])) i++;
+ else if (s[i] is '+' or '-' && !signed) { signed = true; negative = s[i] == '-'; i++; }
+ else if (s[i] == ')' && open && !closed) { closed = true; i++; }
+ else if (!currency && At(s, i, format.CurrencySymbol)) { currency = true; i += format.CurrencySymbol.Length; }
+ else throw NotANumber(text);
+ }
+ if (open != closed || (open && signed)) throw NotANumber(text);
+
+ return negative || open ? "-" + number : number.ToString();
+ }
+
+ private static long RadixNumber(string text, string digits, int radix)
+ {
+ if (digits.Length == 0) throw NotANumber(text);
+ ulong value = 0;
+ foreach (char c in digits)
+ {
+ int digit = c is >= '0' and <= '9' ? c - '0'
+ : c is >= 'A' and <= 'F' ? c - 'A' + 10
+ : c is >= 'a' and <= 'f' ? c - 'a' + 10
+ : int.MaxValue;
+ if (digit >= radix) throw NotANumber(text);
+ value = checked(value * (ulong)radix + (ulong)digit);
+ }
+ return value <= uint.MaxValue ? (int)(uint)value : checked((long)value);
+ }
+
+ private static bool At(string s, int index, string symbol) =>
+ symbol.Length > 0 && string.CompareOrdinal(s, index, symbol, 0, symbol.Length) == 0;
+
+ private static InvalidCastException NotANumber(string text) =>
+ new($"Type mismatch: '{text}' cannot be read as a number.");
+
+ /// Access / is floating division — Decimal when either operand is Decimal/Currency,
+ /// otherwise Double (never integer division; that is \). A date divides as its serial; dividing by
+ /// zero is an error and so is a result past a Double (verified vs ACE: 1 / 0 and 0 / 0 fail).
+ private static object Divide(object left, object right)
+ {
+ if (left is decimal || right is decimal)
+ return ArithmeticDecimal(left) / ArithmeticDecimal(right);
+ // A Single divided with only Singles, Integers or Booleans is a Single (verified vs ACE: TRUE / a Single 1.5
+ // is -0.6666667, and Str(CSng(1) / CSng(10)) is " .1"). OLE DB reports it as a Double, as it does CSng.
+ if ((left is float || right is float) && IsSingleWidth(left) && IsSingleWidth(right))
+ {
+ float singleDivisor = Sng(right);
+ if (singleDivisor == 0)
+ throw new DivideByZeroException("Division by zero.");
+ return Finite(Sng(left) / singleDivisor);
+ }
+ double divisor = Oa(right);
+ if (divisor == 0)
+ throw new DivideByZeroException("Division by zero.");
+ return Finite(Oa(left) / divisor);
+ }
+
+ // Not Byte: ACE treats a Byte column as a Long here (verified: a Byte 1 / a Single 1.5 is 0.6666666666666666).
+ private static bool IsSingleWidth(object v) => v is float or short or bool;
+
+ /// Access integer operators \ (int division) and MOD: operands round to an
+ /// integer (half to even), a date as its serial, and the result keeps the operand's integer type (int, or
+ /// long if either is Int64) — so int \ int is Int32, matching the EF contract. Anything MOD -1 is 0,
+ /// even the smallest Long (verified vs ACE).
+ /// An Int32 result is worked out in Int64 all the same, and only the result has to fit: ACE squeezes
+ /// each operand into a Long first, so a Double, Decimal or Currency past one overflows even where the answer
+ /// would not — 1E12 MOD 7, whose remainder is below 7. Here that is 1, and a quotient past a Long is still
+ /// an overflow, since the column's type is settled before any value is seen. A LibRed extension.
+ private static object IntegerOp(object left, object right, char op)
+ {
+ left = Serial(left);
+ right = Serial(right);
+ long a = Lng(left), b = Lng(right);
+ long result = op == '%' ? (b == -1 ? 0L : a % b) : a / b;
+ // Each branch boxed on its own: a bare `? result : (int)result` is a long, and would widen the Int32 back.
+ return left is long or ulong || right is long or ulong ? (object)result : checked((int)result);
+ }
+
+ /// VBA CBool (verified vs ACE): "True" and "False" as written, otherwise whether the value
+ /// read as a number () is non-zero, so 0.5, '$5' and a date are True.
+ private static bool VbaBool(object v) =>
+ v is bool b ? b
+ : v is string s && bool.TryParse(s, out bool parsed) ? parsed
+ : 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 decimal Dec(object v) => Convert.ToDecimal(Numeric(v), CultureInfo.InvariantCulture);
+ 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).
private static float Sng(object v) => (float)Dbl(v);
@@ -1548,16 +2779,6 @@ private static object IntegerOp(object left, object right, char op)
// 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);
- // Rounds to the nearest whole second — Jet stores no sub-second, and the OA-serial round-trip can leave
- // a value a few ticks shy of/past an exact second (e.g. 21:05:18.9999999).
- private static DateTime RoundToSecond(DateTime d)
- {
- long rem = d.Ticks % TimeSpan.TicksPerSecond;
- return rem >= TimeSpan.TicksPerSecond - rem
- ? d.AddTicks(TimeSpan.TicksPerSecond - rem)
- : d.AddTicks(-rem);
- }
-
private static int Compare(object left, object right)
{
if (IsNumeric(left) && IsNumeric(right))
@@ -1600,8 +2821,16 @@ private static int Compare(object left, object right)
// 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,
// so this only affects pre-1899 dates.
+ //
+ // ToOADate keeps whole milliseconds, which is all a Date/Time column holds, but a DATETIME2 keeps 100-ns
+ // ticks: two of those in the same millisecond are settled by their ticks, the way the serial would run —
+ // later first below the epoch — so they neither compare equal nor sort as a tie.
if (left is DateTime leftDate && right is DateTime rightDate)
- return leftDate.ToOADate().CompareTo(rightDate.ToOADate());
+ {
+ int bySerial = leftDate.ToOADate().CompareTo(rightDate.ToOADate());
+ return bySerial != 0 ? bySerial
+ : leftDate.Ticks.CompareTo(rightDate.Ticks) * (leftDate < OaEpoch ? -1 : 1);
+ }
if (left is IComparable c && left.GetType() == right.GetType())
return c.CompareTo(right);
@@ -1642,13 +2871,13 @@ private static int CompareBytes(byte[] a, byte[] b)
return a.Length.CompareTo(b.Length);
}
- /// Access text comparison: **case-insensitive**, **trailing spaces ignored**, and
- /// **accent-aware ordering** (Access "General" collation). Uses invariant-culture ignore-case, which
- /// matches ACE where ordinal doesn't — an accented letter sorts next to its base letter (verified vs
- /// ACE: 'é' < 'f', 'café' < 'cafz') while accents stay significant for equality
- /// ('café' ≠ 'cafe'). (Ignorable apostrophe/hyphen ordering is not reproduced.)
+ /// 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.
private static int CompareText(string a, string b) =>
- string.Compare(a.TrimEnd(' '), b.TrimEnd(' '), StringComparison.InvariantCultureIgnoreCase);
+ JetTextComparer.Compare(a, b)
+ ?? Math.Sign(string.Compare(a.TrimEnd(' '), b.TrimEnd(' '), StringComparison.InvariantCultureIgnoreCase));
/// Orders two values for SORT (nulls first), using the same coercion as comparisons.
public static int CompareForSort(object? a, object? b) => (a, b) switch
diff --git a/src/LibRed/LibRed.Engine/Execution/JoinPredicateHoisting.cs b/src/LibRed/LibRed.Engine/Execution/JoinPredicateHoisting.cs
index a50f95eb1..1320362fe 100644
--- a/src/LibRed/LibRed.Engine/Execution/JoinPredicateHoisting.cs
+++ b/src/LibRed/LibRed.Engine/Execution/JoinPredicateHoisting.cs
@@ -76,30 +76,9 @@ void Walk(Expression e)
return;
}
- switch (e)
+ foreach (Expression operand in e.Operands() ?? [])
{
- case BinaryExpression b:
- Walk(b.Left);
- Walk(b.Right);
- break;
- case UnaryExpression u:
- Walk(u.Operand);
- break;
- case FunctionCall f:
- foreach (Expression a in f.Arguments)
- {
- Walk(a);
- }
-
- break;
- case InListExpression il:
- Walk(il.Value);
- foreach (Expression i in il.Items)
- {
- Walk(i);
- }
-
- break;
+ Walk(operand);
}
}
}
@@ -112,21 +91,7 @@ void Walk(Expression e)
internal static Expression Substitute(Expression on, IReadOnlyDictionary values) =>
values.TryGetValue(on, out object? value)
? new LiteralExpression(value)
- : on switch
- {
- BinaryExpression b => b with
- {
- Left = Substitute(b.Left, values),
- Right = Substitute(b.Right, values),
- },
- UnaryExpression u => u with { Operand = Substitute(u.Operand, values) },
- FunctionCall f => f with { Arguments = f.Arguments.Select(a => Substitute(a, values)).ToList() },
- InListExpression il => il with
- {
- Value = Substitute(il.Value, values),
- Items = il.Items.Select(i => Substitute(i, values)).ToList(),
- },
- InSubqueryExpression isq => isq with { Value = Substitute(isq.Value, values) },
- _ => on,
- };
+ : on is InSubqueryExpression isq
+ ? isq with { Value = Substitute(isq.Value, values) }
+ : on.MapOperands(o => Substitute(o, values));
}
diff --git a/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs b/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs
new file mode 100644
index 000000000..84858487c
--- /dev/null
+++ b/src/LibRed/LibRed.Engine/Execution/LikeMatcher.cs
@@ -0,0 +1,191 @@
+namespace LibRed.Engine.Execution;
+
+///
+/// The LIKE match with ANSI-92 wildcards, the set EF Core emits over both OLE DB and ODBC (verified vs ACE).
+///
+///
+///
+/// % matches any run and _ any one character. *, ? and # are plain
+/// characters.
+/// [abc] matches one of the characters listed, [!abc] one character not listed, and a-c
+/// inside the brackets a range. A - at either end is itself listed, [!] is the character !, and
+/// [] 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.
+/// 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.
+/// There is no ESCAPE clause; ACE rejects it as a syntax error. A wildcard is matched literally by
+/// bracketing it.
+///
+/// Documentation: https://support.microsoft.com/en-us/office/like-operator-b2f7ef03-9085-4ffb-9829-eef18358e931,
+/// https://support.microsoft.com/en-us/office/access-wildcard-character-reference-af00c501-7972-40ee-8889-e18abaad12d1
+/// and https://support.microsoft.com/en-us/office/use-wildcards-in-queries-and-parameters-in-access-ec057a45-78b1-4d16-8c20-242cde582e0b.
+///
+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
+ {
+ 'ß' => "ss",
+ 'æ' => "ae",
+ 'Æ' => "AE",
+ _ => null,
+ };
+
+ public static bool IsMatch(string value, string pattern)
+ {
+ string text = Fold(value);
+ int[] characterEnd = CharacterEnds(value, text.Length);
+
+ // reach[p] is whether the pattern so far can consume exactly the first p characters of the text.
+ var reach = new bool[text.Length + 1];
+ reach[0] = true;
+ int i = 0;
+ while (i < pattern.Length)
+ {
+ int first = Array.IndexOf(reach, true);
+ if (first < 0)
+ return false;
+
+ if (pattern[i] == '%')
+ {
+ Array.Fill(reach, true, first, reach.Length - first);
+ i++;
+ continue;
+ }
+
+ var next = new bool[reach.Length];
+ if (pattern[i] == '_')
+ {
+ // One character of the value, however many it folds to (verified vs ACE: 'ß' LIKE '_' is True).
+ for (int p = first; p < text.Length; p++)
+ next[characterEnd[p]] |= reach[p];
+ i++;
+ }
+ else if (pattern[i] == '[')
+ {
+ int close = pattern.IndexOf(']', i + 1);
+ if (close < 0)
+ return Invalid(first, text);
+ if (close == i + 1)
+ {
+ i += 2; // [] matches nothing
+ continue;
+ }
+ if (BracketList.Parse(pattern.AsSpan(i + 1, close - i - 1)) is not { } list)
+ return Invalid(first, text);
+ for (int p = first; p < text.Length; p++)
+ {
+ if (reach[p])
+ list.Match(text, p, next);
+ }
+ i = close + 1;
+ }
+ else
+ {
+ int end = pattern.IndexOfAny(['%', '_', '['], i);
+ if (end < 0)
+ end = pattern.Length;
+ string run = Fold(pattern[i..end]);
+ for (int p = first; p + run.Length <= text.Length; p++)
+ {
+ if (reach[p] && text.AsSpan(p).StartsWith(run, StringComparison.Ordinal))
+ next[p + run.Length] = true;
+ }
+ i = end;
+ }
+ reach = next;
+ }
+ return reach[text.Length];
+ }
+
+ /// For each position of the folded value, where the value's character it belongs to ends.
+ private static int[] CharacterEnds(string value, int foldedLength)
+ {
+ var ends = new int[foldedLength];
+ int p = 0;
+ foreach (char c in value)
+ {
+ int end = p + (Expansion(c)?.Length ?? 1);
+ for (; p < end; p++)
+ ends[p] = end;
+ }
+ return ends;
+ }
+
+ 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.
+ private static string Fold(string text)
+ {
+ 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);
+ }
+ return folded.ToString().ToUpperInvariant();
+ }
+
+ private sealed class BracketList(bool negated, List members, List<(char Low, char High)> ranges)
+ {
+ /// The list between the brackets, or null when a range is written backwards.
+ public static BracketList? Parse(ReadOnlySpan body)
+ {
+ bool negated = body.Length > 1 && body[0] == '!';
+ if (negated)
+ body = body[1..];
+
+ var members = new List();
+ var ranges = new List<(char, char)>();
+ for (int i = 0; i < body.Length; i++)
+ {
+ if (i + 2 < body.Length && body[i + 1] == '-')
+ {
+ if (body[i] > body[i + 2])
+ return null;
+ ranges.Add((char.ToUpperInvariant(body[i]), char.ToUpperInvariant(body[i + 2])));
+ i += 2;
+ }
+ else
+ {
+ members.Add(Fold(body[i].ToString()));
+ }
+ }
+ return new BracketList(negated, members, ranges);
+ }
+
+ /// Marks in each position the list can advance to. A
+ /// member that expands matches two characters, where anything else matches one.
+ public void Match(string text, int p, bool[] next)
+ {
+ bool any = false;
+ foreach (string member in members)
+ {
+ if (text.AsSpan(p).StartsWith(member, StringComparison.Ordinal))
+ {
+ any = true;
+ next[p + member.Length] |= !negated;
+ }
+ }
+ foreach ((char low, char high) in ranges)
+ {
+ if (text[p] >= low && text[p] <= high)
+ {
+ any = true;
+ next[p + 1] |= !negated;
+ }
+ }
+ if (negated && !any)
+ next[p + 1] = true;
+ }
+ }
+}
diff --git a/src/LibRed/LibRed.Engine/Execution/ListAgg.cs b/src/LibRed/LibRed.Engine/Execution/ListAgg.cs
new file mode 100644
index 000000000..ab13c93b9
--- /dev/null
+++ b/src/LibRed/LibRed.Engine/Execution/ListAgg.cs
@@ -0,0 +1,42 @@
+using LibRed.Sql.Ast;
+
+namespace LibRed.Engine.Execution;
+
+///
+/// The standard's LISTAGG(x [, separator]) WITHIN GROUP (ORDER BY …): the non-Null values of a group or a
+/// window frame as text, in the WITHIN GROUP order, joined by the separator (none when it is left out). Access has no
+/// such aggregate; this is a LibRed extension.
+///
+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.
+ ///
+ public static string? Of(
+ IEnumerable<(object? Value, object?[] Keys)> rows, string separator, IReadOnlyList directions,
+ bool distinct)
+ {
+ var comparer = Comparer.Create((a, b) =>
+ {
+ for (int k = 0; k < directions.Count; k++)
+ {
+ int c = ExpressionEvaluator.CompareForSort(a[k], b[k]);
+ if (c != 0)
+ return directions[k] == SortDirection.Descending ? -c : c;
+ }
+ return 0;
+ });
+ var values = rows.Where(r => r.Value is not null).OrderBy(r => r.Keys, comparer).Select(r => r.Value!);
+ if (distinct)
+ {
+ var seen = new HashSet();
+ values = values.Where(v => seen.Add(new QueryExecutor.GroupKey([v])));
+ }
+
+ var text = values.Select(ExpressionEvaluator.ConcatText).ToList();
+ return text.Count == 0 ? null : string.Join(separator, text);
+ }
+}
diff --git a/src/LibRed/LibRed.Engine/Execution/ParameterBag.cs b/src/LibRed/LibRed.Engine/Execution/ParameterBag.cs
index 95ba09504..5cd9fb7f4 100644
--- a/src/LibRed/LibRed.Engine/Execution/ParameterBag.cs
+++ b/src/LibRed/LibRed.Engine/Execution/ParameterBag.cs
@@ -5,8 +5,17 @@ namespace LibRed.Engine.Execution;
/// case-insensitively and the leading @ is optional on both the reference and the key,
/// so a value supplied as "p0" satisfies a "@p0" reference and vice versa.
///
+///
+/// A or resolves to the Jet stores a time as —
+/// the time on the OLE epoch, 1899-12-30 — so everything that reads a parameter, saving it included, still sees only
+/// a date, as the evaluator expects. What it was is kept too (), because a date plus or less a
+/// span is a date, where a date less a date is a day count.
+///
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);
+
private readonly Dictionary _values = new(StringComparer.OrdinalIgnoreCase);
public ParameterBag(IReadOnlyDictionary? values)
@@ -21,9 +30,32 @@ public ParameterBag(IReadOnlyDictionary? values)
if (name.StartsWith('?'))
throw new NotSupportedException("Positional ('?') parameters are not supported yet; use named (@name) parameters.");
if (_values.TryGetValue(Normalize(name), out object? value))
- return value;
+ return value switch
+ {
+ TimeSpan span => OleEpoch + span,
+ TimeOnly time => OleEpoch + time.ToTimeSpan(),
+ _ => value,
+ };
throw new InvalidOperationException($"No value was supplied for parameter '{name}'.");
}
+ /// The span a or parameter was bound to; null for any other
+ /// value, an unbound name or a positional reference.
+ public TimeSpan? Duration(string name) =>
+ name.StartsWith('?') || !_values.TryGetValue(Normalize(name), out object? value) ? null
+ : value switch
+ {
+ TimeSpan span => span,
+ TimeOnly time => time.ToTimeSpan(),
+ _ => null,
+ };
+
+ /// The type of the value resolves to; null when it is unbound, positional or Null,
+ /// which declares nothing.
+ public Type? TypeOf(string name) =>
+ name.StartsWith('?') || !_values.TryGetValue(Normalize(name), out object? value) || value is null ? null
+ : value is TimeSpan or TimeOnly ? typeof(DateTime)
+ : value.GetType();
+
private static string Normalize(string name) => name.TrimStart('@');
}
diff --git a/src/LibRed/LibRed.Engine/Execution/Percentile.cs b/src/LibRed/LibRed.Engine/Execution/Percentile.cs
new file mode 100644
index 000000000..9fb047cd4
--- /dev/null
+++ b/src/LibRed/LibRed.Engine/Execution/Percentile.cs
@@ -0,0 +1,72 @@
+using System.Globalization;
+using EntityFrameworkCore.Jet.Data;
+using LibRed.Sql.Ast;
+
+namespace LibRed.Engine.Execution;
+
+///
+/// The standard's inverse distribution functions, PERCENTILE_CONT and PERCENTILE_DISC: the value at a
+/// fraction of the way through the ordered non-Null values of a group or a window frame. Access has neither; this is a
+/// LibRed extension.
+///
+internal static class Percentile
+{
+ /// The declared type: PERCENTILE_DISC returns one of the values, so has the ORDER BY key's type;
+ /// PERCENTILE_CONT interpolates, giving a Double, or a date for dates.
+ public static Type? ResultType(string name, Type? key) =>
+ name.Equals("PERCENTILE_DISC", StringComparison.OrdinalIgnoreCase) ? key
+ : key == typeof(DateTime) ? typeof(DateTime)
+ : typeof(double);
+
+ ///
+ /// (upper case) over — the group's or frame's ORDER BY key values,
+ /// Nulls included, in any order — at . Null when there are no values or the fraction is
+ /// Null; a fraction outside 0 to 1 is an invalid procedure call.
+ ///
+ ///
+ /// PERCENTILE_DISC is the first value, in the given order, whose cumulative share of the values reaches the
+ /// fraction. PERCENTILE_CONT is at position fraction × (n − 1), interpolating linearly between the values
+ /// 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)
+ {
+ if (fraction is null)
+ return null;
+ decimal p = JetDecimalConverter.ToDecimal(ExpressionEvaluator.ConversionNumber(fraction), CultureInfo.InvariantCulture);
+ if (p is < 0 or > 1)
+ 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 present = values.Where(v => v is not null);
+ List sorted = direction == SortDirection.Descending
+ ? [.. present.OrderByDescending(v => v, comparer)]
+ : [.. present.OrderBy(v => v, comparer)];
+ int n = sorted.Count;
+ if (n == 0)
+ return null;
+
+ if (name == "PERCENTILE_DISC")
+ return sorted[Math.Max((int)Math.Ceiling(p * n), 1) - 1];
+
+ decimal position = p * (n - 1);
+ int below = (int)decimal.Floor(position);
+ int above = (int)decimal.Ceiling(position);
+ double share = (double)(position - below);
+ object low = sorted[below]!, high = sorted[above]!;
+ if (low is DateTime from && high is DateTime to)
+ {
+ // Along the timeline, to the whole second — dates carry no less.
+ long ticks = from.Ticks + (long)Math.Round((to.Ticks - from.Ticks) * share);
+ const long second = TimeSpan.TicksPerSecond;
+ return new DateTime((ticks + second / 2) / second * second);
+ }
+ double a = Number(low), b = Number(high);
+ return below == above ? a : a + (b - a) * share;
+ }
+
+ private static double Number(object value) => value is string or char or Guid or byte[]
+ ? throw new InvalidCastException("Type mismatch: PERCENTILE_CONT interpolates numbers and dates.")
+ : Convert.ToDouble(ExpressionEvaluator.ConversionNumber(value), CultureInfo.InvariantCulture);
+}
diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs
index a9b805e8c..da5724aa6 100644
--- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs
+++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs
@@ -4,8 +4,42 @@
namespace LibRed.Engine.Execution;
-/// A column produced by a plan node: an optional table-alias qualifier and a name.
-internal readonly record struct OutputColumn(string? Qualifier, string Name, Type? ClrType = null);
+/// A column produced by a plan node: an optional table-alias qualifier and a name.
+/// marks a Currency value, which shares with Decimal but calculates differently, and
+/// a Decimal's places. marks a column that is a bare NULL, which
+/// has no type of its own, unlike one whose type is merely unknown.
+internal readonly record struct OutputColumn(
+ string? Qualifier, string Name, Type? ClrType = null, bool Currency = false, int? Scale = null, bool Null = false)
+{
+ /// The output of a stored column.
+ public static OutputColumn Of(string? qualifier, LibRed.Catalog.ColumnDef column) =>
+ new(qualifier, column.Name, Schema.JetClrTypeMap.ToClrType(column.Type),
+ column.Type == LibRed.Catalog.JetDataType.Currency,
+ column.Type == LibRed.Catalog.JetDataType.FixedPoint ? column.Scale : null);
+
+ /// A computed column of , computed by .
+ public static OutputColumn Computed(string name, Type? clrType, NumberType type, Expression expression) =>
+ new(null, name, clrType, type.Class == NumberClass.Currency,
+ type.Class == NumberClass.Decimal ? type.Places : null, expression is LiteralExpression { Value: null });
+
+ /// The column names, or null when none or more than one does (execution
+ /// reports the ambiguous reference).
+ public static OutputColumn? Find(IReadOnlyList columns, ColumnReference reference)
+ {
+ OutputColumn? result = null;
+ foreach (OutputColumn column in columns)
+ {
+ if (!string.Equals(column.Name, reference.Column, StringComparison.OrdinalIgnoreCase)
+ || reference.Table is not null
+ && !string.Equals(column.Qualifier, reference.Table, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ if (result is not null) return null;
+ result = column;
+ }
+ return result;
+ }
+}
///
/// Interprets a logical plan tree against the storage layer, producing a
@@ -453,8 +487,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 => new OutputColumn(alias, c.Name, Schema.JetClrTypeMap.ToClrType(c.Type))).ToList();
+ var columns = table.Definition.Columns.Select(c => OutputColumn.Of(alias, c)).ToList();
return (columns, table.Rows());
}
@@ -462,8 +495,7 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope)
{
var table = _database.OpenTable(seek.Table);
string alias = seek.Alias ?? seek.Table;
- var columns = table.Definition.Columns
- .Select(c => new OutputColumn(alias, c.Name, Schema.JetClrTypeMap.ToClrType(c.Type))).ToList();
+ var columns = table.Definition.Columns.Select(c => OutputColumn.Of(alias, c)).ToList();
// Evaluate the key(s) in the outer scope (so an index-nested-loop join can key off the outer
// row); a single-table seek's key is a constant/parameter.
@@ -479,8 +511,7 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope)
{
var table = _database.OpenTable(range.Table);
string alias = range.Alias ?? range.Table;
- var columns = table.Definition.Columns
- .Select(c => new OutputColumn(alias, c.Name, Schema.JetClrTypeMap.ToClrType(c.Type))).ToList();
+ var columns = table.Definition.Columns.Select(c => OutputColumn.Of(alias, c)).ToList();
var evaluator = new ExpressionEvaluator(new EvalScope([], [], outer), this, parameters: _parameters, session: _session);
int col = range.Index.Columns[0].Column.Index;
@@ -497,7 +528,7 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope)
case DerivedTableNode derived:
{
var (inner, rows) = Execute(derived.Input, outer);
- var columns = inner.Select(c => new OutputColumn(derived.Alias, c.Name, c.ClrType)).ToList();
+ var columns = inner.Select(c => c with { Qualifier = derived.Alias }).ToList();
return (columns, rows);
}
@@ -546,7 +577,10 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope)
var projected = rows.Select(row =>
{
var eval = Eval(columns, row, outer);
- return plan.Select(p => p.InputIndex >= 0 ? row[p.InputIndex] : eval.Evaluate(p.Expr!)).ToArray();
+ 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);
@@ -554,10 +588,15 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope)
case SetOperationNode setOp:
{
- // Column names come from the left (leading) query, per SQL.
- var (columns, leftRows) = Execute(setOp.Left, outer);
- var (_, rightRows) = Execute(setOp.Right, outer);
- return (columns, ExecuteSetOp(setOp.Operator, leftRows, rightRows));
+ // Column names come from the left (leading) query, per SQL; each column's type is the one both
+ // queries' values fit, and every value is converted to it before rows are compared.
+ var (leftColumns, leftRows) = Execute(setOp.Left, outer);
+ var (rightColumns, rightRows) = Execute(setOp.Right, outer);
+ if (leftColumns.Count != rightColumns.Count)
+ return (leftColumns, ExecuteSetOp(setOp.Operator, leftRows, rightRows));
+ var columns = leftColumns.Zip(rightColumns, SetOperationColumn).ToList();
+ return (columns, ExecuteSetOp(setOp.Operator,
+ ToColumnTypes(leftRows, leftColumns, columns), ToColumnTypes(rightRows, rightColumns, columns)));
}
case LimitNode limit:
@@ -634,6 +673,58 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope)
}
}
+ ///
+ /// A set operation's column: the left query's, typed as ACE types it (verified vs ACE). A bare NULL takes
+ /// the other query's type. Numbers widen on , a Boolean counting as an Integer
+ /// (-1); a GUID or binary value with anything else makes a binary column; any other mix — text, or a date with a
+ /// number or a Boolean — makes a text column. An unknown type on either side leaves the column untyped.
+ ///
+ private static OutputColumn SetOperationColumn(OutputColumn left, OutputColumn right)
+ {
+ if (right.Null)
+ return left;
+ if (left.Null)
+ return left with { ClrType = right.ClrType, Currency = right.Currency, Scale = right.Scale, Null = false };
+
+ // A Decimal column is Currency unless one side is a Decimal of its own, and keeps a scale both sides share.
+ bool leftDecimal = left.ClrType == typeof(decimal) && !left.Currency;
+ bool rightDecimal = right.ClrType == typeof(decimal) && !right.Currency;
+ Type? type = left.ClrType is not { } l || right.ClrType is not { } r ? null
+ : l == r ? l
+ : l == typeof(Guid) || r == typeof(Guid) || l == typeof(byte[]) || r == typeof(byte[]) ? typeof(byte[])
+ : CommonNumericType(AsInteger(l), AsInteger(r), currency: !leftDecimal && !rightDecimal) ?? typeof(string);
+ bool isDecimal = type == typeof(decimal);
+ return left with
+ {
+ ClrType = type,
+ Currency = isDecimal && !leftDecimal && !rightDecimal,
+ Scale = !isDecimal ? null
+ : leftDecimal && rightDecimal ? (left.Scale == right.Scale ? left.Scale : null)
+ : leftDecimal ? left.Scale : rightDecimal ? right.Scale : null,
+ };
+
+ static Type AsInteger(Type type) => type == typeof(bool) ? typeof(short) : type;
+ }
+
+ /// The rows with each value converted to its output column's type, where the query's own column had
+ /// another.
+ private static IEnumerable ToColumnTypes(
+ IEnumerable rows, IReadOnlyList from, IReadOnlyList to)
+ {
+ int[] changed = Enumerable.Range(0, to.Count)
+ .Where(i => to[i].ClrType is { } type && !from[i].Null && from[i].ClrType != type)
+ .ToArray();
+ if (changed.Length == 0)
+ return rows;
+ return rows.Select(row =>
+ {
+ var converted = (object?[])row.Clone();
+ foreach (int i in changed)
+ converted[i] = ExpressionEvaluator.AsColumnType(converted[i], to[i].ClrType, from[i].Currency);
+ return converted;
+ });
+ }
+
private static IEnumerable ExecuteSetOp(SetOperator op, IEnumerable left, IEnumerable right)
{
switch (op)
@@ -684,9 +775,11 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope)
/// null for expressions whose result type depends on runtime coercion; the ADO layer can still fall back
/// to a non-null runtime value in those cases without publishing misleading metadata for empty results.
/// A ProjectNode's flattened output: the per-item plan (source input index, or an expression to
- /// evaluate) and the resulting output columns. Structural — the same for every outer row.
+ /// evaluate, with the type its values are converted to, if any) and the resulting output columns.
+ /// Structural — the same for every outer row.
private sealed record ProjectionSchema(
- List<(OutputColumn Column, int InputIndex, Expression? Expr)> Plan, List Columns);
+ List<(OutputColumn Column, int InputIndex, Expression? Expr, NumberType Type, Type? ConvertTo)> Plan,
+ List Columns);
/// Builds — or reuses — a ProjectNode's schema. Flattens the projection, expanding a qualified star
/// (Table.*) into the input columns of that source (passed through by index); every other item is an
@@ -696,19 +789,21 @@ 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)>();
+ var plan = new List<(OutputColumn Column, int InputIndex, Expression? Expr, NumberType Type, Type? ConvertTo)>();
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));
+ plan.Add((columns[ci], ci, null, default, null));
}
else
{
string name = item.Alias ?? (item.Value is ColumnReference c ? c.Column : $"Expr{plan.Count + 1}");
- plan.Add((new OutputColumn(null, name, DeclaredType(item.Value, columns)), -1, item.Value));
+ NumberType type = ExpressionEvaluator.NumberTypeOf(item.Value, columns, e => DeclaredType(e, columns));
+ plan.Add((OutputColumn.Computed(name, DeclaredType(item.Value, columns), type, item.Value), -1, item.Value,
+ type, ChoiceConversion(item.Value, columns)));
}
}
@@ -725,6 +820,11 @@ private ProjectionSchema ProjectionSchemaFor(ProjectNode project, IReadOnlyList<
return value.GetType();
case LiteralExpression:
return null;
+ // A parameter is typed by the value bound to it, as a literal is by its own — so `d + @p` declares the
+ // date it returns rather than nothing, which left the reader to guess from the first row. The value is
+ // the one the engine sees: the ADO layer has already turned a TimeSpan or TimeOnly into a DateTime.
+ case ParameterExpression parameter:
+ return _parameters.TypeOf(parameter.Name);
case ColumnReference column:
return DeclaredColumnType(column, columns)
?? (column.Table is null && column.Column.Equals("Now", StringComparison.OrdinalIgnoreCase)
@@ -732,21 +832,27 @@ private ProjectionSchema ProjectionSchemaFor(ProjectNode project, IReadOnlyList<
case SystemVariableExpression variable:
return variable.Name.Equals("ROWCOUNT", StringComparison.OrdinalIgnoreCase)
? typeof(int) : _session?.LastIdentity?.GetType();
- case ExistsExpression or InSubqueryExpression or InListExpression:
+ case ExistsExpression or InSubqueryExpression or InListExpression or BetweenExpression:
return typeof(bool);
case UnaryExpression unary:
return unary.Operator is UnaryOperator.Not or UnaryOperator.IsNull or UnaryOperator.IsNotNull
- ? typeof(bool) : DeclaredType(unary.Operand, columns);
+ ? typeof(bool) : DeclaredUnaryType(unary.Operator, DeclaredType(unary.Operand, columns));
case BinaryExpression binary:
if (binary.Operator is BinaryOperator.Equal or BinaryOperator.NotEqual
or BinaryOperator.LessThan or BinaryOperator.LessThanOrEqual
or BinaryOperator.GreaterThan or BinaryOperator.GreaterThanOrEqual
- or BinaryOperator.And or BinaryOperator.Or or BinaryOperator.Like or BinaryOperator.In)
+ or BinaryOperator.And or BinaryOperator.Or or BinaryOperator.Xor or BinaryOperator.Eqv
+ or BinaryOperator.Imp or BinaryOperator.Like or BinaryOperator.In)
return typeof(bool);
if (binary.Operator == BinaryOperator.Concat)
return typeof(string);
Type? left = DeclaredType(binary.Left, columns);
Type? right = DeclaredType(binary.Right, columns);
+ // A date plus or less a span is a date — ExpressionEvaluator.IsSpan, which the evaluator moves it by.
+ if (left == typeof(DateTime) && IsSpan(binary.Right)
+ && binary.Operator is BinaryOperator.Add or BinaryOperator.Subtract
+ || right == typeof(DateTime) && IsSpan(binary.Left) && binary.Operator == BinaryOperator.Add)
+ return typeof(DateTime);
return DeclaredBinaryType(binary.Operator, left, right);
case FunctionCall function:
return DeclaredFunctionType(function, columns);
@@ -762,7 +868,7 @@ or BinaryOperator.GreaterThan or BinaryOperator.GreaterThanOrEqual
/// result_expressions and the optional else_result_expression". A branch whose own type is unknown
/// contributes nothing rather than poisoning the answer, which is what makes a bare NULL arm
/// harmless: a NULL literal has no type and the standard ignores it for precedence too. Numeric branches
- /// widen along the same ladder as arithmetic, so THEN 1 ELSE 2.5 declares decimal. A genuine mix
+ /// widen on , so THEN 1 ELSE 2.5 declares Double. A genuine mix
/// (a string branch and a numeric one) declares nothing rather than guessing, leaving the column untyped
/// exactly as it was before CASE was understood at all.
///
@@ -777,101 +883,185 @@ private static IEnumerable CaseResults(CaseExpression c)
yield return c.ElseResult;
}
- /// The single type a set of alternative expressions declares — shared by CASE and COALESCE,
- /// which the standard defines in terms of CASE and gives the same precedence rule.
+ /// The single type a set of alternative expressions declares — shared by CASE, IIF, COALESCE,
+ /// GREATEST and LEAST, which the standard defines in terms of CASE or gives the same precedence rule.
+ /// A number written with a decimal point counts as a Decimal here, as ACE reads it (NumberTypeOf), though
+ /// its value is held as a Double: EF writes a decimal zero as 0.0, so its Sum-with-a-default shape
+ /// IIF(SUM(x) IS NULL, 0.0, SUM(x)) would otherwise make every money sum a Double. A Double still wins
+ /// where one is present.
private Type? UnifiedType(IEnumerable alternatives, IReadOnlyList columns)
{
Type? result = null;
+ bool currency = false; // whether a Decimal result so far is a Currency
foreach (Expression alternative in alternatives)
{
- Type? branchType = DeclaredType(alternative, columns);
+ Type? branchType = IsWrittenDecimal(alternative) ? typeof(decimal) : DeclaredType(alternative, columns);
if (branchType is null)
continue;
+ bool branchCurrency = branchType == typeof(decimal)
+ && ExpressionEvaluator.NumberTypeOf(alternative, columns, e => DeclaredType(e, columns)).Class
+ == NumberClass.Currency;
- if (result is null)
+ if (result is null || result == branchType)
{
+ currency = result is null ? branchCurrency : currency && branchCurrency;
result = branchType;
continue;
}
- if (result == branchType)
- continue;
-
- result = WidenNumeric(result, branchType);
+ bool decimalIsCurrency = result == typeof(decimal) ? currency : branchCurrency;
+ result = CommonNumericType(result, branchType, decimalIsCurrency);
if (result is null)
return null;
+ currency = result == typeof(decimal) && decimalIsCurrency;
}
return result;
}
- /// The wider of two numeric types, on the same ladder uses for
- /// arithmetic. Null when either side is not numeric, meaning the two cannot be reconciled.
- private static Type? WidenNumeric(Type left, Type right)
+ /// Whether is a number written with a decimal point, negated or not.
+ private static bool IsWrittenDecimal(Expression expression)
+ {
+ while (expression is UnaryExpression { Operator: UnaryOperator.Negate } negation)
+ expression = negation.Operand;
+ return expression is LiteralExpression { Written: decimal };
+ }
+
+ ///
+ /// The type an expression that picks one of several alternatives () converts its value
+ /// to, so the value has the type the column declares; null when nothing is converted. Only when every
+ /// alternative's type is known is the declared type sure to hold each of them.
+ ///
+ private Type? ChoiceConversion(Expression expression, IReadOnlyList columns)
+ {
+ IEnumerable? alternatives = expression switch
+ {
+ CaseExpression @case => CaseResults(@case),
+ FunctionCall function => function.Name.TrimEnd('$').ToUpperInvariant() switch
+ {
+ "IIF" when function.Arguments.Count == 3 => function.Arguments.Skip(1),
+ "COALESCE" or "GREATEST" or "LEAST" => function.Arguments,
+ _ => null,
+ },
+ _ => null,
+ };
+ if (alternatives is null
+ || alternatives.Any(a => a is not LiteralExpression { Value: null } && DeclaredType(a, columns) is null))
+ return null;
+ return DeclaredType(expression, columns);
+ }
+
+ ///
+ /// The type the values of two numeric types share (verified vs ACE, as it types a UNION): the wider whole
+ /// number of the two; a Single with a Byte or an Integer, and a Double for a Single with anything wider; a
+ /// Decimal (or Currency) with a whole number, and a Double with a Double. A Currency (,
+ /// when the Decimal side is one) cannot hold a Large Number, so the two make a Double. A Decimal with a Single,
+ /// which was not measured, is a Double too. Null when either side is not a number, meaning the two cannot be
+ /// reconciled.
+ ///
+ private static Type? CommonNumericType(Type left, Type right, bool currency = false)
{
if (!IsNumeric(left) || !IsNumeric(right)) return null;
- if (left == typeof(decimal) || right == typeof(decimal)) return typeof(decimal);
+ if (left == right) return left;
if (left == typeof(double) || right == typeof(double)) return typeof(double);
- if (left == typeof(float) || right == typeof(float)) return typeof(float);
- if (IsInt64(left) || IsInt64(right)) return typeof(long);
- return typeof(int);
+ int leftRank = WholeRank(left), rightRank = WholeRank(right);
+ if (left == typeof(decimal) || right == typeof(decimal))
+ return Math.Max(leftRank, rightRank) is var whole && whole < 0 || currency && whole > 2
+ ? typeof(double) : typeof(decimal);
+ if (left == typeof(float) || right == typeof(float))
+ return Math.Max(leftRank, rightRank) <= 1 ? typeof(float) : typeof(double);
+ return WholeTypes[Math.Max(leftRank, rightRank)];
}
+ private static readonly Type[] WholeTypes = [typeof(byte), typeof(short), typeof(int), typeof(long)];
+
+ /// A whole number type's place in (the narrowest that holds it), or -1.
+ private static int WholeRank(Type type) =>
+ type == typeof(byte) ? 0
+ : type == typeof(sbyte) || type == typeof(short) ? 1
+ : type == typeof(ushort) || type == typeof(int) ? 2
+ : type == typeof(uint) || IsInt64(type) ? 3
+ : -1;
+
private static bool IsNumeric(Type type)
=> type == typeof(byte) || type == typeof(sbyte) || type == typeof(short) || type == typeof(ushort)
|| type == typeof(int) || type == typeof(uint) || IsInt64(type)
|| type == typeof(float) || type == typeof(double) || type == typeof(decimal);
- private static Type? DeclaredColumnType(ColumnReference reference, IReadOnlyList columns)
- {
- Type? result = null;
- bool found = false;
- foreach (OutputColumn column in columns)
- {
- if (!string.Equals(column.Name, reference.Column, StringComparison.OrdinalIgnoreCase)
- || reference.Table is not null
- && !string.Equals(column.Qualifier, reference.Table, StringComparison.OrdinalIgnoreCase))
- continue;
+ private static Type? DeclaredColumnType(ColumnReference reference, IReadOnlyList columns) =>
+ OutputColumn.Find(columns, reference)?.ClrType;
- if (found) return null; // execution will report the ambiguous reference
- found = true;
- result = column.ClrType;
- }
- return result;
- }
+ /// The declared type of an aggregate (upper-case name) over an argument of —
+ /// for an ordered-set aggregate, its WITHIN GROUP key — grouped or windowed. Keep in lock-step with
+ /// and .
+ internal static Type? AggregateResultType(string name, Type? argument) => RunningAggregate.Canonical(name) switch
+ {
+ "LISTAGG" => typeof(string),
+ "PERCENTILE_CONT" or "PERCENTILE_DISC" => Percentile.ResultType(name, argument),
+ "COUNT" or "REGR_COUNT" => typeof(int),
+ var pair when RunningAggregate.IsPair(pair) => typeof(double),
+ "SUM" => argument == null ? null
+ : argument == typeof(decimal) || argument == typeof(float) || argument == typeof(long) ? argument
+ : argument == typeof(ulong) ? typeof(long)
+ : argument == typeof(double) || argument == typeof(string) || argument == typeof(DateTime) ? typeof(double)
+ : typeof(int),
+ "AVG" => argument == typeof(decimal) ? typeof(decimal) : typeof(double),
+ "VAR" or "VARP" or "STDEV" or "STDEVP" or "STDDEV" or "STDDEVP" => typeof(double),
+ _ => argument, // MIN, MAX, FIRST and LAST keep the argument's type
+ };
private Type? DeclaredFunctionType(FunctionCall function, IReadOnlyList columns)
{
string name = function.Name.TrimEnd('$').ToUpperInvariant();
- Type? argument = function.Arguments.Count > 0 ? DeclaredType(function.Arguments[0], columns) : null;
+ Type? argument = function.Arguments.Count == 0 ? null
+ : DeclaredType(function.Arguments[function.WithinGroup is null ? 0 : ^1], columns);
+ if (QueryPlanner.IsAggregate(name))
+ return AggregateResultType(name, argument);
return name switch
{
- "COUNT" => typeof(int),
- "SUM" or "MIN" or "MAX" or "FIRST" or "LAST" => argument,
- "AVG" => argument == typeof(decimal) ? typeof(decimal) : typeof(double),
"CBOOL" or "ISDATE" => typeof(bool),
"CBYTE" => typeof(byte),
"CINT" => typeof(short),
"CLNG" => typeof(int),
+ "CLNGLNG" => typeof(long),
"CSNG" => typeof(float),
"CDBL" => typeof(double),
"CDEC" or "CCUR" => typeof(decimal),
+ // Round, Abs, Int and Fix keep their operand's type — ExpressionEvaluator.Round and Numeric1. Undeclared,
+ // a Round beside a whole number in IIF, CASE or COALESCE left the Integer to declare the column while
+ // the Round arm returned a Decimal.
+ "ROUND" or "ABS" => KeptNumberType(argument, typeof(double)),
+ "INT" or "FIX" or "FLOOR" or "CEILING" or "CEIL" => KeptNumberType(argument, typeof(DateTime)),
+ "SGN" or "SIGN" => typeof(int),
"CSTR" or "FORMAT" or "LCASE" or "UCASE" or "TRIM" or "LTRIM" or "RTRIM"
or "LEFT" or "RIGHT" or "MID" or "REPLACE" or "STRING" or "SPACE" or "HEX"
or "OCT" or "WEEKDAYNAME" or "MONTHNAME" or "PARTITION" => typeof(string),
- "LEN" or "INSTR" or "INSTRREV" or "ASC" or "ASCW" or "DATEPART" or "DATEDIFF"
+ // DateDiff's "ms", LibRed's own interval, counts in Int64 — a millisecond difference passes Int32 after
+ // 25 days — where every other interval is a Long Integer. Only a written interval says which: one read
+ // from a parameter or a column is declared as the Long Integer the rest give.
+ "DATEDIFF" => function.Arguments is [LiteralExpression { Value: string interval }, ..]
+ && interval.Equals("ms", StringComparison.OrdinalIgnoreCase)
+ ? typeof(long)
+ : typeof(int),
+ "LEN" or "DATALENGTH" or "INSTR" or "INSTRREV" or "ASC" or "ASCW" or "DATEPART"
or "YEAR" or "MONTH" or "DAY" or "HOUR" or "MINUTE" or "SECOND" or "WEEKDAY" => typeof(int),
"CDATE" or "NOW" or "DATE" or "TIME" or "DATEADD" or "DATESERIAL" or "TIMESERIAL"
or "DATEVALUE" or "TIMEVALUE" => typeof(DateTime),
"SQR" or "SIN" or "COS" or "TAN" or "ATN" or "LOG" or "EXP" or "RND"
- or "PMT" or "FV" or "PV" or "NPER" or "IPMT" or "PPMT" or "DDB" or "RATE" => typeof(double),
- "IIF" when function.Arguments.Count == 3 => SameType(
- DeclaredType(function.Arguments[1], columns), DeclaredType(function.Arguments[2], columns)),
+ or "SQRT" or "LN" or "LOG10" or "POWER" or "ASIN" or "ACOS" or "ATAN" or "ATAN2" or "SINH" or "COSH"
+ or "TANH" or "DEGREES" or "RADIANS" or "PI"
+ or "PMT" or "FV" or "PV" or "NPER" or "IPMT" or "PPMT" or "DDB" or "RATE" or "SLN" or "SYD" => typeof(double),
+ // IIF chooses between two values as CASE does, so it takes CASE's rule rather than ACE's own (which
+ // makes every whole number a Long and lets Currency beat Double).
+ "IIF" when function.Arguments.Count == 3 => UnifiedType(function.Arguments.Skip(1), columns),
// The standard makes COALESCE shorthand for a CASE over its arguments, so it takes the same rule:
// the highest-precedence type among them. Unified the same way, which also means a bare NULL
// argument contributes no type rather than erasing the others.
"COALESCE" => UnifiedType(function.Arguments, columns),
+ // GREATEST/LEAST return one of their arguments, so they declare the type the arguments unify to —
+ // SQL Server's "highest precedence type" rule, the same as COALESCE.
+ "GREATEST" or "LEAST" => UnifiedType(function.Arguments, columns),
// NULLIF returns its first expression, or a NULL of that expression's type — so unlike COALESCE
// it takes the first argument's type outright rather than unifying across both. The second
// argument only ever participates in the comparison.
@@ -880,19 +1070,47 @@ private static bool IsNumeric(Type type)
};
}
- private static Type? SameType(Type? left, Type? right) => left == right ? left : null;
+
+ private bool IsSpan(Expression expression) => ExpressionEvaluator.IsSpan(expression, _parameters.Duration);
+
+ /// The type Round, Abs, Int and Fix give over an operand of : a Decimal, Double,
+ /// Single or Int64 as it is, a narrower integer or a Boolean as a Long Integer, text as a Double, and a date as
+ /// . (Abs of the smallest Long Integer is the one value past a Long, and a Double.)
+ private static Type? KeptNumberType(Type? operand, Type whenDate) =>
+ operand == typeof(decimal) || operand == typeof(double) || operand == typeof(float) || operand == typeof(long)
+ ? operand
+ : operand == typeof(int) || operand == typeof(short) || operand == typeof(byte) || operand == typeof(bool)
+ ? typeof(int)
+ : operand == typeof(DateTime) ? whenDate
+ : operand == typeof(string) || operand == typeof(char) ? typeof(double)
+ : null;
private static Type? DeclaredBinaryType(BinaryOperator op, Type? left, Type? right)
{
if (left is null || right is null)
return null;
- if (op == BinaryOperator.Add && (left == typeof(string) || right == typeof(string)))
+ // '+' concatenates only two texts (a GUID or binary value counts as text). Otherwise an arithmetic
+ // operator reads text as a Double. Keep in lock-step with ExpressionEvaluator.Add and NumericOperand.
+ if (op == BinaryOperator.Add && IsConcatText(left) && IsConcatText(right))
return typeof(string);
+ if (op is BinaryOperator.Add or BinaryOperator.Subtract or BinaryOperator.Multiply or BinaryOperator.Divide
+ or BinaryOperator.IntDivide or BinaryOperator.Modulo or BinaryOperator.Power)
+ {
+ if (left == typeof(string)) left = typeof(double);
+ if (right == typeof(string)) right = typeof(double);
+ }
if (op == BinaryOperator.Power)
return typeof(double);
+ // Keep in lock-step with ExpressionEvaluator.Divide: a Single with only Singles, Integers or Booleans stays one.
if (op == BinaryOperator.Divide)
- return left == typeof(decimal) || right == typeof(decimal) ? typeof(decimal) : typeof(double);
+ return left == typeof(decimal) || right == typeof(decimal) ? typeof(decimal)
+ : (left == typeof(float) || right == typeof(float)) && IsSingleWidth(left) && IsSingleWidth(right) ? typeof(float)
+ : typeof(double);
+ // Keep in lock-step with ExpressionEvaluator.BitwiseOp: two 16-bit operands give an Integer.
+ if (op is BinaryOperator.BitAnd or BinaryOperator.BitOr or BinaryOperator.BitXor
+ && IsSixteenBits(left) && IsSixteenBits(right))
+ return typeof(short);
if (op is BinaryOperator.Modulo or BinaryOperator.IntDivide
or BinaryOperator.BitAnd or BinaryOperator.BitOr or BinaryOperator.BitXor)
return IsInt64(left) || IsInt64(right) ? typeof(long) : typeof(int);
@@ -915,8 +1133,31 @@ private static bool IsNumeric(Type type)
return typeof(int);
}
+ /// The type of -x or BNOT x. Keep in lock-step with ExpressionEvaluator.Negate and BitNot:
+ /// negation reads text as a Double and widens an Integer, Byte or Boolean to a Long; BNOT gives an Integer from
+ /// an Integer or a Boolean, an Int64 from an Int64, and otherwise a Long.
+ private static Type? DeclaredUnaryType(UnaryOperator op, Type? operand)
+ {
+ if (operand is null)
+ return null;
+ if (op == UnaryOperator.BitNot)
+ return IsSixteenBits(operand) ? typeof(short) : IsInt64(operand) ? typeof(long) : typeof(int);
+ if (operand == typeof(string))
+ return typeof(double);
+ if (operand == typeof(short) || operand == typeof(byte) || operand == typeof(bool))
+ return typeof(int);
+ return IsInt64(operand) ? typeof(long) : operand;
+ }
+
private static bool IsInt64(Type type) => type == typeof(long) || type == typeof(ulong);
+ /// A bitwise operand of 16 bits: an Integer or a Boolean (ExpressionEvaluator.BitOperand).
+ private static bool IsSixteenBits(Type? type) => type == typeof(short) || type == typeof(bool);
+
+ private static bool IsSingleWidth(Type type) => type == typeof(float) || type == typeof(short) || type == typeof(bool);
+
+ private static bool IsConcatText(Type type) => type == typeof(string) || type == typeof(Guid) || type == typeof(byte[]);
+
/// The set of source-table qualifiers that supply the DISTINCTROW projection's output columns.
/// An unqualified column is resolved to its source table via the input's columns.
private static HashSet ContributingQualifiers(
@@ -940,11 +1181,7 @@ private static HashSet ContributingQualifiers(
{
ColumnReference c => [(c.Table, c.Column)],
QualifiedStarExpression qs => [(qs.Table, "*")],
- BinaryExpression b => ColumnRefs(b.Left).Concat(ColumnRefs(b.Right)),
- UnaryExpression u => ColumnRefs(u.Operand),
- FunctionCall f => f.Arguments.SelectMany(ColumnRefs),
- InListExpression il => ColumnRefs(il.Value).Concat(il.Items.SelectMany(ColumnRefs)),
- _ => [],
+ _ => expression.Operands()?.SelectMany(ColumnRefs) ?? [],
};
///
@@ -1011,8 +1248,7 @@ private static HashSet ContributingQualifiers(
var innerTable = _database.OpenTable(seek.Table);
string innerAlias = seek.Alias ?? seek.Table;
int innerWidth = innerTable.Definition.Columns.Count;
- var seekColumns = innerTable.Definition.Columns
- .Select(c => new OutputColumn(innerAlias, c.Name, Schema.JetClrTypeMap.ToClrType(c.Type))).ToList();
+ 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();
@@ -1402,12 +1638,7 @@ private ExpressionEvaluator Eval(IReadOnlyList columns, object?[]
private (IReadOnlyList Columns, IEnumerable Rows) ExecuteWindow(WindowNode node, EvalScope? outer)
{
var (inColumns, inRowsEnum) = Execute(node.Input, outer);
-
- var columns = inColumns.Concat(node.Outputs.Select(o => new OutputColumn(
- null,
- o.Name,
- WindowFunctions.Lookup(o.Function.Name).ResultType(
- o.Function.Arguments.Count > 0 ? DeclaredType(o.Function.Arguments[0], inColumns) : null)))).ToList();
+ var (types, columns) = WindowColumns(node.Outputs, inColumns);
IEnumerable Rows()
{
@@ -1416,8 +1647,18 @@ private ExpressionEvaluator Eval(IReadOnlyList columns, object?[]
for (int i = 0; i < rows.Count; i++)
values[i] = new object?[node.Outputs.Count];
+ // One scope/evaluator rebound per row, as the joins do: partition keys, sort keys and arguments are all
+ // evaluated once per row and a fresh pair each time is the dominant cost otherwise.
+ var scope = new EvalScope(inColumns, [], outer);
+ var eval = new ExpressionEvaluator(scope, this, parameters: _parameters, session: _session);
for (int slot = 0; slot < node.Outputs.Count; slot++)
- ComputeWindow(node.Outputs[slot].Function, rows, inColumns, outer, values, slot);
+ {
+ ComputeWindow(node.Outputs[slot].Function, rows.Count, i =>
+ {
+ scope.Rebind(rows[i]);
+ return eval;
+ }, inColumns, values, slot, types[slot]);
+ }
for (int i = 0; i < rows.Count; i++)
yield return [.. rows[i], .. values[i]];
@@ -1426,29 +1667,71 @@ private ExpressionEvaluator Eval(IReadOnlyList columns, object?[]
return (columns, Rows());
}
- /// Computes one window function into of every row's value array.
+ /// The declared type of each window's column, and with those columns
+ /// appended. A windowed aggregate's column is typed as the grouped aggregate's, Currency and places included.
+ private (List Types, List Columns) WindowColumns(
+ IReadOnlyList windows, IReadOnlyList inColumns)
+ {
+ var types = windows
+ .Select(o => WindowFunctions.Lookup(o.Function.Name).ResultType(new WindowTyping(this, o.Function.Arguments, inColumns)))
+ .ToList();
+ var columns = inColumns.Concat(windows.Select((o, i) => OutputColumn.Computed(
+ o.Name,
+ types[i],
+ QueryPlanner.IsAggregate(o.Function.Name)
+ ? ExpressionEvaluator.NumberTypeOf(
+ new FunctionCall(o.Function.Name, o.Function.Arguments, WithinGroup: o.Function.WithinGroup),
+ inColumns, e => DeclaredType(e, inColumns))
+ : default,
+ o.Function))).ToList();
+ return (types, columns);
+ }
+
+ /// A window function's view of its arguments' declared types.
+ private sealed class WindowTyping(QueryExecutor executor, IReadOnlyList arguments, IReadOnlyList columns)
+ : IWindowTyping
+ {
+ public Type? ArgumentType(int argument) =>
+ argument < arguments.Count ? executor.DeclaredType(arguments[argument], columns) : null;
+
+ public Type? SharedType(params int[] indexes)
+ {
+ var present = indexes.Where(i => i < arguments.Count).Select(i => arguments[i]).ToList();
+ return present.Any(a => a is not LiteralExpression { Value: null } && executor.DeclaredType(a, columns) is null)
+ ? null
+ : executor.UnifiedType(present, columns);
+ }
+ }
+
+ /// Computes one window function into of every row's value array, each value as
+ /// the type the column has. The rows are input rows or, over
+ /// a grouped query, groups; gives the evaluator that sees the one at an index, and
+ /// is their schema.
private void ComputeWindow(
- WindowFunction fn, List rows, IReadOnlyList columns, EvalScope? outer,
- object?[][] values, int slot)
+ WindowFunction fn, int count, Func evaluatorFor, IReadOnlyList columns,
+ object?[][] values, int slot, Type? declared)
{
WindowFunctionDef def = WindowFunctions.Lookup(fn.Name);
if (fn.Arguments.Count < def.MinArguments || fn.Arguments.Count > def.MaxArguments)
throw new InvalidOperationException(
$"{fn.Name} takes {(def.MinArguments == def.MaxArguments ? $"{def.MinArguments}" : $"{def.MinArguments} to {def.MaxArguments}")} argument(s).");
-
- // One scope/evaluator rebound per row, as the joins do: partition keys, sort keys and arguments are all
- // evaluated once per row and a fresh pair each time is the dominant cost otherwise.
- var scope = new EvalScope(columns, [], outer);
- var eval = new ExpressionEvaluator(scope, this, parameters: _parameters, session: _session);
+ CheckOptions(fn, def);
+ WindowFrame? frame = fn.Over.Frame;
+ if (frame is not null)
+ CheckFrame(fn, frame);
// Partition, preserving input order within each. A null partition key groups with other nulls exactly as
// GROUP BY does, because this is the same key type.
var partitions = new Dictionary>();
- var sortKeys = new object?[rows.Count][];
- var arguments = new object?[rows.Count][];
- for (int i = 0; i < rows.Count; i++)
+ var sortKeys = new object?[count][];
+ var arguments = new object?[count][];
+ // A frame's offsets, as each row evaluates them; the standard makes them constants, which is a special case.
+ var startOffsets = frame?.Start.Offset is null ? null : new object?[count];
+ var endOffsets = frame?.End.Offset is null ? null : new object?[count];
+ var included = fn.Filter is null ? null : new bool[count];
+ for (int i = 0; i < count; i++)
{
- scope.Rebind(rows[i]);
+ ExpressionEvaluator eval = evaluatorFor(i);
var key = new object?[fn.Over.PartitionBy.Count];
for (int k = 0; k < key.Length; k++)
key[k] = eval.Evaluate(fn.Over.PartitionBy[k]);
@@ -1464,9 +1747,24 @@ private void ComputeWindow(
arguments[i] = fn.Arguments.Count == 0 ? [] : new object?[fn.Arguments.Count];
for (int k = 0; k < fn.Arguments.Count; k++)
- arguments[i][k] = eval.Evaluate(fn.Arguments[k]);
+ arguments[i][k] = fn.Arguments[k] is StarExpression ? null : eval.Evaluate(fn.Arguments[k]);
+ if (startOffsets is not null)
+ startOffsets[i] = eval.Evaluate(frame!.Start.Offset!);
+ if (endOffsets is not null)
+ endOffsets[i] = eval.Evaluate(frame!.End.Offset!);
+ if (included is not null)
+ included[i] = eval.IsTrue(fn.Filter!);
}
+ bool star = fn.Arguments is [StarExpression];
+ var call = new WindowCall(
+ Star: star,
+ Currency: fn.Arguments is [var first] && !star && IsCurrency(first, columns),
+ Distinct: fn.Distinct,
+ IgnoreNulls: fn.IgnoreNulls == true,
+ FromLast: fn.FromLast == true,
+ WithinGroup: fn.WithinGroup);
+
foreach (List members in partitions.Values)
{
// Ties break on the original position, so the window order is stable and a window with no ORDER BY
@@ -1489,25 +1787,71 @@ private void ComputeWindow(
peerOrdinal[i] = samePeer ? peerOrdinal[i - 1] : peerOrdinal[i - 1] + 1;
}
+ WindowFrameInput? frameInput = frame is null ? null : new WindowFrameInput(
+ frame,
+ startOffsets is null ? [] : members.Select(m => startOffsets[m]).ToList(),
+ endOffsets is null ? [] : members.Select(m => endOffsets[m]).ToList(),
+ frame.Unit == FrameUnit.Range && fn.Over.OrderBy.Count == 1 ? members.Select(m => sortKeys[m][0]).ToList() : null,
+ fn.Over.OrderBy is [{ Direction: SortDirection.Descending }]);
+
var output = new object?[members.Count];
def.Evaluate(
- new WindowPartition(peerStart, peerOrdinal, members.Select(m => arguments[m]).ToList()), output);
+ new WindowPartition(peerStart, peerOrdinal, members.Select(m => arguments[m]).ToList(), call, frameInput,
+ included is null ? null : members.Select(m => included[m]).ToList()),
+ output);
// Scatter back to the input positions: the node emits rows in input order, not window order.
for (int i = 0; i < members.Count; i++)
- values[members[i]][slot] = output[i];
+ values[members[i]][slot] = ExpressionEvaluator.AsColumnType(output[i], declared, currency: false);
}
}
+ /// What the call is written with has to be what the function takes (): a frame
+ /// clause, RESPECT/IGNORE NULLS, FROM FIRST/LAST, DISTINCT.
+ private static void CheckOptions(WindowFunction fn, WindowFunctionDef def)
+ {
+ void Require(bool written, WindowOptions option, string what)
+ {
+ if (written && !def.Options.HasFlag(option))
+ throw new InvalidOperationException($"{fn.Name} takes no {what}.");
+ }
+ Require(fn.Over.Frame is not null, WindowOptions.Frame, "window frame");
+ Require(fn.IgnoreNulls is not null, WindowOptions.NullTreatment, "RESPECT NULLS or IGNORE NULLS");
+ Require(fn.FromLast is not null, WindowOptions.FromLast, "FROM FIRST or FROM LAST");
+ Require(fn.Distinct, WindowOptions.Distinct, "DISTINCT");
+ Require(fn.Filter is not null, WindowOptions.Filter, "FILTER");
+ }
+
+ ///
+ /// The standard's rules for a frame clause that the window, not the syntax, decides: GROUPS counts peer groups,
+ /// so needs an ORDER BY; and a RANGE offset measures from one ORDER BY key, so needs exactly one.
+ ///
+ private static void CheckFrame(WindowFunction fn, WindowFrame frame)
+ {
+ if (frame.Unit == FrameUnit.Groups && fn.Over.OrderBy.Count == 0)
+ throw new InvalidOperationException("A GROUPS frame needs an ORDER BY in its window.");
+ if (frame.Unit == FrameUnit.Range && (frame.Start.Offset ?? frame.End.Offset) is not null && fn.Over.OrderBy.Count != 1)
+ throw new InvalidOperationException("A RANGE frame with an offset needs exactly one ORDER BY key in its window.");
+ }
+
private (IReadOnlyList Columns, IEnumerable Rows) ExecuteAggregate(AggregateNode node, EvalScope? outer)
{
var (inColumns, inRowsEnum) = Execute(node.Input, outer);
+ // Windows over the groups publish a column each, which the projection and ORDER BY read as any other.
+ IReadOnlyList windows = node.Windows ?? [];
+ var (windowTypes, columns) = WindowColumns(windows, inColumns);
+
+ var outTypes = node.Projection
+ .Select(item => ExpressionEvaluator.NumberTypeOf(item.Value, columns, e => DeclaredType(e, columns))).ToList();
var outColumns = node.Projection
- .Select((item, i) => new OutputColumn(null,
+ .Select((item, i) => OutputColumn.Computed(
item.Alias ?? (item.Value is ColumnReference c ? c.Column : $"Expr{i + 1}"),
- DeclaredType(item.Value, inColumns)))
+ DeclaredType(item.Value, columns),
+ outTypes[i],
+ item.Value))
.ToList();
+ var conversions = node.Projection.Select(item => ChoiceConversion(item.Value, columns)).ToList();
// A bare `SELECT COUNT(*)` wants the number of rows, not the rows. Everything below materialises the
// whole input first — which for this shape is the entire cost, and pure waste: holding every decoded row
@@ -1519,16 +1863,15 @@ private void ComputeWindow(
var inRows = inRowsEnum.ToList();
// Aggregates can appear in the projection, HAVING (e.g. HAVING COUNT(*) > 30) and ORDER BY
// (e.g. ORDER BY COUNT(*)); precompute all of them per group so each instance resolves.
+ // A window's arguments and keys may hold aggregates too — RANK() OVER (ORDER BY SUM(x)).
var aggregateCalls = node.Projection.SelectMany(i => Aggregates(i.Value))
.Concat(node.Having is { } h ? Aggregates(h) : [])
.Concat(node.OrderBy.SelectMany(k => Aggregates(k.Value)))
+ .Concat(windows.SelectMany(w => w.Function.Expressions().SelectMany(Aggregates)))
.ToList();
- // 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)>();
+ // 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)>();
foreach (List group in GroupRows(inRows, node.GroupBy, inColumns, outer))
{
var values = new Dictionary(ReferenceComparer.Instance);
@@ -1551,8 +1894,32 @@ private void ComputeWindow(
// HAVING filters whole groups after aggregation.
if (node.Having is not null && !eval.IsTrue(node.Having))
continue;
+ groups.Add((keyRow, values, eval));
+ }
+
+ // 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++)
+ 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]);
- object?[] row = node.Projection.Select(item => eval.Evaluate(item.Value)).ToArray();
+ // 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)>();
+ 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], currency: false), outTypes[i]))
+ .ToArray();
object?[] sortKeys = node.OrderBy.Select(k => eval.Evaluate(k.Value)).ToArray();
object?[] groupKeys = node.GroupBy.Select(k => eval.Evaluate(k)).ToArray();
outRows.Add((row, sortKeys, groupKeys));
@@ -1591,7 +1958,7 @@ private static bool IsBareCountStar(AggregateNode node) =>
node.GroupBy.Count == 0
&& node.Having is null
&& node.OrderBy.Count == 0
- && node.Projection is [{ Value: FunctionCall { Distinct: false, Arguments: [StarExpression] } call }]
+ && node.Projection is [{ Value: FunctionCall { Distinct: false, Filter: null, Arguments: [StarExpression] } call }]
&& string.Equals(call.Name, "COUNT", StringComparison.OrdinalIgnoreCase);
/// Counts a row sequence without retaining it. COUNT is an Access Long Integer, so the count is an
@@ -1643,14 +2010,13 @@ private static int CountRows(IEnumerable rows)
ExpressionEvaluator.ValidateArity(name, call.Arguments.Count);
Expression? arg = call.Arguments.Count > 0 ? call.Arguments[0] : null;
- // COUNT is an Access Long Integer (32-bit) — EF reads it with GetInt32, so return int, not long.
- if (name == "COUNT")
- {
- if (arg is StarExpression or null)
- return group.Count; // COUNT(*) counts rows; DISTINCT is meaningless (and EF never emits it)
- var counted = group.Select(r => Eval(columns, r, outer).Evaluate(arg)).Where(v => v is not null);
- return call.Distinct ? DistinctValues(counted).Count : counted.Count();
- }
+ // 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();
+
+ // COUNT(*) counts rows; DISTINCT is meaningless there (and EF never emits it).
+ if (name == "COUNT" && arg is StarExpression or null)
+ return group.Count;
// 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).
@@ -1659,59 +2025,59 @@ private static int CountRows(IEnumerable rows)
if (name == "LAST")
return group.Count == 0 ? null : Eval(columns, group[^1], outer).Evaluate(arg!);
- var values = group.Select(r => Eval(columns, r, outer).Evaluate(arg!)).Where(v => v is not null).ToList();
- // SUM(DISTINCT)/AVG(DISTINCT)/… aggregate the distinct set of the argument's values. MIN/MAX are
+ if (call.WithinGroup is { } directions)
+ {
+ if (group.Count == 0)
+ return null;
+ if (name == "LISTAGG")
+ {
+ IReadOnlyList keys = call.WithinGroupKeys;
+ return ListAgg.Of(
+ group.Select(r =>
+ {
+ ExpressionEvaluator e = Eval(columns, r, outer);
+ 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! : "",
+ directions,
+ call.Distinct);
+ }
+ // 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]),
+ directions[0]);
+ }
+
+ // A binary set function reads a pair from each row; the standard gives it no DISTINCT.
+ if (RunningAggregate.IsPair(name))
+ {
+ if (call.Distinct)
+ throw new NotSupportedException($"{call.Name} takes no DISTINCT.");
+ var pair = new RunningAggregate(name, countRows: false, currency: false);
+ foreach (object?[] row in group)
+ {
+ ExpressionEvaluator rowEval = Eval(columns, row, outer);
+ pair.AddPair(rowEval.Evaluate(call.Arguments[0]), rowEval.Evaluate(call.Arguments[1]));
+ }
+ return pair.Result;
+ }
+
+ IEnumerable values = group.Select(r => Eval(columns, r, outer).Evaluate(arg!));
+ // 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)
- values = DistinctValues(values);
- if (values.Count == 0)
- return null; // SUM/AVG/MIN/MAX of nothing is NULL (COUNT already returned above)
-
- // Result types: SUM **preserves the input type** (int→int, long→long, decimal→decimal, …) so the EF
- // provider (which emits a bare SUM and reads by the LINQ operand type) round-trips without a cast.
- // AVG is Double unless the input is Currency/Decimal (matches Access and LINQ). MIN/MAX keep the
- // column's own value and type.
- var inv = System.Globalization.CultureInfo.InvariantCulture;
- return name switch
- {
- "SUM" => SumPreservingType(values, inv),
- "AVG" => values[0] is decimal ? values.Average(v => Convert.ToDecimal(v, inv)) : values.Average(v => Convert.ToDouble(v, inv)),
- "MIN" => values.Aggregate((a, b) => ExpressionEvaluator.CompareForSort(a, b) <= 0 ? a : b),
- "MAX" => values.Aggregate((a, b) => ExpressionEvaluator.CompareForSort(a, b) >= 0 ? a : b),
- // Statistical aggregates. Sample forms (StDev/Var) divide by n-1 and are NULL for a single value;
- // population forms (StDevP/VarP) divide by n. Verified vs ACE.
- "VAR" or "STDEV" or "STDDEV" or "VARP" or "STDEVP" or "STDDEVP" => Statistic(name, values, inv),
- _ => throw new NotSupportedException($"Aggregate {call.Name} is not supported."),
- };
- }
+ values = DistinctValues(values.Where(v => v is not null));
- /// Access statistical aggregates over the non-null values (verified vs ACE). VAR/STDEV are the
- /// **sample** forms (divide by n−1, NULL for a single value); VARP/STDEVP the **population** forms (divide by
- /// n). STDEV/STDEVP are the square roots of VAR/VARP.
- private static object? Statistic(string name, List values, System.Globalization.CultureInfo inv)
- {
- int n = values.Count;
- double mean = values.Average(v => Convert.ToDouble(v, inv));
- double sumSq = values.Sum(v => { double d = Convert.ToDouble(v, inv) - mean; return d * d; });
- bool sample = !name.EndsWith("P", StringComparison.Ordinal); // VAR/STDEV sample; VARP/STDEVP population
- if (sample && n < 2) return null; // sample variance of one value is undefined
- double variance = sumSq / (sample ? n - 1 : n);
- bool stdev = name.Contains("DEV", StringComparison.Ordinal);
- return stdev ? Math.Sqrt(variance) : variance;
+ var aggregate = new RunningAggregate(name, countRows: false, currency: IsCurrency(arg!, columns));
+ foreach (object? value in values)
+ aggregate.Add(value);
+ return aggregate.Result;
}
- /// SUM keeping the operand's numeric type (as LINQ's Sum overloads do): integer types
- /// (byte/short/int) sum to Int32, Int64 to Int64, Single to Single, Double to Double, Decimal/Currency
- /// to Decimal.
- private static object SumPreservingType(List values, System.Globalization.CultureInfo inv) =>
- values[0] switch
- {
- decimal => values.Sum(v => Convert.ToDecimal(v, inv)),
- double => values.Sum(v => Convert.ToDouble(v, inv)),
- float => (float)values.Sum(v => Convert.ToDouble(v, inv)),
- long or ulong => values.Sum(v => Convert.ToInt64(v, inv)),
- _ => values.Sum(v => Convert.ToInt32(v, inv)),
- };
+ /// Whether an aggregate's argument is a Currency, which the statistical aggregates square exactly.
+ private bool IsCurrency(Expression argument, IReadOnlyList columns) =>
+ ExpressionEvaluator.NumberTypeOf(argument, columns, e => DeclaredType(e, columns)).Class == NumberClass.Currency;
private static IEnumerable Aggregates(Expression e)
{
@@ -1720,15 +2086,6 @@ private static IEnumerable Aggregates(Expression e)
case FunctionCall f when QueryPlanner.IsAggregate(f.Name):
yield return f;
break;
- case FunctionCall f:
- foreach (FunctionCall a in f.Arguments.SelectMany(Aggregates)) yield return a;
- break;
- case BinaryExpression b:
- foreach (FunctionCall a in Aggregates(b.Left).Concat(Aggregates(b.Right))) yield return a;
- break;
- case UnaryExpression u:
- foreach (FunctionCall a in Aggregates(u.Operand)) yield return a;
- break;
// Descend into subqueries: an aggregate over an *outer* column may appear there (a correlated
// subquery). Its own aggregates come along too, but are skipped when they can't be computed in
// this group's scope.
@@ -1741,17 +2098,12 @@ private static IEnumerable Aggregates(Expression e)
case InSubqueryExpression i:
foreach (FunctionCall a in Aggregates(i.Value).Concat(AggregatesInSelect(i.Query))) yield return a;
break;
- case InListExpression i:
- foreach (FunctionCall a in Aggregates(i.Value).Concat(i.Items.SelectMany(Aggregates))) yield return a;
- break;
- // Both halves of every arm, and the ELSE. An aggregate in a CASE is computed for the group up
- // front and handed to the evaluator by reference — the standard specifies the same order, that
- // aggregates in a WHEN are evaluated before the CASE rather than by it. Conditions matter as much
+ // Operands include both halves of every CASE arm, and the ELSE. An aggregate in a CASE is computed for
+ // the group up front and handed to the evaluator by reference — the standard specifies the same order,
+ // that aggregates in a WHEN are evaluated before the CASE rather than by it. Conditions matter as much
// as results: `HAVING CASE WHEN COUNT(*) > 1 THEN …` carries the aggregate in the condition.
- case CaseExpression c:
- foreach (FunctionCall a in c.WhenClauses
- .SelectMany(w => Aggregates(w.Condition).Concat(Aggregates(w.Result)))
- .Concat(c.ElseResult is { } e2 ? Aggregates(e2) : []))
+ default:
+ foreach (FunctionCall a in e.Operands()?.SelectMany(Aggregates) ?? [])
yield return a;
break;
}
@@ -1778,7 +2130,7 @@ private static IEnumerable Aggregates(Expression e)
/// Groups by structural equality of the key value tuple.
// DISTINCT / GROUP BY / INTERSECT / EXCEPT key. String keys use Access text semantics — case-insensitive
// and trailing-space-insensitive — so 'London' and 'LONDON ' group together as Access does.
- private sealed class GroupKey(object?[] values) : IEquatable
+ internal sealed class GroupKey(object?[] values) : IEquatable
{
private readonly object?[] _values = values;
diff --git a/src/LibRed/LibRed.Engine/Execution/RunningAggregate.cs b/src/LibRed/LibRed.Engine/Execution/RunningAggregate.cs
new file mode 100644
index 000000000..faa3e16e6
--- /dev/null
+++ b/src/LibRed/LibRed.Engine/Execution/RunningAggregate.cs
@@ -0,0 +1,237 @@
+using System.Globalization;
+using EntityFrameworkCore.Jet.Data;
+
+namespace LibRed.Engine.Execution;
+
+///
+/// An aggregate fed its argument's values one at a time, whose result can be read after any of them — the arithmetic
+/// behind both GROUP BY's aggregates and the windowed ones, so the two give the same values. Null values are skipped,
+/// except where COUNT(*) counts rows.
+///
+///
+/// COUNT is an Access Long Integer (32-bit) — EF reads it with GetInt32, so it is an int, not a long.
+/// The numeric aggregates read each value as the conversion functions read it (verified vs ACE): text as a
+/// number (text that is not one is a type mismatch), a date as its serial, True as -1; a GUID or binary value is a
+/// type mismatch. SUM keeps the type of the first value, as LINQ's Sum overloads do — the EF provider emits a
+/// bare SUM and reads it by the LINQ operand type: Boolean, Byte, Integer and Long sum to Int32, Int64 to Int64, Single
+/// to Single, Double (text and dates included) to Double, Decimal and Currency to Decimal. AVG is Double unless the
+/// first value is a Decimal or Currency, and a Decimal average keeps its full precision, as LINQ's does; ACE rounds a
+/// Currency average to four places and cuts a Decimal one to ten.
+/// MIN and MAX keep the value and its type, in the sort order. Empty text wins against any other text but then
+/// counts as no value, so the next value replaces it, and empty text left at the end is Null (verified vs ACE: Min of
+/// '5', 'x', '', '7' is '7', and Min of '5', '' is Null).
+/// The statistical aggregates are verified vs ACE to the last bit. VAR and STDEV are the sample forms, Null for
+/// a single value; VARP and STDEVP the population forms; the STDEVs are the square roots. ACE works them out as
+/// (n·Σx² − (Σx)²) / (n·(n−1)), or / n² for the population, in doubles; a Single is squared in single precision, and
+/// a Currency's square and squared sum are Currency products, rounded to four places. The standard's names —
+/// STDDEV_SAMP, STDDEV_POP, VAR_SAMP and VAR_POP — are the same aggregates.
+/// The standard's binary set functions — CORR, COVAR_POP, COVAR_SAMP and the REGR_ family — take a pair, the
+/// dependent value first (), and use only the pairs where neither is Null. ACE has none of them,
+/// so there is nothing to match bit for bit: the sums of squares and products are kept by the Youngs–Cramer update,
+/// as PostgreSQL keeps them, which does not lose the small differences that the textbook Σx² − (Σx)²/n cancels away.
+/// REGR_COUNT is an int, as COUNT is; the rest are Doubles, Null over no pairs. The standard's rules for a degenerate
+/// input apply: COVAR_SAMP needs two pairs; REGR_SLOPE, REGR_INTERCEPT and REGR_R2 are Null when every x is the same,
+/// and CORR when every x or every y is; REGR_R2 is 1 when every y is the same.
+///
+internal sealed class RunningAggregate
+{
+ private static readonly Dictionary StandardNames = new()
+ {
+ ["STDDEV_SAMP"] = "STDEV",
+ ["STDDEV_POP"] = "STDEVP",
+ ["VAR_SAMP"] = "VAR",
+ ["VAR_POP"] = "VARP",
+ };
+
+ private static readonly HashSet PairNames =
+ [
+ "CORR", "COVAR_POP", "COVAR_SAMP", "REGR_COUNT", "REGR_AVGX", "REGR_AVGY", "REGR_SXX", "REGR_SYY", "REGR_SXY",
+ "REGR_SLOPE", "REGR_INTERCEPT", "REGR_R2",
+ ];
+
+ private readonly string _name;
+ private readonly bool _countRows;
+ private readonly bool _currency;
+
+ // The pair aggregates: sums of x and y, and the sums of squared and multiplied deviations from their means.
+ private double _sumX, _sumY, _sxx, _syy, _sxy;
+
+ private int _count;
+ private object? _extreme;
+ private object? _first;
+ private decimal _decimalSum;
+ private double _doubleSum;
+ private long _longSum;
+ private int _intSum;
+ private double _squares;
+ private decimal _exactSum;
+ private decimal _exactSquares;
+ private bool _allDecimal = true;
+
+ /// The aggregate, in upper case.
+ /// 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)
+ {
+ _name = Supports(name) ? Canonical(name) : throw new NotSupportedException($"Aggregate {name} is not supported.");
+ _countRows = countRows;
+ _currency = currency;
+ }
+
+ /// Whether (upper case) is an aggregate this computes.
+ public static bool Supports(string name) =>
+ Canonical(name) is "COUNT" or "SUM" or "AVG" or "MIN" or "MAX" or "VAR" or "VARP" or "STDEV" or "STDEVP"
+ or "STDDEV" or "STDDEVP"
+ || IsPair(name);
+
+ /// Whether (upper case) is a binary set function, fed by .
+ public static bool IsPair(string name) => PairNames.Contains(name);
+
+ /// The Access name for one of the standard's: STDDEV_SAMP is STDEV, VAR_POP is VARP, ….
+ public static string Canonical(string name) => StandardNames.GetValueOrDefault(name, name);
+
+ /// Adds a pair to a binary set function: , the dependent value, and
+ /// . The pair counts only when neither is Null; each is read as the conversion functions
+ /// read a value.
+ public void AddPair(object? y, object? x)
+ {
+ if (y is null || x is null)
+ return;
+ double dy = Convert.ToDouble(ExpressionEvaluator.ConversionNumber(y), CultureInfo.InvariantCulture);
+ double dx = Convert.ToDouble(ExpressionEvaluator.ConversionNumber(x), CultureInfo.InvariantCulture);
+ double n = ++_count;
+ _sumX += dx;
+ _sumY += dy;
+ if (n > 1)
+ {
+ double tx = dx * n - _sumX, ty = dy * n - _sumY, scale = 1.0 / (n * (n - 1));
+ _sxx += tx * tx * scale;
+ _syy += ty * ty * scale;
+ _sxy += tx * ty * scale;
+ }
+ }
+
+ private static bool IsStatistic(string name) => name is not ("COUNT" or "SUM" or "AVG" or "MIN" or "MAX");
+
+ public void Add(object? value)
+ {
+ if (_countRows)
+ {
+ _count++;
+ return;
+ }
+ if (value is null)
+ return;
+ _count++;
+
+ if (_name is "COUNT")
+ return;
+ if (_name is "MIN" or "MAX")
+ {
+ if (_extreme is null or string { Length: 0 }
+ || (_name == "MAX"
+ ? ExpressionEvaluator.CompareForSort(value, _extreme) > 0
+ : ExpressionEvaluator.CompareForSort(value, _extreme) < 0))
+ _extreme = value;
+ return;
+ }
+
+ object number = ExpressionEvaluator.ConversionNumber(value);
+ _first ??= number;
+ CultureInfo invariant = CultureInfo.InvariantCulture;
+ if (IsStatistic(_name))
+ {
+ if (number is not decimal)
+ _allDecimal = false;
+ else if (_currency)
+ {
+ decimal m = (decimal)number;
+ _exactSum += m;
+ _exactSquares += decimal.Round(m * m, 4);
+ }
+
+ if (number is float f)
+ {
+ _doubleSum += f;
+ _squares += f * f;
+ }
+ else
+ {
+ double d = Convert.ToDouble(number, invariant);
+ _doubleSum += d;
+ _squares += d * d;
+ }
+ return;
+ }
+
+ switch (_first)
+ {
+ case decimal:
+ _decimalSum += JetDecimalConverter.ToDecimal(number, invariant);
+ break;
+ case long or ulong when _name == "SUM":
+ _longSum = checked(_longSum + Convert.ToInt64(number, invariant));
+ break;
+ case double or float:
+ case not null when _name == "AVG":
+ _doubleSum += Convert.ToDouble(number, invariant);
+ break;
+ default:
+ _intSum = checked(_intSum + Convert.ToInt32(number, invariant));
+ break;
+ }
+ }
+
+ /// The aggregate of the values added so far.
+ public object? Result => _name switch
+ {
+ "COUNT" or "REGR_COUNT" => _count,
+ "MIN" or "MAX" => _extreme is string { Length: 0 } ? null : _extreme,
+ _ when _count == 0 => null,
+ _ when IsPair(_name) => PairResult(),
+ "SUM" => _first switch
+ {
+ decimal => _decimalSum,
+ double => _doubleSum,
+ float => (float)_doubleSum,
+ long or ulong => _longSum,
+ _ => _intSum,
+ },
+ "AVG" => _first is decimal ? _decimalSum / _count : _doubleSum / _count,
+ _ => Statistic(),
+ };
+
+ private double? PairResult()
+ {
+ double n = _count;
+ return _name switch
+ {
+ "COVAR_POP" => _sxy / n,
+ "COVAR_SAMP" => n < 2 ? null : _sxy / (n - 1),
+ "CORR" => _sxx == 0 || _syy == 0 ? null : _sxy / (Math.Sqrt(_sxx) * Math.Sqrt(_syy)),
+ "REGR_AVGX" => _sumX / n,
+ "REGR_AVGY" => _sumY / n,
+ "REGR_SXX" => _sxx,
+ "REGR_SYY" => _syy,
+ "REGR_SXY" => _sxy,
+ "REGR_SLOPE" => _sxx == 0 ? null : _sxy / _sxx,
+ "REGR_INTERCEPT" => _sxx == 0 ? null : (_sumY - _sumX * _sxy / _sxx) / n,
+ _ => _sxx == 0 ? null : _syy == 0 ? 1.0 : _sxy * _sxy / (_sxx * _syy), // REGR_R2
+ };
+ }
+
+ private object? Statistic()
+ {
+ bool sample = !_name.EndsWith('P');
+ if (sample && _count < 2)
+ return null;
+
+ (double squares, double squaredSum) = _currency && _allDecimal
+ ? ((double)_exactSquares, (double)decimal.Round(_exactSum * _exactSum, 4))
+ : (_squares, _doubleSum * _doubleSum);
+ double n = _count;
+ double variance = (n * squares - squaredSum) / (sample ? n * (n - 1) : n * n);
+ return _name.Contains("DEV", StringComparison.Ordinal) ? Math.Sqrt(variance) : variance;
+ }
+}
diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs
index 6c976e951..0bad79747 100644
--- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs
+++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs
@@ -1,4 +1,5 @@
using LibRed.Catalog;
+using LibRed.Engine.Planning;
using LibRed.Sql.Ast;
using LibRed.Sql.Parsing;
using LibRed.Storage;
@@ -66,16 +67,7 @@ private int ExecuteCreateTable(CreateTableStatement statement)
ValidateColumnDefault(spec, def.Default);
IReadOnlyList? primaryKey = statement.PrimaryKey.Count > 0 ? statement.PrimaryKey : null;
- var relationships = statement.ForeignKeys.Select(fk => new RelationshipSpec(
- Name: fk.Name ?? DefaultRelationshipName(statement.Table, fk),
- ReferencedTable: fk.ReferencedTable,
- Columns: PairColumns(fk),
- IsEnforced: true,
- CascadeUpdate: fk.OnUpdate == ReferentialAction.Cascade,
- CascadeDelete: fk.OnDelete == ReferentialAction.Cascade,
- NoIndex: fk.NoIndex,
- DeleteSetNull: fk.OnDelete == ReferentialAction.SetNull,
- UpdateSetNull: fk.OnUpdate == ReferentialAction.SetNull)).ToList();
+ var relationships = statement.ForeignKeys.Select(fk => ToRelationshipSpec(statement.Table, fk)).ToList();
var uniques = statement.UniqueConstraints.Select((u, i) => new UniqueIndexSpec(
Name: u.Name ?? $"UQ_{statement.Table}_{i}",
@@ -128,8 +120,7 @@ private static void EnforceRequired(string table, IReadOnlyList colum
private void EnforceCheckConstraints(TableDef definition, object?[] values)
{
if (definition.CheckConstraints.Count == 0) return;
- var schema = definition.Columns
- .Select(c => new OutputColumn(definition.Name, c.Name, Schema.JetClrTypeMap.ToClrType(c.Type))).ToList();
+ 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)
@@ -138,14 +129,22 @@ private void EnforceCheckConstraints(TableDef definition, object?[] values)
"Enter a value that the expression for this field can accept.");
}
- /// Pairs each child FK column with its referenced parent column, in key order.
- private static List<(string Column, string ReferencedColumn)> PairColumns(ForeignKeyConstraint fk)
- {
- if (fk.ReferencedColumns.Count != fk.Columns.Count)
- throw new InvalidOperationException(
- $"Foreign key on '{fk.ReferencedTable}' has {fk.Columns.Count} columns but references {fk.ReferencedColumns.Count}.");
- return fk.Columns.Zip(fk.ReferencedColumns).ToList();
- }
+ /// The relationship a parsed foreign key creates on , for CREATE TABLE and
+ /// ALTER TABLE alike. A REFERENCES table with no column list names only its child columns; the table
+ /// creator resolves it to the parent's primary key.
+ private static RelationshipSpec ToRelationshipSpec(string childTable, ForeignKeyConstraint fk) => new(
+ Name: fk.Name ?? DefaultRelationshipName(childTable, fk),
+ ReferencedTable: fk.ReferencedTable,
+ Columns: fk.ReferencedColumns.Count == 0
+ ? RelationshipSpec.ChildColumnsOnly(fk.Columns)
+ : RelationshipSpec.PairColumns(fk.ReferencedTable, fk.Columns, fk.ReferencedColumns),
+ IsEnforced: true,
+ CascadeUpdate: fk.OnUpdate == ReferentialAction.Cascade,
+ CascadeDelete: fk.OnDelete == ReferentialAction.Cascade,
+ NoIndex: fk.NoIndex,
+ DeleteSetNull: fk.OnDelete == ReferentialAction.SetNull,
+ UpdateSetNull: fk.OnUpdate == ReferentialAction.SetNull,
+ ReferencesPrimaryKey: fk.ReferencedColumns.Count == 0);
/// Access-style fallback name when the constraint is unnamed: "childparent".
private static string DefaultRelationshipName(string childTable, ForeignKeyConstraint fk) =>
@@ -448,7 +447,7 @@ private int ExecuteCreateProcedure(CreateProcedureStatement statement)
// MSysRelationships, so removing those rows disables it.
DropConstraintAction drop => DropConstraint(statement.Table, drop.Name),
// ADD COLUMN: append the column's descriptor/name to the TDEF (existing rows read it as NULL).
- AddColumnAction add => AddColumn(statement.Table, add.Column),
+ AddColumnAction add => AddColumn(statement.Table, add),
// DROP COLUMN: a metadata-only TDEF edit (remove the descriptor + name, decrement ColumnCount).
DropColumnAction dropCol => DropColumn(statement.Table, dropCol.Field),
// ALTER COLUMN field type: change the column's declared type (a variable text/binary length change is a
@@ -525,13 +524,23 @@ private int DropConstraint(string table, string name)
"constraint of that name exists.");
}
- private int AddColumn(string table, ColumnDefinition column)
+ private int AddColumn(string table, AddColumnAction add)
{
+ ColumnDefinition column = add.Column;
// NOT NULL and DEFAULT are written to the column's LvProp properties (Required / DefaultValue).
ColumnSpec spec = MapColumn(column);
ValidateColumnDefault(spec, column.Default);
if (!_database.AddColumn(table, spec, column.Default))
throw new InvalidOperationException($"ALTER TABLE '{table}' ADD COLUMN '{column.Name}': the column already exists.");
+ // The column's constraints apply to it as in CREATE TABLE — a primary key, a unique index, a relationship —
+ // in that order, as ACE's ADD COLUMN does. Each fails the whole statement when ACE's would: a second
+ // primary key, or a primary key over a column the existing rows leave NULL.
+ if (column.PrimaryKey)
+ AddPrimaryKey(table, new AddPrimaryKeyAction(add.PrimaryKeyName, [column.Name]));
+ if (add.Unique is { } unique)
+ AddUnique(table, new AddUniqueAction(unique));
+ if (add.References is { } references)
+ AddForeignKey(table, references);
return 0;
}
@@ -619,7 +628,8 @@ private int AddCheck(string table, AddCheckAction chk)
private int AlterColumn(string table, AlterColumnAction alter)
{
- var colDef = new ColumnDefinition(alter.Field, alter.TypeName, alter.Size, alter.Scale, NotNull: false, PrimaryKey: false);
+ var colDef = new ColumnDefinition(alter.Field, alter.TypeName, alter.Size, alter.Scale, NotNull: false, PrimaryKey: false,
+ Identity: alter.Identity);
ColumnSpec spec = MapColumn(colDef);
// Validated against the column's NEW type, and before the type change, so a rejected DEFAULT leaves
// the whole statement having done nothing.
@@ -648,16 +658,7 @@ private int DropColumnDefault(string table, AlterColumnDropDefaultAction drop)
private int AddForeignKey(string table, ForeignKeyConstraint fk)
{
- _database.AddForeignKey(table, new RelationshipSpec(
- Name: fk.Name ?? DefaultRelationshipName(table, fk),
- ReferencedTable: fk.ReferencedTable,
- Columns: PairColumns(fk),
- IsEnforced: true,
- CascadeUpdate: fk.OnUpdate == ReferentialAction.Cascade,
- CascadeDelete: fk.OnDelete == ReferentialAction.Cascade,
- NoIndex: fk.NoIndex,
- DeleteSetNull: fk.OnDelete == ReferentialAction.SetNull,
- UpdateSetNull: fk.OnUpdate == ReferentialAction.SetNull));
+ _database.AddForeignKey(table, ToRelationshipSpec(table, fk));
return 0;
}
@@ -796,6 +797,40 @@ private Dictionary SourceColumnsFor(SelectStatement statement
/// from: NULL stores NULL, DEFAULT takes whatever the column declares.
private static readonly object DefaultRowValue = new();
+ /// A table's columns with a DEFAULT value (parsed once), and its AutoNumber column if it has one — Jet
+ /// allows at most one, and its value after an insert is @@IDENTITY.
+ private sealed record RowDefaults(List<(int Index, Expression Expression)> Columns, ColumnDef? AutoNumber);
+
+ ///
+ /// The defaults a new row takes for the columns it omits, matching Access — EF Core relies on the store default
+ /// rather than supplying the value itself. AutoNumber columns are excluded: their value is assigned by the row
+ /// 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(
+ definition.Columns
+ .Where(c => c.DefaultValue is not null && !c.IsAutoNumber)
+ .Select(c => (c.Index, Expression: ParseDefaultExpression(c.DefaultValue!)))
+ .ToList(),
+ definition.Columns.FirstOrDefault(c => c.IsAutoNumber));
+
+ /// Writes a new row: every column not in takes its default (an explicit
+ /// NULL is left as NULL), the row is checked as any insert is, and it is written. Returns its AutoNumber value,
+ /// if it has one.
+ private object? InsertNewRow(string tableName, Table table, RowDefaults defaults, object?[] values, IReadOnlySet provided)
+ {
+ var evaluator = new ExpressionEvaluator(new EvalScope([], [], null), _scalarRunner, parameters: _parameters);
+ foreach (var (index, expression) in defaults.Columns)
+ if (!provided.Contains(index))
+ values[index] = evaluator.Evaluate(expression);
+
+ EnforceRequired(tableName, table.Definition.Columns, values);
+ EnforceReferentialIntegrity(tableName, table, values);
+ EnforceCheckConstraints(table.Definition, values);
+ table.Insert(values); // fills values[autoNumber.Index] with the generated id (array mutated in place)
+ return defaults.AutoNumber is { } autoNumber ? values[autoNumber.Index] : null;
+ }
+
private int ExecuteInsert(InsertStatement statement)
{
Table table = _database.OpenTable(statement.Table);
@@ -811,19 +846,7 @@ private int ExecuteInsert(InsertStatement statement)
var evaluator = new ExpressionEvaluator(
new EvalScope([], [], null), _scalarRunner, parameters: _parameters);
-
- // Columns with a DEFAULT value (parsed once): applied to any row that omits the column, matching
- // Access — EF Core relies on the store default rather than supplying the value itself.
- // AutoNumber columns are excluded: their value is assigned by the row 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.
- var defaultColumns = columns
- .Where(c => c.DefaultValue is not null && !c.IsAutoNumber)
- .Select(c => (c.Index, Expression: ParseDefaultExpression(c.DefaultValue!)))
- .ToList();
-
- // Jet allows at most one AutoNumber column; its post-insert value is @@IDENTITY.
- ColumnDef? autoNumber = columns.FirstOrDefault(c => c.IsAutoNumber);
+ RowDefaults defaults = DefaultsOf(table.Definition);
int affected = 0;
object? lastIdentity = null;
@@ -844,7 +867,7 @@ void InsertRow(ReadOnlySpan supplied)
?? throw new InvalidOperationException($"Column '{targets[i]}' does not exist in '{statement.Table}'.");
// An explicit DEFAULT is not a value: leaving the column out of `provided` routes it through
- // the default-filling loop below, so it takes its declared default, or stays NULL when it has
+ // InsertNewRow's default filling, so it takes its declared default, or stays NULL when it has
// none — which is what the standard specifies. A NOT NULL column with no default then fails
// EnforceRequired, as it should. An AutoNumber column takes its generated id, the same as it
// would from INSERT INTO t DEFAULT VALUES.
@@ -855,17 +878,9 @@ void InsertRow(ReadOnlySpan supplied)
provided.Add(column.Index);
}
- // Fill defaults for columns the insert didn't mention (an explicit NULL is left as NULL).
- foreach (var (index, expression) in defaultColumns)
- if (!provided.Contains(index))
- values[index] = evaluator.Evaluate(expression);
-
- EnforceRequired(statement.Table, columns, values);
- EnforceReferentialIntegrity(statement.Table, table, values);
- EnforceCheckConstraints(table.Definition, values);
- table.Insert(values); // fills values[autoNumber.Index] with the generated id (array mutated in place)
- if (autoNumber is not null)
- lastIdentity = values[autoNumber.Index];
+ object? identity = InsertNewRow(statement.Table, table, defaults, values, provided);
+ if (defaults.AutoNumber is not null)
+ lastIdentity = identity;
affected++;
}
@@ -915,7 +930,7 @@ void InsertRow(ReadOnlySpan supplied)
if (_session is not null)
{
_session.RowCount = affected;
- if (autoNumber is not null)
+ if (defaults.AutoNumber is not null)
_session.LastIdentity = lastIdentity;
}
return affected;
@@ -927,8 +942,19 @@ void InsertRow(ReadOnlySpan supplied)
/// null and its already-materialised (never a target).
/// The plan of a LATERAL source — an APPLY's right side — which is re-executed once
/// per outer row instead of being materialised, because it may correlate to the rows joined before it.
+ /// A writable derived table is one source table per table it reads, in a run: the first carries the
+ /// joined rows the derived query chose (, tables wide) and the rest
+ /// are , filled from them. tells a table's rows apart from another
+ /// table's under the same alias.
private sealed record SourceTable(string Alias, Table? Table, IReadOnlyList Columns,
- IReadOnlyList? DerivedRows, Plan.PlanNode? Lateral = null);
+ IReadOnlyList? DerivedRows, Plan.PlanNode? Lateral = null)
+ {
+ public IReadOnlyList<(RowId Id, object?[] Values)[]>? Combos { get; init; }
+ public int ComboWidth { get; init; } = 1;
+ public bool InCombo { get; init; }
+ public string? Key { get; init; }
+ public string CacheKey => Key ?? Alias;
+ }
/// How a join kind is handled in a DML source.
private enum JoinShape
@@ -966,30 +992,51 @@ private enum JoinShape
/// Flattens the UPDATE/DELETE table source into its tables in order, each paired with the join that
/// introduced it: its and ON condition (the first/base table is Inner with a null ON).
- /// INNER/CROSS/LEFT joins over named tables are supported — the left-deep form EF and Access emit.
- private (List Tables, List Kinds, List Ons) ResolveSource(TableReference from)
+ /// INNER/CROSS/LEFT joins over named tables are supported — the left-deep form EF and Access emit — and the
+ /// bracketed groups WalkGroup describes. A table's group base, when it has one, is the table it is null-extended
+ /// with.
+ private (List Tables, List Kinds, List Ons, List GroupBases) ResolveSource(
+ TableReference from)
{
var tables = new List();
var kinds = new List();
var ons = new List();
+ var groupBases = new List();
void EmitTable(NamedTable n, JoinKind kind, Expression? on)
{
Table t = _database.OpenTable(n.Name);
string alias = n.Alias ?? n.Name;
- tables.Add(new SourceTable(alias, t, t.Definition.Columns
- .Select(c => new OutputColumn(alias, c.Name, Schema.JetClrTypeMap.ToClrType(c.Type))).ToList(), null));
+ tables.Add(new SourceTable(alias, t, t.Definition.Columns.Select(c => OutputColumn.Of(alias, c)).ToList(), null));
kinds.Add(kind);
ons.Add(on);
+ groupBases.Add(null);
}
void EmitDerived(SubqueryTable sq, JoinKind kind, Expression? on)
{
- string alias = sq.Alias ?? throw new NotSupportedException("A derived table in an UPDATE/DELETE source requires an alias.");
+ if (WritableDerivedTable(sq) is { } writable)
+ {
+ // The ON and join kind go with the run's first table, which brings in all of the run's rows.
+ foreach (SourceTable table in writable)
+ {
+ tables.Add(table);
+ kinds.Add(table.InCombo ? JoinKind.Inner : kind);
+ ons.Add(table.InCombo ? null : on);
+ groupBases.Add(null);
+ }
+ return;
+ }
+
+ // Without an alias a derived table can only be the one being written, which this one cannot be.
+ string alias = sq.Alias ?? throw new NotSupportedException(
+ "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));
kinds.Add(kind);
ons.Add(on);
+ groupBases.Add(null);
}
void EmitLateral(SubqueryTable sq, JoinKind kind, Expression? on)
@@ -1010,9 +1057,10 @@ void EmitLateral(SubqueryTable sq, JoinKind kind, Expression? on)
plan, new EvalScope(outerColumns, new object?[outerColumns.Count], null));
tables.Add(new SourceTable(
- alias, null, columns.Select(c => new OutputColumn(alias, c.Name, c.ClrType)).ToList(), null, plan));
+ alias, null, columns.Select(c => c with { Qualifier = alias }).ToList(), null, plan));
kinds.Add(kind);
ons.Add(on);
+ groupBases.Add(null);
}
void Walk(TableReference r, JoinKind kind, Expression? on)
@@ -1044,6 +1092,10 @@ void WalkJoin(JoinTable j)
Walk(j.Right, j.Kind, j.On);
break;
+ case JoinShape.Conditional when j.Right is JoinTable group:
+ WalkGroup(j, group);
+ break;
+
// RIGHT JOIN keeps the right side: model it as the right side preserved (the base) with the left
// side LEFT-joined onto it. The SET/DELETE target is usually that left side — which then becomes
// the nullable side, so a right row with no match yields a null target that the WHERE drops.
@@ -1070,8 +1122,166 @@ void WalkJoin(JoinTable j)
}
}
+ // A join onto a bracketed group — UPDATE a, b INNER JOIN c ON … parses as a CROSS (b INNER c) — lays the
+ // group's tables out after the left side's, which gives the same rows in the shapes ACE accepts (verified):
+ // - an inner or cross join whose ON reads only the tables before the group's first outer join, checked once
+ // those have joined (a INNER JOIN (b LEFT JOIN c) ON a.x = b.x);
+ // - a LEFT join onto a table followed only by LEFT joins, whose ON reads only that first table; when the
+ // first table is null-extended so is the rest of the group (a LEFT JOIN (b LEFT JOIN c) ON a.x = b.x).
+ // ACE refuses the others ("JOIN expression not supported"), a LEFT join onto an inner join among them.
+ void WalkGroup(JoinTable j, JoinTable group)
+ {
+ Walk(j.Left, JoinKind.Inner, null);
+ int first = tables.Count;
+ Walk(group, JoinKind.Inner, null);
+ int end = tables.Count;
+
+ // The group's tables as units: a writable derived table's run is one, joined and null-extended whole
+ // (verified vs ACE, which accepts a derived join anywhere a table can be in these shapes).
+ List units = Enumerable.Range(first, end - first).Where(t => !tables[t].InCombo).ToList();
+ int innerUnits = 1;
+ while (innerUnits < units.Count && kinds[units[innerUnits]] is JoinKind.Inner or JoinKind.Cross)
+ innerUnits++;
+ int innerEnd = innerUnits < units.Count ? units[innerUnits] : end;
+ bool left = j.Kind == JoinKind.Left;
+ HashSet readable = tables.Take(left ? first + tables[first].ComboWidth : innerEnd)
+ .Select(t => t.Alias).ToHashSet(StringComparer.OrdinalIgnoreCase);
+ bool onFits = j.On is null || Planning.IndexSelection.ReferencesOnly(j.On, readable);
+
+ if (!left && onFits)
+ {
+ int last = units[innerUnits - 1];
+ if (j.On is not null)
+ {
+ ons[last] = ons[last] is { } own ? new BinaryExpression(BinaryOperator.And, own, j.On) : j.On;
+ kinds[last] = JoinKind.Inner;
+ }
+ }
+ else if (left && onFits && innerUnits == 1 && units.Skip(1).All(u => kinds[u] == JoinKind.Left))
+ {
+ kinds[first] = JoinKind.Left;
+ ons[first] = j.On;
+ for (int i = first + tables[first].ComboWidth; i < end; i++)
+ groupBases[i] ??= first;
+ }
+ else
+ {
+ throw new NotSupportedException($"UPDATE/DELETE over a {j.Kind} join onto this group is not supported.");
+ }
+ }
+
Walk(from, JoinKind.Inner, null);
- return (tables, kinds, ons);
+ return (tables, kinds, ons, groupBases);
+ }
+
+ ///
+ /// A derived table an UPDATE or DELETE can write through, as ACE allows (verified: UPDATE (SELECT TOP 2 * FROM t
+ /// ORDER BY Id DESC) SET x = 1 updates those two rows of t, and DELETE * FROM (SELECT …) deletes them): a SELECT
+ /// filtered, ordered and cut by its TOP or OFFSET, with no grouping or DISTINCT, over a table or a join. Its rows
+ /// are its tables' rows, so they can be written back: it becomes one source table per table it reads, whose
+ /// rows come together, as the query chose them. Without an alias it takes the name of the one table it reads.
+ /// Only the columns it selects can be read or set, under the names it gives them (verified vs ACE: SELECT X
+ /// AS Y makes SET Y write X, and a column not selected is unknown). A computed item neither stops the rest being
+ /// written nor can itself be read. Null for any other query.
+ ///
+ private List? WritableDerivedTable(SubqueryTable sq)
+ {
+ if (sq.Query is not SelectStatement
+ {
+ From: NamedTable or JoinTable, GroupBy.Count: 0, Having: null, Distinct: false, Into: null,
+ } select
+ || select.Projection.Any(item => item.Value is StarExpression or QualifiedStarExpression && item.Alias is not null))
+ return null;
+
+ var (inner, kinds, ons, groupBases) = ResolveSource(select.From);
+ var innerColumns = inner.SelectMany(t => t.Columns).ToList();
+
+ // The name each column goes by, per table and by position; a column the query does not select has none.
+ var names = inner.Select(t => new string?[t.Columns.Count]).ToArray();
+ void Name(int table, int column, string name) => names[table][column] = name;
+ if (select.IsSelectStar)
+ for (int t = 0; t < inner.Count; t++)
+ for (int c = 0; c < inner[t].Columns.Count; c++) Name(t, c, inner[t].Columns[c].Name);
+ foreach (SelectItem item in select.Projection)
+ {
+ if (item.Value is ColumnReference reference)
+ {
+ // The one table's column it names; an ambiguous or unknown one exposes nothing.
+ var found = FindColumns(inner, reference.Table, reference.Column);
+ if (found.Count == 1)
+ Name(found[0].Table, found[0].Position, item.Alias ?? inner[found[0].Table].Columns[found[0].Position].Name);
+ continue;
+ }
+ for (int t = 0; t < inner.Count; t++)
+ {
+ if (item.Value is StarExpression
+ || item.Value is QualifiedStarExpression star && string.Equals(star.Table, inner[t].Alias, StringComparison.OrdinalIgnoreCase))
+ {
+ for (int c = 0; c < inner[t].Columns.Count; c++) names[t][c] ??= inner[t].Columns[c].Name;
+ }
+ }
+ }
+ ExpressionEvaluator Over(object?[] values) =>
+ new(new EvalScope(innerColumns, values, null), _scalarRunner, _parameters, _session);
+
+ var rows = JoinRows(inner, kinds, ons, groupBases, select.Where, innerColumns);
+ object?[] Flat((RowId Id, object?[] Values)[] combo) => combo.SelectMany(c => c.Values).ToArray();
+ if (select.OrderBy.Count > 0)
+ {
+ // Stable, as a SELECT's ORDER BY is: rows with equal keys keep the order they were read in.
+ rows = rows
+ .Select(row => (Row: row, Keys: select.OrderBy.Select(o => Over(Flat(row)).Evaluate(o.Value)).ToArray()))
+ .OrderBy(keyed => keyed.Keys, Comparer.Create((a, b) =>
+ {
+ for (int i = 0; i < a.Length; i++)
+ {
+ int order = ExpressionEvaluator.CompareForSort(a[i], b[i]);
+ if (order != 0)
+ return select.OrderBy[i].Direction == SortDirection.Descending ? -order : order;
+ }
+ return 0;
+ }))
+ .Select(keyed => keyed.Row)
+ .ToList();
+ }
+
+ var counts = new ExpressionEvaluator(new EvalScope([], [], null), _scalarRunner, _parameters, _session);
+ int Count(Expression e) => Convert.ToInt32(counts.Evaluate(e), System.Globalization.CultureInfo.InvariantCulture);
+ if (select.Offset is { } offset)
+ rows = rows.Skip(Math.Max(0, Count(offset))).ToList();
+ if (select.Top is { } top)
+ {
+ // TOP n PERCENT takes ceil(rows × n / 100), as a SELECT does.
+ int n = Count(top);
+ rows = rows.Take(select.TopPercent ? (int)(((long)rows.Count * n + 99) / 100) : Math.Max(0, n)).ToList();
+ }
+
+ // A column without a name stays in the row, where writing it back needs it, under a name no SQL can write.
+ string alias = sq.Alias ?? (inner.Count == 1 ? inner[0].Alias : "\0derived");
+ return inner.Select((t, i) => t with
+ {
+ Alias = alias,
+ Key = $"{alias}\0{t.CacheKey}",
+ Columns = t.Columns.Select((c, position) => c with { Qualifier = alias, Name = names[i][position] ?? HiddenColumn }).ToList(),
+ Combos = i == 0 ? rows : null,
+ ComboWidth = i == 0 ? inner.Count : 1,
+ InCombo = i > 0,
+ }).ToList();
+ }
+
+ private const string HiddenColumn = "\0hidden";
+
+ /// The table and position of every source column named : among the tables
+ /// names — by alias, or failing that by table name — or among all of them.
+ private static List<(int Table, int Position)> FindColumns(List tables, string? qualifier, string column)
+ {
+ IEnumerable candidates = qualifier is null ? Enumerable.Range(0, tables.Count) : NamedTables(tables, qualifier);
+ var found = new List<(int, int)>();
+ foreach (int t in candidates)
+ for (int c = 0; c < tables[t].Columns.Count; c++)
+ if (string.Equals(tables[t].Columns[c].Name, column, StringComparison.OrdinalIgnoreCase))
+ found.Add((t, c));
+ return found;
}
/// Runs a derived-table subquery (uncorrelated — a FROM/JOIN source) through the full query
@@ -1095,7 +1305,8 @@ void WalkJoin(JoinTable j)
/// value accumulates across matches — matching Access (e.g. a "one"-side counter incremented per match).
///
private List<(RowId Id, object?[] Values)[]> JoinRows(
- List tables, List kinds, List ons, Expression? where, IReadOnlyList columns)
+ List tables, List kinds, List ons, List groupBases, Expression? where,
+ IReadOnlyList columns)
{
// A physical row's value array is shared across every join row it appears in (see the method summary).
var cache = new Dictionary<(string, RowId), object?[]>();
@@ -1129,6 +1340,19 @@ void WalkJoin(JoinTable j)
var result = new List<(RowId, object?[])[]>();
var acc = new (RowId, object?[])[tables.Count];
+ var nullExtended = new bool[tables.Count];
+
+ // A writable derived table's run of tables is null-extended, like its rows are joined, together.
+ void NullExtend(int i)
+ {
+ int width = tables[i].ComboWidth;
+ for (int k = i; k < i + width; k++)
+ {
+ acc[k] = (default, new object?[tables[k].Columns.Count]);
+ nullExtended[k] = true;
+ }
+ Recurse(i + width);
+ }
bool Holds(Expression? predicate, int depth) =>
predicate is null || new ExpressionEvaluator(
@@ -1144,6 +1368,38 @@ void Recurse(int i)
return;
}
+ // A table inside a LEFT-joined group has no row when the group's first table has none.
+ if (groupBases[i] is { } groupBase && nullExtended[groupBase])
+ {
+ NullExtend(i);
+ return;
+ }
+ nullExtended[i] = false;
+
+ // A writable derived table's rows, already joined, filtered, ordered and cut: each fills its run of
+ // tables at once, and this table's ON is checked once all of them are in.
+ if (tables[i].Combos is { } combos)
+ {
+ int width = tables[i].ComboWidth;
+ bool comboMatched = false;
+ foreach ((RowId, object?[])[] combo in combos)
+ {
+ for (int k = 0; k < width; k++)
+ {
+ acc[i + k] = combo[k];
+ nullExtended[i + k] = false;
+ }
+ if (Holds(ons[i], i + width))
+ {
+ comboMatched = true;
+ Recurse(i + width);
+ }
+ }
+ if (kinds[i] is JoinKind.Left or JoinKind.OuterApply && !comboMatched)
+ NullExtend(i);
+ return;
+ }
+
// A derived table's rows are already materialised and have no RowId (never a target). A physical
// table is seeked when its ON allows (index-nested-loop), else scanned.
IEnumerable<(RowId Id, object?[] Values)> rows;
@@ -1177,7 +1433,7 @@ void Recurse(int i)
{
// Share a physical row's array across the combos it appears in (counter-accumulation semantics);
// a derived row has no identity to share on, so use it directly.
- acc[i] = tables[i].Table is null ? (id, values) : (id, Shared(tables[i].Alias, id, values));
+ acc[i] = tables[i].Table is null ? (id, values) : (id, Shared(tables[i].CacheKey, id, values));
if (Holds(ons[i], i + 1))
{
matched = true;
@@ -1188,10 +1444,7 @@ void Recurse(int i)
// LEFT join, and OUTER APPLY for the same reason: an outer row with no matching inner row is still
// emitted, with the inner side all-null.
if (kinds[i] is JoinKind.Left or JoinKind.OuterApply && !matched)
- {
- acc[i] = (default, new object?[tables[i].Columns.Count]);
- Recurse(i + 1);
- }
+ NullExtend(i);
}
Recurse(0);
@@ -1211,7 +1464,8 @@ void Recurse(int i)
/// expression (to seek it per outer row); else null (scan it).
private static (IndexDef Index, Expression Key)? SeekPlanFor(int i, List tables, Expression? on)
{
- if (on is null || tables[i].Table is null) return null; // a derived table has no index to seek
+ // A derived table has no index to seek, and a writable one's rows are already chosen.
+ if (on is null || tables[i].Table is null || tables[i].Combos is not null || tables[i].InCombo) return null;
TableDef def = tables[i].Table!.Definition;
string alias = tables[i].Alias;
HashSet earlier = tables.Take(i).Select(t => t.Alias).ToHashSet(StringComparer.OrdinalIgnoreCase);
@@ -1241,31 +1495,34 @@ private static (IndexDef Index, Expression Key)? MatchSeek(
IndexDef? index = def.Indexes.FirstOrDefault(ix => ix.RootPage > 0 && ix.Columns.Count == 1
&& string.Equals(ix.Columns[0].Column.Name, c.Column, StringComparison.OrdinalIgnoreCase));
- return index is not null && ReferencesOnly(keySide, earlier) ? (index, keySide) : null;
+ return index is not null && IndexSelection.ReferencesOnly(keySide, earlier) ? (index, keySide) : null;
}
- private static bool ReferencesOnly(Expression e, HashSet aliases) => e switch
+ /// The tables a qualifier names: by alias, or failing that by table name.
+ private static List NamedTables(List tables, string qualifier)
{
- ColumnReference { Table: { } t } => aliases.Contains(t),
- ColumnReference => false, // unqualified — can't attribute it to an earlier table safely
- LiteralExpression or ParameterExpression or SystemVariableExpression => true,
- BinaryExpression b => ReferencesOnly(b.Left, aliases) && ReferencesOnly(b.Right, aliases),
- UnaryExpression u => ReferencesOnly(u.Operand, aliases),
- FunctionCall f => f.Arguments.All(a => ReferencesOnly(a, aliases)),
- _ => false,
- };
+ var byAlias = Enumerable.Range(0, tables.Count)
+ .Where(t => string.Equals(tables[t].Alias, qualifier, StringComparison.OrdinalIgnoreCase)).ToList();
+ return byAlias.Count > 0
+ ? byAlias
+ : Enumerable.Range(0, tables.Count)
+ .Where(t => string.Equals(tables[t].Table?.Name, qualifier, StringComparison.OrdinalIgnoreCase)).ToList();
+ }
- /// The source-table index a SET assignment (or a delete target) applies to: the alias/table-name
- /// qualifier if given, else the single table (ambiguous when there are several).
- private static int TargetIndex(List tables, string? qualifier, string what)
+ /// The source table a DELETE removes rows from: the one its target.* names, or the only one (a
+ /// join without a target is ambiguous). A writable derived table over a join is never one, as in ACE (verified:
+ /// "Could not delete from specified tables").
+ private static int DeleteTarget(List tables, string? qualifier)
{
+ List named = qualifier is null ? [.. Enumerable.Range(0, tables.Count)] : NamedTables(tables, qualifier);
+ if (named.Any(t => tables[t].ComboWidth > 1 || tables[t].InCombo))
+ throw new InvalidOperationException("Could not delete from specified tables.");
if (qualifier is null)
return tables.Count == 1 ? 0
- : throw new InvalidOperationException($"{what} must be table-qualified when the statement joins several tables.");
- int i = tables.FindIndex(t =>
- string.Equals(t.Alias, qualifier, StringComparison.OrdinalIgnoreCase) ||
- string.Equals(t.Table?.Name, qualifier, StringComparison.OrdinalIgnoreCase));
- return i >= 0 ? i : throw new InvalidOperationException($"{what} '{qualifier}' is not one of the statement's tables.");
+ : throw new InvalidOperationException("DELETE target must be table-qualified when the statement joins several tables.");
+ return named.Count > 0
+ ? named[0]
+ : throw new InvalidOperationException($"DELETE target '{qualifier}' is not one of the statement's tables.");
}
/// The physical a SET/DELETE targets — a derived table (subquery source) has no
@@ -1275,39 +1532,73 @@ private static Table TargetTable(List tables, int ti) =>
///
/// Executes UPDATE tableexpression SET col = expr, … [WHERE criteria]. The table expression may be a join,
- /// and each SET target may name a specific joined table (Access's multi-table update). Each SET expression
- /// may reference the current values; the WHERE is an ordinary expression (correlated EXISTS included).
- /// Rows are rewritten in place (row id preserved). @@ROWCOUNT = matched join rows.
+ /// and each SET target may name a specific joined table (Access's multi-table update). The WHERE is an
+ /// ordinary expression (correlated EXISTS included). Rows are rewritten in place (row id preserved).
+ /// @@ROWCOUNT = matched join rows.
+ /// Every SET expression reads the joined row as it was before any of its SETs, so SET x = y, y = x
+ /// swaps the two (verified vs ACE). A target row that several joined rows match is updated once per match, each
+ /// match reading what the one before left (verified vs ACE: SET a.x = a.x + 1 over two matches adds 2). A column
+ /// set twice is an error, as in ACE ("Duplicate output destination").
+ /// A SET on the side of an outer join that a joined row has no match in writes a new row into that table,
+ /// one per such joined row, holding the SET values and the table's defaults (verified vs ACE: SET b.v = 3 over
+ /// a LEFT JOIN b adds a row with v = 3 for each unmatched row of the left table, even when the value is
+ /// Null).
///
private int ExecuteUpdate(UpdateStatement statement)
{
- var (tables, kinds, ons) = ResolveSource(statement.From);
+ var (tables, kinds, ons, groupBases) = ResolveSource(statement.From);
var columns = tables.SelectMany(t => t.Columns).ToList();
- List<(RowId Id, object?[] Values)[]> joinRows = JoinRows(tables, kinds, ons, statement.Where, columns);
+ List<(RowId Id, object?[] Values)[]> joinRows = JoinRows(tables, kinds, ons, groupBases, statement.Where, columns);
// Resolve each assignment to its (table index, column) once.
var targets = statement.Assignments.Select(a =>
{
- int ti = TargetIndex(tables, a.Table, "UPDATE SET column");
+ // By the name the source gives the column, which a writable derived table may change or leave out. An
+ // unqualified name is looked for in every table, as ACE does, and must be in exactly one.
+ var found = FindColumns(tables, a.Table, a.Column);
+ if (found.Count > 1)
+ throw new InvalidOperationException(
+ $"The specified field '{a.Column}' could refer to more than one table listed in the FROM clause.");
+ if (found.Count == 0)
+ throw new InvalidOperationException(a.Table is not null && NamedTables(tables, a.Table).Count == 0
+ ? $"UPDATE SET column '{a.Table}' is not one of the statement's tables."
+ : $"Column '{(a.Table is null ? "" : a.Table + ".")}{a.Column}' does not exist.");
+ (int ti, int position) = found[0];
Table tt = TargetTable(tables, ti);
- ColumnDef col = tt.Definition.FindColumn(a.Column)
- ?? throw new InvalidOperationException($"Column '{a.Column}' does not exist in '{tt.Name}'.");
+ ColumnDef col = tt.Definition.Columns[position];
return (TableIndex: ti, Column: col, a.Value);
}).ToList();
+ if (targets.GroupBy(t => (t.TableIndex, t.Column.Index)).FirstOrDefault(g => g.Count() > 1) is { } duplicate)
+ throw new InvalidOperationException(
+ $"Duplicate output destination '{tables[duplicate.Key.TableIndex].Alias}.{duplicate.First().Column.Name}'.");
// 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.
+ var newRows = new Dictionary Provided)>(ReferenceEqualityComparer.Instance);
foreach (var combo in joinRows)
{
- foreach ((int ti, ColumnDef col, Expression valueExpr) in targets)
+ // Every SET of this joined row reads the row as it was before any of them.
+ object?[] flat = combo.SelectMany(c => c.Values).ToArray();
+ var eval = new ExpressionEvaluator(new EvalScope(columns, flat, null), _scalarRunner, _parameters, _session);
+ object?[] results = targets.Select(t => eval.Evaluate(t.Value)).ToArray();
+
+ for (int i = 0; i < targets.Count; i++)
{
+ (int ti, ColumnDef col, _) = targets[i];
object?[] shared = combo[ti].Values;
- var key = (tables[ti].Alias, combo[ti].Id);
- if (!dirty.ContainsKey(key)) dirty[key] = (TargetTable(tables, ti), combo[ti].Id, (object?[])shared.Clone(), shared);
-
- object?[] flat = combo.SelectMany(c => c.Item2).ToArray();
- var eval = new ExpressionEvaluator(new EvalScope(columns, flat, null), _scalarRunner, _parameters, _session);
- shared[col.Index] = eval.Evaluate(valueExpr);
+ if (IsNullExtended(combo[ti]))
+ {
+ if (!newRows.TryGetValue(shared, out var newRow))
+ newRows[shared] = newRow = (TargetTable(tables, ti), []);
+ newRow.Provided.Add(col.Index);
+ }
+ else
+ {
+ var key = (tables[ti].CacheKey, combo[ti].Id);
+ if (!dirty.ContainsKey(key)) dirty[key] = (TargetTable(tables, ti), combo[ti].Id, (object?[])shared.Clone(), shared);
+ }
+ shared[col.Index] = results[i];
}
}
@@ -1353,34 +1644,48 @@ private int ExecuteUpdate(UpdateStatement statement)
table.MoveIndexEntry(index, original, values, id);
}
+ foreach (var (values, (table, provided)) in newRows)
+ InsertNewRow(table.Name, table, DefaultsOf(table.Definition), values, provided);
+
int affected = joinRows.Count;
if (_session is not null) _session.RowCount = affected;
return affected;
}
+ /// Whether a joined row's entry for a table is the null row an outer join supplies where the table has
+ /// no match. Row id 0/0 is never a data row: page 0 is the database header.
+ private static bool IsNullExtended((RowId Id, object?[] Values) entry) => entry.Id.Equals(default(RowId));
+
///
/// Executes DELETE [target.*] FROM tableexpression [WHERE criteria]. The table expression may be a join;
/// target.* selects which joined table's rows to delete (defaults to the single table). Each
/// matched target row's index entries are removed and the row is soft-deleted (row id kept, TDEF row
- /// count decremented). @@ROWCOUNT = distinct rows deleted.
+ /// count decremented). @@ROWCOUNT = distinct rows deleted, plus the joined rows where an outer join left the
+ /// target without a row, which delete nothing but which ACE counts (verified).
///
private int ExecuteDelete(DeleteStatement statement)
{
- var (tables, kinds, ons) = ResolveSource(statement.From);
+ var (tables, kinds, ons, groupBases) = ResolveSource(statement.From);
var columns = tables.SelectMany(t => t.Columns).ToList();
- List<(RowId Id, object?[] Values)[]> joinRows = JoinRows(tables, kinds, ons, statement.Where, columns);
+ List<(RowId Id, object?[] Values)[]> joinRows = JoinRows(tables, kinds, ons, groupBases, statement.Where, columns);
- int ti = TargetIndex(tables, statement.TargetTable, "DELETE target");
+ int ti = DeleteTarget(tables, statement.TargetTable);
Table target = TargetTable(tables, ti);
var deleted = new Dictionary();
+ int withoutRow = 0;
foreach (var combo in joinRows)
- deleted.TryAdd(combo[ti].Id, combo[ti].Values); // one delete per distinct target row
+ {
+ if (IsNullExtended(combo[ti]))
+ withoutRow++;
+ else
+ deleted.TryAdd(combo[ti].Id, combo[ti].Values); // one delete per distinct target row
+ }
// Delete the distinct target rows and everything ON DELETE CASCADE reaches, children before parents.
CascadeDelete(target, deleted.Select(kv => (kv.Key, kv.Value)));
- int affected = deleted.Count;
+ int affected = deleted.Count + withoutRow;
if (_session is not null) _session.RowCount = affected;
return affected;
}
diff --git a/src/LibRed/LibRed.Engine/Execution/SubqueryHoisting.cs b/src/LibRed/LibRed.Engine/Execution/SubqueryHoisting.cs
index 26842889c..c6d6b9ccc 100644
--- a/src/LibRed/LibRed.Engine/Execution/SubqueryHoisting.cs
+++ b/src/LibRed/LibRed.Engine/Execution/SubqueryHoisting.cs
@@ -67,14 +67,10 @@ internal static bool MayReferenceOuter(Expression expression, HashSet ou
ColumnReference => false,
LiteralExpression or ParameterExpression or SystemVariableExpression
or StarExpression or QualifiedStarExpression => false,
- BinaryExpression b => HasUnqualifiedColumn(b.Left) || HasUnqualifiedColumn(b.Right),
- UnaryExpression u => HasUnqualifiedColumn(u.Operand),
- FunctionCall f => f.Arguments.Any(HasUnqualifiedColumn),
- InListExpression il => HasUnqualifiedColumn(il.Value) || il.Items.Any(HasUnqualifiedColumn),
ScalarSubquery sq => StatementHasUnqualified(sq.Query),
ExistsExpression ex => StatementHasUnqualified(ex.Query),
InSubqueryExpression isq => HasUnqualifiedColumn(isq.Value) || StatementHasUnqualified(isq.Query),
- _ => true, // unknown shape: assume the worst
+ _ => expression.Operands()?.Any(HasUnqualifiedColumn) ?? true, // unknown shape: assume the worst
};
private static bool StatementHasUnqualified(SqlStatement statement)
@@ -133,13 +129,9 @@ private static bool Statement(SqlStatement statement, HashSet outer)
ColumnReference => false,
LiteralExpression or ParameterExpression or SystemVariableExpression
or StarExpression or QualifiedStarExpression => false,
- BinaryExpression b => Expr(b.Left, outer) || Expr(b.Right, outer),
- UnaryExpression u => Expr(u.Operand, outer),
- FunctionCall f => f.Arguments.Any(a => Expr(a, outer)),
- InListExpression il => Expr(il.Value, outer) || il.Items.Any(i => Expr(i, outer)),
ScalarSubquery sq => Statement(sq.Query, outer),
ExistsExpression ex => Statement(ex.Query, outer),
InSubqueryExpression isq => Expr(isq.Value, outer) || Statement(isq.Query, outer),
- _ => true, // unknown expression shape
+ _ => e.Operands()?.Any(o => Expr(o, outer)) ?? true, // unknown expression shape
};
}
diff --git a/src/LibRed/LibRed.Engine/Execution/VbaDateText.cs b/src/LibRed/LibRed.Engine/Execution/VbaDateText.cs
new file mode 100644
index 000000000..f2275d5c6
--- /dev/null
+++ b/src/LibRed/LibRed.Engine/Execution/VbaDateText.cs
@@ -0,0 +1,322 @@
+using System.Globalization;
+
+namespace LibRed.Engine.Execution;
+
+///
+/// Text read as a date and time the way CDate reads it — OLE Automation's own parser rather than .NET's
+/// (verified vs ACE; documented at
+/// https://learn.microsoft.com/en-us/office/vba/Language/Concepts/Getting-Started/type-conversion-functions).
+///
+///
+///
+/// The text is numbers and words split by separators. /-,, spaces and the regional date
+/// separator split a date; :. and the regional time separator split a time, so 2.5 is 02:05.
+/// A regional date separator of . makes it a date separator instead.
+/// A time is two or three numbers split by time separators, or one number with AM or PM; hours run to 23 and
+/// minutes and seconds to 59. AM or PM moves an hour up to 12 (12 AM is midnight) and leaves a later one alone.
+/// A time may come before or after the date.
+/// A date of two numbers takes the regional order of day and month, then the other order, then month and year
+/// (1,2020 is January 2020); with no year it is in the current year. Three numbers are year, month and day
+/// when the first cannot be a day, otherwise in the regional order, then with day and month swapped.
+/// A month name, full or abbreviated, takes one number as its day, or as its year when it cannot be one
+/// (Feb 30 is 2030-02-01), or two as day and year. A day-of-week name is not recognised.
+/// A year under 100 is placed by the calendar's two-digit window (0 is 2000, 99 1999); a year past
+/// 9999 is invalid.
+/// Anything else — one bare number, a stray separator, an unknown word — is not a date, and the caller reads the
+/// text as a number instead.
+///
+///
+internal static class VbaDateText
+{
+ private enum Kind { Number, Month, Meridiem, DateSeparator, TimeSeparator }
+
+ /// A part of the text. A number keeps how many digits it was written with; a time separator is
+ /// when it was a period.
+ private readonly record struct Token(Kind Kind, int Value, int Digits = 0);
+
+ private const int Dot = 1;
+
+ public static bool TryParse(string text, CultureInfo culture, out DateTime value)
+ {
+ value = default;
+ if (Tokenize(text, culture) is not { } tokens)
+ return false;
+
+ TimeSpan? time = null;
+ var dateNumbers = new List();
+ int? month = null;
+ Token? separator = null; // the separator just read, if the last token was one
+
+ for (int i = 0; i < tokens.Count; i++)
+ {
+ Token token = tokens[i];
+ switch (token.Kind)
+ {
+ case Kind.Number when IsTimeStart(tokens, i):
+ // A time follows spacing or starts the text; an explicit date separator cannot lead into one.
+ if (time is not null || separator is { Value: not Spacing } || ReadTime(tokens, ref i) is not { } read)
+ return false;
+ time = read;
+ separator = null;
+ break;
+ case Kind.Number:
+ dateNumbers.Add(token.Value);
+ separator = null;
+ break;
+ case Kind.Month:
+ if (month is not null)
+ return false;
+ month = token.Value;
+ separator = null;
+ break;
+ case Kind.DateSeparator:
+ // Between two parts only: not first, not doubled, not last.
+ if (separator is not null || i == 0 || i == tokens.Count - 1)
+ return false;
+ separator = token;
+ break;
+ default:
+ return false;
+ }
+ }
+
+ DateTime date;
+ if (month is null && dateNumbers.Count == 0)
+ {
+ if (time is null)
+ return false;
+ date = new DateTime(1899, 12, 30);
+ }
+ else if (DateOf(dateNumbers, month, culture) is { } parsed)
+ {
+ date = parsed;
+ }
+ else
+ {
+ return false;
+ }
+
+ value = date + (time ?? TimeSpan.Zero);
+ return true;
+ }
+
+ /// The words, numbers and separators of the text, or null when it holds anything else.
+ private static List? Tokenize(string text, CultureInfo culture)
+ {
+ DateTimeFormatInfo format = culture.DateTimeFormat;
+ bool dotIsDate = format.DateSeparator == ".";
+ var tokens = new List();
+ int i = 0;
+ while (i < text.Length)
+ {
+ char c = text[i];
+ if (char.IsWhiteSpace(c))
+ {
+ // Spacing separates only two parts with nothing else between them.
+ int end = i;
+ while (end < text.Length && char.IsWhiteSpace(text[end])) end++;
+ if (tokens.Count > 0 && tokens[^1].Kind is Kind.Number or Kind.Month or Kind.Meridiem
+ && end < text.Length && char.IsLetterOrDigit(text[end]))
+ tokens.Add(new Token(Kind.DateSeparator, Spacing));
+ i = end;
+ }
+ else if (char.IsDigit(c))
+ {
+ int start = i;
+ while (i < text.Length && char.IsDigit(text[i])) i++;
+ if (i - start > 9)
+ return null;
+ tokens.Add(new Token(Kind.Number, int.Parse(text.AsSpan(start, i - start), CultureInfo.InvariantCulture), i - start));
+ }
+ else if (char.IsLetter(c))
+ {
+ int start = i;
+ while (i < text.Length && char.IsLetter(text[i])) i++;
+ string word = text[start..i];
+ if (Meridiem(word, format) is { } pm)
+ tokens.Add(new Token(Kind.Meridiem, pm ? 1 : 0));
+ else if (MonthOf(word, format) is { } m)
+ tokens.Add(new Token(Kind.Month, m));
+ else
+ return null;
+ }
+ else if (c == '.' && (!dotIsDate || AfterSeconds(tokens))
+ || c == ':' || format.TimeSeparator.Length == 1 && c == format.TimeSeparator[0])
+ {
+ DropSpacing(tokens);
+ tokens.Add(new Token(Kind.TimeSeparator, c == '.' ? Dot : 0));
+ i++;
+ SkipSpacing(text, ref i);
+ }
+ else if (c is '/' or '-' or ',' || format.DateSeparator.Length == 1 && c == format.DateSeparator[0])
+ {
+ DropSpacing(tokens);
+ tokens.Add(new Token(Kind.DateSeparator, 0));
+ i++;
+ SkipSpacing(text, ref i);
+ }
+ else
+ {
+ return null;
+ }
+ }
+ return tokens;
+ }
+
+ /// The value of a date separator that is only spacing, which a time may follow.
+ private const int Spacing = 1;
+
+ /// Whether the tokens so far end in hours, minutes and seconds written with colons, so a period can only
+ /// start their fraction.
+ private static bool AfterSeconds(List tokens) =>
+ tokens.Count >= 5
+ && tokens[^1].Kind == Kind.Number && tokens[^2] is { Kind: Kind.TimeSeparator, Value: not Dot }
+ && tokens[^3].Kind == Kind.Number && tokens[^4] is { Kind: Kind.TimeSeparator, Value: not Dot }
+ && tokens[^5].Kind == Kind.Number;
+
+ /// An explicit separator absorbs the spacing before it.
+ private static void DropSpacing(List tokens)
+ {
+ if (tokens.Count > 0 && tokens[^1] is { Kind: Kind.DateSeparator, Value: Spacing })
+ tokens.RemoveAt(tokens.Count - 1);
+ }
+
+ private static void SkipSpacing(string text, ref int i)
+ {
+ while (i < text.Length && char.IsWhiteSpace(text[i])) i++;
+ }
+
+ /// Whether the number at starts a time: a time separator or AM/PM follows it.
+ private static bool IsTimeStart(List tokens, int i)
+ {
+ int next = i + 1;
+ if (next < tokens.Count && tokens[next].Kind == Kind.DateSeparator && next + 1 < tokens.Count
+ && tokens[next + 1].Kind == Kind.Meridiem)
+ next++; // "3 PM": the spacing before the designator
+ return next < tokens.Count && tokens[next].Kind is Kind.TimeSeparator or Kind.Meridiem;
+ }
+
+ /// The time starting at , leaving on its last token; null when
+ /// it is not a valid time.
+ private static TimeSpan? ReadTime(List tokens, ref int i)
+ {
+ var parts = new List { tokens[i].Value };
+ long fraction = 0;
+ bool dotted = false;
+ while (i + 2 < tokens.Count && tokens[i + 1].Kind == Kind.TimeSeparator && tokens[i + 2].Kind == Kind.Number)
+ {
+ // A LibRed extension: after hours, minutes and seconds written with colons, a period and up to seven digits
+ // are a fraction of a second, as LibRed's own SQL writes a time with milliseconds
+ // (TIMEVALUE('12:30:45.123')). ACE refuses a fourth part.
+ if (parts.Count == 3 && !dotted && tokens[i + 1].Value == Dot && tokens[i + 2].Digits <= 7)
+ {
+ fraction = tokens[i + 2].Value * (long)Math.Pow(10, 7 - tokens[i + 2].Digits);
+ i += 2;
+ break;
+ }
+ dotted |= tokens[i + 1].Value == Dot;
+ parts.Add(tokens[i + 2].Value);
+ i += 2;
+ }
+ if (i + 1 < tokens.Count && tokens[i + 1].Kind == Kind.TimeSeparator)
+ return null; // a separator with no number after it
+
+ bool? pm = null;
+ int next = i + 1;
+ if (next < tokens.Count && tokens[next].Kind == Kind.DateSeparator && next + 1 < tokens.Count
+ && tokens[next + 1].Kind == Kind.Meridiem)
+ next++;
+ if (next < tokens.Count && tokens[next].Kind == Kind.Meridiem)
+ {
+ pm = tokens[next].Value == 1;
+ i = next;
+ }
+
+ if (parts.Count > 3 || parts.Count == 1 && pm is null)
+ return null;
+ int hour = parts[0], minute = parts.Count > 1 ? parts[1] : 0, second = parts.Count > 2 ? parts[2] : 0;
+ if (hour > 23 || minute > 59 || second > 59)
+ return null;
+ if (pm is { } afternoon && hour <= 12)
+ hour = afternoon ? (hour == 12 ? 12 : hour + 12) : (hour == 12 ? 0 : hour);
+ return new TimeSpan(hour, minute, second) + TimeSpan.FromTicks(fraction);
+ }
+
+ private static DateTime? DateOf(List numbers, int? month, CultureInfo culture)
+ {
+ int currentYear = DateTime.Today.Year;
+ if (month is { } m)
+ {
+ return numbers.Count switch
+ {
+ 1 => Make(currentYear, m, numbers[0]) ?? Make(Year(numbers[0], culture), m, 1),
+ 2 => Make(Year(numbers[1], culture), m, numbers[0]),
+ _ => null,
+ };
+ }
+
+ bool dayFirst = DayBeforeMonth(culture.DateTimeFormat.ShortDatePattern);
+ switch (numbers.Count)
+ {
+ case 2:
+ {
+ (int day, int mon) = dayFirst ? (numbers[0], numbers[1]) : (numbers[1], numbers[0]);
+ return Make(currentYear, mon, day)
+ ?? Make(currentYear, day, mon)
+ ?? Make(Year(numbers[1], culture), numbers[0], 1)
+ ?? Make(Year(numbers[0], culture), numbers[1], 1);
+ }
+ case 3:
+ {
+ if (numbers[0] > 31)
+ return Make(Year(numbers[0], culture), numbers[1], numbers[2]);
+ (int day, int mon, int year) = YearFirst(culture.DateTimeFormat.ShortDatePattern)
+ ? (numbers[2], numbers[1], numbers[0])
+ : dayFirst ? (numbers[0], numbers[1], numbers[2]) : (numbers[1], numbers[0], numbers[2]);
+ int y = Year(year, culture);
+ return Make(y, mon, day) ?? Make(y, day, mon);
+ }
+ default:
+ return null;
+ }
+ }
+
+ private static int Year(int year, CultureInfo culture) =>
+ year < 100 ? culture.Calendar.ToFourDigitYear(year) : year;
+
+ private static DateTime? Make(int year, int month, int day) =>
+ year is >= 1 and <= 9999 && month is >= 1 and <= 12 && day >= 1 && day <= DateTime.DaysInMonth(year, month)
+ ? new DateTime(year, month, day)
+ : null;
+
+ private static bool DayBeforeMonth(string pattern)
+ {
+ int d = pattern.IndexOf('d'), m = pattern.IndexOf('M');
+ return d >= 0 && m >= 0 && d < m;
+ }
+
+ private static bool YearFirst(string pattern)
+ {
+ int y = pattern.IndexOf('y'), m = pattern.IndexOf('M');
+ return y >= 0 && m >= 0 && y < m;
+ }
+
+ private static bool? Meridiem(string word, DateTimeFormatInfo format) =>
+ Is(word, format.AMDesignator) || Is(word, "AM") ? false
+ : Is(word, format.PMDesignator) || Is(word, "PM") ? true
+ : null;
+
+ private static int? MonthOf(string word, DateTimeFormatInfo format)
+ {
+ for (int i = 0; i < 12; i++)
+ {
+ if (Is(word, format.MonthNames[i]) || Is(word, format.AbbreviatedMonthNames[i])
+ || Is(word, format.MonthGenitiveNames[i]) || Is(word, format.AbbreviatedMonthGenitiveNames[i]))
+ return i + 1;
+ }
+ return null;
+ }
+
+ private static bool Is(string word, string name) =>
+ name.Length > 0 && string.Equals(word, name.TrimEnd('.'), StringComparison.OrdinalIgnoreCase);
+}
diff --git a/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs b/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs
index 13cd93cac..c44bdcafa 100644
--- a/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs
+++ b/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs
@@ -1,38 +1,47 @@
-namespace LibRed.Engine.Execution;
+namespace LibRed.Engine.Execution;
-///
-/// One partition of a window's input, its rows already in window order, together with the peer-group
-/// information the ranking functions need. A peer group is a run of adjacent rows whose ORDER BY keys
-/// compare equal; with no ORDER BY the whole partition is one peer group, which is what makes RANK constant
-/// over an unordered window.
-///
-/// Positions are indexes into the partition's own window order, not into the input.
-internal sealed class WindowPartition(
- IReadOnlyList peerStart, IReadOnlyList peerOrdinal, IReadOnlyList arguments)
+/// Fills — one slot per row of the partition, in window order.
+internal delegate void WindowEvaluator(WindowPartition partition, object?[] output);
+
+/// What a window function's declared type can depend on: its arguments' declared types.
+internal interface IWindowTyping
{
- /// Rows in this partition.
- public int Count => peerStart.Count;
+ /// The declared type of an argument; null when it is absent or its type is unknown.
+ Type? ArgumentType(int argument);
- /// Position of the first row of the peer group holding . RANK is this
- /// plus one, which is why ranks skip after a tie.
- public int PeerStart(int position) => peerStart[position];
+ /// The one type the given arguments' values share, as CASE's alternatives do; an absent argument or a
+ /// bare Null takes no part. Null when any other has an unknown type or the types cannot be reconciled.
+ Type? SharedType(params int[] arguments);
+}
- /// Zero-based ordinal of the peer group holding , counted from the
- /// start of the partition. DENSE_RANK is this plus one, which is why it does not skip.
- public int PeerOrdinal(int position) => peerOrdinal[position];
+/// What a window function may be written with, beyond its arguments and OVER (PARTITION BY … ORDER BY …).
+[Flags]
+internal enum WindowOptions
+{
+ None = 0,
- /// The value of the call's th argument on the row at
- /// — for the functions that take one (NTILE, LAG, FIRST_VALUE, …).
- public object? Argument(int position, int argument) => arguments[position][argument];
-}
+ /// A frame clause: the function reads the frame. The standard gives the ranking and offset functions
+ /// none — they see the whole partition.
+ Frame = 1,
-/// Fills — one slot per row of the partition, in window order.
-internal delegate void WindowEvaluator(WindowPartition partition, object?[] output);
+ /// RESPECT NULLS or IGNORE NULLS.
+ NullTreatment = 2,
+
+ /// FROM FIRST or FROM LAST.
+ FromLast = 4,
-/// The declared CLR type of the result, given the declared type of the first argument
-/// (null when there is none) — the window counterpart of QueryExecutor.DeclaredFunctionType.
+ /// DISTINCT, as the aggregates take it.
+ Distinct = 8,
+
+ /// FILTER (WHERE …), as every aggregate takes it.
+ Filter = 16,
+}
+
+/// The declared CLR type of the result — the window counterpart of
+/// QueryExecutor.DeclaredFunctionType. The values a function returns are converted to it.
internal sealed record WindowFunctionDef(
- int MinArguments, int MaxArguments, Func ResultType, WindowEvaluator Evaluate);
+ int MinArguments, int MaxArguments, Func ResultType, WindowEvaluator Evaluate,
+ WindowOptions Options = WindowOptions.None);
///
/// The window functions the engine implements. This table IS the extension point: because the grammar hangs
@@ -41,6 +50,15 @@ internal sealed record WindowFunctionDef(
///
internal static class WindowFunctions
{
+ // The value on the frame's first or last row, Null for an empty frame. Over the default frame the first is the
+ // partition's, and the last is the current row's last peer — not the partition's last. Declared before Registry,
+ // which a static initializer reads in textual order.
+ private static readonly WindowFunctionDef FirstValue = new(1, 1, static t => t.ArgumentType(0),
+ static (p, o) => FrameValue(p, o, fromLast: false), WindowOptions.Frame | WindowOptions.NullTreatment);
+
+ private static readonly WindowFunctionDef LastValue = new(1, 1, static t => t.ArgumentType(0),
+ static (p, o) => FrameValue(p, o, fromLast: true), WindowOptions.Frame | WindowOptions.NullTreatment);
+
private static readonly Dictionary Registry = new(StringComparer.OrdinalIgnoreCase)
{
// Position within the partition, 1-based. The only window function EF Core emits.
@@ -54,15 +72,285 @@ internal static class WindowFunctions
// The same, counting peer GROUPS rather than rows, so nothing is skipped.
["DENSE_RANK"] = new(0, 0, static _ => typeof(int),
static (p, o) => { for (int i = 0; i < o.Length; i++) o[i] = p.PeerOrdinal(i) + 1; }),
+
+ // (RANK − 1) / (rows − 1): the share of the partition ranked before the row, 0 for a partition of one.
+ ["PERCENT_RANK"] = new(0, 0, static _ => typeof(double),
+ static (p, o) =>
+ {
+ for (int i = 0; i < o.Length; i++)
+ o[i] = o.Length == 1 ? 0.0 : (double)p.PeerStart(i) / (o.Length - 1);
+ }),
+
+ // The share of the partition up to and including the row's last peer.
+ ["CUME_DIST"] = new(0, 0, static _ => typeof(double),
+ static (p, o) => { for (int i = 0; i < o.Length; i++) o[i] = (double)p.PeerEnd(i) / o.Length; }),
+
+ ["NTILE"] = new(1, 1, static _ => typeof(int), Ntile),
+ ["LAG"] = new(1, 3, static t => t.SharedType(0, 2), static (p, o) => Offset(p, o, forward: false),
+ WindowOptions.NullTreatment),
+ ["LEAD"] = new(1, 3, static t => t.SharedType(0, 2), static (p, o) => Offset(p, o, forward: true),
+ WindowOptions.NullTreatment),
+
+ ["FIRST_VALUE"] = FirstValue,
+ ["LAST_VALUE"] = LastValue,
+ ["NTH_VALUE"] = new(2, 2, static t => t.ArgumentType(0), NthValue,
+ WindowOptions.Frame | WindowOptions.NullTreatment | WindowOptions.FromLast),
+
+ // The ordered-set aggregates over each frame; the parser has put the WITHIN GROUP key after the fraction.
+ ["PERCENTILE_CONT"] = PercentileOf("PERCENTILE_CONT"),
+ ["PERCENTILE_DISC"] = PercentileOf("PERCENTILE_DISC"),
+ ["LISTAGG"] = new(2, int.MaxValue, static _ => typeof(string), ListAggOf,
+ WindowOptions.Frame | WindowOptions.Distinct | WindowOptions.Filter),
+
+ // Access's own First and Last, over the frame rather than the group: the same rows as FIRST_VALUE and
+ // LAST_VALUE, as the grouped forms take the group's first and last row.
+ ["FIRST"] = FirstValue,
+ ["LAST"] = LastValue,
+
+ // Every aggregate RunningAggregate computes is a window function too — see Lookup.
};
+ ///
+ /// An aggregate over each row's frame — by default, with an ORDER BY, the partition's rows up to the current row
+ /// and its peers (a running total, ties sharing its value); without one, every row of the partition. The values
+ /// are the grouped aggregate's (), and so is the declared type; an empty frame gives
+ /// what an empty group does. With DISTINCT each frame's value counts once, however many of its rows carry it; a
+ /// binary set function (CORR, REGR_SLOPE, …) reads a pair from each row and takes no DISTINCT.
+ ///
+ private static WindowFunctionDef Aggregate(string name) => new(
+ RunningAggregate.IsPair(name) ? 2 : 1,
+ RunningAggregate.IsPair(name) ? 2 : 1,
+ typing => QueryExecutor.AggregateResultType(name, typing.ArgumentType(0)),
+ (p, o) =>
+ {
+ WindowCall call = p.Call;
+ if (call.Star && name != "COUNT")
+ throw new InvalidOperationException($"{name}(*) is not an aggregate; only COUNT takes *.");
+ var frames = new FrameRows[o.Length];
+ for (int i = 0; i < frames.Length; i++)
+ frames[i] = p.Frame(i);
+ if (frames.Length == 0)
+ return;
+
+ // A frame that only ever grows at one end — every running total, the default frame included — is added
+ // to row by row rather than summed afresh: from the front when the frames share their first row, from
+ // the back when they share their last.
+ bool contiguous = frames.All(f => !f.HasExclusion);
+ if (contiguous && frames.All(f => f.Start == frames[0].Start) && Growing(frames, f => f.End))
+ {
+ var aggregate = new FrameAggregate(name, p);
+ for (int i = 0, added = frames[0].Start; i < frames.Length; i++)
+ {
+ for (; added < frames[i].End; added++)
+ aggregate.Add(added);
+ o[i] = aggregate.Result;
+ }
+ }
+ else if (contiguous && frames.All(f => f.End == frames[0].End) && Growing(frames, f => f.Start))
+ {
+ var aggregate = new FrameAggregate(name, p);
+ for (int i = frames.Length - 1, added = frames[0].End; i >= 0; i--)
+ {
+ for (; added > frames[i].Start; added--)
+ aggregate.Add(added - 1);
+ o[i] = aggregate.Result;
+ }
+ }
+ else
+ {
+ for (int i = 0; i < frames.Length; i++)
+ {
+ if (i > 0 && frames[i] == frames[i - 1])
+ {
+ o[i] = o[i - 1];
+ continue;
+ }
+ var aggregate = new FrameAggregate(name, p);
+ foreach (int position in frames[i].Positions())
+ aggregate.Add(position);
+ o[i] = aggregate.Result;
+ }
+ }
+ },
+ WindowOptions.Frame | WindowOptions.Filter | (RunningAggregate.IsPair(name) ? 0 : WindowOptions.Distinct));
+
+ /// A fed from a partition's rows; under DISTINCT each value goes in once,
+ /// 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 HashSet? _seen = p.Call.Distinct ? [] : null;
+
+ public object? Result => _aggregate.Result;
+
+ public void Add(int position)
+ {
+ if (!p.Includes(position))
+ return;
+ if (p.ArgumentCount == 2)
+ {
+ _aggregate.AddPair(p.Argument(position, 0), p.Argument(position, 1));
+ 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]))))
+ return;
+ _aggregate.Add(value);
+ }
+ }
+
+ ///
+ /// PERCENTILE_CONT(fraction) WITHIN GROUP (ORDER BY key) and PERCENTILE_DISC over each row's frame —
+ /// by default the partition up to the row's last peer, or all of it; see . Each row's own
+ /// fraction applies.
+ ///
+ private static WindowFunctionDef PercentileOf(string name) => new(2, 2,
+ typing => Percentile.ResultType(name, typing.ArgumentType(1)),
+ (p, o) =>
+ {
+ FrameRows previous = default;
+ for (int i = 0; i < o.Length; i++)
+ {
+ // Peers under the default frame, and every row of an unordered window, share their frame.
+ FrameRows frame = p.Frame(i);
+ 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]);
+ previous = frame;
+ }
+ },
+ WindowOptions.Frame | WindowOptions.Filter);
+
+ ///
+ /// LISTAGG(x [, separator]) WITHIN GROUP (ORDER BY …) over each row's frame; see . The
+ /// arguments are the value, the separator when written, and the WITHIN GROUP keys.
+ ///
+ private static void ListAggOf(WindowPartition p, object?[] o)
+ {
+ IReadOnlyList directions = p.Call.WithinGroup!;
+ int keys = directions.Count;
+ string separator = p.ArgumentCount - keys == 2 && o.Length > 0 ? (string)p.Argument(0, 1)! : "";
+ FrameRows previous = default;
+ for (int i = 0; i < o.Length; i++)
+ {
+ FrameRows frame = p.Frame(i);
+ o[i] = i > 0 && frame == previous
+ ? o[i - 1]
+ : 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);
+ previous = frame;
+ }
+ }
+
+ /// Whether a frame edge never moves back along the window order.
+ private static bool Growing(FrameRows[] frames, Func edge)
+ {
+ for (int i = 1; i < frames.Length; i++)
+ if (edge(frames[i]) < edge(frames[i - 1]))
+ return false;
+ return true;
+ }
+
+ ///
+ /// NTILE(n): the partition cut in window order into n numbered buckets as even as can be, the
+ /// first ones taking the rows left over (10 rows in 4 buckets are 3, 3, 2, 2). Ties do not matter. The standard
+ /// makes n a constant, so the partition's first row gives it; read as CLng reads it. A Null n
+ /// gives Null, and one below 1 is an invalid procedure call.
+ ///
+ private static void Ntile(WindowPartition p, object?[] o)
+ {
+ if (o.Length == 0 || WholeArgument(p.Argument(0, 0), "NTILE's bucket count", least: 1) is not { } buckets)
+ return;
+ long size = o.Length / buckets, larger = o.Length % buckets;
+ long bucket = 1, filled = 0;
+ for (int i = 0; i < o.Length; i++)
+ {
+ if (filled == size + (bucket <= larger ? 1 : 0))
+ {
+ bucket++;
+ filled = 0;
+ }
+ o[i] = (int)bucket;
+ filled++;
+ }
+ }
+
+ ///
+ /// LAG(x [, offset [, default]]) and LEAD: x on the row offset rows before or after in
+ /// window order (1 when omitted; 0 is the row itself), or default (Null when omitted) where the partition
+ /// has no such row. Rows are counted one by one, ties or not — under IGNORE NULLS, only those where x is not
+ /// Null. Each row's own offset and default apply; a Null offset gives Null, and a negative one is an invalid
+ /// procedure call.
+ ///
+ private static void Offset(WindowPartition p, object?[] o, bool forward)
+ {
+ for (int i = 0; i < o.Length; i++)
+ {
+ long offset = 1;
+ if (p.ArgumentCount > 1)
+ {
+ if (WholeArgument(p.Argument(i, 1), "The offset", least: 0) is not { } given)
+ {
+ o[i] = null;
+ continue;
+ }
+ offset = given;
+ }
+ long target = p.Step(i, offset, forward);
+ o[i] = target >= 0
+ ? p.Argument((int)target, 0)
+ : p.ArgumentCount > 2 ? p.Argument(i, 2) : null;
+ }
+ }
+
+ ///
+ /// The first argument's value on the frame's first row or, , its last; Null for an
+ /// empty frame. By default Nulls are respected — a Null on that row is the result; under IGNORE NULLS it is the
+ /// first or last row whose value is not Null.
+ ///
+ private static void FrameValue(WindowPartition p, object?[] o, bool fromLast)
+ {
+ for (int i = 0; i < o.Length; i++)
+ o[i] = p.Pick(p.Frame(i), 0, fromLast) is var position and >= 0 ? p.Argument(position, 0) : null;
+ }
+
+ ///
+ /// NTH_VALUE(x, n) [FROM FIRST | FROM LAST]: x on the frame's nth row, counted from its first
+ /// row or its last, or Null while the frame has fewer rows — so over the default frame the rows before the
+ /// nth peer group are Null. IGNORE NULLS counts only the rows where x is not Null. Each row's own
+ /// n applies, as LAG's offset does; a Null n gives Null, and one below 1 is an invalid procedure
+ /// call.
+ ///
+ private static void NthValue(WindowPartition p, object?[] o)
+ {
+ for (int i = 0; i < o.Length; i++)
+ o[i] = WholeArgument(p.Argument(i, 1), "NTH_VALUE's row number", least: 1) is { } n
+ && p.Pick(p.Frame(i), n - 1, p.Call.FromLast) is var position and >= 0
+ ? p.Argument(position, 0)
+ : null;
+ }
+
+ /// A whole-number argument, read as CLng reads it; null for Null, and an invalid procedure call below
+ /// .
+ private static long? WholeArgument(object? value, string what, long least)
+ {
+ if (value is null)
+ return null;
+ long n = Convert.ToInt64(ExpressionEvaluator.ConversionNumber(value), System.Globalization.CultureInfo.InvariantCulture);
+ return n >= least ? n : throw new ArgumentException($"Invalid procedure call: {what} cannot be {n}.");
+ }
+
/// Whether names a window function this engine can compute. A call with an
/// OVER clause that this returns false for is a parse-level window function the engine has no evaluator
/// for — then reports it by name.
- public static bool IsWindowFunction(string name) => Registry.ContainsKey(name);
+ public static bool IsWindowFunction(string name) =>
+ Registry.ContainsKey(name) || RunningAggregate.Supports(name.ToUpperInvariant());
public static WindowFunctionDef Lookup(string name) =>
- Registry.TryGetValue(name, out WindowFunctionDef? def)
- ? def
- : throw new NotSupportedException($"Window function '{name}' is not supported.");
+ Registry.TryGetValue(name, out WindowFunctionDef? def) ? def
+ : RunningAggregate.Supports(name.ToUpperInvariant()) ? Aggregate(name.ToUpperInvariant())
+ : throw new NotSupportedException($"Window function '{name}' is not supported.");
}
diff --git a/src/LibRed/LibRed.Engine/Execution/WindowPartition.cs b/src/LibRed/LibRed.Engine/Execution/WindowPartition.cs
new file mode 100644
index 000000000..925fd66a4
--- /dev/null
+++ b/src/LibRed/LibRed.Engine/Execution/WindowPartition.cs
@@ -0,0 +1,332 @@
+using System.Globalization;
+using LibRed.Sql.Ast;
+
+namespace LibRed.Engine.Execution;
+
+///
+/// A window's frame clause with what it needs from each row: the bounds' offsets, as each row evaluates them, and —
+/// for a RANGE offset — the one ORDER BY key and its direction.
+///
+internal sealed record WindowFrameInput(
+ WindowFrame Frame, IReadOnlyList StartOffsets, IReadOnlyList EndOffsets,
+ IReadOnlyList? RangeKeys = null, bool Descending = false)
+{
+ /// The standard's default frame, which needs nothing from the rows.
+ public static readonly WindowFrameInput Default = new(WindowFrame.Default, [], []);
+}
+
+/// How the call is written, beyond its name and arguments.
+/// The argument is *, as in COUNT(*), which has no values.
+/// The first argument is a Currency, which the statistical aggregates square exactly.
+/// An aggregate over the distinct values in each frame.
+/// IGNORE NULLS: rows whose first argument is Null are not counted.
+/// FROM LAST: NTH_VALUE counts from the frame's last row.
+/// An ordered-set aggregate's ordering: the directions of its last arguments, one each.
+internal sealed record WindowCall(
+ bool Star = false, bool Currency = false, bool Distinct = false, bool IgnoreNulls = false, bool FromLast = false,
+ IReadOnlyList? WithinGroup = null)
+{
+ public static readonly WindowCall Plain = new();
+}
+
+///
+/// The rows of one frame, as positions in the partition's window order: [Start, End) less the excluded
+/// [ExcludeStart, ExcludeEnd), which leaves in when it is set (EXCLUDE TIES).
+///
+internal readonly record struct FrameRows(int Start, int End, int ExcludeStart = 0, int ExcludeEnd = 0, int Kept = -1)
+{
+ private int ExcludedFrom => Math.Clamp(ExcludeStart, Start, End);
+
+ private int ExcludedTo => Math.Clamp(ExcludeEnd, ExcludedFrom, End);
+
+ private bool KeepsOne => Kept >= ExcludedFrom && Kept < ExcludedTo;
+
+ /// Whether a row is left out inside [Start, End).
+ public bool HasExclusion => ExcludedTo > ExcludedFrom;
+
+ public int Count => End - Start - (ExcludedTo - ExcludedFrom) + (KeepsOne ? 1 : 0);
+
+ /// The position of the frame's th row, from 0; -1 when it has no such row.
+ public int Nth(int n)
+ {
+ if (n < 0 || n >= Count)
+ return -1;
+ int before = ExcludedFrom - Start;
+ if (n < before)
+ return Start + n;
+ n -= before;
+ if (KeepsOne)
+ {
+ if (n == 0)
+ return Kept;
+ n--;
+ }
+ return ExcludedTo + n;
+ }
+
+ /// The frame's rows, in window order.
+ public IEnumerable Positions()
+ {
+ for (int n = 0, count = Count; n < count; n++)
+ yield return Nth(n);
+ }
+
+ /// The frame as at most three runs of adjacent positions, [From, To), in window order: the rows
+ /// before the exclusion, the one it keeps, and the rows after.
+ public IEnumerable<(int From, int To)> Runs()
+ {
+ if (ExcludedFrom > Start)
+ yield return (Start, ExcludedFrom);
+ if (KeepsOne)
+ yield return (Kept, Kept + 1);
+ if (End > ExcludedTo)
+ yield return (ExcludedTo, End);
+ }
+}
+
+///
+/// One partition of a window's input, its rows already in window order, together with the peer-group
+/// information the ranking functions need and the frame each row sees. A peer group is a run of adjacent rows
+/// whose ORDER BY keys compare equal; with no ORDER BY the whole partition is one peer group, which is what makes
+/// RANK constant over an unordered window.
+///
+/// Positions are indexes into the partition's own window order, not into the input.
+/// 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,
+ WindowCall? call = null, WindowFrameInput? frame = null, IReadOnlyList? included = null)
+{
+ /// Whether an aggregate takes in the row at : its FILTER holds there.
+ public bool Includes(int position) => included is null || included[position];
+
+ private readonly WindowFrameInput _frame = frame ?? WindowFrameInput.Default;
+
+ // Where each peer group starts, by ordinal; built on first use by GROUPS and the peer ends.
+ private int[]? _groupStarts;
+
+ // The run of rows whose RANGE key is not Null — Null keys sort together at one end of the partition.
+ private (int Start, int End)? _keyed;
+
+ // The positions whose first argument is not Null, ascending; built on first use by IGNORE NULLS.
+ private int[]? _valued;
+
+ public WindowCall Call { get; } = call ?? WindowCall.Plain;
+
+ /// Rows in this partition.
+ public int Count => peerStart.Count;
+
+ /// Position of the first row of the peer group holding . RANK is this
+ /// plus one, which is why ranks skip after a tie.
+ public int PeerStart(int position) => peerStart[position];
+
+ /// Zero-based ordinal of the peer group holding , counted from the
+ /// start of the partition. DENSE_RANK is this plus one, which is why it does not skip.
+ public int PeerOrdinal(int position) => peerOrdinal[position];
+
+ /// The position after the last peer of the row at .
+ public int PeerEnd(int position) => GroupEnd(peerOrdinal[position]);
+
+ /// The value of the call's th argument on the row at
+ /// — for the functions that take one (NTILE, LAG, FIRST_VALUE, …).
+ public object? Argument(int position, int argument) => arguments[position][argument];
+
+ /// How many arguments the call was given.
+ public int ArgumentCount => arguments.Count == 0 ? 0 : arguments[0].Length;
+
+ ///
+ /// The position of the th row of , from 0, counted from its first row
+ /// or, , its last; -1 when it has no such row. Under IGNORE NULLS only the rows whose
+ /// first argument is not Null are counted.
+ ///
+ public int Pick(FrameRows frame, long n, bool fromLast)
+ {
+ var runs = frame.Runs();
+ foreach ((int from, int to) in fromLast ? runs.Reverse() : runs)
+ {
+ int count = Call.IgnoreNulls ? ValuedBefore(to) - ValuedBefore(from) : to - from;
+ if (n < count)
+ {
+ int k = (int)(fromLast ? count - 1 - n : n);
+ return Call.IgnoreNulls ? Valued[ValuedBefore(from) + k] : from + k;
+ }
+ n -= count;
+ }
+ return -1;
+ }
+
+ ///
+ /// The position rows before (or, , after) the one at
+ /// in the partition; -1 past its edge. Under IGNORE NULLS only the rows whose first
+ /// argument is not Null are counted, and offset 0 is still the row itself.
+ ///
+ public long Step(int position, long offset, bool forward)
+ {
+ if (!Call.IgnoreNulls || offset == 0)
+ {
+ long target = forward ? position + offset : position - offset;
+ return target >= 0 && target < Count ? target : -1;
+ }
+ long index = forward ? ValuedBefore(position + 1) + offset - 1 : ValuedBefore(position) - offset;
+ return index >= 0 && index < Valued.Length ? Valued[(int)index] : -1;
+ }
+
+ private int[] Valued => _valued ??= Enumerable.Range(0, Count).Where(i => arguments[i][0] is not null).ToArray();
+
+ /// How many valued positions lie before .
+ private int ValuedBefore(int position)
+ {
+ int index = Array.BinarySearch(Valued, position);
+ return index >= 0 ? index : ~index;
+ }
+
+ /// The rows of the frame the row at sees.
+ public FrameRows Frame(int position)
+ {
+ WindowFrame spec = _frame.Frame;
+ int start = Bound(spec.Start, position, _frame.StartOffsets, isStart: true);
+ int end = Math.Max(start, Bound(spec.End, position, _frame.EndOffsets, isStart: false));
+ return spec.Exclusion switch
+ {
+ FrameExclusion.CurrentRow => new(start, end, position, position + 1),
+ FrameExclusion.Group => new(start, end, PeerStart(position), PeerEnd(position)),
+ FrameExclusion.Ties => new(start, end, PeerStart(position), PeerEnd(position), position),
+ _ => new(start, end),
+ };
+ }
+
+ private int[] GroupStarts => _groupStarts ??= Enumerable.Range(0, Count).Where(i => peerStart[i] == i).ToArray();
+
+ private int GroupEnd(int ordinal) => ordinal + 1 < GroupStarts.Length ? GroupStarts[ordinal + 1] : Count;
+
+ ///
+ /// Where a bound puts the frame's first row () or the position after its last: in
+ /// ROWS the offset counts rows; in GROUPS, peer groups, the bound taking in all of its group; in RANGE, the ORDER
+ /// BY key's distance from the row's. CURRENT ROW is the row itself in ROWS, and its peer group otherwise.
+ ///
+ private int Bound(FrameBound bound, int position, IReadOnlyList offsets, bool isStart)
+ {
+ switch (bound.Kind)
+ {
+ case FrameBoundKind.UnboundedPreceding:
+ return 0;
+ case FrameBoundKind.UnboundedFollowing:
+ return Count;
+ case FrameBoundKind.CurrentRow:
+ return _frame.Frame.Unit == FrameUnit.Rows
+ ? (isStart ? position : position + 1)
+ : (isStart ? PeerStart(position) : PeerEnd(position));
+ }
+
+ bool preceding = bound.Kind == FrameBoundKind.Preceding;
+ object? offset = offsets[position];
+ if (_frame.Frame.Unit == FrameUnit.Range)
+ return RangeBound(position, RangeOffset(offset), preceding, isStart);
+
+ long n = WholeOffset(offset);
+ if (_frame.Frame.Unit == FrameUnit.Rows)
+ {
+ long row = preceding ? position - n : position + n;
+ return (int)Math.Clamp(isStart ? row : row + 1, 0, Count);
+ }
+
+ long group = preceding ? PeerOrdinal(position) - n : PeerOrdinal(position) + n;
+ if (group < 0)
+ return 0;
+ if (group >= GroupStarts.Length)
+ return Count;
+ return isStart ? GroupStarts[group] : GroupEnd((int)group);
+ }
+
+ ///
+ /// A RANGE offset bound: the first row (), or the row after the last, whose key lies
+ /// within of the current row's on the bound's side. A row with a Null key has only its
+ /// peers — the other Nulls — for an offset bound, and a Null key is never within an offset of a value.
+ ///
+ private int RangeBound(int position, object offset, bool preceding, bool isStart)
+ {
+ IReadOnlyList keys = _frame.RangeKeys!;
+ if (keys[position] is not { } key)
+ return isStart ? PeerStart(position) : PeerEnd(position);
+
+ (int low, int high) = _keyed ??= Keyed(keys);
+ object here = RangeNumber(key);
+
+ // Over the keyed run, a row's signed distance from this one grows along the window order, so the bound is
+ // where it first reaches the offset (a start) or first passes it (an end).
+ int lo = low, hi = high;
+ while (lo < hi)
+ {
+ int mid = (lo + hi) >>> 1;
+ int c = CompareDistance(RangeNumber(keys[mid]!), here, offset, preceding);
+ if (isStart ? c >= 0 : c > 0)
+ hi = mid;
+ else
+ lo = mid + 1;
+ }
+ return lo;
+ }
+
+ /// The signed distance of from in window order, compared
+ /// with the bound's offset: negative for PRECEDING, positive for FOLLOWING. Exact where no side is a
+ /// floating-point number.
+ private int CompareDistance(object other, object here, object offset, bool preceding)
+ {
+ int direction = _frame.Descending ? -1 : 1;
+ if (other is not (double or float) && here is not (double or float) && offset is not (double or float))
+ {
+ try
+ {
+ decimal distance = direction * (Convert.ToDecimal(other, CultureInfo.InvariantCulture)
+ - Convert.ToDecimal(here, CultureInfo.InvariantCulture));
+ decimal limit = Convert.ToDecimal(offset, CultureInfo.InvariantCulture);
+ return distance.CompareTo(preceding ? -limit : limit);
+ }
+ catch (OverflowException)
+ {
+ // Past a Decimal: the Double comparison below.
+ }
+ }
+ double d = direction * (Convert.ToDouble(other, CultureInfo.InvariantCulture)
+ - Convert.ToDouble(here, CultureInfo.InvariantCulture));
+ double l = Convert.ToDouble(offset, CultureInfo.InvariantCulture);
+ return d.CompareTo(preceding ? -l : l);
+ }
+
+ private (int Start, int End) Keyed(IReadOnlyList keys)
+ {
+ int start = 0, end = Count;
+ while (start < end && keys[start] is null)
+ start++;
+ while (end > start && keys[end - 1] is null)
+ end--;
+ return (start, end);
+ }
+
+ /// A RANGE key as a number: a date as its serial, as dates sort; text and other kinds are a type
+ /// mismatch.
+ private static object RangeNumber(object key) => key is string or char or Guid or byte[]
+ ? throw new InvalidCastException("Type mismatch: a RANGE offset needs a number or date to order by.")
+ : ExpressionEvaluator.ConversionNumber(key);
+
+ /// A RANGE offset: a number of the key's units — days, for a date — Null and negative refused.
+ private static object RangeOffset(object? offset)
+ {
+ if (offset is null)
+ throw new ArgumentException("Invalid procedure call: a frame offset cannot be Null.");
+ object n = ExpressionEvaluator.ConversionNumber(offset);
+ return Convert.ToDouble(n, CultureInfo.InvariantCulture) >= 0
+ ? n
+ : throw new ArgumentException($"Invalid procedure call: a frame offset cannot be {n}.");
+ }
+
+ /// A ROWS or GROUPS offset: a whole number, read as CLng reads it, Null and negative refused.
+ private static long WholeOffset(object? offset)
+ {
+ if (offset is null)
+ throw new ArgumentException("Invalid procedure call: a frame offset cannot be Null.");
+ long n = Convert.ToInt64(ExpressionEvaluator.ConversionNumber(offset), CultureInfo.InvariantCulture);
+ return n >= 0 ? n : throw new ArgumentException($"Invalid procedure call: a frame offset cannot be {n}.");
+ }
+}
diff --git a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs
index 1813f952c..aa84f4b0e 100644
--- a/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs
+++ b/src/LibRed/LibRed.Engine/Plan/PlanNodes.cs
@@ -81,14 +81,17 @@ public sealed record HashJoinNode(
///
/// Groups input rows by the key expressions and emits one row per
/// group by evaluating — where aggregate calls are computed over
-/// the group and other expressions see the group's key values.
+/// the group and other expressions see the group's key values. are computed over the
+/// groups HAVING keeps, each a row, and published to the projection and ORDER BY as
+/// publishes them.
///
public sealed record AggregateNode(
PlanNode Input,
IReadOnlyList GroupBy,
IReadOnlyList Projection,
Expression? Having,
- IReadOnlyList OrderBy) : PlanNode
+ IReadOnlyList OrderBy,
+ IReadOnlyList? Windows = null) : PlanNode
{
public override IReadOnlyList Children => [Input];
}
diff --git a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs
index c1deb3d47..2aff72399 100644
--- a/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs
+++ b/src/LibRed/LibRed.Engine/Planning/IndexSelection.cs
@@ -1,4 +1,5 @@
using LibRed.Catalog;
+using LibRed.Engine.Execution;
using LibRed.Engine.Plan;
using LibRed.Sql.Ast;
@@ -89,6 +90,12 @@ private static PlanNode RewriteFilterOverScan(FilterNode filter, ScanNode scan,
Expression? low = null, high = null;
foreach (Expression conjunct in conjuncts)
{
+ if (Between(conjunct, colName, alias, def) is { } range)
+ {
+ low ??= range.Low;
+ high ??= range.High;
+ continue;
+ }
if (Bound(conjunct, colName, alias, def) is not { } b)
continue;
if (b.Op is BinaryOperator.GreaterThan or BinaryOperator.GreaterThanOrEqual)
@@ -182,6 +189,33 @@ private static (BinaryOperator Op, Expression Value)? Bound(Expression conjunct,
return null;
}
+ /// If is col BETWEEN a AND b on column , the
+ /// two bounds lowest first; else null.
+ /// 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)
+ {
+ if (conjunct is not BetweenExpression { Negated: false } between
+ || Column(between.Value, alias, def) is not { } column
+ || !string.Equals(column.Column, colName, StringComparison.OrdinalIgnoreCase)
+ || between.Low is not LiteralExpression { Value: { } low }
+ || between.High is not LiteralExpression { Value: { } high })
+ return null;
+
+ bool sameKind = (low, high) switch
+ {
+ (bool, _) or (_, bool) => false,
+ (string, string) or (DateTime, DateTime) => true,
+ _ => ExpressionEvaluator.IsNumeric(low) && ExpressionEvaluator.IsNumeric(high),
+ };
+ if (!sameKind)
+ return null;
+ return ExpressionEvaluator.CompareForSort(low, high) <= 0
+ ? (between.Low, between.High)
+ : (between.High, between.Low);
+ }
+
private static BinaryOperator Flip(BinaryOperator op) => op switch
{
BinaryOperator.GreaterThan => BinaryOperator.LessThan,
@@ -385,11 +419,7 @@ JetDataType.Boolean or JetDataType.Byte or JetDataType.Int16 or JetDataType.Int3
ColumnReference { Table: { } t } => aliases.Contains(t),
ColumnReference => false, // unqualified — can't attribute it to the outer side safely
LiteralExpression or ParameterExpression or SystemVariableExpression => true,
- BinaryExpression b => ReferencesOnly(b.Left, aliases) && ReferencesOnly(b.Right, aliases),
- UnaryExpression u => ReferencesOnly(u.Operand, aliases),
- FunctionCall f => f.Arguments.All(a => ReferencesOnly(a, aliases)),
- InListExpression il => ReferencesOnly(il.Value, aliases) && il.Items.All(a => ReferencesOnly(a, aliases)),
- _ => false,
+ _ => e.Operands()?.All(a => ReferencesOnly(a, aliases)) ?? false,
};
/// The column reference if is a column of the given scan (its qualifier is
@@ -411,11 +441,7 @@ JetDataType.Boolean or JetDataType.Byte or JetDataType.Int16 or JetDataType.Int3
// Same reasoning, and the same reason to be explicit: this switch's default means "no column reference"
// — i.e. usable as a seek bound — so an unlisted node fails towards a WRONG seek, not a missed one.
WindowFunction => true,
- BinaryExpression b => HasColumnRef(b.Left) || HasColumnRef(b.Right),
- UnaryExpression u => HasColumnRef(u.Operand),
- FunctionCall f => f.Arguments.Any(HasColumnRef),
- InListExpression il => HasColumnRef(il.Value) || il.Items.Any(HasColumnRef),
- _ => false, // literals, parameters, system vars
+ _ => e.Operands()?.Any(HasColumnRef) ?? false, // no operands: literals, parameters, system vars
};
/// The AND-conjuncts of a predicate; none at all for an absent one (a missing WHERE or HAVING).
diff --git a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs
index 06bd1aa93..b2722c431 100644
--- a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs
+++ b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs
@@ -25,7 +25,7 @@ public 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),
- set.OrderBy ?? [], set.Top, set.Offset),
+ OrderByPositions(set.OrderBy ?? [], _ => null), set.Top, set.Offset),
ValuesStatement values => new ValuesNode(values.Rows),
_ => throw new NotImplementedException(
$"Planning for {statement.GetType().Name} is not yet implemented."),
@@ -53,6 +53,18 @@ public static PlanNode PlanSelect(SelectStatement select)
// applied over the source columns (before projection) so it can reference them.
PlanNode node = PlanFrom(select.From);
+ // The sort runs on the source rows, below the projection, so a position becomes the projected expression
+ // it names; only a SELECT * sorts rows that are already its output.
+ if (select.OrderBy.Count > 0)
+ {
+ SelectStatement written = select;
+ select = select with
+ {
+ OrderBy = OrderByPositions(written.OrderBy,
+ written.IsSelectStar ? _ => null : position => ProjectedAt(written, position)),
+ };
+ }
+
if (select.Where is not null)
node = PushPredicates(node, select.Where);
@@ -63,25 +75,21 @@ public static PlanNode PlanSelect(SelectStatement select)
var windows = new List();
select = ExtractWindows(select, windows);
+ // A window whose arguments or keys hold an aggregate — RANK() OVER (ORDER BY SUM(x)) — makes the query
+ // grouped by itself, as an aggregate in the projection does.
bool aggregate = select.GroupBy.Count > 0 || select.Having is not null
- || select.Projection.Any(i => HasAggregate(i.Value));
+ || select.Projection.Any(i => HasAggregate(i.Value))
+ || windows.Any(w => w.Function.Expressions().Any(HasAggregate));
- if (windows.Count > 0)
- {
- if (aggregate)
- // AggregateNode owns the projection, HAVING and ORDER BY and collapses rows, so a window over
- // grouped output would need its projection split across the two nodes. EF Core always puts such
- // a window in its own derived table, so nothing needs this yet — refuse loudly rather than hand
- // the call to AggregateNode, whose per-group evaluation swallows the resulting error.
- throw new NotSupportedException(
- "A window function over a grouped query (GROUP BY / HAVING / an aggregate projection) is not supported.");
+ // Over a grouped query the windows run over the groups, after HAVING and before the projection and ORDER BY
+ // — the standard's order — so the aggregate node computes them itself.
+ if (windows.Count > 0 && !aggregate)
node = new WindowNode(node, windows);
- }
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);
+ node = new AggregateNode(node, select.GroupBy, select.Projection, select.Having, select.OrderBy, windows);
else if (select.OrderBy.Count > 0)
node = PushSort(node, select.OrderBy);
@@ -119,6 +127,39 @@ public static PlanNode PlanSelect(SelectStatement select)
return node;
}
+ ///
+ /// ORDER BY items that are a whole number written as such name the output column at that position (verified vs
+ /// ACE, as SQL-92 has it: ORDER BY 2 DESC sorts by the second column, and so does (2)); any other constant —
+ /// 1.5, '2', 1 + 1 — is left as a constant. A position below 1 names nothing and is an error, as is one past the
+ /// last column. gives the expression at a position, or null where the rows sorted
+ /// are the output itself and the position is read from them.
+ ///
+ private static IReadOnlyList OrderByPositions(
+ IReadOnlyList orderBy, Func projected) =>
+ orderBy.Select(item => item.Value is LiteralExpression { Value: int or long or short or byte } literal
+ ? item with
+ {
+ Value = Convert.ToInt32(literal.Value, System.Globalization.CultureInfo.InvariantCulture) is var position
+ && position >= 1
+ ? projected(position) ?? new OutputColumnPosition(position)
+ : throw NoSuchPosition(literal.Value!),
+ }
+ : item).ToList();
+
+ private static InvalidOperationException NoSuchPosition(object position) =>
+ new($"'{position}' is not a valid field name or expression: ORDER BY {position} names no output column.");
+
+ /// The projected expression at a 1-based position. A table.* before it would need the table's
+ /// columns counted, which the planner cannot, and is refused.
+ private static Expression ProjectedAt(SelectStatement select, int position)
+ {
+ if (position > select.Projection.Count)
+ throw NoSuchPosition(position);
+ if (select.Projection.Take(position).Any(item => item.Value is StarExpression or QualifiedStarExpression))
+ throw new NotSupportedException($"ORDER BY {position} after a table.* in the projection is not supported.");
+ return select.Projection[position - 1].Value;
+ }
+
///
/// Places an ORDER BY as deep in the join tree as it can go: when every key comes from one side of a join,
/// sorting that side and letting the join stream produces the same order as sorting the join's output —
@@ -175,24 +216,23 @@ private static PlanNode PushSort(PlanNode node, IReadOnlyList keys)
/// The aggregate function names recognised by the planner/executor. Includes the Access statistical
/// aggregates StDev/StDevP (sample/population standard deviation) and Var/VarP (sample/population variance);
- /// the "StdDev"/"StdDevP" spellings are accepted as aliases.
+ /// the "StdDev"/"StdDevP" spellings are accepted as aliases. Beyond Access, a LibRed extension: the standard's
+ /// names for the statistics, its binary set functions (CORR, COVAR_*, REGR_*) — all of them
+ /// 's — and its ordered-set aggregates PERCENTILE_CONT, PERCENTILE_DISC
+ /// and LISTAGG.
internal static bool IsAggregate(string name) =>
- name.ToUpperInvariant() is "COUNT" or "SUM" or "AVG" or "MIN" or "MAX" or "FIRST" or "LAST"
- or "STDEV" or "STDEVP" or "STDDEV" or "STDDEVP" or "VAR" or "VARP";
+ name.ToUpperInvariant() is var upper
+ && (upper is "FIRST" or "LAST" or "PERCENTILE_CONT" or "PERCENTILE_DISC" or "LISTAGG"
+ || Execution.RunningAggregate.Supports(upper));
internal static bool HasAggregate(Expression e) => e switch
{
FunctionCall f when IsAggregate(f.Name) => true,
- FunctionCall f => f.Arguments.Any(HasAggregate),
- BinaryExpression b => HasAggregate(b.Left) || HasAggregate(b.Right),
- UnaryExpression u => HasAggregate(u.Operand),
- // An aggregate inside a CASE has to be found here so it is computed per group and handed to the
- // evaluator, rather than being reached during evaluation when no group scope can resolve it. The
- // standard says the same: aggregates in a WHEN are evaluated before the CASE, not by it. Conditions
- // count as well as results — HAVING CASE WHEN COUNT(*) > 1 … puts the aggregate in the condition.
- CaseExpression c => c.WhenClauses.Any(w => HasAggregate(w.Condition) || HasAggregate(w.Result))
- || (c.ElseResult is not null && HasAggregate(c.ElseResult)),
- _ => false,
+ // Operands include a CASE's: an aggregate inside a CASE has to be found here so it is computed per group
+ // and handed to the evaluator, rather than being reached during evaluation when no group scope can
+ // resolve it. The standard says the same: aggregates in a WHEN are evaluated before the CASE, not by it.
+ // Conditions count as well as results — HAVING CASE WHEN COUNT(*) > 1 … puts the aggregate in the condition.
+ _ => e.Operands()?.Any(HasAggregate) ?? false,
};
///
@@ -222,48 +262,19 @@ private static SelectStatement ExtractWindows(SelectStatement select, List e switch
{
WindowFunction => true,
- FunctionCall f => f.Arguments.Any(HasWindow),
- BinaryExpression b => HasWindow(b.Left) || HasWindow(b.Right),
- UnaryExpression u => HasWindow(u.Operand),
- CaseExpression c => c.WhenClauses.Any(w => HasWindow(w.Condition) || HasWindow(w.Result))
- || (c.ElseResult is not null && HasWindow(c.ElseResult)),
- InListExpression il => HasWindow(il.Value) || il.Items.Any(HasWindow),
- _ => false,
+ _ => e.Operands()?.Any(HasWindow) ?? false,
};
private static Expression LiftWindows(Expression e, List windows)
{
- switch (e)
- {
- case WindowFunction w:
- // A name no identifier can spell: IDENTIFIER allows '$' only as a trailing character, so this
- // cannot collide with a real column and be silently shadowed.
- string name = $"$window{windows.Count}";
- windows.Add(new WindowOutput(name, w));
- return new ColumnReference(null, name);
- case FunctionCall f:
- return f with { Arguments = f.Arguments.Select(a => LiftWindows(a, windows)).ToList() };
- case BinaryExpression b:
- return b with { Left = LiftWindows(b.Left, windows), Right = LiftWindows(b.Right, windows) };
- case UnaryExpression u:
- return u with { Operand = LiftWindows(u.Operand, windows) };
- case CaseExpression c:
- return c with
- {
- WhenClauses = c.WhenClauses
- .Select(w => w with { Condition = LiftWindows(w.Condition, windows), Result = LiftWindows(w.Result, windows) })
- .ToList(),
- ElseResult = c.ElseResult is null ? null : LiftWindows(c.ElseResult, windows),
- };
- case InListExpression il:
- return il with
- {
- Value = LiftWindows(il.Value, windows),
- Items = il.Items.Select(i => LiftWindows(i, windows)).ToList(),
- };
- default:
- return e;
- }
+ if (e is not WindowFunction w)
+ return e.MapOperands(o => LiftWindows(o, windows));
+
+ // A name no identifier can spell: IDENTIFIER allows '$' only as a trailing character, so this cannot
+ // collide with a real column and be silently shadowed.
+ string name = $"$window{windows.Count}";
+ windows.Add(new WindowOutput(name, w));
+ return new ColumnReference(null, name);
}
private static PlanNode PlanFrom(TableReference? from) => from switch
@@ -382,11 +393,8 @@ private static (PlanNode Node, List Unplaced) Place(
ColumnReference { Table: { } t } => Add(acc, t),
ColumnReference => false, // unqualified — can't determine its table
LiteralExpression or ParameterExpression or SystemVariableExpression => true,
- BinaryExpression b => Collect(b.Left, acc) && Collect(b.Right, acc),
- UnaryExpression u => Collect(u.Operand, acc),
- FunctionCall f => f.Arguments.All(a => Collect(a, acc)),
- InListExpression il => Collect(il.Value, acc) && il.Items.All(a => Collect(a, acc)),
- _ => false, // subqueries (scalar/EXISTS/IN), qualified star, etc. — don't push
+ // Anything without operands — subqueries (scalar/EXISTS/IN), qualified star, etc. — isn't pushed.
+ _ => e.Operands()?.All(a => Collect(a, acc)) ?? false,
};
static bool Add(HashSet acc, string t) { acc.Add(t); return true; }
}
diff --git a/src/LibRed/LibRed.Engine/Planning/ViewExpander.cs b/src/LibRed/LibRed.Engine/Planning/ViewExpander.cs
index b8f5f101d..328a9b0bc 100644
--- a/src/LibRed/LibRed.Engine/Planning/ViewExpander.cs
+++ b/src/LibRed/LibRed.Engine/Planning/ViewExpander.cs
@@ -62,14 +62,7 @@ private static Expression RewriteExpression(
Value = RewriteExpression(i.Value, views, parser, active),
Query = Rewrite(i.Query, views, parser, active),
},
- BinaryExpression b => b with
- {
- Left = RewriteExpression(b.Left, views, parser, active),
- Right = RewriteExpression(b.Right, views, parser, active),
- },
- UnaryExpression u => u with { Operand = RewriteExpression(u.Operand, views, parser, active) },
- FunctionCall f => f with { Arguments = f.Arguments.Select(a => RewriteExpression(a, views, parser, active)).ToList() },
- _ => expr,
+ _ => expr.MapOperands(o => RewriteExpression(o, views, parser, active)),
};
private static TableReference RewriteSource(
diff --git a/src/LibRed/LibRed.Sql/Ast/ExpressionTree.cs b/src/LibRed/LibRed.Sql/Ast/ExpressionTree.cs
new file mode 100644
index 000000000..d9d72a79f
--- /dev/null
+++ b/src/LibRed/LibRed.Sql/Ast/ExpressionTree.cs
@@ -0,0 +1,43 @@
+namespace LibRed.Sql.Ast;
+
+///
+/// The expressions an expression is built from, so that a walker names only the nodes it treats specially and a new
+/// kind of node is taught to every walker here, once.
+///
+public static class ExpressionTree
+{
+ ///
+ /// The expressions directly inside and evaluated in its scope: operands, function
+ /// arguments (and an aggregate's FILTER), IN-list items, BETWEEN bounds, and a CASE's conditions and results. Null for anything else — a leaf,
+ /// a subquery, an IN subquery or a window function — which each walker treats in its own way.
+ ///
+ public static IEnumerable? Operands(this Expression expression) => expression switch
+ {
+ BinaryExpression b => new[] { b.Left, b.Right },
+ UnaryExpression u => new[] { u.Operand },
+ FunctionCall f => f.Filter is null ? f.Arguments : f.Arguments.Append(f.Filter),
+ InListExpression il => il.Items.Prepend(il.Value),
+ BetweenExpression be => new[] { be.Value, be.Low, be.High },
+ CaseExpression c => c.WhenClauses
+ .SelectMany(w => new[] { w.Condition, w.Result })
+ .Concat(c.ElseResult is { } otherwise ? new[] { otherwise } : []),
+ _ => null,
+ };
+
+ /// with applied to each of its
+ /// , in order; the expression itself when it has none.
+ public static Expression MapOperands(this Expression expression, Func map) => expression switch
+ {
+ BinaryExpression b => b with { Left = map(b.Left), Right = map(b.Right) },
+ UnaryExpression u => u with { Operand = map(u.Operand) },
+ FunctionCall f => f with { Arguments = f.Arguments.Select(map).ToList(), Filter = f.Filter is null ? null : map(f.Filter) },
+ InListExpression il => il with { Value = map(il.Value), Items = il.Items.Select(map).ToList() },
+ BetweenExpression be => be with { Value = map(be.Value), Low = map(be.Low), High = map(be.High) },
+ CaseExpression c => c with
+ {
+ WhenClauses = c.WhenClauses.Select(w => w with { Condition = map(w.Condition), Result = map(w.Result) }).ToList(),
+ ElseResult = c.ElseResult is null ? null : map(c.ElseResult),
+ },
+ _ => expression,
+ };
+}
diff --git a/src/LibRed/LibRed.Sql/Ast/Expressions.cs b/src/LibRed/LibRed.Sql/Ast/Expressions.cs
index df1b4c78b..f763cb258 100644
--- a/src/LibRed/LibRed.Sql/Ast/Expressions.cs
+++ b/src/LibRed/LibRed.Sql/Ast/Expressions.cs
@@ -3,8 +3,10 @@ namespace LibRed.Sql.Ast;
/// Base type for scalar/boolean expressions.
public abstract record Expression : SqlNode;
-/// A literal constant (number, string, date, boolean or null).
-public sealed record LiteralExpression(object? Value) : Expression;
+/// A literal constant (number, string, date, boolean or null). is the exact value of
+/// a number written with a decimal point and no exponent, its scale the digits written after the point (1.50 is
+/// 1.50); ACE reads such a literal as a Decimal and keeps no more places than that in a product or quotient.
+public sealed record LiteralExpression(object? Value, decimal? Written = null) : Expression;
/// A reference to a column, optionally table-qualified.
public sealed record ColumnReference(string? Table, string Column) : Expression;
@@ -18,6 +20,11 @@ public sealed record ParameterExpression(string Name) : Expression;
/// without the leading @@.
public sealed record SystemVariableExpression(string Name) : Expression;
+/// The th (1-based) column of the rows being sorted — what ORDER BY n names
+/// where those rows are already the query's output (a SELECT *, or a set operation). Made by the planner, never
+/// parsed.
+public sealed record OutputColumnPosition(int Position) : Expression;
+
/// * in a projection or aggregate.
public sealed record StarExpression : Expression;
@@ -29,7 +36,7 @@ public enum BinaryOperator
{
Add, Subtract, Multiply, Divide, Modulo, IntDivide, Power, Concat,
Equal, NotEqual, LessThan, LessThanOrEqual, GreaterThan, GreaterThanOrEqual,
- And, Or, Like, In,
+ And, Or, Xor, Eqv, Imp, Like, In,
BitAnd, BitOr, BitXor, // Access bitwise operators BAND / BOR / BXOR (integers only)
}
@@ -41,16 +48,59 @@ public sealed record UnaryExpression(UnaryOperator Operator, Expression Operand)
/// A scalar/aggregate function call, e.g. Count(*), IIf(...), Format(...).
/// is set for the ANSI aggregate form COUNT(DISTINCT col) — the aggregate
-/// runs over the distinct set of the argument's values (not distinct rows).
-public sealed record FunctionCall(string Name, IReadOnlyList Arguments, bool Distinct = false) : Expression;
+/// runs over the distinct set of the argument's values (not distinct rows).
+/// is set for an ordered-set aggregate, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY
+/// x DESC): its ORDER BY keys are the last of the , so that every walker sees them as
+/// it sees an argument, and this holds their directions, one per key. is an aggregate's
+/// FILTER (WHERE …): only the rows it is true for go in.
+public sealed record FunctionCall(
+ string Name, IReadOnlyList Arguments, bool Distinct = false,
+ IReadOnlyList? WithinGroup = null, Expression? Filter = null) : Expression
+{
+ /// Whether is an ordered-set aggregate, which takes WITHIN GROUP and needs it.
+ public static bool IsOrderedSetAggregate(string name) =>
+ name.Equals("PERCENTILE_CONT", StringComparison.OrdinalIgnoreCase)
+ || name.Equals("PERCENTILE_DISC", StringComparison.OrdinalIgnoreCase)
+ || name.Equals("LISTAGG", StringComparison.OrdinalIgnoreCase);
+
+ /// The WITHIN GROUP keys: the last arguments, one per direction.
+ public IReadOnlyList WithinGroupKeys =>
+ WithinGroup is null ? [] : Arguments.Skip(Arguments.Count - WithinGroup.Count).ToList();
+}
/// The OVER (…) of a window function: how the input is cut into partitions and how rows are
/// ordered within one. An empty means a single partition over the whole input;
-/// an empty means every row of a partition is a peer. A frame clause belongs here
-/// when one is needed — adding it is a new optional property on this record and nothing else.
+/// an empty means every row of a partition is a peer. A null
+/// is the standard's default frame, RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
+/// ().
public sealed record WindowSpec(
IReadOnlyList PartitionBy,
- IReadOnlyList OrderBy) : SqlNode;
+ IReadOnlyList OrderBy,
+ WindowFrame? Frame = null) : SqlNode;
+
+/// How a frame's bounds are counted: in rows, in ORDER BY values, or in peer groups.
+public enum FrameUnit { Rows, Range, Groups }
+
+/// The kinds of frame bound, in window order — a frame's start never comes after its end in this order.
+public enum FrameBoundKind { UnboundedPreceding, Preceding, CurrentRow, Following, UnboundedFollowing }
+
+/// One end of a window frame; is set for n PRECEDING and n FOLLOWING.
+public sealed record FrameBound(FrameBoundKind Kind, Expression? Offset = null) : SqlNode;
+
+/// The rows a frame leaves out around the current row: none, the row itself, its peer group, or its peers
+/// but not itself.
+public enum FrameExclusion { NoOthers, CurrentRow, Group, Ties }
+
+/// A window's frame: the rows of the partition, around the current row, that a frame-reading function —
+/// an aggregate, FIRST_VALUE, … — sees.
+public sealed record WindowFrame(FrameUnit Unit, FrameBound Start, FrameBound End, FrameExclusion Exclusion = FrameExclusion.NoOthers)
+ : SqlNode
+{
+ /// RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: with an ORDER BY, the partition up to the
+ /// current row's last peer; without one, the whole partition, every row then being a peer.
+ public static readonly WindowFrame Default =
+ new(FrameUnit.Range, new(FrameBoundKind.UnboundedPreceding), new(FrameBoundKind.CurrentRow));
+}
///
/// A window function call: ROW_NUMBER() OVER (PARTITION BY … ORDER BY …). Access has none of these —
@@ -63,10 +113,30 @@ public sealed record WindowSpec(
/// AggregateNode. As a sibling record it falls through to "not an aggregate", which is correct — a window
/// function returns one value per ROW, not per group, whatever its name.
///
+/// A windowed aggregate over the distinct values of its argument in each frame.
+/// IGNORE NULLS (true) or RESPECT NULLS (false); null when neither is written.
+/// FROM LAST (true) or FROM FIRST (false); null when neither is written.
+/// An ordered-set aggregate's ordering, as on .
+/// An aggregate's FILTER (WHERE …), as on .
public sealed record WindowFunction(
string Name,
IReadOnlyList Arguments,
- WindowSpec Over) : Expression;
+ WindowSpec Over,
+ bool Distinct = false,
+ bool? IgnoreNulls = null,
+ bool? FromLast = null,
+ IReadOnlyList? WithinGroup = null,
+ Expression? Filter = null) : Expression
+{
+ /// Every expression the call evaluates on a row: arguments, FILTER, partition and sort keys, and frame
+ /// offsets.
+ public IEnumerable Expressions() =>
+ Arguments
+ .Concat(Filter is null ? [] : [Filter])
+ .Concat(Over.PartitionBy)
+ .Concat(Over.OrderBy.Select(o => o.Value))
+ .Concat(new[] { Over.Frame?.Start.Offset, Over.Frame?.End.Offset }.OfType());
+}
/// A subquery used as a scalar value: (SELECT … ). May correlate to the outer query.
///
@@ -93,6 +163,10 @@ public sealed record InSubqueryExpression(Expression Value, SqlStatement Query,
/// three-valued semantics as .
public sealed record InListExpression(Expression Value, IReadOnlyList Items, bool Negated) : Expression;
+/// value [NOT] BETWEEN low AND high: inclusive, with the bounds in either order, and Null when any of
+/// the three is Null.
+public sealed record BetweenExpression(Expression Value, Expression Low, Expression High, bool Negated) : Expression;
+
///
/// The DEFAULT keyword used as a row value in an INSERT's table value constructor:
/// VALUES ('Advertisement', DEFAULT). It is a marker rather than a value — the column takes its
diff --git a/src/LibRed/LibRed.Sql/Ast/Statements.cs b/src/LibRed/LibRed.Sql/Ast/Statements.cs
index 3c2ea16d5..89f7137e6 100644
--- a/src/LibRed/LibRed.Sql/Ast/Statements.cs
+++ b/src/LibRed/LibRed.Sql/Ast/Statements.cs
@@ -135,7 +135,15 @@ public sealed record ColumnDefinition(
/// A calculated column's expression, from the LibRed-only AS (…) clause, kept as source
/// text because that text is what goes on disk and what the engine re-evaluates. Null for an ordinary
/// column.
- string? Calculated = null);
+ string? Calculated = null,
+ /// A trailing IDENTITY [(seed [, increment])] attribute, or null when the column has none.
+ /// The last one written wins when several are.
+ IdentityAttribute? Identity = null);
+
+/// ACE's IDENTITY [(seed [, increment])] column attribute. It makes a Long column an AutoNumber
+/// counting from its seed by its increment — each 1 when omitted, replacing any the type declared — and is
+/// ignored on every other type.
+public sealed record IdentityAttribute(int? Seed, int? Increment);
/// Referential action for a foreign key's ON DELETE / ON UPDATE clause. Jet records only
/// enforce + cascade-update + cascade-delete, so NoAction/SetNull/SetDefault collapse to "no cascade".
@@ -143,7 +151,8 @@ public enum ReferentialAction { NoAction, Cascade, SetNull, SetDefault }
/// A FOREIGN KEY constraint (table-level, or a column-level REFERENCES): the child columns,
/// the referenced (parent) table and its columns, the ON DELETE / ON UPDATE actions, and whether the
-/// FOREIGN KEY NO INDEX modifier was given (suppresses the backing index).
+/// FOREIGN KEY NO INDEX modifier was given (suppresses the backing index). An empty
+/// means REFERENCES table with no column list: the parent's primary key.
public sealed record ForeignKeyConstraint(
string? Name,
IReadOnlyList Columns,
@@ -254,8 +263,14 @@ public sealed record CreateActionProcedureStatement(
/// One action of an ALTER TABLE statement (Access allows exactly one per statement).
public abstract record AlterTableAction;
-/// ADD [COLUMN] field type … — add a new column (with its inline constraints).
-public sealed record AddColumnAction(ColumnDefinition Column) : AlterTableAction;
+/// ADD [COLUMN] field type … — add a new column with its inline constraints: a PRIMARY KEY
+/// (, named when a CONSTRAINT name was given),
+/// a constraint and a column-level , each null when absent.
+public sealed record AddColumnAction(
+ ColumnDefinition Column,
+ ForeignKeyConstraint? References = null,
+ UniqueConstraint? Unique = null,
+ string? PrimaryKeyName = null) : AlterTableAction;
/// ADD CONSTRAINT … FOREIGN KEY … — add a foreign key.
public sealed record AddForeignKeyAction(ForeignKeyConstraint ForeignKey) : AlterTableAction;
@@ -272,7 +287,8 @@ public sealed record AddCheckAction(CheckConstraint Check) : AlterTableAction;
/// ALTER COLUMN field type[(size[,scale])] [NOT NULL|NULL] [DEFAULT expr] — change a column's data
/// type, and optionally its nullability (: true = NOT NULL, false = NULL, null = leave
/// as-is) and default.
-public sealed record AlterColumnAction(string Field, string TypeName, int? Size, int? Scale, string? Default = null, bool? NotNull = null) : AlterTableAction;
+public sealed record AlterColumnAction(string Field, string TypeName, int? Size, int? Scale, string? Default = null, bool? NotNull = null,
+ IdentityAttribute? Identity = null) : AlterTableAction;
/// ALTER COLUMN field SET DEFAULT expr — set (replace) a column's default, without retyping it.
public sealed record AlterColumnSetDefaultAction(string Field, string Default) : AlterTableAction;
diff --git a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4
index 377754bb1..509fe49a4 100644
--- a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4
+++ b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4
@@ -4,12 +4,36 @@
// derived-table subqueries, WHERE, ORDER BY, TOP. The parse tree is lowered into
// LibRed.Sql.Ast by AstBuilder, so the rest of the engine never sees these generated types.
//
-// Dialect notes (vs ANSI): '&' string concat; MOD / '\' operators; '*'/'?' LIKE wildcards;
-// TOP n (no OFFSET); #1/1/2020# date literals; [bracketed] and `backtick` identifiers;
-// booleans -1/0.
+// Dialect notes (vs ANSI): '&' string concat; MOD / '\' operators; TOP n (no OFFSET);
+// #1/1/2020# date literals; [bracketed] and `backtick` identifiers; booleans -1/0.
grammar AccessSql;
+@lexer::members {
+ // Whether the input ahead is any number of dashes and then a number: a digit, or a point and a digit.
+ private bool DashesThenNumber()
+ {
+ int i = 1;
+ while (InputStream.LA(i) == '-')
+ i++;
+ int c = InputStream.LA(i);
+ return c is >= '0' and <= '9' || c == '.' && InputStream.LA(i + 1) is >= '0' and <= '9';
+ }
+}
+
+@parser::members {
+ // The precedence the generated parser gives a BETWEEN's upper bound: that of the comparisons, one above BETWEEN.
+ private const int BetweenBoundPrecedence = 13;
+
+ // ANTLR parses the middle operand of an alternative at precedence 0 and has no syntax to raise it, which lets a
+ // BETWEEN's lower bound swallow the AND that ends it: `x BETWEEN 1 AND 10 AND y = 2` would read as
+ // `x BETWEEN (1 AND 10) AND (y = 2)`. The lower bound is the only operand parsed while its BETWEEN has no lower
+ // bound yet, so it is given the upper bound's precedence here.
+ public override void EnterRecursionRule(ParserRuleContext localctx, int state, int ruleIndex, int precedence) =>
+ base.EnterRecursionRule(localctx, state, ruleIndex,
+ localctx.Parent is BetweenExprContext { lo: null } ? Math.Max(precedence, BetweenBoundPrecedence) : precedence);
+}
+
// A single statement, optionally terminated by ';' (EF Core emits a trailing semicolon).
statement : parametersClause? (ifThenStatement | createTableStatement | createIndexStatement | createViewStatement | createProcedureStatement | alterTableStatement | dropStatement | insertStatement | updateStatement | deleteStatement | transactionStatement | executeStatement | systemVariableSelect | queryExpression) SEMI? EOF ;
@@ -25,14 +49,16 @@ executeStatement : (EXECUTE | EXEC) name=identifier (expression (COMMA expressio
// UPDATE table SET col = expr, … [WHERE criteria]. The WHERE criteria is an ordinary expression, the same
// as a SELECT's; each SET value expression may reference the row's current column values.
// UPDATE tableexpression SET col=expr, … [WHERE …]. The tableexpression is a table SOURCE (Access allows a
-// join here), and a SET target may be table-qualified (col or alias.col) to touch a specific joined table.
-updateStatement : UPDATE tableSource SET assignment (COMMA assignment)* whereClause? ;
+// 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? ;
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).
-deleteStatement : DELETE (target=identifier DOT STAR | STAR)? FROM tableSource whereClause? ;
+// 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? ;
// 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
@@ -139,31 +165,41 @@ calculatedClause : AS LPAREN expression RPAREN ;
// A second word handles two-word ANSI aliases like CHARACTER VARYING / BIT VARYING.
// Up to three words to cover multi-word SQL type names: "char varying", "national character varying", etc.
-dataType : typeName=identifier extra=identifier? extra2=identifier? (LPAREN size=signedInteger (COMMA scale=signedInteger)? RPAREN)? ;
+// IDENTITY is reserved, as ACE reserves it, so it is named here as a type of its own: `Id IDENTITY(5, 2)`. After a
+// type it is a column constraint instead (see IdentityConstraint).
+dataType : (typeName=identifier | identityType=IDENTITY) extra=identifier? extra2=identifier? (LPAREN size=signedInteger (COMMA scale=signedInteger)? RPAREN)? ;
// A possibly-negative integer — needed for a descending COUNTER(seed, increment) whose increment is negative.
signedInteger : MINUS? INTEGER_LITERAL ;
// Single-field constraints (after the column's data type). A CONSTRAINT name may prefix any of them.
+// PRIMARY KEY and UNIQUE take an optional CLUSTERED or NONCLUSTERED, in a column or table constraint alike, as
+// ACE's CONSTRAINT clause does. ACE stores nothing for either word — the file is byte-identical without it and
+// DAO reports Clustered = False — so it is parsed and dropped. ACE rejects it anywhere else: after FOREIGN KEY,
+// between PRIMARY and KEY, on a bare column, or in CREATE INDEX.
columnConstraint
: NOT NULL # NotNullConstraint
| NULL # NullableConstraint
| DEFAULT expression # DefaultConstraint
| WITH (COMPRESSION | COMP) # CompressionConstraint
| (CONSTRAINT cname=identifier)? CHECK LPAREN checkBody RPAREN # CheckColumnConstraint
- | (CONSTRAINT cname=identifier)? PRIMARY KEY # PrimaryKeyConstraint
- | (CONSTRAINT cname=identifier)? UNIQUE # UniqueColumnConstraint
+ | (CONSTRAINT cname=identifier)? PRIMARY KEY clusteredOption? # PrimaryKeyConstraint
+ | (CONSTRAINT cname=identifier)? UNIQUE clusteredOption? # UniqueColumnConstraint
| (CONSTRAINT cname=identifier)? REFERENCES refTable=identifier
(LPAREN refColumns+=identifier (COMMA refColumns+=identifier)* RPAREN)?
foreignKeyAction* # ColumnReferencesConstraint
+ // IDENTITY [(seed [, increment])] — ACE's AutoNumber attribute. It may follow only the type, NULL/NOT NULL or
+ // another IDENTITY, which the AST builder checks; it makes a Long column an AutoNumber and is ignored on any
+ // other type, both as ACE does.
+ | IDENTITY (LPAREN seed=signedInteger (COMMA increment=signedInteger)? RPAREN)? # IdentityConstraint
;
// EF Core emits named table constraints: CONSTRAINT `PK_x` PRIMARY KEY (`col`, ...) and
// CONSTRAINT `FK_x` FOREIGN KEY (`col`, ...) REFERENCES `Parent` (`col`, ...) ON DELETE CASCADE.
tableConstraint
- : (CONSTRAINT name=identifier)? PRIMARY KEY
+ : (CONSTRAINT name=identifier)? PRIMARY KEY clusteredOption?
LPAREN columns+=identifier (COMMA columns+=identifier)* RPAREN # PrimaryKeyTableConstraint
- | (CONSTRAINT name=identifier)? UNIQUE
+ | (CONSTRAINT name=identifier)? UNIQUE clusteredOption?
LPAREN columns+=identifier (COMMA columns+=identifier)* RPAREN # UniqueTableConstraint
| (CONSTRAINT name=identifier)? FOREIGN KEY (noIndex=NO INDEX)?
LPAREN columns+=identifier (COMMA columns+=identifier)* RPAREN
@@ -364,21 +400,39 @@ orderByClause : ORDER BY orderByItem (COMMA orderByItem)* ;
orderByItem : expression (dir=(ASC | DESC))? ;
expression
- : NOT expression # NotExpr
- | BNOT expression # BitNotExpr
- | MINUS expression # NegateExpr
- | left=expression CARET right=expression # PowExpr
- | left=expression op=(STAR | SLASH | MOD | BACKSLASH) right=expression # MulDivExpr
- | left=expression op=(PLUS | MINUS | AMP) right=expression # AddConcatExpr
+ : left=expression CARET right=expression # PowExpr
+ // Negation binds looser than '^' and tighter than '*' (VBA operator precedence), except that a minus written
+ // against a number is part of that number (see AstBuilder.SignedNumber). A unary plus sits with it and leaves its
+ // operand as it is, text included (verified vs ACE: +'abc' is 'abc').
+ | op=(MINUS | PLUS) expression # NegateExpr
+ // '*' '/', then '\', then MOD, each its own level (VBA operator precedence; verified vs ACE: 7 \ 2 * 3 is 1,
+ // 5 MOD 3 * 2 is 5, 10 MOD 4 \ 2 is 0).
+ | left=expression op=(STAR | SLASH) right=expression # MulDivExpr
+ | left=expression op=BACKSLASH right=expression # IntDivExpr
+ | left=expression op=MOD right=expression # ModExpr
+ | left=expression op=(PLUS | MINUS) right=expression # AddSubExpr
+ // '&' binds looser than '+'/'-' and tighter than the comparisons (VBA operator precedence; verified vs
+ // ACE: 1 & 2 + 3 is '15').
+ | left=expression op=AMP right=expression # ConcatExpr
| left=expression op=(EQ | NEQ | LT | LTE | GT | GTE) right=expression # ComparisonExpr
+ // The lower bound is parsed at the upper bound's precedence (see EnterRecursionRule above), so the AND between
+ // them is never taken into it.
| val=expression not=NOT? BETWEEN lo=expression AND hi=expression # BetweenExpr
| left=expression not=NOT? LIKE right=expression # LikeExpr
| val=expression not=NOT? IN LPAREN sub=queryExpression RPAREN # InSubqueryExpr
| val=expression not=NOT? IN LPAREN items+=expression (COMMA items+=expression)* RPAREN # InExpr
| operand=expression IS not=NOT? NULL # IsNullExpr
- | left=expression op=(BAND | BOR | BXOR) right=expression # BitwiseExpr
- | left=expression AND right=expression # AndExpr
- | left=expression OR right=expression # OrExpr
+ // NOT binds looser than the comparisons and tighter than AND (VBA operator precedence; verified vs ACE:
+ // NOT 1 = 2 is True). BNOT sits with it, and each bitwise operator with its logical one, left to right
+ // (verified vs ACE: BNOT 1 + 1 is -3, NOT 0 BAND 1 is 1, 2 AND 1 BAND 3 is 3, 0 OR 0 BOR 4 is 4).
+ | op=(NOT | BNOT) expression # NotExpr
+ | left=expression op=(AND | BAND) right=expression # AndExpr
+ | left=expression op=(OR | BOR) right=expression # OrExpr
+ // XOR, then EQV, then IMP, each looser than the one before (VBA operator precedence; verified vs ACE:
+ // TRUE XOR TRUE OR TRUE is False, FALSE IMP FALSE EQV FALSE is True).
+ | left=expression op=(XOR | BXOR) right=expression # XorExpr
+ | left=expression op=EQV right=expression # EqvExpr
+ | left=expression op=IMP right=expression # ImpExpr
| primary # PrimaryExpr
;
@@ -416,17 +470,22 @@ caseWhen : WHEN condition=expression THEN result=expression ;
// a new window function cost NO grammar at all: ROW_NUMBER, RANK, NTILE and friends already lex as IDENTIFIER
// and reach here through `functionName`, and `SUM(x) OVER (…)` — an aggregate over a window — parses for free
// as the same shape. Access has no window functions; this is a LibRed extension for extended mode.
+// WITHIN GROUP gives an ordered-set aggregate (PERCENTILE_CONT, PERCENTILE_DISC, LISTAGG) its ordering, and FILTER
+// an aggregate the rows it takes in. FROM FIRST/LAST and RESPECT/IGNORE NULLS sit between the call and OVER, where
+// the standard puts them, and only with an OVER: a lone `FROM Last` after a call is the FROM clause naming a table
+// called Last, which the lookahead to OVER tells apart.
functionCall
: name=functionName LPAREN (star=STAR | (distinct=DISTINCT? expression (COMMA expression)*))? RPAREN
- (OVER windowSpecification)?
+ withinGroup? filterClause?
+ (nthRowFrom? nullTreatment? OVER windowSpecification)?
;
// A function name is an identifier, or the LEFT/RIGHT/ASC keywords used as the Left()/Right()/Asc() functions —
// unambiguous with LEFT/RIGHT JOIN and ORDER BY ... ASC because a function call is always followed by '(' and
// never appears in the FROM/ORDER BY clause.
// Keywords that are also function names have to be readmitted here or the lexer's keyword token wins and the
// call stops parsing: Left/Right/Asc, and FIRST — which `offsetFetchClause` needs as a keyword for
-// `FETCH FIRST`, but which is also the Access aggregate First(). (LAST is not listed because nothing else
-// claims it as a keyword.)
+// `FETCH FIRST`, but which is also the Access aggregate First(). (LAST is not listed because it is a non-reserved
+// keyword, which `identifier` already admits.)
// PARTITION is readmitted for the same reason: `PARTITION BY` makes it a keyword, but Access has a real VBA
// Partition(number, start, stop, interval) function that LibRed implements and tests. A function call is always
// followed by '(' and `PARTITION BY` never is, so the two never collide.
@@ -434,7 +493,7 @@ functionName : identifier | LEFT | RIGHT | ASC | FIRST | PARTITION ;
columnRef : (qualifier=identifier DOT)? name=identifier ;
-identifier : IDENTIFIER | BRACKET_ID | BACKTICK_ID ;
+identifier : IDENTIFIER | BRACKET_ID | BACKTICK_ID | nonReservedKeyword ;
literal
: INTEGER_LITERAL # IntLiteral
@@ -465,13 +524,50 @@ standaloneExpression : expression EOF ;
// A window function's OVER (…). Both parts are optional here even though EF Core always emits both and the
// standard's defaults differ (no PARTITION BY = one partition over the whole input; no ORDER BY = every row a
// peer), because rejecting them in the grammar would report a parse error where a semantic one is clearer.
-// A frame clause (ROWS/RANGE BETWEEN …) goes before the RPAREN when something needs one — nothing emits one
-// today, and admitting it now would reserve five more keywords (RANGE, PRECEDING, FOLLOWING, UNBOUNDED,
-// CURRENT) to buy nothing. Kept after the existing parser rules so adding it does not renumber their ids.
+// Kept after the existing parser rules so adding it does not renumber their ids.
windowSpecification
- : LPAREN (PARTITION BY partition+=expression (COMMA partition+=expression)*)? orderByClause? RPAREN
+ : LPAREN (PARTITION BY partition+=expression (COMMA partition+=expression)*)? orderByClause? windowFrame? RPAREN
+ ;
+
+// CLUSTERED / NONCLUSTERED after PRIMARY KEY or UNIQUE — accepted and ignored (see columnConstraint). Kept after
+// the existing parser rules so adding it does not renumber their ids.
+clusteredOption : CLUSTERED | NONCLUSTERED ;
+
+// A window frame, as the standard has it: the rows of the partition a frame-reading function (an aggregate,
+// FIRST_VALUE, …) sees from the current row. A lone bound is the frame's start, ending at the current row.
+// ROWS | RANGE | GROUPS [BETWEEN start AND end | start] [EXCLUDE CURRENT ROW | GROUP | TIES | NO OTHERS]
+// UNBOUNDED is listed before the offset form so `UNBOUNDED PRECEDING` is the bound, not a column named Unbounded
+// (ANTLR settles an ambiguity on the lower alternative); bracket such a column to use it as an offset.
+windowFrame
+ : unit=(ROWS | RANGE | GROUPS) (BETWEEN start=frameBound AND end=frameBound | start=frameBound)
+ (EXCLUDE exclusion=frameExclusion)?
+ ;
+frameBound
+ : UNBOUNDED direction=(PRECEDING | FOLLOWING)
+ | CURRENT ROW
+ | offset=expression direction=(PRECEDING | FOLLOWING)
+ ;
+frameExclusion : CURRENT ROW | GROUP | TIES | NO OTHERS ;
+
+// The window clauses' words are keywords only there: each is also admitted as an identifier, so a column named
+// Range or Current keeps working unbracketed, as it does in ACE, which reserves none of them.
+nonReservedKeyword
+ : RANGE | GROUPS | UNBOUNDED | PRECEDING | FOLLOWING | CURRENT | EXCLUDE | TIES | OTHERS
+ | WITHIN | LAST | RESPECT | NULLS | FILTER
;
+// An aggregate's FILTER: only the rows for which the condition is true go into it.
+filterClause : FILTER LPAREN WHERE condition=expression RPAREN ;
+
+// An ordered-set aggregate's ordering: PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY x).
+withinGroup : WITHIN GROUP LPAREN orderByClause RPAREN ;
+
+// NTH_VALUE's direction: counted from the frame's first row (the default) or its last.
+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 ;
+
// ---- Lexer ----
SELECT : [Ss][Ee][Ll][Ee][Cc][Tt] ;
@@ -482,6 +578,9 @@ AS : [Aa][Ss] ;
AND : [Aa][Nn][Dd] ;
OR : [Oo][Rr] ;
NOT : [Nn][Oo][Tt] ;
+XOR : [Xx][Oo][Rr] ;
+EQV : [Ee][Qq][Vv] ;
+IMP : [Ii][Mm][Pp] ;
BAND : [Bb][Aa][Nn][Dd] ;
BOR : [Bb][Oo][Rr] ;
BXOR : [Bb][Xx][Oo][Rr] ;
@@ -557,6 +656,11 @@ SET : [Ss][Ee][Tt] ;
DEFAULT : [Dd][Ee][Ff][Aa][Uu][Ll][Tt] ;
NO : [Nn][Oo] ;
UNIQUE : [Uu][Nn][Ii][Qq][Uu][Ee] ;
+// Reserved as ACE reserves them: neither may name a table, column or alias unbracketed.
+CLUSTERED : [Cc][Ll][Uu][Ss][Tt][Ee][Rr][Ee][Dd] ;
+// Reserved as ACE reserves it. @@IDENTITY still lexes as one SYSVAR token, the longer match.
+IDENTITY : [Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy] ;
+NONCLUSTERED : [Nn][Oo][Nn][Cc][Ll][Uu][Ss][Tt][Ee][Rr][Ee][Dd] ;
INDEX : [Ii][Nn][Dd][Ee][Xx] ;
TEMPORARY : [Tt][Ee][Mm][Pp][Oo][Rr][Aa][Rr][Yy] ;
WITH : [Ww][Ii][Tt][Hh] ;
@@ -575,6 +679,21 @@ DESC : [Dd][Ee][Ss][Cc] ;
TRUE : [Tt][Rr][Uu][Ee] ;
FALSE : [Ff][Aa][Ll][Ss][Ee] ;
NULL : [Nn][Uu][Ll][Ll] ;
+// The window clauses' words — not reserved; see nonReservedKeyword.
+RANGE : [Rr][Aa][Nn][Gg][Ee] ;
+GROUPS : [Gg][Rr][Oo][Uu][Pp][Ss] ;
+UNBOUNDED : [Uu][Nn][Bb][Oo][Uu][Nn][Dd][Ee][Dd] ;
+PRECEDING : [Pp][Rr][Ee][Cc][Ee][Dd][Ii][Nn][Gg] ;
+FOLLOWING : [Ff][Oo][Ll][Ll][Oo][Ww][Ii][Nn][Gg] ;
+CURRENT : [Cc][Uu][Rr][Rr][Ee][Nn][Tt] ;
+EXCLUDE : [Ee][Xx][Cc][Ll][Uu][Dd][Ee] ;
+TIES : [Tt][Ii][Ee][Ss] ;
+OTHERS : [Oo][Tt][Hh][Ee][Rr][Ss] ;
+WITHIN : [Ww][Ii][Tt][Hh][Ii][Nn] ;
+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] ;
STAR : '*' ;
SLASH : '/' ;
@@ -623,5 +742,7 @@ IDENTIFIER : [A-Za-z_][A-Za-z_0-9]* '$'? ;
WS : [ \t\r\n]+ -> skip ;
// SQL comments — EF Core query tags prepend a `-- tag` line comment to the statement; also block comments.
-LINE_COMMENT : '--' ~[\r\n]* -> skip ;
+// '--' starts a comment unless nothing but more dashes stands between it and a number, so '--2' and '---2' are
+// repeated negation as in ACE (which has no comments at all), while '--Before' is a comment.
+LINE_COMMENT : '--' {!DashesThenNumber()}? ~[\r\n]* -> skip ;
BLOCK_COMMENT : '/*' .*? '*/' -> skip ;
diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs
index ec56f31a3..0401ab57e 100644
--- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs
+++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs
@@ -8,7 +8,7 @@
//
//------------------------------------------------------------------------------
-// Generated from AccessSql.g4 by ANTLR 4.13.1
+// Generated from D:/toolkits/efcorejetlibred/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 by ANTLR 4.13.1
// Unreachable code detected
#pragma warning disable 0162
@@ -551,6 +551,17 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result.
public virtual Result VisitColumnReferencesConstraint([NotNull] AccessSqlParser.ColumnReferencesConstraintContext context) { return VisitChildren(context); }
///
+ /// Visit a parse tree produced by the IdentityConstraint
+ /// labeled alternative in .
+ ///
+ /// The default implementation returns the result of calling
+ /// on .
+ ///
+ ///
+ /// The parse tree.
+ /// The visitor result.
+ public virtual Result VisitIdentityConstraint([NotNull] AccessSqlParser.IdentityConstraintContext context) { return VisitChildren(context); }
+ ///
/// Visit a parse tree produced by the PrimaryKeyTableConstraint
/// labeled alternative in .
///
@@ -1059,6 +1070,17 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result.
public virtual Result VisitAndExpr([NotNull] AccessSqlParser.AndExprContext context) { return VisitChildren(context); }
///
+ /// Visit a parse tree produced by the IntDivExpr
+ /// labeled alternative in .
+ ///
+ /// The default implementation returns the result of calling
+ /// on .
+ ///
+ ///
+ /// The parse tree.
+ /// The visitor result.
+ public virtual Result VisitIntDivExpr([NotNull] AccessSqlParser.IntDivExprContext context) { return VisitChildren(context); }
+ ///
/// Visit a parse tree produced by the LikeExpr
/// labeled alternative in .
///
@@ -1092,7 +1114,7 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result.
public virtual Result VisitComparisonExpr([NotNull] AccessSqlParser.ComparisonExprContext context) { return VisitChildren(context); }
///
- /// Visit a parse tree produced by the BetweenExpr
+ /// Visit a parse tree produced by the XorExpr
/// labeled alternative in .
///
/// The default implementation returns the result of calling
@@ -1101,9 +1123,9 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitor
/// The parse tree.
/// The visitor result.
- public virtual Result VisitBetweenExpr([NotNull] AccessSqlParser.BetweenExprContext context) { return VisitChildren(context); }
+ public virtual Result VisitXorExpr([NotNull] AccessSqlParser.XorExprContext context) { return VisitChildren(context); }
///
- /// Visit a parse tree produced by the BitNotExpr
+ /// Visit a parse tree produced by the BetweenExpr
/// labeled alternative in .
///
/// The default implementation returns the result of calling
@@ -1112,7 +1134,7 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitor
/// The parse tree.
/// The visitor result.
- public virtual Result VisitBitNotExpr([NotNull] AccessSqlParser.BitNotExprContext context) { return VisitChildren(context); }
+ public virtual Result VisitBetweenExpr([NotNull] AccessSqlParser.BetweenExprContext context) { return VisitChildren(context); }
///
/// Visit a parse tree produced by the PowExpr
/// labeled alternative in .
@@ -1158,6 +1180,17 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result.
public virtual Result VisitInSubqueryExpr([NotNull] AccessSqlParser.InSubqueryExprContext context) { return VisitChildren(context); }
///
+ /// Visit a parse tree produced by the ConcatExpr
+ /// labeled alternative in .
+ ///
+ /// The default implementation returns the result of calling
+ /// on .
+ ///
+ ///
+ /// The parse tree.
+ /// The visitor result.
+ public virtual Result VisitConcatExpr([NotNull] AccessSqlParser.ConcatExprContext context) { return VisitChildren(context); }
+ ///
/// Visit a parse tree produced by the MulDivExpr
/// labeled alternative in .
///
@@ -1169,7 +1202,7 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result.
public virtual Result VisitMulDivExpr([NotNull] AccessSqlParser.MulDivExprContext context) { return VisitChildren(context); }
///
- /// Visit a parse tree produced by the BitwiseExpr
+ /// Visit a parse tree produced by the ImpExpr
/// labeled alternative in .
///
/// The default implementation returns the result of calling
@@ -1178,7 +1211,7 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitor
/// The parse tree.
/// The visitor result.
- public virtual Result VisitBitwiseExpr([NotNull] AccessSqlParser.BitwiseExprContext context) { return VisitChildren(context); }
+ public virtual Result VisitImpExpr([NotNull] AccessSqlParser.ImpExprContext context) { return VisitChildren(context); }
///
/// Visit a parse tree produced by the PrimaryExpr
/// labeled alternative in .
@@ -1202,6 +1235,17 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result.
public virtual Result VisitNotExpr([NotNull] AccessSqlParser.NotExprContext context) { return VisitChildren(context); }
///
+ /// Visit a parse tree produced by the ModExpr
+ /// labeled alternative in .
+ ///
+ /// The default implementation returns the result of calling
+ /// on .
+ ///
+ ///
+ /// The parse tree.
+ /// The visitor result.
+ public virtual Result VisitModExpr([NotNull] AccessSqlParser.ModExprContext context) { return VisitChildren(context); }
+ ///
/// Visit a parse tree produced by the InExpr
/// labeled alternative in .
///
@@ -1213,7 +1257,18 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe visitor result.
public virtual Result VisitInExpr([NotNull] AccessSqlParser.InExprContext context) { return VisitChildren(context); }
///
- /// Visit a parse tree produced by the AddConcatExpr
+ /// Visit a parse tree produced by the AddSubExpr
+ /// labeled alternative in .
+ ///
+ /// The default implementation returns the result of calling
+ /// on .
+ ///
+ ///
+ /// The parse tree.
+ /// The visitor result.
+ public virtual Result VisitAddSubExpr([NotNull] AccessSqlParser.AddSubExprContext context) { return VisitChildren(context); }
+ ///
+ /// Visit a parse tree produced by the EqvExpr
/// labeled alternative in .
///
/// The default implementation returns the result of calling
@@ -1222,7 +1277,7 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitor
/// The parse tree.
/// The visitor result.
- public virtual Result VisitAddConcatExpr([NotNull] AccessSqlParser.AddConcatExprContext context) { return VisitChildren(context); }
+ public virtual Result VisitEqvExpr([NotNull] AccessSqlParser.EqvExprContext context) { return VisitChildren(context); }
///
/// Visit a parse tree produced by the LiteralPrimary
/// labeled alternative in .
@@ -1534,5 +1589,95 @@ public partial class AccessSqlBaseVisitor : AbstractParseTreeVisitorThe parse tree.
/// The visitor result.
public virtual Result VisitWindowSpecification([NotNull] AccessSqlParser.WindowSpecificationContext 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 VisitClusteredOption([NotNull] AccessSqlParser.ClusteredOptionContext 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 VisitWindowFrame([NotNull] AccessSqlParser.WindowFrameContext 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 VisitFrameBound([NotNull] AccessSqlParser.FrameBoundContext 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 VisitFrameExclusion([NotNull] AccessSqlParser.FrameExclusionContext 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 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 VisitFilterClause([NotNull] AccessSqlParser.FilterClauseContext 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 VisitWithinGroup([NotNull] AccessSqlParser.WithinGroupContext 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 VisitNthRowFrom([NotNull] AccessSqlParser.NthRowFromContext 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 VisitNullTreatment([NotNull] AccessSqlParser.NullTreatmentContext context) { return VisitChildren(context); }
}
} // namespace LibRed.Sql.Grammar
diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs
index 975e1ea36..a4257e173 100644
--- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs
+++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs
@@ -8,7 +8,7 @@
//
//------------------------------------------------------------------------------
-// Generated from AccessSql.g4 by ANTLR 4.13.1
+// Generated from D:/toolkits/efcorejetlibred/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 by ANTLR 4.13.1
// Unreachable code detected
#pragma warning disable 0162
@@ -34,25 +34,29 @@ public partial class AccessSqlLexer : Lexer {
protected static DFA[] decisionToDFA;
protected static PredictionContextCache sharedContextCache = new PredictionContextCache();
public const int
- SELECT=1, FROM=2, WHERE=3, TOP=4, AS=5, AND=6, OR=7, NOT=8, BAND=9, BOR=10,
- BXOR=11, BNOT=12, LIKE=13, MOD=14, INNER=15, LEFT=16, RIGHT=17, FULL=18,
- OUTER=19, JOIN=20, IN=21, ON=22, ORDER=23, GROUP=24, IS=25, BY=26, HAVING=27,
- EXISTS=28, IF=29, THEN=30, DISTINCTROW=31, DISTINCT=32, PERCENT=33, CROSS=34,
- APPLY=35, OVER=36, PARTITION=37, CASE=38, WHEN=39, ELSE=40, END=41, OFFSET=42,
- FETCH=43, NEXT=44, FIRST=45, ROWS=46, ROW=47, ONLY=48, BETWEEN=49, UNION=50,
- ALL=51, INTERSECT=52, EXCEPT=53, CREATE=54, TABLE=55, BEGIN=56, COMMIT=57,
- ROLLBACK=58, TRANSACTION=59, WORK=60, ALTER=61, RENAME=62, TO=63, ADD=64,
- DROP=65, COLUMN=66, INSERT=67, INTO=68, VALUES=69, PRIMARY=70, KEY=71,
- CONSTRAINT=72, FOREIGN=73, REFERENCES=74, DELETE=75, UPDATE=76, CASCADE=77,
- RESTRICT=78, ACTION=79, SET=80, DEFAULT=81, NO=82, UNIQUE=83, INDEX=84,
- TEMPORARY=85, WITH=86, COMPRESSION=87, COMP=88, DISALLOW=89, IGNORE=90,
- CHECK=91, VIEW=92, PROCEDURE=93, PARAMETERS=94, EXECUTE=95, EXEC=96, ASC=97,
- DESC=98, TRUE=99, FALSE=100, NULL=101, STAR=102, SLASH=103, BACKSLASH=104,
- CARET=105, PLUS=106, MINUS=107, AMP=108, EQ=109, NEQ=110, LTE=111, GTE=112,
- LT=113, GT=114, LPAREN=115, RPAREN=116, COMMA=117, DOT=118, SEMI=119,
- SYSVAR=120, PARAM=121, HEX_LITERAL=122, INTEGER_LITERAL=123, NUMBER_LITERAL=124,
- STRING_LITERAL=125, DATE_LITERAL=126, GUID_LITERAL=127, BRACKET_ID=128,
- BACKTICK_ID=129, IDENTIFIER=130, WS=131, LINE_COMMENT=132, BLOCK_COMMENT=133;
+ SELECT=1, FROM=2, WHERE=3, TOP=4, AS=5, AND=6, OR=7, NOT=8, XOR=9, EQV=10,
+ IMP=11, BAND=12, BOR=13, BXOR=14, BNOT=15, LIKE=16, MOD=17, INNER=18,
+ LEFT=19, RIGHT=20, FULL=21, OUTER=22, JOIN=23, IN=24, ON=25, ORDER=26,
+ GROUP=27, IS=28, BY=29, HAVING=30, EXISTS=31, IF=32, THEN=33, DISTINCTROW=34,
+ DISTINCT=35, PERCENT=36, CROSS=37, APPLY=38, OVER=39, PARTITION=40, CASE=41,
+ WHEN=42, ELSE=43, END=44, OFFSET=45, FETCH=46, NEXT=47, FIRST=48, ROWS=49,
+ ROW=50, ONLY=51, BETWEEN=52, UNION=53, ALL=54, INTERSECT=55, EXCEPT=56,
+ CREATE=57, TABLE=58, BEGIN=59, COMMIT=60, ROLLBACK=61, TRANSACTION=62,
+ WORK=63, ALTER=64, RENAME=65, TO=66, ADD=67, DROP=68, COLUMN=69, INSERT=70,
+ INTO=71, VALUES=72, PRIMARY=73, KEY=74, CONSTRAINT=75, FOREIGN=76, REFERENCES=77,
+ DELETE=78, UPDATE=79, CASCADE=80, RESTRICT=81, ACTION=82, SET=83, DEFAULT=84,
+ 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;
public static string[] channelNames = {
"DEFAULT_TOKEN_CHANNEL", "HIDDEN"
};
@@ -62,27 +66,41 @@ public const int
};
public static readonly string[] ruleNames = {
- "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "BAND", "BOR",
- "BXOR", "BNOT", "LIKE", "MOD", "INNER", "LEFT", "RIGHT", "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",
+ "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", "STAR", "SLASH", "BACKSLASH", "CARET",
- "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN",
- "RPAREN", "COMMA", "DOT", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", "INTEGER_LITERAL",
+ "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",
"HEXDIGIT", "BRACKET_ID", "BACKTICK_ID", "IDENTIFIER", "WS", "LINE_COMMENT",
"BLOCK_COMMENT"
};
+ // Whether the input ahead is any number of dashes and then a number: a digit, or a point and a digit.
+ private bool DashesThenNumber()
+ {
+ int i = 1;
+ while (InputStream.LA(i) == '-')
+ i++;
+ int c = InputStream.LA(i);
+ return c is >= '0' and <= '9' || c == '.' && InputStream.LA(i + 1) is >= '0' and <= '9';
+ }
+
+
public AccessSqlLexer(ICharStream input)
: this(input, Console.Out, Console.Error) { }
@@ -101,26 +119,30 @@ 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, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, "'*'", "'/'", "'\\'", "'^'", "'+'", "'-'", "'&'", "'='", null,
+ "'<='", "'>='", "'<'", "'>'", "'('", "')'", "','", "'.'", "';'"
};
private static readonly string[] _SymbolicNames = {
- null, "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "BAND",
- "BOR", "BXOR", "BNOT", "LIKE", "MOD", "INNER", "LEFT", "RIGHT", "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",
+ null, "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", "STAR", "SLASH", "BACKSLASH", "CARET",
- "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN",
- "RPAREN", "COMMA", "DOT", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", "INTEGER_LITERAL",
+ "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"
};
@@ -151,8 +173,21 @@ static AccessSqlLexer() {
decisionToDFA[i] = new DFA(_ATN.GetDecisionState(i), i);
}
}
+ public override bool Sempred(RuleContext _localctx, int ruleIndex, int predIndex) {
+ switch (ruleIndex) {
+ case 153 : return LINE_COMMENT_sempred(_localctx, predIndex);
+ }
+ return true;
+ }
+ private bool LINE_COMMENT_sempred(RuleContext _localctx, int predIndex) {
+ switch (predIndex) {
+ case 0: return !DashesThenNumber();
+ }
+ return true;
+ }
+
private static int[] _serializedATN = {
- 4,0,133,1130,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,153,1319,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,
@@ -172,364 +207,429 @@ static AccessSqlLexer() {
7,116,2,117,7,117,2,118,7,118,2,119,7,119,2,120,7,120,2,121,7,121,2,122,
7,122,2,123,7,123,2,124,7,124,2,125,7,125,2,126,7,126,2,127,7,127,2,128,
7,128,2,129,7,129,2,130,7,130,2,131,7,131,2,132,7,132,2,133,7,133,2,134,
- 7,134,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,1,1,1,1,1,1,1,1,1,1,2,1,2,1,2,1,2,
- 1,2,1,2,1,3,1,3,1,3,1,3,1,4,1,4,1,4,1,5,1,5,1,5,1,5,1,6,1,6,1,6,1,7,1,
- 7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,9,1,10,1,10,1,10,1,10,1,10,
- 1,11,1,11,1,11,1,11,1,11,1,12,1,12,1,12,1,12,1,12,1,13,1,13,1,13,1,13,
- 1,14,1,14,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15,1,16,1,16,1,16,
- 1,16,1,16,1,16,1,17,1,17,1,17,1,17,1,17,1,18,1,18,1,18,1,18,1,18,1,18,
- 1,19,1,19,1,19,1,19,1,19,1,20,1,20,1,20,1,21,1,21,1,21,1,22,1,22,1,22,
- 1,22,1,22,1,22,1,23,1,23,1,23,1,23,1,23,1,23,1,24,1,24,1,24,1,25,1,25,
- 1,25,1,26,1,26,1,26,1,26,1,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,
- 1,27,1,28,1,28,1,28,1,29,1,29,1,29,1,29,1,29,1,30,1,30,1,30,1,30,1,30,
- 1,30,1,30,1,30,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,
- 1,31,1,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,32,1,33,1,33,1,33,1,33,
- 1,33,1,33,1,34,1,34,1,34,1,34,1,34,1,34,1,35,1,35,1,35,1,35,1,35,1,36,
- 1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,37,1,37,1,37,1,37,1,37,
- 1,38,1,38,1,38,1,38,1,38,1,39,1,39,1,39,1,39,1,39,1,40,1,40,1,40,1,40,
- 1,41,1,41,1,41,1,41,1,41,1,41,1,41,1,42,1,42,1,42,1,42,1,42,1,42,1,43,
- 1,43,1,43,1,43,1,43,1,44,1,44,1,44,1,44,1,44,1,44,1,45,1,45,1,45,1,45,
- 1,45,1,46,1,46,1,46,1,46,1,47,1,47,1,47,1,47,1,47,1,48,1,48,1,48,1,48,
- 1,48,1,48,1,48,1,48,1,49,1,49,1,49,1,49,1,49,1,49,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,51,1,51,1,52,1,52,1,52,1,52,
- 1,52,1,52,1,52,1,53,1,53,1,53,1,53,1,53,1,53,1,53,1,54,1,54,1,54,1,54,
- 1,54,1,54,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,57,1,57,1,57,1,58,1,58,1,58,1,58,
- 1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59,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,62,1,62,
- 1,62,1,63,1,63,1,63,1,63,1,64,1,64,1,64,1,64,1,64,1,65,1,65,1,65,1,65,
- 1,65,1,65,1,65,1,66,1,66,1,66,1,66,1,66,1,66,1,66,1,67,1,67,1,67,1,67,
- 1,67,1,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,69,1,70,1,70,1,70,1,70,1,71,1,71,1,71,1,71,1,71,1,71,1,71,1,71,
- 1,71,1,71,1,71,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,72,1,73,1,73,1,73,
- 1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,73,1,74,1,74,1,74,1,74,1,74,1,74,
- 1,74,1,75,1,75,1,75,1,75,1,75,1,75,1,75,1,76,1,76,1,76,1,76,1,76,1,76,
- 1,76,1,76,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,77,1,78,1,78,1,78,
- 1,78,1,78,1,78,1,78,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,81,1,81,1,81,1,82,1,82,1,82,1,82,1,82,1,82,1,82,1,83,1,83,
- 1,83,1,83,1,83,1,83,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,1,84,
- 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,86,1,86,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,89,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,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,93,1,93,1,93,1,93,1,93,1,93,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,95,1,95,1,95,1,95,1,95,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,99,
- 1,99,1,99,1,99,1,99,1,99,1,100,1,100,1,100,1,100,1,100,1,101,1,101,1,102,
- 1,102,1,103,1,103,1,104,1,104,1,105,1,105,1,106,1,106,1,107,1,107,1,108,
- 1,108,1,109,1,109,1,109,1,109,3,109,912,8,109,1,110,1,110,1,110,1,111,
- 1,111,1,111,1,112,1,112,1,113,1,113,1,114,1,114,1,115,1,115,1,116,1,116,
- 1,117,1,117,1,118,1,118,1,119,1,119,1,119,1,119,1,119,5,119,939,8,119,
- 10,119,12,119,942,9,119,1,120,1,120,1,120,1,120,5,120,948,8,120,10,120,
- 12,120,951,9,120,3,120,953,8,120,1,121,1,121,1,121,4,121,958,8,121,11,
- 121,12,121,959,1,122,4,122,963,8,122,11,122,12,122,964,1,123,4,123,968,
- 8,123,11,123,12,123,969,1,123,1,123,5,123,974,8,123,10,123,12,123,977,
- 9,123,1,123,3,123,980,8,123,1,123,1,123,4,123,984,8,123,11,123,12,123,
- 985,1,123,3,123,989,8,123,1,123,4,123,992,8,123,11,123,12,123,993,1,123,
- 3,123,997,8,123,1,124,1,124,3,124,1001,8,124,1,124,4,124,1004,8,124,11,
- 124,12,124,1005,1,125,1,125,1,125,1,125,5,125,1012,8,125,10,125,12,125,
- 1015,9,125,1,125,1,125,1,125,1,125,1,125,5,125,1022,8,125,10,125,12,125,
- 1025,9,125,1,125,3,125,1028,8,125,1,126,1,126,5,126,1032,8,126,10,126,
- 12,126,1035,9,126,1,126,1,126,1,127,1,127,4,127,1041,8,127,11,127,12,127,
- 1042,1,127,1,127,4,127,1047,8,127,11,127,12,127,1048,1,127,1,127,4,127,
- 1053,8,127,11,127,12,127,1054,1,127,1,127,4,127,1059,8,127,11,127,12,127,
- 1060,1,127,1,127,4,127,1065,8,127,11,127,12,127,1066,1,127,1,127,1,128,
- 1,128,1,129,1,129,4,129,1075,8,129,11,129,12,129,1076,1,129,1,129,1,130,
- 1,130,4,130,1083,8,130,11,130,12,130,1084,1,130,1,130,1,131,1,131,5,131,
- 1091,8,131,10,131,12,131,1094,9,131,1,131,3,131,1097,8,131,1,132,4,132,
- 1100,8,132,11,132,12,132,1101,1,132,1,132,1,133,1,133,1,133,1,133,5,133,
- 1110,8,133,10,133,12,133,1113,9,133,1,133,1,133,1,134,1,134,1,134,1,134,
- 5,134,1121,8,134,10,134,12,134,1124,9,134,1,134,1,134,1,134,1,134,1,134,
- 1,1122,0,135,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,0,251,
- 125,253,126,255,127,257,0,259,128,261,129,263,130,265,131,267,132,269,
- 133,1,0,37,2,0,83,83,115,115,2,0,69,69,101,101,2,0,76,76,108,108,2,0,67,
- 67,99,99,2,0,84,84,116,116,2,0,70,70,102,102,2,0,82,82,114,114,2,0,79,
- 79,111,111,2,0,77,77,109,109,2,0,87,87,119,119,2,0,72,72,104,104,2,0,80,
- 80,112,112,2,0,65,65,97,97,2,0,78,78,110,110,2,0,68,68,100,100,2,0,66,
- 66,98,98,2,0,88,88,120,120,2,0,73,73,105,105,2,0,75,75,107,107,2,0,71,
- 71,103,103,2,0,85,85,117,117,2,0,74,74,106,106,2,0,89,89,121,121,2,0,86,
- 86,118,118,2,0,81,81,113,113,3,0,65,90,95,95,97,122,4,0,48,57,65,90,95,
- 95,97,122,3,0,48,57,65,70,97,102,1,0,48,57,2,0,43,43,45,45,1,0,34,34,1,
- 0,39,39,1,0,35,35,1,0,93,93,1,0,96,96,3,0,9,10,13,13,32,32,2,0,10,10,13,
- 13,1161,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,251,1,0,0,0,0,253,1,0,0,0,0,255,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,1,271,1,0,0,0,
- 3,278,1,0,0,0,5,283,1,0,0,0,7,289,1,0,0,0,9,293,1,0,0,0,11,296,1,0,0,0,
- 13,300,1,0,0,0,15,303,1,0,0,0,17,307,1,0,0,0,19,312,1,0,0,0,21,316,1,0,
- 0,0,23,321,1,0,0,0,25,326,1,0,0,0,27,331,1,0,0,0,29,335,1,0,0,0,31,341,
- 1,0,0,0,33,346,1,0,0,0,35,352,1,0,0,0,37,357,1,0,0,0,39,363,1,0,0,0,41,
- 368,1,0,0,0,43,371,1,0,0,0,45,374,1,0,0,0,47,380,1,0,0,0,49,386,1,0,0,
- 0,51,389,1,0,0,0,53,392,1,0,0,0,55,399,1,0,0,0,57,406,1,0,0,0,59,409,1,
- 0,0,0,61,414,1,0,0,0,63,426,1,0,0,0,65,435,1,0,0,0,67,443,1,0,0,0,69,449,
- 1,0,0,0,71,455,1,0,0,0,73,460,1,0,0,0,75,470,1,0,0,0,77,475,1,0,0,0,79,
- 480,1,0,0,0,81,485,1,0,0,0,83,489,1,0,0,0,85,496,1,0,0,0,87,502,1,0,0,
- 0,89,507,1,0,0,0,91,513,1,0,0,0,93,518,1,0,0,0,95,522,1,0,0,0,97,527,1,
- 0,0,0,99,535,1,0,0,0,101,541,1,0,0,0,103,545,1,0,0,0,105,555,1,0,0,0,107,
- 562,1,0,0,0,109,569,1,0,0,0,111,575,1,0,0,0,113,581,1,0,0,0,115,588,1,
- 0,0,0,117,597,1,0,0,0,119,609,1,0,0,0,121,614,1,0,0,0,123,620,1,0,0,0,
- 125,627,1,0,0,0,127,630,1,0,0,0,129,634,1,0,0,0,131,639,1,0,0,0,133,646,
- 1,0,0,0,135,653,1,0,0,0,137,658,1,0,0,0,139,665,1,0,0,0,141,673,1,0,0,
- 0,143,677,1,0,0,0,145,688,1,0,0,0,147,696,1,0,0,0,149,707,1,0,0,0,151,
- 714,1,0,0,0,153,721,1,0,0,0,155,729,1,0,0,0,157,738,1,0,0,0,159,745,1,
- 0,0,0,161,749,1,0,0,0,163,757,1,0,0,0,165,760,1,0,0,0,167,767,1,0,0,0,
- 169,773,1,0,0,0,171,783,1,0,0,0,173,788,1,0,0,0,175,800,1,0,0,0,177,805,
- 1,0,0,0,179,814,1,0,0,0,181,821,1,0,0,0,183,827,1,0,0,0,185,832,1,0,0,
- 0,187,842,1,0,0,0,189,853,1,0,0,0,191,861,1,0,0,0,193,866,1,0,0,0,195,
- 870,1,0,0,0,197,875,1,0,0,0,199,880,1,0,0,0,201,886,1,0,0,0,203,891,1,
- 0,0,0,205,893,1,0,0,0,207,895,1,0,0,0,209,897,1,0,0,0,211,899,1,0,0,0,
- 213,901,1,0,0,0,215,903,1,0,0,0,217,905,1,0,0,0,219,911,1,0,0,0,221,913,
- 1,0,0,0,223,916,1,0,0,0,225,919,1,0,0,0,227,921,1,0,0,0,229,923,1,0,0,
- 0,231,925,1,0,0,0,233,927,1,0,0,0,235,929,1,0,0,0,237,931,1,0,0,0,239,
- 933,1,0,0,0,241,952,1,0,0,0,243,954,1,0,0,0,245,962,1,0,0,0,247,996,1,
- 0,0,0,249,998,1,0,0,0,251,1027,1,0,0,0,253,1029,1,0,0,0,255,1038,1,0,0,
- 0,257,1070,1,0,0,0,259,1072,1,0,0,0,261,1080,1,0,0,0,263,1088,1,0,0,0,
- 265,1099,1,0,0,0,267,1105,1,0,0,0,269,1116,1,0,0,0,271,272,7,0,0,0,272,
- 273,7,1,0,0,273,274,7,2,0,0,274,275,7,1,0,0,275,276,7,3,0,0,276,277,7,
- 4,0,0,277,2,1,0,0,0,278,279,7,5,0,0,279,280,7,6,0,0,280,281,7,7,0,0,281,
- 282,7,8,0,0,282,4,1,0,0,0,283,284,7,9,0,0,284,285,7,10,0,0,285,286,7,1,
- 0,0,286,287,7,6,0,0,287,288,7,1,0,0,288,6,1,0,0,0,289,290,7,4,0,0,290,
- 291,7,7,0,0,291,292,7,11,0,0,292,8,1,0,0,0,293,294,7,12,0,0,294,295,7,
- 0,0,0,295,10,1,0,0,0,296,297,7,12,0,0,297,298,7,13,0,0,298,299,7,14,0,
- 0,299,12,1,0,0,0,300,301,7,7,0,0,301,302,7,6,0,0,302,14,1,0,0,0,303,304,
- 7,13,0,0,304,305,7,7,0,0,305,306,7,4,0,0,306,16,1,0,0,0,307,308,7,15,0,
- 0,308,309,7,12,0,0,309,310,7,13,0,0,310,311,7,14,0,0,311,18,1,0,0,0,312,
- 313,7,15,0,0,313,314,7,7,0,0,314,315,7,6,0,0,315,20,1,0,0,0,316,317,7,
- 15,0,0,317,318,7,16,0,0,318,319,7,7,0,0,319,320,7,6,0,0,320,22,1,0,0,0,
- 321,322,7,15,0,0,322,323,7,13,0,0,323,324,7,7,0,0,324,325,7,4,0,0,325,
- 24,1,0,0,0,326,327,7,2,0,0,327,328,7,17,0,0,328,329,7,18,0,0,329,330,7,
- 1,0,0,330,26,1,0,0,0,331,332,7,8,0,0,332,333,7,7,0,0,333,334,7,14,0,0,
- 334,28,1,0,0,0,335,336,7,17,0,0,336,337,7,13,0,0,337,338,7,13,0,0,338,
- 339,7,1,0,0,339,340,7,6,0,0,340,30,1,0,0,0,341,342,7,2,0,0,342,343,7,1,
- 0,0,343,344,7,5,0,0,344,345,7,4,0,0,345,32,1,0,0,0,346,347,7,6,0,0,347,
- 348,7,17,0,0,348,349,7,19,0,0,349,350,7,10,0,0,350,351,7,4,0,0,351,34,
- 1,0,0,0,352,353,7,5,0,0,353,354,7,20,0,0,354,355,7,2,0,0,355,356,7,2,0,
- 0,356,36,1,0,0,0,357,358,7,7,0,0,358,359,7,20,0,0,359,360,7,4,0,0,360,
- 361,7,1,0,0,361,362,7,6,0,0,362,38,1,0,0,0,363,364,7,21,0,0,364,365,7,
- 7,0,0,365,366,7,17,0,0,366,367,7,13,0,0,367,40,1,0,0,0,368,369,7,17,0,
- 0,369,370,7,13,0,0,370,42,1,0,0,0,371,372,7,7,0,0,372,373,7,13,0,0,373,
- 44,1,0,0,0,374,375,7,7,0,0,375,376,7,6,0,0,376,377,7,14,0,0,377,378,7,
- 1,0,0,378,379,7,6,0,0,379,46,1,0,0,0,380,381,7,19,0,0,381,382,7,6,0,0,
- 382,383,7,7,0,0,383,384,7,20,0,0,384,385,7,11,0,0,385,48,1,0,0,0,386,387,
- 7,17,0,0,387,388,7,0,0,0,388,50,1,0,0,0,389,390,7,15,0,0,390,391,7,22,
- 0,0,391,52,1,0,0,0,392,393,7,10,0,0,393,394,7,12,0,0,394,395,7,23,0,0,
- 395,396,7,17,0,0,396,397,7,13,0,0,397,398,7,19,0,0,398,54,1,0,0,0,399,
- 400,7,1,0,0,400,401,7,16,0,0,401,402,7,17,0,0,402,403,7,0,0,0,403,404,
- 7,4,0,0,404,405,7,0,0,0,405,56,1,0,0,0,406,407,7,17,0,0,407,408,7,5,0,
- 0,408,58,1,0,0,0,409,410,7,4,0,0,410,411,7,10,0,0,411,412,7,1,0,0,412,
- 413,7,13,0,0,413,60,1,0,0,0,414,415,7,14,0,0,415,416,7,17,0,0,416,417,
- 7,0,0,0,417,418,7,4,0,0,418,419,7,17,0,0,419,420,7,13,0,0,420,421,7,3,
- 0,0,421,422,7,4,0,0,422,423,7,6,0,0,423,424,7,7,0,0,424,425,7,9,0,0,425,
- 62,1,0,0,0,426,427,7,14,0,0,427,428,7,17,0,0,428,429,7,0,0,0,429,430,7,
- 4,0,0,430,431,7,17,0,0,431,432,7,13,0,0,432,433,7,3,0,0,433,434,7,4,0,
- 0,434,64,1,0,0,0,435,436,7,11,0,0,436,437,7,1,0,0,437,438,7,6,0,0,438,
- 439,7,3,0,0,439,440,7,1,0,0,440,441,7,13,0,0,441,442,7,4,0,0,442,66,1,
- 0,0,0,443,444,7,3,0,0,444,445,7,6,0,0,445,446,7,7,0,0,446,447,7,0,0,0,
- 447,448,7,0,0,0,448,68,1,0,0,0,449,450,7,12,0,0,450,451,7,11,0,0,451,452,
- 7,11,0,0,452,453,7,2,0,0,453,454,7,22,0,0,454,70,1,0,0,0,455,456,7,7,0,
- 0,456,457,7,23,0,0,457,458,7,1,0,0,458,459,7,6,0,0,459,72,1,0,0,0,460,
- 461,7,11,0,0,461,462,7,12,0,0,462,463,7,6,0,0,463,464,7,4,0,0,464,465,
- 7,17,0,0,465,466,7,4,0,0,466,467,7,17,0,0,467,468,7,7,0,0,468,469,7,13,
- 0,0,469,74,1,0,0,0,470,471,7,3,0,0,471,472,7,12,0,0,472,473,7,0,0,0,473,
- 474,7,1,0,0,474,76,1,0,0,0,475,476,7,9,0,0,476,477,7,10,0,0,477,478,7,
- 1,0,0,478,479,7,13,0,0,479,78,1,0,0,0,480,481,7,1,0,0,481,482,7,2,0,0,
- 482,483,7,0,0,0,483,484,7,1,0,0,484,80,1,0,0,0,485,486,7,1,0,0,486,487,
- 7,13,0,0,487,488,7,14,0,0,488,82,1,0,0,0,489,490,7,7,0,0,490,491,7,5,0,
- 0,491,492,7,5,0,0,492,493,7,0,0,0,493,494,7,1,0,0,494,495,7,4,0,0,495,
- 84,1,0,0,0,496,497,7,5,0,0,497,498,7,1,0,0,498,499,7,4,0,0,499,500,7,3,
- 0,0,500,501,7,10,0,0,501,86,1,0,0,0,502,503,7,13,0,0,503,504,7,1,0,0,504,
- 505,7,16,0,0,505,506,7,4,0,0,506,88,1,0,0,0,507,508,7,5,0,0,508,509,7,
- 17,0,0,509,510,7,6,0,0,510,511,7,0,0,0,511,512,7,4,0,0,512,90,1,0,0,0,
- 513,514,7,6,0,0,514,515,7,7,0,0,515,516,7,9,0,0,516,517,7,0,0,0,517,92,
- 1,0,0,0,518,519,7,6,0,0,519,520,7,7,0,0,520,521,7,9,0,0,521,94,1,0,0,0,
- 522,523,7,7,0,0,523,524,7,13,0,0,524,525,7,2,0,0,525,526,7,22,0,0,526,
- 96,1,0,0,0,527,528,7,15,0,0,528,529,7,1,0,0,529,530,7,4,0,0,530,531,7,
- 9,0,0,531,532,7,1,0,0,532,533,7,1,0,0,533,534,7,13,0,0,534,98,1,0,0,0,
- 535,536,7,20,0,0,536,537,7,13,0,0,537,538,7,17,0,0,538,539,7,7,0,0,539,
- 540,7,13,0,0,540,100,1,0,0,0,541,542,7,12,0,0,542,543,7,2,0,0,543,544,
- 7,2,0,0,544,102,1,0,0,0,545,546,7,17,0,0,546,547,7,13,0,0,547,548,7,4,
- 0,0,548,549,7,1,0,0,549,550,7,6,0,0,550,551,7,0,0,0,551,552,7,1,0,0,552,
- 553,7,3,0,0,553,554,7,4,0,0,554,104,1,0,0,0,555,556,7,1,0,0,556,557,7,
- 16,0,0,557,558,7,3,0,0,558,559,7,1,0,0,559,560,7,11,0,0,560,561,7,4,0,
- 0,561,106,1,0,0,0,562,563,7,3,0,0,563,564,7,6,0,0,564,565,7,1,0,0,565,
- 566,7,12,0,0,566,567,7,4,0,0,567,568,7,1,0,0,568,108,1,0,0,0,569,570,7,
- 4,0,0,570,571,7,12,0,0,571,572,7,15,0,0,572,573,7,2,0,0,573,574,7,1,0,
- 0,574,110,1,0,0,0,575,576,7,15,0,0,576,577,7,1,0,0,577,578,7,19,0,0,578,
- 579,7,17,0,0,579,580,7,13,0,0,580,112,1,0,0,0,581,582,7,3,0,0,582,583,
- 7,7,0,0,583,584,7,8,0,0,584,585,7,8,0,0,585,586,7,17,0,0,586,587,7,4,0,
- 0,587,114,1,0,0,0,588,589,7,6,0,0,589,590,7,7,0,0,590,591,7,2,0,0,591,
- 592,7,2,0,0,592,593,7,15,0,0,593,594,7,12,0,0,594,595,7,3,0,0,595,596,
- 7,18,0,0,596,116,1,0,0,0,597,598,7,4,0,0,598,599,7,6,0,0,599,600,7,12,
- 0,0,600,601,7,13,0,0,601,602,7,0,0,0,602,603,7,12,0,0,603,604,7,3,0,0,
- 604,605,7,4,0,0,605,606,7,17,0,0,606,607,7,7,0,0,607,608,7,13,0,0,608,
- 118,1,0,0,0,609,610,7,9,0,0,610,611,7,7,0,0,611,612,7,6,0,0,612,613,7,
- 18,0,0,613,120,1,0,0,0,614,615,7,12,0,0,615,616,7,2,0,0,616,617,7,4,0,
- 0,617,618,7,1,0,0,618,619,7,6,0,0,619,122,1,0,0,0,620,621,7,6,0,0,621,
- 622,7,1,0,0,622,623,7,13,0,0,623,624,7,12,0,0,624,625,7,8,0,0,625,626,
- 7,1,0,0,626,124,1,0,0,0,627,628,7,4,0,0,628,629,7,7,0,0,629,126,1,0,0,
- 0,630,631,7,12,0,0,631,632,7,14,0,0,632,633,7,14,0,0,633,128,1,0,0,0,634,
- 635,7,14,0,0,635,636,7,6,0,0,636,637,7,7,0,0,637,638,7,11,0,0,638,130,
- 1,0,0,0,639,640,7,3,0,0,640,641,7,7,0,0,641,642,7,2,0,0,642,643,7,20,0,
- 0,643,644,7,8,0,0,644,645,7,13,0,0,645,132,1,0,0,0,646,647,7,17,0,0,647,
- 648,7,13,0,0,648,649,7,0,0,0,649,650,7,1,0,0,650,651,7,6,0,0,651,652,7,
- 4,0,0,652,134,1,0,0,0,653,654,7,17,0,0,654,655,7,13,0,0,655,656,7,4,0,
- 0,656,657,7,7,0,0,657,136,1,0,0,0,658,659,7,23,0,0,659,660,7,12,0,0,660,
- 661,7,2,0,0,661,662,7,20,0,0,662,663,7,1,0,0,663,664,7,0,0,0,664,138,1,
- 0,0,0,665,666,7,11,0,0,666,667,7,6,0,0,667,668,7,17,0,0,668,669,7,8,0,
- 0,669,670,7,12,0,0,670,671,7,6,0,0,671,672,7,22,0,0,672,140,1,0,0,0,673,
- 674,7,18,0,0,674,675,7,1,0,0,675,676,7,22,0,0,676,142,1,0,0,0,677,678,
- 7,3,0,0,678,679,7,7,0,0,679,680,7,13,0,0,680,681,7,0,0,0,681,682,7,4,0,
- 0,682,683,7,6,0,0,683,684,7,12,0,0,684,685,7,17,0,0,685,686,7,13,0,0,686,
- 687,7,4,0,0,687,144,1,0,0,0,688,689,7,5,0,0,689,690,7,7,0,0,690,691,7,
- 6,0,0,691,692,7,1,0,0,692,693,7,17,0,0,693,694,7,19,0,0,694,695,7,13,0,
- 0,695,146,1,0,0,0,696,697,7,6,0,0,697,698,7,1,0,0,698,699,7,5,0,0,699,
- 700,7,1,0,0,700,701,7,6,0,0,701,702,7,1,0,0,702,703,7,13,0,0,703,704,7,
- 3,0,0,704,705,7,1,0,0,705,706,7,0,0,0,706,148,1,0,0,0,707,708,7,14,0,0,
- 708,709,7,1,0,0,709,710,7,2,0,0,710,711,7,1,0,0,711,712,7,4,0,0,712,713,
- 7,1,0,0,713,150,1,0,0,0,714,715,7,20,0,0,715,716,7,11,0,0,716,717,7,14,
- 0,0,717,718,7,12,0,0,718,719,7,4,0,0,719,720,7,1,0,0,720,152,1,0,0,0,721,
- 722,7,3,0,0,722,723,7,12,0,0,723,724,7,0,0,0,724,725,7,3,0,0,725,726,7,
- 12,0,0,726,727,7,14,0,0,727,728,7,1,0,0,728,154,1,0,0,0,729,730,7,6,0,
- 0,730,731,7,1,0,0,731,732,7,0,0,0,732,733,7,4,0,0,733,734,7,6,0,0,734,
- 735,7,17,0,0,735,736,7,3,0,0,736,737,7,4,0,0,737,156,1,0,0,0,738,739,7,
- 12,0,0,739,740,7,3,0,0,740,741,7,4,0,0,741,742,7,17,0,0,742,743,7,7,0,
- 0,743,744,7,13,0,0,744,158,1,0,0,0,745,746,7,0,0,0,746,747,7,1,0,0,747,
- 748,7,4,0,0,748,160,1,0,0,0,749,750,7,14,0,0,750,751,7,1,0,0,751,752,7,
- 5,0,0,752,753,7,12,0,0,753,754,7,20,0,0,754,755,7,2,0,0,755,756,7,4,0,
- 0,756,162,1,0,0,0,757,758,7,13,0,0,758,759,7,7,0,0,759,164,1,0,0,0,760,
- 761,7,20,0,0,761,762,7,13,0,0,762,763,7,17,0,0,763,764,7,24,0,0,764,765,
- 7,20,0,0,765,766,7,1,0,0,766,166,1,0,0,0,767,768,7,17,0,0,768,769,7,13,
- 0,0,769,770,7,14,0,0,770,771,7,1,0,0,771,772,7,16,0,0,772,168,1,0,0,0,
- 773,774,7,4,0,0,774,775,7,1,0,0,775,776,7,8,0,0,776,777,7,11,0,0,777,778,
- 7,7,0,0,778,779,7,6,0,0,779,780,7,12,0,0,780,781,7,6,0,0,781,782,7,22,
- 0,0,782,170,1,0,0,0,783,784,7,9,0,0,784,785,7,17,0,0,785,786,7,4,0,0,786,
- 787,7,10,0,0,787,172,1,0,0,0,788,789,7,3,0,0,789,790,7,7,0,0,790,791,7,
- 8,0,0,791,792,7,11,0,0,792,793,7,6,0,0,793,794,7,1,0,0,794,795,7,0,0,0,
- 795,796,7,0,0,0,796,797,7,17,0,0,797,798,7,7,0,0,798,799,7,13,0,0,799,
- 174,1,0,0,0,800,801,7,3,0,0,801,802,7,7,0,0,802,803,7,8,0,0,803,804,7,
- 11,0,0,804,176,1,0,0,0,805,806,7,14,0,0,806,807,7,17,0,0,807,808,7,0,0,
- 0,808,809,7,12,0,0,809,810,7,2,0,0,810,811,7,2,0,0,811,812,7,7,0,0,812,
- 813,7,9,0,0,813,178,1,0,0,0,814,815,7,17,0,0,815,816,7,19,0,0,816,817,
- 7,13,0,0,817,818,7,7,0,0,818,819,7,6,0,0,819,820,7,1,0,0,820,180,1,0,0,
- 0,821,822,7,3,0,0,822,823,7,10,0,0,823,824,7,1,0,0,824,825,7,3,0,0,825,
- 826,7,18,0,0,826,182,1,0,0,0,827,828,7,23,0,0,828,829,7,17,0,0,829,830,
- 7,1,0,0,830,831,7,9,0,0,831,184,1,0,0,0,832,833,7,11,0,0,833,834,7,6,0,
- 0,834,835,7,7,0,0,835,836,7,3,0,0,836,837,7,1,0,0,837,838,7,14,0,0,838,
- 839,7,20,0,0,839,840,7,6,0,0,840,841,7,1,0,0,841,186,1,0,0,0,842,843,7,
- 11,0,0,843,844,7,12,0,0,844,845,7,6,0,0,845,846,7,12,0,0,846,847,7,8,0,
- 0,847,848,7,1,0,0,848,849,7,4,0,0,849,850,7,1,0,0,850,851,7,6,0,0,851,
- 852,7,0,0,0,852,188,1,0,0,0,853,854,7,1,0,0,854,855,7,16,0,0,855,856,7,
- 1,0,0,856,857,7,3,0,0,857,858,7,20,0,0,858,859,7,4,0,0,859,860,7,1,0,0,
- 860,190,1,0,0,0,861,862,7,1,0,0,862,863,7,16,0,0,863,864,7,1,0,0,864,865,
- 7,3,0,0,865,192,1,0,0,0,866,867,7,12,0,0,867,868,7,0,0,0,868,869,7,3,0,
- 0,869,194,1,0,0,0,870,871,7,14,0,0,871,872,7,1,0,0,872,873,7,0,0,0,873,
- 874,7,3,0,0,874,196,1,0,0,0,875,876,7,4,0,0,876,877,7,6,0,0,877,878,7,
- 20,0,0,878,879,7,1,0,0,879,198,1,0,0,0,880,881,7,5,0,0,881,882,7,12,0,
- 0,882,883,7,2,0,0,883,884,7,0,0,0,884,885,7,1,0,0,885,200,1,0,0,0,886,
- 887,7,13,0,0,887,888,7,20,0,0,888,889,7,2,0,0,889,890,7,2,0,0,890,202,
- 1,0,0,0,891,892,5,42,0,0,892,204,1,0,0,0,893,894,5,47,0,0,894,206,1,0,
- 0,0,895,896,5,92,0,0,896,208,1,0,0,0,897,898,5,94,0,0,898,210,1,0,0,0,
- 899,900,5,43,0,0,900,212,1,0,0,0,901,902,5,45,0,0,902,214,1,0,0,0,903,
- 904,5,38,0,0,904,216,1,0,0,0,905,906,5,61,0,0,906,218,1,0,0,0,907,908,
- 5,60,0,0,908,912,5,62,0,0,909,910,5,33,0,0,910,912,5,61,0,0,911,907,1,
- 0,0,0,911,909,1,0,0,0,912,220,1,0,0,0,913,914,5,60,0,0,914,915,5,61,0,
- 0,915,222,1,0,0,0,916,917,5,62,0,0,917,918,5,61,0,0,918,224,1,0,0,0,919,
- 920,5,60,0,0,920,226,1,0,0,0,921,922,5,62,0,0,922,228,1,0,0,0,923,924,
- 5,40,0,0,924,230,1,0,0,0,925,926,5,41,0,0,926,232,1,0,0,0,927,928,5,44,
- 0,0,928,234,1,0,0,0,929,930,5,46,0,0,930,236,1,0,0,0,931,932,5,59,0,0,
- 932,238,1,0,0,0,933,934,5,64,0,0,934,935,5,64,0,0,935,936,1,0,0,0,936,
- 940,7,25,0,0,937,939,7,26,0,0,938,937,1,0,0,0,939,942,1,0,0,0,940,938,
- 1,0,0,0,940,941,1,0,0,0,941,240,1,0,0,0,942,940,1,0,0,0,943,953,5,63,0,
- 0,944,945,5,64,0,0,945,949,7,25,0,0,946,948,7,26,0,0,947,946,1,0,0,0,948,
- 951,1,0,0,0,949,947,1,0,0,0,949,950,1,0,0,0,950,953,1,0,0,0,951,949,1,
- 0,0,0,952,943,1,0,0,0,952,944,1,0,0,0,953,242,1,0,0,0,954,955,5,48,0,0,
- 955,957,7,16,0,0,956,958,7,27,0,0,957,956,1,0,0,0,958,959,1,0,0,0,959,
- 957,1,0,0,0,959,960,1,0,0,0,960,244,1,0,0,0,961,963,7,28,0,0,962,961,1,
- 0,0,0,963,964,1,0,0,0,964,962,1,0,0,0,964,965,1,0,0,0,965,246,1,0,0,0,
- 966,968,7,28,0,0,967,966,1,0,0,0,968,969,1,0,0,0,969,967,1,0,0,0,969,970,
- 1,0,0,0,970,971,1,0,0,0,971,975,5,46,0,0,972,974,7,28,0,0,973,972,1,0,
- 0,0,974,977,1,0,0,0,975,973,1,0,0,0,975,976,1,0,0,0,976,979,1,0,0,0,977,
- 975,1,0,0,0,978,980,3,249,124,0,979,978,1,0,0,0,979,980,1,0,0,0,980,997,
- 1,0,0,0,981,983,5,46,0,0,982,984,7,28,0,0,983,982,1,0,0,0,984,985,1,0,
- 0,0,985,983,1,0,0,0,985,986,1,0,0,0,986,988,1,0,0,0,987,989,3,249,124,
- 0,988,987,1,0,0,0,988,989,1,0,0,0,989,997,1,0,0,0,990,992,7,28,0,0,991,
- 990,1,0,0,0,992,993,1,0,0,0,993,991,1,0,0,0,993,994,1,0,0,0,994,995,1,
- 0,0,0,995,997,3,249,124,0,996,967,1,0,0,0,996,981,1,0,0,0,996,991,1,0,
- 0,0,997,248,1,0,0,0,998,1000,7,1,0,0,999,1001,7,29,0,0,1000,999,1,0,0,
- 0,1000,1001,1,0,0,0,1001,1003,1,0,0,0,1002,1004,7,28,0,0,1003,1002,1,0,
- 0,0,1004,1005,1,0,0,0,1005,1003,1,0,0,0,1005,1006,1,0,0,0,1006,250,1,0,
- 0,0,1007,1013,5,34,0,0,1008,1012,8,30,0,0,1009,1010,5,34,0,0,1010,1012,
- 5,34,0,0,1011,1008,1,0,0,0,1011,1009,1,0,0,0,1012,1015,1,0,0,0,1013,1011,
- 1,0,0,0,1013,1014,1,0,0,0,1014,1016,1,0,0,0,1015,1013,1,0,0,0,1016,1028,
- 5,34,0,0,1017,1023,5,39,0,0,1018,1022,8,31,0,0,1019,1020,5,39,0,0,1020,
- 1022,5,39,0,0,1021,1018,1,0,0,0,1021,1019,1,0,0,0,1022,1025,1,0,0,0,1023,
- 1021,1,0,0,0,1023,1024,1,0,0,0,1024,1026,1,0,0,0,1025,1023,1,0,0,0,1026,
- 1028,5,39,0,0,1027,1007,1,0,0,0,1027,1017,1,0,0,0,1028,252,1,0,0,0,1029,
- 1033,5,35,0,0,1030,1032,8,32,0,0,1031,1030,1,0,0,0,1032,1035,1,0,0,0,1033,
- 1031,1,0,0,0,1033,1034,1,0,0,0,1034,1036,1,0,0,0,1035,1033,1,0,0,0,1036,
- 1037,5,35,0,0,1037,254,1,0,0,0,1038,1040,5,123,0,0,1039,1041,3,257,128,
- 0,1040,1039,1,0,0,0,1041,1042,1,0,0,0,1042,1040,1,0,0,0,1042,1043,1,0,
- 0,0,1043,1044,1,0,0,0,1044,1046,5,45,0,0,1045,1047,3,257,128,0,1046,1045,
- 1,0,0,0,1047,1048,1,0,0,0,1048,1046,1,0,0,0,1048,1049,1,0,0,0,1049,1050,
- 1,0,0,0,1050,1052,5,45,0,0,1051,1053,3,257,128,0,1052,1051,1,0,0,0,1053,
- 1054,1,0,0,0,1054,1052,1,0,0,0,1054,1055,1,0,0,0,1055,1056,1,0,0,0,1056,
- 1058,5,45,0,0,1057,1059,3,257,128,0,1058,1057,1,0,0,0,1059,1060,1,0,0,
- 0,1060,1058,1,0,0,0,1060,1061,1,0,0,0,1061,1062,1,0,0,0,1062,1064,5,45,
- 0,0,1063,1065,3,257,128,0,1064,1063,1,0,0,0,1065,1066,1,0,0,0,1066,1064,
- 1,0,0,0,1066,1067,1,0,0,0,1067,1068,1,0,0,0,1068,1069,5,125,0,0,1069,256,
- 1,0,0,0,1070,1071,7,27,0,0,1071,258,1,0,0,0,1072,1074,5,91,0,0,1073,1075,
- 8,33,0,0,1074,1073,1,0,0,0,1075,1076,1,0,0,0,1076,1074,1,0,0,0,1076,1077,
- 1,0,0,0,1077,1078,1,0,0,0,1078,1079,5,93,0,0,1079,260,1,0,0,0,1080,1082,
- 5,96,0,0,1081,1083,8,34,0,0,1082,1081,1,0,0,0,1083,1084,1,0,0,0,1084,1082,
- 1,0,0,0,1084,1085,1,0,0,0,1085,1086,1,0,0,0,1086,1087,5,96,0,0,1087,262,
- 1,0,0,0,1088,1092,7,25,0,0,1089,1091,7,26,0,0,1090,1089,1,0,0,0,1091,1094,
- 1,0,0,0,1092,1090,1,0,0,0,1092,1093,1,0,0,0,1093,1096,1,0,0,0,1094,1092,
- 1,0,0,0,1095,1097,5,36,0,0,1096,1095,1,0,0,0,1096,1097,1,0,0,0,1097,264,
- 1,0,0,0,1098,1100,7,35,0,0,1099,1098,1,0,0,0,1100,1101,1,0,0,0,1101,1099,
- 1,0,0,0,1101,1102,1,0,0,0,1102,1103,1,0,0,0,1103,1104,6,132,0,0,1104,266,
- 1,0,0,0,1105,1106,5,45,0,0,1106,1107,5,45,0,0,1107,1111,1,0,0,0,1108,1110,
- 8,36,0,0,1109,1108,1,0,0,0,1110,1113,1,0,0,0,1111,1109,1,0,0,0,1111,1112,
- 1,0,0,0,1112,1114,1,0,0,0,1113,1111,1,0,0,0,1114,1115,6,133,0,0,1115,268,
- 1,0,0,0,1116,1117,5,47,0,0,1117,1118,5,42,0,0,1118,1122,1,0,0,0,1119,1121,
- 9,0,0,0,1120,1119,1,0,0,0,1121,1124,1,0,0,0,1122,1123,1,0,0,0,1122,1120,
- 1,0,0,0,1123,1125,1,0,0,0,1124,1122,1,0,0,0,1125,1126,5,42,0,0,1126,1127,
- 5,47,0,0,1127,1128,1,0,0,0,1128,1129,6,134,0,0,1129,270,1,0,0,0,34,0,911,
- 940,949,952,959,964,969,975,979,985,988,993,996,1000,1005,1011,1013,1021,
- 1023,1027,1033,1042,1048,1054,1060,1066,1076,1084,1092,1096,1101,1111,
- 1122,1,6,0,0
+ 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,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,1100,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,1127,8,139,10,139,12,139,1130,9,139,1,140,
+ 1,140,1,140,1,140,5,140,1136,8,140,10,140,12,140,1139,9,140,3,140,1141,
+ 8,140,1,141,1,141,1,141,4,141,1146,8,141,11,141,12,141,1147,1,142,4,142,
+ 1151,8,142,11,142,12,142,1152,1,143,4,143,1156,8,143,11,143,12,143,1157,
+ 1,143,1,143,5,143,1162,8,143,10,143,12,143,1165,9,143,1,143,3,143,1168,
+ 8,143,1,143,1,143,4,143,1172,8,143,11,143,12,143,1173,1,143,3,143,1177,
+ 8,143,1,143,4,143,1180,8,143,11,143,12,143,1181,1,143,3,143,1185,8,143,
+ 1,144,1,144,3,144,1189,8,144,1,144,4,144,1192,8,144,11,144,12,144,1193,
+ 1,145,1,145,1,145,1,145,5,145,1200,8,145,10,145,12,145,1203,9,145,1,145,
+ 1,145,1,145,1,145,1,145,5,145,1210,8,145,10,145,12,145,1213,9,145,1,145,
+ 3,145,1216,8,145,1,146,1,146,5,146,1220,8,146,10,146,12,146,1223,9,146,
+ 1,146,1,146,1,147,1,147,4,147,1229,8,147,11,147,12,147,1230,1,147,1,147,
+ 4,147,1235,8,147,11,147,12,147,1236,1,147,1,147,4,147,1241,8,147,11,147,
+ 12,147,1242,1,147,1,147,4,147,1247,8,147,11,147,12,147,1248,1,147,1,147,
+ 4,147,1253,8,147,11,147,12,147,1254,1,147,1,147,1,148,1,148,1,149,1,149,
+ 4,149,1263,8,149,11,149,12,149,1264,1,149,1,149,1,150,1,150,4,150,1271,
+ 8,150,11,150,12,150,1272,1,150,1,150,1,151,1,151,5,151,1279,8,151,10,151,
+ 12,151,1282,9,151,1,151,3,151,1285,8,151,1,152,4,152,1288,8,152,11,152,
+ 12,152,1289,1,152,1,152,1,153,1,153,1,153,1,153,1,153,5,153,1299,8,153,
+ 10,153,12,153,1302,9,153,1,153,1,153,1,154,1,154,1,154,1,154,5,154,1310,
+ 8,154,10,154,12,154,1313,9,154,1,154,1,154,1,154,1,154,1,154,1,1311,0,
+ 155,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,148,301,149,303,150,305,151,307,152,
+ 309,153,1,0,37,2,0,83,83,115,115,2,0,69,69,101,101,2,0,76,76,108,108,2,
+ 0,67,67,99,99,2,0,84,84,116,116,2,0,70,70,102,102,2,0,82,82,114,114,2,
+ 0,79,79,111,111,2,0,77,77,109,109,2,0,87,87,119,119,2,0,72,72,104,104,
+ 2,0,80,80,112,112,2,0,65,65,97,97,2,0,78,78,110,110,2,0,68,68,100,100,
+ 2,0,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,1,0,93,93,1,0,96,96,3,0,9,10,13,13,32,32,2,0,10,
+ 10,13,13,1350,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,299,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,1,311,1,
+ 0,0,0,3,318,1,0,0,0,5,323,1,0,0,0,7,329,1,0,0,0,9,333,1,0,0,0,11,336,1,
+ 0,0,0,13,340,1,0,0,0,15,343,1,0,0,0,17,347,1,0,0,0,19,351,1,0,0,0,21,355,
+ 1,0,0,0,23,359,1,0,0,0,25,364,1,0,0,0,27,368,1,0,0,0,29,373,1,0,0,0,31,
+ 378,1,0,0,0,33,383,1,0,0,0,35,387,1,0,0,0,37,393,1,0,0,0,39,398,1,0,0,
+ 0,41,404,1,0,0,0,43,409,1,0,0,0,45,415,1,0,0,0,47,420,1,0,0,0,49,423,1,
+ 0,0,0,51,426,1,0,0,0,53,432,1,0,0,0,55,438,1,0,0,0,57,441,1,0,0,0,59,444,
+ 1,0,0,0,61,451,1,0,0,0,63,458,1,0,0,0,65,461,1,0,0,0,67,466,1,0,0,0,69,
+ 478,1,0,0,0,71,487,1,0,0,0,73,495,1,0,0,0,75,501,1,0,0,0,77,507,1,0,0,
+ 0,79,512,1,0,0,0,81,522,1,0,0,0,83,527,1,0,0,0,85,532,1,0,0,0,87,537,1,
+ 0,0,0,89,541,1,0,0,0,91,548,1,0,0,0,93,554,1,0,0,0,95,559,1,0,0,0,97,565,
+ 1,0,0,0,99,570,1,0,0,0,101,574,1,0,0,0,103,579,1,0,0,0,105,587,1,0,0,0,
+ 107,593,1,0,0,0,109,597,1,0,0,0,111,607,1,0,0,0,113,614,1,0,0,0,115,621,
+ 1,0,0,0,117,627,1,0,0,0,119,633,1,0,0,0,121,640,1,0,0,0,123,649,1,0,0,
+ 0,125,661,1,0,0,0,127,666,1,0,0,0,129,672,1,0,0,0,131,679,1,0,0,0,133,
+ 682,1,0,0,0,135,686,1,0,0,0,137,691,1,0,0,0,139,698,1,0,0,0,141,705,1,
+ 0,0,0,143,710,1,0,0,0,145,717,1,0,0,0,147,725,1,0,0,0,149,729,1,0,0,0,
+ 151,740,1,0,0,0,153,748,1,0,0,0,155,759,1,0,0,0,157,766,1,0,0,0,159,773,
+ 1,0,0,0,161,781,1,0,0,0,163,790,1,0,0,0,165,797,1,0,0,0,167,801,1,0,0,
+ 0,169,809,1,0,0,0,171,812,1,0,0,0,173,819,1,0,0,0,175,829,1,0,0,0,177,
+ 838,1,0,0,0,179,851,1,0,0,0,181,857,1,0,0,0,183,867,1,0,0,0,185,872,1,
+ 0,0,0,187,884,1,0,0,0,189,889,1,0,0,0,191,898,1,0,0,0,193,905,1,0,0,0,
+ 195,911,1,0,0,0,197,916,1,0,0,0,199,926,1,0,0,0,201,937,1,0,0,0,203,945,
+ 1,0,0,0,205,950,1,0,0,0,207,954,1,0,0,0,209,959,1,0,0,0,211,964,1,0,0,
+ 0,213,970,1,0,0,0,215,975,1,0,0,0,217,981,1,0,0,0,219,988,1,0,0,0,221,
+ 998,1,0,0,0,223,1008,1,0,0,0,225,1018,1,0,0,0,227,1026,1,0,0,0,229,1034,
+ 1,0,0,0,231,1039,1,0,0,0,233,1046,1,0,0,0,235,1053,1,0,0,0,237,1058,1,
+ 0,0,0,239,1066,1,0,0,0,241,1072,1,0,0,0,243,1079,1,0,0,0,245,1081,1,0,
+ 0,0,247,1083,1,0,0,0,249,1085,1,0,0,0,251,1087,1,0,0,0,253,1089,1,0,0,
+ 0,255,1091,1,0,0,0,257,1093,1,0,0,0,259,1099,1,0,0,0,261,1101,1,0,0,0,
+ 263,1104,1,0,0,0,265,1107,1,0,0,0,267,1109,1,0,0,0,269,1111,1,0,0,0,271,
+ 1113,1,0,0,0,273,1115,1,0,0,0,275,1117,1,0,0,0,277,1119,1,0,0,0,279,1121,
+ 1,0,0,0,281,1140,1,0,0,0,283,1142,1,0,0,0,285,1150,1,0,0,0,287,1184,1,
+ 0,0,0,289,1186,1,0,0,0,291,1215,1,0,0,0,293,1217,1,0,0,0,295,1226,1,0,
+ 0,0,297,1258,1,0,0,0,299,1260,1,0,0,0,301,1268,1,0,0,0,303,1276,1,0,0,
+ 0,305,1287,1,0,0,0,307,1293,1,0,0,0,309,1305,1,0,0,0,311,312,7,0,0,0,312,
+ 313,7,1,0,0,313,314,7,2,0,0,314,315,7,1,0,0,315,316,7,3,0,0,316,317,7,
+ 4,0,0,317,2,1,0,0,0,318,319,7,5,0,0,319,320,7,6,0,0,320,321,7,7,0,0,321,
+ 322,7,8,0,0,322,4,1,0,0,0,323,324,7,9,0,0,324,325,7,10,0,0,325,326,7,1,
+ 0,0,326,327,7,6,0,0,327,328,7,1,0,0,328,6,1,0,0,0,329,330,7,4,0,0,330,
+ 331,7,7,0,0,331,332,7,11,0,0,332,8,1,0,0,0,333,334,7,12,0,0,334,335,7,
+ 0,0,0,335,10,1,0,0,0,336,337,7,12,0,0,337,338,7,13,0,0,338,339,7,14,0,
+ 0,339,12,1,0,0,0,340,341,7,7,0,0,341,342,7,6,0,0,342,14,1,0,0,0,343,344,
+ 7,13,0,0,344,345,7,7,0,0,345,346,7,4,0,0,346,16,1,0,0,0,347,348,7,15,0,
+ 0,348,349,7,7,0,0,349,350,7,6,0,0,350,18,1,0,0,0,351,352,7,1,0,0,352,353,
+ 7,16,0,0,353,354,7,17,0,0,354,20,1,0,0,0,355,356,7,18,0,0,356,357,7,8,
+ 0,0,357,358,7,11,0,0,358,22,1,0,0,0,359,360,7,19,0,0,360,361,7,12,0,0,
+ 361,362,7,13,0,0,362,363,7,14,0,0,363,24,1,0,0,0,364,365,7,19,0,0,365,
+ 366,7,7,0,0,366,367,7,6,0,0,367,26,1,0,0,0,368,369,7,19,0,0,369,370,7,
+ 15,0,0,370,371,7,7,0,0,371,372,7,6,0,0,372,28,1,0,0,0,373,374,7,19,0,0,
+ 374,375,7,13,0,0,375,376,7,7,0,0,376,377,7,4,0,0,377,30,1,0,0,0,378,379,
+ 7,2,0,0,379,380,7,18,0,0,380,381,7,20,0,0,381,382,7,1,0,0,382,32,1,0,0,
+ 0,383,384,7,8,0,0,384,385,7,7,0,0,385,386,7,14,0,0,386,34,1,0,0,0,387,
+ 388,7,18,0,0,388,389,7,13,0,0,389,390,7,13,0,0,390,391,7,1,0,0,391,392,
+ 7,6,0,0,392,36,1,0,0,0,393,394,7,2,0,0,394,395,7,1,0,0,395,396,7,5,0,0,
+ 396,397,7,4,0,0,397,38,1,0,0,0,398,399,7,6,0,0,399,400,7,18,0,0,400,401,
+ 7,21,0,0,401,402,7,10,0,0,402,403,7,4,0,0,403,40,1,0,0,0,404,405,7,5,0,
+ 0,405,406,7,22,0,0,406,407,7,2,0,0,407,408,7,2,0,0,408,42,1,0,0,0,409,
+ 410,7,7,0,0,410,411,7,22,0,0,411,412,7,4,0,0,412,413,7,1,0,0,413,414,7,
+ 6,0,0,414,44,1,0,0,0,415,416,7,23,0,0,416,417,7,7,0,0,417,418,7,18,0,0,
+ 418,419,7,13,0,0,419,46,1,0,0,0,420,421,7,18,0,0,421,422,7,13,0,0,422,
+ 48,1,0,0,0,423,424,7,7,0,0,424,425,7,13,0,0,425,50,1,0,0,0,426,427,7,7,
+ 0,0,427,428,7,6,0,0,428,429,7,14,0,0,429,430,7,1,0,0,430,431,7,6,0,0,431,
+ 52,1,0,0,0,432,433,7,21,0,0,433,434,7,6,0,0,434,435,7,7,0,0,435,436,7,
+ 22,0,0,436,437,7,11,0,0,437,54,1,0,0,0,438,439,7,18,0,0,439,440,7,0,0,
+ 0,440,56,1,0,0,0,441,442,7,19,0,0,442,443,7,24,0,0,443,58,1,0,0,0,444,
+ 445,7,10,0,0,445,446,7,12,0,0,446,447,7,17,0,0,447,448,7,18,0,0,448,449,
+ 7,13,0,0,449,450,7,21,0,0,450,60,1,0,0,0,451,452,7,1,0,0,452,453,7,15,
+ 0,0,453,454,7,18,0,0,454,455,7,0,0,0,455,456,7,4,0,0,456,457,7,0,0,0,457,
+ 62,1,0,0,0,458,459,7,18,0,0,459,460,7,5,0,0,460,64,1,0,0,0,461,462,7,4,
+ 0,0,462,463,7,10,0,0,463,464,7,1,0,0,464,465,7,13,0,0,465,66,1,0,0,0,466,
+ 467,7,14,0,0,467,468,7,18,0,0,468,469,7,0,0,0,469,470,7,4,0,0,470,471,
+ 7,18,0,0,471,472,7,13,0,0,472,473,7,3,0,0,473,474,7,4,0,0,474,475,7,6,
+ 0,0,475,476,7,7,0,0,476,477,7,9,0,0,477,68,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,70,1,0,0,0,487,488,7,11,0,
+ 0,488,489,7,1,0,0,489,490,7,6,0,0,490,491,7,3,0,0,491,492,7,1,0,0,492,
+ 493,7,13,0,0,493,494,7,4,0,0,494,72,1,0,0,0,495,496,7,3,0,0,496,497,7,
+ 6,0,0,497,498,7,7,0,0,498,499,7,0,0,0,499,500,7,0,0,0,500,74,1,0,0,0,501,
+ 502,7,12,0,0,502,503,7,11,0,0,503,504,7,11,0,0,504,505,7,2,0,0,505,506,
+ 7,24,0,0,506,76,1,0,0,0,507,508,7,7,0,0,508,509,7,17,0,0,509,510,7,1,0,
+ 0,510,511,7,6,0,0,511,78,1,0,0,0,512,513,7,11,0,0,513,514,7,12,0,0,514,
+ 515,7,6,0,0,515,516,7,4,0,0,516,517,7,18,0,0,517,518,7,4,0,0,518,519,7,
+ 18,0,0,519,520,7,7,0,0,520,521,7,13,0,0,521,80,1,0,0,0,522,523,7,3,0,0,
+ 523,524,7,12,0,0,524,525,7,0,0,0,525,526,7,1,0,0,526,82,1,0,0,0,527,528,
+ 7,9,0,0,528,529,7,10,0,0,529,530,7,1,0,0,530,531,7,13,0,0,531,84,1,0,0,
+ 0,532,533,7,1,0,0,533,534,7,2,0,0,534,535,7,0,0,0,535,536,7,1,0,0,536,
+ 86,1,0,0,0,537,538,7,1,0,0,538,539,7,13,0,0,539,540,7,14,0,0,540,88,1,
+ 0,0,0,541,542,7,7,0,0,542,543,7,5,0,0,543,544,7,5,0,0,544,545,7,0,0,0,
+ 545,546,7,1,0,0,546,547,7,4,0,0,547,90,1,0,0,0,548,549,7,5,0,0,549,550,
+ 7,1,0,0,550,551,7,4,0,0,551,552,7,3,0,0,552,553,7,10,0,0,553,92,1,0,0,
+ 0,554,555,7,13,0,0,555,556,7,1,0,0,556,557,7,15,0,0,557,558,7,4,0,0,558,
+ 94,1,0,0,0,559,560,7,5,0,0,560,561,7,18,0,0,561,562,7,6,0,0,562,563,7,
+ 0,0,0,563,564,7,4,0,0,564,96,1,0,0,0,565,566,7,6,0,0,566,567,7,7,0,0,567,
+ 568,7,9,0,0,568,569,7,0,0,0,569,98,1,0,0,0,570,571,7,6,0,0,571,572,7,7,
+ 0,0,572,573,7,9,0,0,573,100,1,0,0,0,574,575,7,7,0,0,575,576,7,13,0,0,576,
+ 577,7,2,0,0,577,578,7,24,0,0,578,102,1,0,0,0,579,580,7,19,0,0,580,581,
+ 7,1,0,0,581,582,7,4,0,0,582,583,7,9,0,0,583,584,7,1,0,0,584,585,7,1,0,
+ 0,585,586,7,13,0,0,586,104,1,0,0,0,587,588,7,22,0,0,588,589,7,13,0,0,589,
+ 590,7,18,0,0,590,591,7,7,0,0,591,592,7,13,0,0,592,106,1,0,0,0,593,594,
+ 7,12,0,0,594,595,7,2,0,0,595,596,7,2,0,0,596,108,1,0,0,0,597,598,7,18,
+ 0,0,598,599,7,13,0,0,599,600,7,4,0,0,600,601,7,1,0,0,601,602,7,6,0,0,602,
+ 603,7,0,0,0,603,604,7,1,0,0,604,605,7,3,0,0,605,606,7,4,0,0,606,110,1,
+ 0,0,0,607,608,7,1,0,0,608,609,7,15,0,0,609,610,7,3,0,0,610,611,7,1,0,0,
+ 611,612,7,11,0,0,612,613,7,4,0,0,613,112,1,0,0,0,614,615,7,3,0,0,615,616,
+ 7,6,0,0,616,617,7,1,0,0,617,618,7,12,0,0,618,619,7,4,0,0,619,620,7,1,0,
+ 0,620,114,1,0,0,0,621,622,7,4,0,0,622,623,7,12,0,0,623,624,7,19,0,0,624,
+ 625,7,2,0,0,625,626,7,1,0,0,626,116,1,0,0,0,627,628,7,19,0,0,628,629,7,
+ 1,0,0,629,630,7,21,0,0,630,631,7,18,0,0,631,632,7,13,0,0,632,118,1,0,0,
+ 0,633,634,7,3,0,0,634,635,7,7,0,0,635,636,7,8,0,0,636,637,7,8,0,0,637,
+ 638,7,18,0,0,638,639,7,4,0,0,639,120,1,0,0,0,640,641,7,6,0,0,641,642,7,
+ 7,0,0,642,643,7,2,0,0,643,644,7,2,0,0,644,645,7,19,0,0,645,646,7,12,0,
+ 0,646,647,7,3,0,0,647,648,7,20,0,0,648,122,1,0,0,0,649,650,7,4,0,0,650,
+ 651,7,6,0,0,651,652,7,12,0,0,652,653,7,13,0,0,653,654,7,0,0,0,654,655,
+ 7,12,0,0,655,656,7,3,0,0,656,657,7,4,0,0,657,658,7,18,0,0,658,659,7,7,
+ 0,0,659,660,7,13,0,0,660,124,1,0,0,0,661,662,7,9,0,0,662,663,7,7,0,0,663,
+ 664,7,6,0,0,664,665,7,20,0,0,665,126,1,0,0,0,666,667,7,12,0,0,667,668,
+ 7,2,0,0,668,669,7,4,0,0,669,670,7,1,0,0,670,671,7,6,0,0,671,128,1,0,0,
+ 0,672,673,7,6,0,0,673,674,7,1,0,0,674,675,7,13,0,0,675,676,7,12,0,0,676,
+ 677,7,8,0,0,677,678,7,1,0,0,678,130,1,0,0,0,679,680,7,4,0,0,680,681,7,
+ 7,0,0,681,132,1,0,0,0,682,683,7,12,0,0,683,684,7,14,0,0,684,685,7,14,0,
+ 0,685,134,1,0,0,0,686,687,7,14,0,0,687,688,7,6,0,0,688,689,7,7,0,0,689,
+ 690,7,11,0,0,690,136,1,0,0,0,691,692,7,3,0,0,692,693,7,7,0,0,693,694,7,
+ 2,0,0,694,695,7,22,0,0,695,696,7,8,0,0,696,697,7,13,0,0,697,138,1,0,0,
+ 0,698,699,7,18,0,0,699,700,7,13,0,0,700,701,7,0,0,0,701,702,7,1,0,0,702,
+ 703,7,6,0,0,703,704,7,4,0,0,704,140,1,0,0,0,705,706,7,18,0,0,706,707,7,
+ 13,0,0,707,708,7,4,0,0,708,709,7,7,0,0,709,142,1,0,0,0,710,711,7,17,0,
+ 0,711,712,7,12,0,0,712,713,7,2,0,0,713,714,7,22,0,0,714,715,7,1,0,0,715,
+ 716,7,0,0,0,716,144,1,0,0,0,717,718,7,11,0,0,718,719,7,6,0,0,719,720,7,
+ 18,0,0,720,721,7,8,0,0,721,722,7,12,0,0,722,723,7,6,0,0,723,724,7,24,0,
+ 0,724,146,1,0,0,0,725,726,7,20,0,0,726,727,7,1,0,0,727,728,7,24,0,0,728,
+ 148,1,0,0,0,729,730,7,3,0,0,730,731,7,7,0,0,731,732,7,13,0,0,732,733,7,
+ 0,0,0,733,734,7,4,0,0,734,735,7,6,0,0,735,736,7,12,0,0,736,737,7,18,0,
+ 0,737,738,7,13,0,0,738,739,7,4,0,0,739,150,1,0,0,0,740,741,7,5,0,0,741,
+ 742,7,7,0,0,742,743,7,6,0,0,743,744,7,1,0,0,744,745,7,18,0,0,745,746,7,
+ 21,0,0,746,747,7,13,0,0,747,152,1,0,0,0,748,749,7,6,0,0,749,750,7,1,0,
+ 0,750,751,7,5,0,0,751,752,7,1,0,0,752,753,7,6,0,0,753,754,7,1,0,0,754,
+ 755,7,13,0,0,755,756,7,3,0,0,756,757,7,1,0,0,757,758,7,0,0,0,758,154,1,
+ 0,0,0,759,760,7,14,0,0,760,761,7,1,0,0,761,762,7,2,0,0,762,763,7,1,0,0,
+ 763,764,7,4,0,0,764,765,7,1,0,0,765,156,1,0,0,0,766,767,7,22,0,0,767,768,
+ 7,11,0,0,768,769,7,14,0,0,769,770,7,12,0,0,770,771,7,4,0,0,771,772,7,1,
+ 0,0,772,158,1,0,0,0,773,774,7,3,0,0,774,775,7,12,0,0,775,776,7,0,0,0,776,
+ 777,7,3,0,0,777,778,7,12,0,0,778,779,7,14,0,0,779,780,7,1,0,0,780,160,
+ 1,0,0,0,781,782,7,6,0,0,782,783,7,1,0,0,783,784,7,0,0,0,784,785,7,4,0,
+ 0,785,786,7,6,0,0,786,787,7,18,0,0,787,788,7,3,0,0,788,789,7,4,0,0,789,
+ 162,1,0,0,0,790,791,7,12,0,0,791,792,7,3,0,0,792,793,7,4,0,0,793,794,7,
+ 18,0,0,794,795,7,7,0,0,795,796,7,13,0,0,796,164,1,0,0,0,797,798,7,0,0,
+ 0,798,799,7,1,0,0,799,800,7,4,0,0,800,166,1,0,0,0,801,802,7,14,0,0,802,
+ 803,7,1,0,0,803,804,7,5,0,0,804,805,7,12,0,0,805,806,7,22,0,0,806,807,
+ 7,2,0,0,807,808,7,4,0,0,808,168,1,0,0,0,809,810,7,13,0,0,810,811,7,7,0,
+ 0,811,170,1,0,0,0,812,813,7,22,0,0,813,814,7,13,0,0,814,815,7,18,0,0,815,
+ 816,7,16,0,0,816,817,7,22,0,0,817,818,7,1,0,0,818,172,1,0,0,0,819,820,
+ 7,3,0,0,820,821,7,2,0,0,821,822,7,22,0,0,822,823,7,0,0,0,823,824,7,4,0,
+ 0,824,825,7,1,0,0,825,826,7,6,0,0,826,827,7,1,0,0,827,828,7,14,0,0,828,
+ 174,1,0,0,0,829,830,7,18,0,0,830,831,7,14,0,0,831,832,7,1,0,0,832,833,
+ 7,13,0,0,833,834,7,4,0,0,834,835,7,18,0,0,835,836,7,4,0,0,836,837,7,24,
+ 0,0,837,176,1,0,0,0,838,839,7,13,0,0,839,840,7,7,0,0,840,841,7,13,0,0,
+ 841,842,7,3,0,0,842,843,7,2,0,0,843,844,7,22,0,0,844,845,7,0,0,0,845,846,
+ 7,4,0,0,846,847,7,1,0,0,847,848,7,6,0,0,848,849,7,1,0,0,849,850,7,14,0,
+ 0,850,178,1,0,0,0,851,852,7,18,0,0,852,853,7,13,0,0,853,854,7,14,0,0,854,
+ 855,7,1,0,0,855,856,7,15,0,0,856,180,1,0,0,0,857,858,7,4,0,0,858,859,7,
+ 1,0,0,859,860,7,8,0,0,860,861,7,11,0,0,861,862,7,7,0,0,862,863,7,6,0,0,
+ 863,864,7,12,0,0,864,865,7,6,0,0,865,866,7,24,0,0,866,182,1,0,0,0,867,
+ 868,7,9,0,0,868,869,7,18,0,0,869,870,7,4,0,0,870,871,7,10,0,0,871,184,
+ 1,0,0,0,872,873,7,3,0,0,873,874,7,7,0,0,874,875,7,8,0,0,875,876,7,11,0,
+ 0,876,877,7,6,0,0,877,878,7,1,0,0,878,879,7,0,0,0,879,880,7,0,0,0,880,
+ 881,7,18,0,0,881,882,7,7,0,0,882,883,7,13,0,0,883,186,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,188,1,0,0,
+ 0,889,890,7,14,0,0,890,891,7,18,0,0,891,892,7,0,0,0,892,893,7,12,0,0,893,
+ 894,7,2,0,0,894,895,7,2,0,0,895,896,7,7,0,0,896,897,7,9,0,0,897,190,1,
+ 0,0,0,898,899,7,18,0,0,899,900,7,21,0,0,900,901,7,13,0,0,901,902,7,7,0,
+ 0,902,903,7,6,0,0,903,904,7,1,0,0,904,192,1,0,0,0,905,906,7,3,0,0,906,
+ 907,7,10,0,0,907,908,7,1,0,0,908,909,7,3,0,0,909,910,7,20,0,0,910,194,
+ 1,0,0,0,911,912,7,17,0,0,912,913,7,18,0,0,913,914,7,1,0,0,914,915,7,9,
+ 0,0,915,196,1,0,0,0,916,917,7,11,0,0,917,918,7,6,0,0,918,919,7,7,0,0,919,
+ 920,7,3,0,0,920,921,7,1,0,0,921,922,7,14,0,0,922,923,7,22,0,0,923,924,
+ 7,6,0,0,924,925,7,1,0,0,925,198,1,0,0,0,926,927,7,11,0,0,927,928,7,12,
+ 0,0,928,929,7,6,0,0,929,930,7,12,0,0,930,931,7,8,0,0,931,932,7,1,0,0,932,
+ 933,7,4,0,0,933,934,7,1,0,0,934,935,7,6,0,0,935,936,7,0,0,0,936,200,1,
+ 0,0,0,937,938,7,1,0,0,938,939,7,15,0,0,939,940,7,1,0,0,940,941,7,3,0,0,
+ 941,942,7,22,0,0,942,943,7,4,0,0,943,944,7,1,0,0,944,202,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,204,1,0,0,
+ 0,950,951,7,12,0,0,951,952,7,0,0,0,952,953,7,3,0,0,953,206,1,0,0,0,954,
+ 955,7,14,0,0,955,956,7,1,0,0,956,957,7,0,0,0,957,958,7,3,0,0,958,208,1,
+ 0,0,0,959,960,7,4,0,0,960,961,7,6,0,0,961,962,7,22,0,0,962,963,7,1,0,0,
+ 963,210,1,0,0,0,964,965,7,5,0,0,965,966,7,12,0,0,966,967,7,2,0,0,967,968,
+ 7,0,0,0,968,969,7,1,0,0,969,212,1,0,0,0,970,971,7,13,0,0,971,972,7,22,
+ 0,0,972,973,7,2,0,0,973,974,7,2,0,0,974,214,1,0,0,0,975,976,7,6,0,0,976,
+ 977,7,12,0,0,977,978,7,13,0,0,978,979,7,21,0,0,979,980,7,1,0,0,980,216,
+ 1,0,0,0,981,982,7,21,0,0,982,983,7,6,0,0,983,984,7,7,0,0,984,985,7,22,
+ 0,0,985,986,7,11,0,0,986,987,7,0,0,0,987,218,1,0,0,0,988,989,7,22,0,0,
+ 989,990,7,13,0,0,990,991,7,19,0,0,991,992,7,7,0,0,992,993,7,22,0,0,993,
+ 994,7,13,0,0,994,995,7,14,0,0,995,996,7,1,0,0,996,997,7,14,0,0,997,220,
+ 1,0,0,0,998,999,7,11,0,0,999,1000,7,6,0,0,1000,1001,7,1,0,0,1001,1002,
+ 7,3,0,0,1002,1003,7,1,0,0,1003,1004,7,14,0,0,1004,1005,7,18,0,0,1005,1006,
+ 7,13,0,0,1006,1007,7,21,0,0,1007,222,1,0,0,0,1008,1009,7,5,0,0,1009,1010,
+ 7,7,0,0,1010,1011,7,2,0,0,1011,1012,7,2,0,0,1012,1013,7,7,0,0,1013,1014,
+ 7,9,0,0,1014,1015,7,18,0,0,1015,1016,7,13,0,0,1016,1017,7,21,0,0,1017,
+ 224,1,0,0,0,1018,1019,7,3,0,0,1019,1020,7,22,0,0,1020,1021,7,6,0,0,1021,
+ 1022,7,6,0,0,1022,1023,7,1,0,0,1023,1024,7,13,0,0,1024,1025,7,4,0,0,1025,
+ 226,1,0,0,0,1026,1027,7,1,0,0,1027,1028,7,15,0,0,1028,1029,7,3,0,0,1029,
+ 1030,7,2,0,0,1030,1031,7,22,0,0,1031,1032,7,14,0,0,1032,1033,7,1,0,0,1033,
+ 228,1,0,0,0,1034,1035,7,4,0,0,1035,1036,7,18,0,0,1036,1037,7,1,0,0,1037,
+ 1038,7,0,0,0,1038,230,1,0,0,0,1039,1040,7,7,0,0,1040,1041,7,4,0,0,1041,
+ 1042,7,10,0,0,1042,1043,7,1,0,0,1043,1044,7,6,0,0,1044,1045,7,0,0,0,1045,
+ 232,1,0,0,0,1046,1047,7,9,0,0,1047,1048,7,18,0,0,1048,1049,7,4,0,0,1049,
+ 1050,7,10,0,0,1050,1051,7,18,0,0,1051,1052,7,13,0,0,1052,234,1,0,0,0,1053,
+ 1054,7,2,0,0,1054,1055,7,12,0,0,1055,1056,7,0,0,0,1056,1057,7,4,0,0,1057,
+ 236,1,0,0,0,1058,1059,7,6,0,0,1059,1060,7,1,0,0,1060,1061,7,0,0,0,1061,
+ 1062,7,11,0,0,1062,1063,7,1,0,0,1063,1064,7,3,0,0,1064,1065,7,4,0,0,1065,
+ 238,1,0,0,0,1066,1067,7,13,0,0,1067,1068,7,22,0,0,1068,1069,7,2,0,0,1069,
+ 1070,7,2,0,0,1070,1071,7,0,0,0,1071,240,1,0,0,0,1072,1073,7,5,0,0,1073,
+ 1074,7,18,0,0,1074,1075,7,2,0,0,1075,1076,7,4,0,0,1076,1077,7,1,0,0,1077,
+ 1078,7,6,0,0,1078,242,1,0,0,0,1079,1080,5,42,0,0,1080,244,1,0,0,0,1081,
+ 1082,5,47,0,0,1082,246,1,0,0,0,1083,1084,5,92,0,0,1084,248,1,0,0,0,1085,
+ 1086,5,94,0,0,1086,250,1,0,0,0,1087,1088,5,43,0,0,1088,252,1,0,0,0,1089,
+ 1090,5,45,0,0,1090,254,1,0,0,0,1091,1092,5,38,0,0,1092,256,1,0,0,0,1093,
+ 1094,5,61,0,0,1094,258,1,0,0,0,1095,1096,5,60,0,0,1096,1100,5,62,0,0,1097,
+ 1098,5,33,0,0,1098,1100,5,61,0,0,1099,1095,1,0,0,0,1099,1097,1,0,0,0,1100,
+ 260,1,0,0,0,1101,1102,5,60,0,0,1102,1103,5,61,0,0,1103,262,1,0,0,0,1104,
+ 1105,5,62,0,0,1105,1106,5,61,0,0,1106,264,1,0,0,0,1107,1108,5,60,0,0,1108,
+ 266,1,0,0,0,1109,1110,5,62,0,0,1110,268,1,0,0,0,1111,1112,5,40,0,0,1112,
+ 270,1,0,0,0,1113,1114,5,41,0,0,1114,272,1,0,0,0,1115,1116,5,44,0,0,1116,
+ 274,1,0,0,0,1117,1118,5,46,0,0,1118,276,1,0,0,0,1119,1120,5,59,0,0,1120,
+ 278,1,0,0,0,1121,1122,5,64,0,0,1122,1123,5,64,0,0,1123,1124,1,0,0,0,1124,
+ 1128,7,25,0,0,1125,1127,7,26,0,0,1126,1125,1,0,0,0,1127,1130,1,0,0,0,1128,
+ 1126,1,0,0,0,1128,1129,1,0,0,0,1129,280,1,0,0,0,1130,1128,1,0,0,0,1131,
+ 1141,5,63,0,0,1132,1133,5,64,0,0,1133,1137,7,25,0,0,1134,1136,7,26,0,0,
+ 1135,1134,1,0,0,0,1136,1139,1,0,0,0,1137,1135,1,0,0,0,1137,1138,1,0,0,
+ 0,1138,1141,1,0,0,0,1139,1137,1,0,0,0,1140,1131,1,0,0,0,1140,1132,1,0,
+ 0,0,1141,282,1,0,0,0,1142,1143,5,48,0,0,1143,1145,7,15,0,0,1144,1146,7,
+ 27,0,0,1145,1144,1,0,0,0,1146,1147,1,0,0,0,1147,1145,1,0,0,0,1147,1148,
+ 1,0,0,0,1148,284,1,0,0,0,1149,1151,7,28,0,0,1150,1149,1,0,0,0,1151,1152,
+ 1,0,0,0,1152,1150,1,0,0,0,1152,1153,1,0,0,0,1153,286,1,0,0,0,1154,1156,
+ 7,28,0,0,1155,1154,1,0,0,0,1156,1157,1,0,0,0,1157,1155,1,0,0,0,1157,1158,
+ 1,0,0,0,1158,1159,1,0,0,0,1159,1163,5,46,0,0,1160,1162,7,28,0,0,1161,1160,
+ 1,0,0,0,1162,1165,1,0,0,0,1163,1161,1,0,0,0,1163,1164,1,0,0,0,1164,1167,
+ 1,0,0,0,1165,1163,1,0,0,0,1166,1168,3,289,144,0,1167,1166,1,0,0,0,1167,
+ 1168,1,0,0,0,1168,1185,1,0,0,0,1169,1171,5,46,0,0,1170,1172,7,28,0,0,1171,
+ 1170,1,0,0,0,1172,1173,1,0,0,0,1173,1171,1,0,0,0,1173,1174,1,0,0,0,1174,
+ 1176,1,0,0,0,1175,1177,3,289,144,0,1176,1175,1,0,0,0,1176,1177,1,0,0,0,
+ 1177,1185,1,0,0,0,1178,1180,7,28,0,0,1179,1178,1,0,0,0,1180,1181,1,0,0,
+ 0,1181,1179,1,0,0,0,1181,1182,1,0,0,0,1182,1183,1,0,0,0,1183,1185,3,289,
+ 144,0,1184,1155,1,0,0,0,1184,1169,1,0,0,0,1184,1179,1,0,0,0,1185,288,1,
+ 0,0,0,1186,1188,7,1,0,0,1187,1189,7,29,0,0,1188,1187,1,0,0,0,1188,1189,
+ 1,0,0,0,1189,1191,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,290,1,0,0,0,1195,1201,
+ 5,34,0,0,1196,1200,8,30,0,0,1197,1198,5,34,0,0,1198,1200,5,34,0,0,1199,
+ 1196,1,0,0,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,1204,1,0,0,0,1203,1201,1,0,0,0,1204,1216,5,34,0,0,1205,
+ 1211,5,39,0,0,1206,1210,8,31,0,0,1207,1208,5,39,0,0,1208,1210,5,39,0,0,
+ 1209,1206,1,0,0,0,1209,1207,1,0,0,0,1210,1213,1,0,0,0,1211,1209,1,0,0,
+ 0,1211,1212,1,0,0,0,1212,1214,1,0,0,0,1213,1211,1,0,0,0,1214,1216,5,39,
+ 0,0,1215,1195,1,0,0,0,1215,1205,1,0,0,0,1216,292,1,0,0,0,1217,1221,5,35,
+ 0,0,1218,1220,8,32,0,0,1219,1218,1,0,0,0,1220,1223,1,0,0,0,1221,1219,1,
+ 0,0,0,1221,1222,1,0,0,0,1222,1224,1,0,0,0,1223,1221,1,0,0,0,1224,1225,
+ 5,35,0,0,1225,294,1,0,0,0,1226,1228,5,123,0,0,1227,1229,3,297,148,0,1228,
+ 1227,1,0,0,0,1229,1230,1,0,0,0,1230,1228,1,0,0,0,1230,1231,1,0,0,0,1231,
+ 1232,1,0,0,0,1232,1234,5,45,0,0,1233,1235,3,297,148,0,1234,1233,1,0,0,
+ 0,1235,1236,1,0,0,0,1236,1234,1,0,0,0,1236,1237,1,0,0,0,1237,1238,1,0,
+ 0,0,1238,1240,5,45,0,0,1239,1241,3,297,148,0,1240,1239,1,0,0,0,1241,1242,
+ 1,0,0,0,1242,1240,1,0,0,0,1242,1243,1,0,0,0,1243,1244,1,0,0,0,1244,1246,
+ 5,45,0,0,1245,1247,3,297,148,0,1246,1245,1,0,0,0,1247,1248,1,0,0,0,1248,
+ 1246,1,0,0,0,1248,1249,1,0,0,0,1249,1250,1,0,0,0,1250,1252,5,45,0,0,1251,
+ 1253,3,297,148,0,1252,1251,1,0,0,0,1253,1254,1,0,0,0,1254,1252,1,0,0,0,
+ 1254,1255,1,0,0,0,1255,1256,1,0,0,0,1256,1257,5,125,0,0,1257,296,1,0,0,
+ 0,1258,1259,7,27,0,0,1259,298,1,0,0,0,1260,1262,5,91,0,0,1261,1263,8,33,
+ 0,0,1262,1261,1,0,0,0,1263,1264,1,0,0,0,1264,1262,1,0,0,0,1264,1265,1,
+ 0,0,0,1265,1266,1,0,0,0,1266,1267,5,93,0,0,1267,300,1,0,0,0,1268,1270,
+ 5,96,0,0,1269,1271,8,34,0,0,1270,1269,1,0,0,0,1271,1272,1,0,0,0,1272,1270,
+ 1,0,0,0,1272,1273,1,0,0,0,1273,1274,1,0,0,0,1274,1275,5,96,0,0,1275,302,
+ 1,0,0,0,1276,1280,7,25,0,0,1277,1279,7,26,0,0,1278,1277,1,0,0,0,1279,1282,
+ 1,0,0,0,1280,1278,1,0,0,0,1280,1281,1,0,0,0,1281,1284,1,0,0,0,1282,1280,
+ 1,0,0,0,1283,1285,5,36,0,0,1284,1283,1,0,0,0,1284,1285,1,0,0,0,1285,304,
+ 1,0,0,0,1286,1288,7,35,0,0,1287,1286,1,0,0,0,1288,1289,1,0,0,0,1289,1287,
+ 1,0,0,0,1289,1290,1,0,0,0,1290,1291,1,0,0,0,1291,1292,6,152,0,0,1292,306,
+ 1,0,0,0,1293,1294,5,45,0,0,1294,1295,5,45,0,0,1295,1296,1,0,0,0,1296,1300,
+ 4,153,0,0,1297,1299,8,36,0,0,1298,1297,1,0,0,0,1299,1302,1,0,0,0,1300,
+ 1298,1,0,0,0,1300,1301,1,0,0,0,1301,1303,1,0,0,0,1302,1300,1,0,0,0,1303,
+ 1304,6,153,0,0,1304,308,1,0,0,0,1305,1306,5,47,0,0,1306,1307,5,42,0,0,
+ 1307,1311,1,0,0,0,1308,1310,9,0,0,0,1309,1308,1,0,0,0,1310,1313,1,0,0,
+ 0,1311,1312,1,0,0,0,1311,1309,1,0,0,0,1312,1314,1,0,0,0,1313,1311,1,0,
+ 0,0,1314,1315,5,42,0,0,1315,1316,5,47,0,0,1316,1317,1,0,0,0,1317,1318,
+ 6,154,0,0,1318,310,1,0,0,0,34,0,1099,1128,1137,1140,1147,1152,1157,1163,
+ 1167,1173,1176,1181,1184,1188,1193,1199,1201,1209,1211,1215,1221,1230,
+ 1236,1242,1248,1254,1264,1272,1280,1284,1289,1300,1311,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 51833008e..d70f31e88 100644
--- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs
+++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs
@@ -8,7 +8,7 @@
//
//------------------------------------------------------------------------------
-// Generated from AccessSql.g4 by ANTLR 4.13.1
+// Generated from D:/toolkits/efcorejetlibred/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 by ANTLR 4.13.1
// Unreachable code detected
#pragma warning disable 0162
@@ -37,25 +37,29 @@ public partial class AccessSqlParser : Parser {
protected static DFA[] decisionToDFA;
protected static PredictionContextCache sharedContextCache = new PredictionContextCache();
public const int
- SELECT=1, FROM=2, WHERE=3, TOP=4, AS=5, AND=6, OR=7, NOT=8, BAND=9, BOR=10,
- BXOR=11, BNOT=12, LIKE=13, MOD=14, INNER=15, LEFT=16, RIGHT=17, FULL=18,
- OUTER=19, JOIN=20, IN=21, ON=22, ORDER=23, GROUP=24, IS=25, BY=26, HAVING=27,
- EXISTS=28, IF=29, THEN=30, DISTINCTROW=31, DISTINCT=32, PERCENT=33, CROSS=34,
- APPLY=35, OVER=36, PARTITION=37, CASE=38, WHEN=39, ELSE=40, END=41, OFFSET=42,
- FETCH=43, NEXT=44, FIRST=45, ROWS=46, ROW=47, ONLY=48, BETWEEN=49, UNION=50,
- ALL=51, INTERSECT=52, EXCEPT=53, CREATE=54, TABLE=55, BEGIN=56, COMMIT=57,
- ROLLBACK=58, TRANSACTION=59, WORK=60, ALTER=61, RENAME=62, TO=63, ADD=64,
- DROP=65, COLUMN=66, INSERT=67, INTO=68, VALUES=69, PRIMARY=70, KEY=71,
- CONSTRAINT=72, FOREIGN=73, REFERENCES=74, DELETE=75, UPDATE=76, CASCADE=77,
- RESTRICT=78, ACTION=79, SET=80, DEFAULT=81, NO=82, UNIQUE=83, INDEX=84,
- TEMPORARY=85, WITH=86, COMPRESSION=87, COMP=88, DISALLOW=89, IGNORE=90,
- CHECK=91, VIEW=92, PROCEDURE=93, PARAMETERS=94, EXECUTE=95, EXEC=96, ASC=97,
- DESC=98, TRUE=99, FALSE=100, NULL=101, STAR=102, SLASH=103, BACKSLASH=104,
- CARET=105, PLUS=106, MINUS=107, AMP=108, EQ=109, NEQ=110, LTE=111, GTE=112,
- LT=113, GT=114, LPAREN=115, RPAREN=116, COMMA=117, DOT=118, SEMI=119,
- SYSVAR=120, PARAM=121, HEX_LITERAL=122, INTEGER_LITERAL=123, NUMBER_LITERAL=124,
- STRING_LITERAL=125, DATE_LITERAL=126, GUID_LITERAL=127, BRACKET_ID=128,
- BACKTICK_ID=129, IDENTIFIER=130, WS=131, LINE_COMMENT=132, BLOCK_COMMENT=133;
+ SELECT=1, FROM=2, WHERE=3, TOP=4, AS=5, AND=6, OR=7, NOT=8, XOR=9, EQV=10,
+ IMP=11, BAND=12, BOR=13, BXOR=14, BNOT=15, LIKE=16, MOD=17, INNER=18,
+ LEFT=19, RIGHT=20, FULL=21, OUTER=22, JOIN=23, IN=24, ON=25, ORDER=26,
+ GROUP=27, IS=28, BY=29, HAVING=30, EXISTS=31, IF=32, THEN=33, DISTINCTROW=34,
+ DISTINCT=35, PERCENT=36, CROSS=37, APPLY=38, OVER=39, PARTITION=40, CASE=41,
+ WHEN=42, ELSE=43, END=44, OFFSET=45, FETCH=46, NEXT=47, FIRST=48, ROWS=49,
+ ROW=50, ONLY=51, BETWEEN=52, UNION=53, ALL=54, INTERSECT=55, EXCEPT=56,
+ CREATE=57, TABLE=58, BEGIN=59, COMMIT=60, ROLLBACK=61, TRANSACTION=62,
+ WORK=63, ALTER=64, RENAME=65, TO=66, ADD=67, DROP=68, COLUMN=69, INSERT=70,
+ INTO=71, VALUES=72, PRIMARY=73, KEY=74, CONSTRAINT=75, FOREIGN=76, REFERENCES=77,
+ DELETE=78, UPDATE=79, CASCADE=80, RESTRICT=81, ACTION=82, SET=83, DEFAULT=84,
+ 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;
public const int
RULE_statement = 0, RULE_ifThenStatement = 1, RULE_thenBody = 2, RULE_executeStatement = 3,
RULE_updateStatement = 4, RULE_assignment = 5, RULE_deleteStatement = 6,
@@ -77,7 +81,9 @@ public const int
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_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;
public static readonly string[] ruleNames = {
"statement", "ifThenStatement", "thenBody", "executeStatement", "updateStatement",
"assignment", "deleteStatement", "systemVariableSelect", "sysVarItem",
@@ -93,7 +99,9 @@ public const int
"joinClause", "joinType", "whereClause", "orderByClause", "orderByItem",
"expression", "primary", "caseExpression", "caseWhen", "functionCall",
"functionName", "columnRef", "identifier", "literal", "transactionStatement",
- "standaloneExpression", "windowSpecification"
+ "standaloneExpression", "windowSpecification", "clusteredOption", "windowFrame",
+ "frameBound", "frameExclusion", "nonReservedKeyword", "filterClause",
+ "withinGroup", "nthRowFrom", "nullTreatment"
};
private static readonly string[] _LiteralNames = {
@@ -105,26 +113,30 @@ 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, null, null, null, null, null, null, null,
+ null, null, null, null, null, null, null, null, null, null, null, null,
+ null, null, "'*'", "'/'", "'\\'", "'^'", "'+'", "'-'", "'&'", "'='", null,
+ "'<='", "'>='", "'<'", "'>'", "'('", "')'", "','", "'.'", "';'"
};
private static readonly string[] _SymbolicNames = {
- null, "SELECT", "FROM", "WHERE", "TOP", "AS", "AND", "OR", "NOT", "BAND",
- "BOR", "BXOR", "BNOT", "LIKE", "MOD", "INNER", "LEFT", "RIGHT", "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",
+ null, "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", "STAR", "SLASH", "BACKSLASH", "CARET",
- "PLUS", "MINUS", "AMP", "EQ", "NEQ", "LTE", "GTE", "LT", "GT", "LPAREN",
- "RPAREN", "COMMA", "DOT", "SEMI", "SYSVAR", "PARAM", "HEX_LITERAL", "INTEGER_LITERAL",
+ "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"
};
@@ -152,6 +164,18 @@ static AccessSqlParser() {
}
}
+
+ // The precedence the generated parser gives a BETWEEN's upper bound: that of the comparisons, one above BETWEEN.
+ private const int BetweenBoundPrecedence = 13;
+
+ // ANTLR parses the middle operand of an alternative at precedence 0 and has no syntax to raise it, which lets a
+ // BETWEEN's lower bound swallow the AND that ends it: `x BETWEEN 1 AND 10 AND y = 2` would read as
+ // `x BETWEEN (1 AND 10) AND (y = 2)`. The lower bound is the only operand parsed while its BETWEEN has no lower
+ // bound yet, so it is given the upper bound's precedence here.
+ public override void EnterRecursionRule(ParserRuleContext localctx, int state, int ruleIndex, int precedence) =>
+ base.EnterRecursionRule(localctx, state, ruleIndex,
+ localctx.Parent is BetweenExprContext { lo: null } ? Math.Max(precedence, BetweenBoundPrecedence) : precedence);
+
public AccessSqlParser(ITokenStream input) : this(input, Console.Out, Console.Error) { }
public AccessSqlParser(ITokenStream input, TextWriter output, TextWriter errorOutput)
@@ -229,115 +253,115 @@ public StatementContext statement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 137;
+ State = 155;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==PARAMETERS) {
{
- State = 136;
+ State = 154;
parametersClause();
}
}
- State = 153;
+ State = 171;
ErrorHandler.Sync(this);
switch ( Interpreter.AdaptivePredict(TokenStream,1,Context) ) {
case 1:
{
- State = 139;
+ State = 157;
ifThenStatement();
}
break;
case 2:
{
- State = 140;
+ State = 158;
createTableStatement();
}
break;
case 3:
{
- State = 141;
+ State = 159;
createIndexStatement();
}
break;
case 4:
{
- State = 142;
+ State = 160;
createViewStatement();
}
break;
case 5:
{
- State = 143;
+ State = 161;
createProcedureStatement();
}
break;
case 6:
{
- State = 144;
+ State = 162;
alterTableStatement();
}
break;
case 7:
{
- State = 145;
+ State = 163;
dropStatement();
}
break;
case 8:
{
- State = 146;
+ State = 164;
insertStatement();
}
break;
case 9:
{
- State = 147;
+ State = 165;
updateStatement();
}
break;
case 10:
{
- State = 148;
+ State = 166;
deleteStatement();
}
break;
case 11:
{
- State = 149;
+ State = 167;
transactionStatement();
}
break;
case 12:
{
- State = 150;
+ State = 168;
executeStatement();
}
break;
case 13:
{
- State = 151;
+ State = 169;
systemVariableSelect();
}
break;
case 14:
{
- State = 152;
+ State = 170;
queryExpression();
}
break;
}
- State = 156;
+ State = 174;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==SEMI) {
{
- State = 155;
+ State = 173;
Match(SEMI);
}
}
- State = 158;
+ State = 176;
Match(Eof);
}
}
@@ -387,29 +411,29 @@ public IfThenStatementContext ifThenStatement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 160;
+ State = 178;
Match(IF);
- State = 162;
+ State = 180;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==NOT) {
{
- State = 161;
+ State = 179;
_localctx.not = Match(NOT);
}
}
- State = 164;
+ State = 182;
Match(EXISTS);
- State = 165;
+ State = 183;
Match(LPAREN);
- State = 166;
+ State = 184;
queryExpression();
- State = 167;
+ State = 185;
Match(RPAREN);
- State = 168;
+ State = 186;
Match(THEN);
- State = 169;
+ State = 187;
thenBody();
}
}
@@ -476,83 +500,83 @@ public ThenBodyContext thenBody() {
ThenBodyContext _localctx = new ThenBodyContext(Context, State);
EnterRule(_localctx, 4, RULE_thenBody);
try {
- State = 182;
+ State = 200;
ErrorHandler.Sync(this);
switch ( Interpreter.AdaptivePredict(TokenStream,4,Context) ) {
case 1:
EnterOuterAlt(_localctx, 1);
{
- State = 171;
+ State = 189;
createTableStatement();
}
break;
case 2:
EnterOuterAlt(_localctx, 2);
{
- State = 172;
+ State = 190;
createIndexStatement();
}
break;
case 3:
EnterOuterAlt(_localctx, 3);
{
- State = 173;
+ State = 191;
createViewStatement();
}
break;
case 4:
EnterOuterAlt(_localctx, 4);
{
- State = 174;
+ State = 192;
createProcedureStatement();
}
break;
case 5:
EnterOuterAlt(_localctx, 5);
{
- State = 175;
+ State = 193;
alterTableStatement();
}
break;
case 6:
EnterOuterAlt(_localctx, 6);
{
- State = 176;
+ State = 194;
dropStatement();
}
break;
case 7:
EnterOuterAlt(_localctx, 7);
{
- State = 177;
+ State = 195;
insertStatement();
}
break;
case 8:
EnterOuterAlt(_localctx, 8);
{
- State = 178;
+ State = 196;
updateStatement();
}
break;
case 9:
EnterOuterAlt(_localctx, 9);
{
- State = 179;
+ State = 197;
deleteStatement();
}
break;
case 10:
EnterOuterAlt(_localctx, 10);
{
- State = 180;
+ State = 198;
executeStatement();
}
break;
case 11:
EnterOuterAlt(_localctx, 11);
{
- State = 181;
+ State = 199;
queryExpression();
}
break;
@@ -607,7 +631,7 @@ public ExecuteStatementContext executeStatement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 184;
+ State = 202;
_la = TokenStream.LA(1);
if ( !(_la==EXECUTE || _la==EXEC) ) {
ErrorHandler.RecoverInline(this);
@@ -616,28 +640,28 @@ public ExecuteStatementContext executeStatement() {
ErrorHandler.ReportMatch(this);
Consume();
}
- State = 185;
+ State = 203;
_localctx.name = identifier();
- State = 194;
+ State = 212;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 35596957585664L) != 0) || ((((_la - 97)) & ~0x3f) == 0 && ((1L << (_la - 97)) & 17171743773L) != 0)) {
+ if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775660683520L) != 0) || ((((_la - 103)) & ~0x3f) == 0 && ((1L << (_la - 103)) & 281341858414589L) != 0)) {
{
- State = 186;
+ State = 204;
expression(0);
- State = 191;
+ State = 209;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 187;
+ State = 205;
Match(COMMA);
- State = 188;
+ State = 206;
expression(0);
}
}
- State = 193;
+ State = 211;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -659,8 +683,11 @@ public ExecuteStatementContext executeStatement() {
public partial class UpdateStatementContext : ParserRuleContext {
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode UPDATE() { return GetToken(AccessSqlParser.UPDATE, 0); }
- [System.Diagnostics.DebuggerNonUserCode] public TableSourceContext tableSource() {
- return GetRuleContext(0);
+ [System.Diagnostics.DebuggerNonUserCode] public TableSourceContext[] tableSource() {
+ return GetRuleContexts();
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public TableSourceContext tableSource(int i) {
+ return GetRuleContext(i);
}
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode SET() { return GetToken(AccessSqlParser.SET, 0); }
[System.Diagnostics.DebuggerNonUserCode] public AssignmentContext[] assignment() {
@@ -697,36 +724,52 @@ public UpdateStatementContext updateStatement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 196;
+ State = 214;
Match(UPDATE);
- State = 197;
+ State = 215;
tableSource();
- State = 198;
+ State = 220;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ while (_la==COMMA) {
+ {
+ {
+ State = 216;
+ Match(COMMA);
+ State = 217;
+ tableSource();
+ }
+ }
+ State = 222;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ }
+ State = 223;
Match(SET);
- State = 199;
+ State = 224;
assignment();
- State = 204;
+ State = 229;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 200;
+ State = 225;
Match(COMMA);
- State = 201;
+ State = 226;
assignment();
}
}
- State = 206;
+ State = 231;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 208;
+ State = 233;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==WHERE) {
{
- State = 207;
+ State = 232;
whereClause();
}
}
@@ -773,11 +816,11 @@ public AssignmentContext assignment() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 210;
+ State = 235;
_localctx.target = columnRef();
- State = 211;
+ State = 236;
Match(EQ);
- State = 212;
+ State = 237;
expression(0);
}
}
@@ -796,11 +839,18 @@ public partial class DeleteStatementContext : ParserRuleContext {
public IdentifierContext target;
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DELETE() { return GetToken(AccessSqlParser.DELETE, 0); }
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode FROM() { return GetToken(AccessSqlParser.FROM, 0); }
- [System.Diagnostics.DebuggerNonUserCode] public TableSourceContext tableSource() {
- return GetRuleContext(0);
+ [System.Diagnostics.DebuggerNonUserCode] public TableSourceContext[] tableSource() {
+ return GetRuleContexts();
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public TableSourceContext tableSource(int i) {
+ return GetRuleContext(i);
}
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode DOT() { return GetToken(AccessSqlParser.DOT, 0); }
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode STAR() { return GetToken(AccessSqlParser.STAR, 0); }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode[] COMMA() { return GetTokens(AccessSqlParser.COMMA); }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMMA(int i) {
+ return GetToken(AccessSqlParser.COMMA, i);
+ }
[System.Diagnostics.DebuggerNonUserCode] public WhereClauseContext whereClause() {
return GetRuleContext(0);
}
@@ -828,26 +878,40 @@ public DeleteStatementContext deleteStatement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 214;
+ State = 239;
Match(DELETE);
- State = 220;
+ State = 245;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
+ 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:
{
- State = 215;
+ State = 240;
_localctx.target = identifier();
- State = 216;
+ State = 241;
Match(DOT);
- State = 217;
+ State = 242;
Match(STAR);
}
break;
case STAR:
{
- State = 219;
+ State = 244;
Match(STAR);
}
break;
@@ -856,16 +920,32 @@ public DeleteStatementContext deleteStatement() {
default:
break;
}
- State = 222;
+ State = 247;
Match(FROM);
- State = 223;
+ State = 248;
tableSource();
- State = 225;
+ State = 253;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ while (_la==COMMA) {
+ {
+ {
+ State = 249;
+ Match(COMMA);
+ State = 250;
+ tableSource();
+ }
+ }
+ State = 255;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ }
+ State = 257;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==WHERE) {
{
- State = 224;
+ State = 256;
whereClause();
}
}
@@ -916,23 +996,23 @@ public SystemVariableSelectContext systemVariableSelect() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 227;
+ State = 259;
Match(SELECT);
- State = 228;
+ State = 260;
sysVarItem();
- State = 233;
+ State = 265;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 229;
+ State = 261;
Match(COMMA);
- State = 230;
+ State = 262;
sysVarItem();
}
}
- State = 235;
+ State = 267;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -977,24 +1057,24 @@ public SysVarItemContext sysVarItem() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 236;
+ State = 268;
Match(SYSVAR);
- State = 241;
+ State = 273;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- if (_la==AS || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 7L) != 0)) {
+ if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) {
{
- State = 238;
+ State = 270;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==AS) {
{
- State = 237;
+ State = 269;
Match(AS);
}
}
- State = 240;
+ State = 272;
_localctx.alias = identifier();
}
}
@@ -1046,27 +1126,27 @@ public ParametersClauseContext parametersClause() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 243;
+ State = 275;
Match(PARAMETERS);
- State = 244;
+ State = 276;
procParam();
- State = 249;
+ State = 281;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 245;
+ State = 277;
Match(COMMA);
- State = 246;
+ State = 278;
procParam();
}
}
- State = 251;
+ State = 283;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 252;
+ State = 284;
Match(SEMI);
}
}
@@ -1130,61 +1210,61 @@ public CreateTableStatementContext createTableStatement() {
int _alt;
EnterOuterAlt(_localctx, 1);
{
- State = 254;
+ State = 286;
Match(CREATE);
- State = 256;
+ State = 288;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==TEMPORARY) {
{
- State = 255;
+ State = 287;
_localctx.temp = Match(TEMPORARY);
}
}
- State = 258;
+ State = 290;
Match(TABLE);
- State = 259;
+ State = 291;
_localctx.table = identifier();
- State = 260;
+ State = 292;
Match(LPAREN);
- State = 261;
+ State = 293;
columnDefinition();
- State = 266;
+ State = 298;
ErrorHandler.Sync(this);
- _alt = Interpreter.AdaptivePredict(TokenStream,16,Context);
+ _alt = Interpreter.AdaptivePredict(TokenStream,18,Context);
while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) {
if ( _alt==1 ) {
{
{
- State = 262;
+ State = 294;
Match(COMMA);
- State = 263;
+ State = 295;
columnDefinition();
}
}
}
- State = 268;
+ State = 300;
ErrorHandler.Sync(this);
- _alt = Interpreter.AdaptivePredict(TokenStream,16,Context);
+ _alt = Interpreter.AdaptivePredict(TokenStream,18,Context);
}
- State = 273;
+ State = 305;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 269;
+ State = 301;
Match(COMMA);
- State = 270;
+ State = 302;
tableConstraint();
}
}
- State = 275;
+ State = 307;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 276;
+ State = 308;
Match(RPAREN);
}
}
@@ -1243,47 +1323,47 @@ public CreateViewStatementContext createViewStatement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 278;
+ State = 310;
Match(CREATE);
- State = 279;
+ State = 311;
Match(VIEW);
- State = 280;
+ State = 312;
_localctx.name = identifier();
- State = 292;
+ State = 324;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==LPAREN) {
{
- State = 281;
+ State = 313;
Match(LPAREN);
- State = 282;
+ State = 314;
_localctx._identifier = identifier();
_localctx._columns.Add(_localctx._identifier);
- State = 287;
+ State = 319;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 283;
+ State = 315;
Match(COMMA);
- State = 284;
+ State = 316;
_localctx._identifier = identifier();
_localctx._columns.Add(_localctx._identifier);
}
}
- State = 289;
+ State = 321;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 290;
+ State = 322;
Match(RPAREN);
}
}
- State = 294;
+ State = 326;
Match(AS);
- State = 295;
+ State = 327;
_localctx.query = queryExpression();
}
}
@@ -1334,25 +1414,25 @@ public CreateProcedureStatementContext createProcedureStatement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 297;
+ State = 329;
Match(CREATE);
- State = 298;
+ State = 330;
Match(PROCEDURE);
- State = 299;
+ State = 331;
_localctx.name = identifier();
- State = 301;
+ State = 333;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- if (((((_la - 115)) & ~0x3f) == 0 && ((1L << (_la - 115)) & 57409L) != 0)) {
+ if (((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7705305563135L) != 0)) {
{
- State = 300;
+ State = 332;
procParamList();
}
}
- State = 303;
+ State = 335;
Match(AS);
- State = 304;
+ State = 336;
_localctx.body = procedureBody();
}
}
@@ -1399,57 +1479,71 @@ public ProcParamListContext procParamList() {
EnterRule(_localctx, 26, RULE_procParamList);
int _la;
try {
- State = 325;
+ State = 357;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case LPAREN:
EnterOuterAlt(_localctx, 1);
{
- State = 306;
+ State = 338;
Match(LPAREN);
- State = 307;
+ State = 339;
procParam();
- State = 312;
+ State = 344;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 308;
+ State = 340;
Match(COMMA);
- State = 309;
+ State = 341;
procParam();
}
}
- State = 314;
+ State = 346;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 315;
+ State = 347;
Match(RPAREN);
}
break;
+ 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 PARAM:
case BRACKET_ID:
case BACKTICK_ID:
case IDENTIFIER:
EnterOuterAlt(_localctx, 2);
{
- State = 317;
+ State = 349;
procParam();
- State = 322;
+ State = 354;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 318;
+ State = 350;
Match(COMMA);
- State = 319;
+ State = 351;
procParam();
}
}
- State = 324;
+ State = 356;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -1498,9 +1592,9 @@ public ProcParamContext procParam() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 327;
+ State = 359;
_localctx.pname = procParamName();
- State = 328;
+ State = 360;
dataType();
}
}
@@ -1538,22 +1632,36 @@ public ProcParamNameContext procParamName() {
ProcParamNameContext _localctx = new ProcParamNameContext(Context, State);
EnterRule(_localctx, 30, RULE_procParamName);
try {
- State = 332;
+ State = 364;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
+ 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 = 330;
+ State = 362;
identifier();
}
break;
case PARAM:
EnterOuterAlt(_localctx, 2);
{
- State = 331;
+ State = 363;
Match(PARAM);
}
break;
@@ -1602,13 +1710,13 @@ public AlterTableStatementContext alterTableStatement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 334;
+ State = 366;
Match(ALTER);
- State = 335;
+ State = 367;
Match(TABLE);
- State = 336;
+ State = 368;
_localctx.table = identifier();
- State = 337;
+ State = 369;
alterTableAction();
}
}
@@ -1815,26 +1923,26 @@ public AlterTableActionContext alterTableAction() {
EnterRule(_localctx, 34, RULE_alterTableAction);
int _la;
try {
- State = 396;
+ State = 428;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,30,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,32,Context) ) {
case 1:
_localctx = new AddColumnActionContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 339;
+ State = 371;
Match(ADD);
- State = 341;
+ State = 373;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==COLUMN) {
{
- State = 340;
+ State = 372;
Match(COLUMN);
}
}
- State = 343;
+ State = 375;
columnDefinition();
}
break;
@@ -1842,9 +1950,9 @@ public AlterTableActionContext alterTableAction() {
_localctx = new AddConstraintActionContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 344;
+ State = 376;
Match(ADD);
- State = 345;
+ State = 377;
tableConstraint();
}
break;
@@ -1852,33 +1960,33 @@ public AlterTableActionContext alterTableAction() {
_localctx = new AlterColumnActionContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 346;
+ State = 378;
Match(ALTER);
- State = 348;
+ State = 380;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==COLUMN) {
{
- State = 347;
+ State = 379;
Match(COLUMN);
}
}
- State = 350;
+ State = 382;
((AlterColumnActionContext)_localctx).field = identifier();
- State = 351;
+ State = 383;
dataType();
- State = 355;
+ State = 387;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- while (_la==NOT || ((((_la - 70)) & ~0x3f) == 0 && ((1L << (_la - 70)) & 2149656597L) != 0)) {
+ while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 17197213717L) != 0)) {
{
{
- State = 352;
+ State = 384;
columnConstraint();
}
}
- State = 357;
+ State = 389;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -1888,25 +1996,25 @@ public AlterTableActionContext alterTableAction() {
_localctx = new AlterColumnSetDefaultActionContext(_localctx);
EnterOuterAlt(_localctx, 4);
{
- State = 358;
+ State = 390;
Match(ALTER);
- State = 360;
+ State = 392;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==COLUMN) {
{
- State = 359;
+ State = 391;
Match(COLUMN);
}
}
- State = 362;
+ State = 394;
((AlterColumnSetDefaultActionContext)_localctx).field = identifier();
- State = 363;
+ State = 395;
Match(SET);
- State = 364;
+ State = 396;
Match(DEFAULT);
- State = 365;
+ State = 397;
expression(0);
}
break;
@@ -1914,23 +2022,23 @@ public AlterTableActionContext alterTableAction() {
_localctx = new AlterColumnDropDefaultActionContext(_localctx);
EnterOuterAlt(_localctx, 5);
{
- State = 367;
+ State = 399;
Match(ALTER);
- State = 369;
+ State = 401;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==COLUMN) {
{
- State = 368;
+ State = 400;
Match(COLUMN);
}
}
- State = 371;
+ State = 403;
((AlterColumnDropDefaultActionContext)_localctx).field = identifier();
- State = 372;
+ State = 404;
Match(DROP);
- State = 373;
+ State = 405;
Match(DEFAULT);
}
break;
@@ -1938,11 +2046,11 @@ public AlterTableActionContext alterTableAction() {
_localctx = new DropColumnActionContext(_localctx);
EnterOuterAlt(_localctx, 6);
{
- State = 375;
+ State = 407;
Match(DROP);
- State = 376;
+ State = 408;
Match(COLUMN);
- State = 377;
+ State = 409;
((DropColumnActionContext)_localctx).field = identifier();
}
break;
@@ -1950,11 +2058,11 @@ public AlterTableActionContext alterTableAction() {
_localctx = new DropConstraintActionContext(_localctx);
EnterOuterAlt(_localctx, 7);
{
- State = 378;
+ State = 410;
Match(DROP);
- State = 379;
+ State = 411;
Match(CONSTRAINT);
- State = 380;
+ State = 412;
((DropConstraintActionContext)_localctx).cname = identifier();
}
break;
@@ -1962,11 +2070,11 @@ public AlterTableActionContext alterTableAction() {
_localctx = new RenameTableActionContext(_localctx);
EnterOuterAlt(_localctx, 8);
{
- State = 381;
+ State = 413;
Match(RENAME);
- State = 382;
+ State = 414;
Match(TO);
- State = 383;
+ State = 415;
((RenameTableActionContext)_localctx).newName = identifier();
}
break;
@@ -1974,15 +2082,15 @@ public AlterTableActionContext alterTableAction() {
_localctx = new RenameColumnActionContext(_localctx);
EnterOuterAlt(_localctx, 9);
{
- State = 384;
+ State = 416;
Match(RENAME);
- State = 385;
+ State = 417;
Match(COLUMN);
- State = 386;
+ State = 418;
((RenameColumnActionContext)_localctx).field = identifier();
- State = 387;
+ State = 419;
Match(TO);
- State = 388;
+ State = 420;
((RenameColumnActionContext)_localctx).newName = identifier();
}
break;
@@ -1990,15 +2098,15 @@ public AlterTableActionContext alterTableAction() {
_localctx = new RenameIndexActionContext(_localctx);
EnterOuterAlt(_localctx, 10);
{
- State = 390;
+ State = 422;
Match(RENAME);
- State = 391;
+ State = 423;
Match(INDEX);
- State = 392;
+ State = 424;
((RenameIndexActionContext)_localctx).index = identifier();
- State = 393;
+ State = 425;
Match(TO);
- State = 394;
+ State = 426;
((RenameIndexActionContext)_localctx).newName = identifier();
}
break;
@@ -2043,7 +2151,7 @@ public ProcedureBodyContext procedureBody() {
ProcedureBodyContext _localctx = new ProcedureBodyContext(Context, State);
EnterRule(_localctx, 36, RULE_procedureBody);
try {
- State = 401;
+ State = 433;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case SELECT:
@@ -2051,21 +2159,21 @@ public ProcedureBodyContext procedureBody() {
case LPAREN:
EnterOuterAlt(_localctx, 1);
{
- State = 398;
+ State = 430;
queryExpression();
}
break;
case INSERT:
EnterOuterAlt(_localctx, 2);
{
- State = 399;
+ State = 431;
insertStatement();
}
break;
case CREATE:
EnterOuterAlt(_localctx, 3);
{
- State = 400;
+ State = 432;
createTableStatement();
}
break;
@@ -2135,56 +2243,56 @@ public CreateIndexStatementContext createIndexStatement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 403;
+ State = 435;
Match(CREATE);
- State = 405;
+ State = 437;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==UNIQUE) {
{
- State = 404;
+ State = 436;
_localctx.unique = Match(UNIQUE);
}
}
- State = 407;
+ State = 439;
Match(INDEX);
- State = 408;
+ State = 440;
_localctx.name = identifier();
- State = 409;
+ State = 441;
Match(ON);
- State = 410;
+ State = 442;
_localctx.table = identifier();
- State = 411;
+ State = 443;
Match(LPAREN);
- State = 412;
+ State = 444;
indexColumn();
- State = 417;
+ State = 449;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 413;
+ State = 445;
Match(COMMA);
- State = 414;
+ State = 446;
indexColumn();
}
}
- State = 419;
+ State = 451;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 420;
+ State = 452;
Match(RPAREN);
- State = 423;
+ State = 455;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==WITH) {
{
- State = 421;
+ State = 453;
Match(WITH);
- State = 422;
+ State = 454;
withOption();
}
}
@@ -2285,18 +2393,18 @@ public DropStatementContext dropStatement() {
DropStatementContext _localctx = new DropStatementContext(Context, State);
EnterRule(_localctx, 40, RULE_dropStatement);
try {
- State = 440;
+ State = 472;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,35,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,37,Context) ) {
case 1:
_localctx = new DropTableStatementContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 425;
+ State = 457;
Match(DROP);
- State = 426;
+ State = 458;
Match(TABLE);
- State = 427;
+ State = 459;
((DropTableStatementContext)_localctx).table = identifier();
}
break;
@@ -2304,15 +2412,15 @@ public DropStatementContext dropStatement() {
_localctx = new DropIndexStatementContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 428;
+ State = 460;
Match(DROP);
- State = 429;
+ State = 461;
Match(INDEX);
- State = 430;
+ State = 462;
((DropIndexStatementContext)_localctx).index = identifier();
- State = 431;
+ State = 463;
Match(ON);
- State = 432;
+ State = 464;
((DropIndexStatementContext)_localctx).table = identifier();
}
break;
@@ -2320,11 +2428,11 @@ public DropStatementContext dropStatement() {
_localctx = new DropProcedureStatementContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 434;
+ State = 466;
Match(DROP);
- State = 435;
+ State = 467;
Match(PROCEDURE);
- State = 436;
+ State = 468;
((DropProcedureStatementContext)_localctx).proc = identifier();
}
break;
@@ -2332,11 +2440,11 @@ public DropStatementContext dropStatement() {
_localctx = new DropViewStatementContext(_localctx);
EnterOuterAlt(_localctx, 4);
{
- State = 437;
+ State = 469;
Match(DROP);
- State = 438;
+ State = 470;
Match(VIEW);
- State = 439;
+ State = 471;
((DropViewStatementContext)_localctx).view = identifier();
}
break;
@@ -2382,14 +2490,14 @@ public IndexColumnContext indexColumn() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 442;
+ State = 474;
_localctx.col = identifier();
- State = 444;
+ State = 476;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==ASC || _la==DESC) {
{
- State = 443;
+ State = 475;
_localctx.dir = TokenStream.LT(1);
_la = TokenStream.LA(1);
if ( !(_la==ASC || _la==DESC) ) {
@@ -2465,14 +2573,14 @@ public WithOptionContext withOption() {
WithOptionContext _localctx = new WithOptionContext(Context, State);
EnterRule(_localctx, 44, RULE_withOption);
try {
- State = 451;
+ State = 483;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case PRIMARY:
_localctx = new WithPrimaryContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 446;
+ State = 478;
Match(PRIMARY);
}
break;
@@ -2480,9 +2588,9 @@ public WithOptionContext withOption() {
_localctx = new WithDisallowNullContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 447;
+ State = 479;
Match(DISALLOW);
- State = 448;
+ State = 480;
Match(NULL);
}
break;
@@ -2490,9 +2598,9 @@ public WithOptionContext withOption() {
_localctx = new WithIgnoreNullContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 449;
+ State = 481;
Match(IGNORE);
- State = 450;
+ State = 482;
Match(NULL);
}
break;
@@ -2549,31 +2657,31 @@ public ColumnDefinitionContext columnDefinition() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 453;
+ State = 485;
_localctx.name = identifier();
- State = 454;
+ State = 486;
dataType();
- State = 456;
+ State = 488;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==AS) {
{
- State = 455;
+ State = 487;
calculatedClause();
}
}
- State = 461;
+ State = 493;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- while (_la==NOT || ((((_la - 70)) & ~0x3f) == 0 && ((1L << (_la - 70)) & 2149656597L) != 0)) {
+ while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 17197213717L) != 0)) {
{
{
- State = 458;
+ State = 490;
columnConstraint();
}
}
- State = 463;
+ State = 495;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -2617,13 +2725,13 @@ public CalculatedClauseContext calculatedClause() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 464;
+ State = 496;
Match(AS);
- State = 465;
+ State = 497;
Match(LPAREN);
- State = 466;
+ State = 498;
expression(0);
- State = 467;
+ State = 499;
Match(RPAREN);
}
}
@@ -2640,6 +2748,7 @@ public CalculatedClauseContext calculatedClause() {
public partial class DataTypeContext : ParserRuleContext {
public IdentifierContext typeName;
+ public IToken identityType;
public IdentifierContext extra;
public IdentifierContext extra2;
public SignedIntegerContext size;
@@ -2650,6 +2759,7 @@ [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext[] identifier()
[System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier(int i) {
return GetRuleContext(i);
}
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode IDENTITY() { return GetToken(AccessSqlParser.IDENTITY, 0); }
[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 SignedIntegerContext[] signedInteger() {
@@ -2680,50 +2790,82 @@ public DataTypeContext dataType() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 469;
- _localctx.typeName = identifier();
- State = 471;
+ State = 503;
+ ErrorHandler.Sync(this);
+ switch (TokenStream.LA(1)) {
+ 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:
+ {
+ State = 501;
+ _localctx.typeName = identifier();
+ }
+ break;
+ case IDENTITY:
+ {
+ State = 502;
+ _localctx.identityType = Match(IDENTITY);
+ }
+ break;
+ default:
+ throw new NoViableAltException(this);
+ }
+ State = 506;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,40,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,43,Context) ) {
case 1:
{
- State = 470;
+ State = 505;
_localctx.extra = identifier();
}
break;
}
- State = 474;
+ State = 509;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- if (((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 7L) != 0)) {
+ if (((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) {
{
- State = 473;
+ State = 508;
_localctx.extra2 = identifier();
}
}
- State = 484;
+ State = 519;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==LPAREN) {
{
- State = 476;
+ State = 511;
Match(LPAREN);
- State = 477;
+ State = 512;
_localctx.size = signedInteger();
- State = 480;
+ State = 515;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==COMMA) {
{
- State = 478;
+ State = 513;
Match(COMMA);
- State = 479;
+ State = 514;
_localctx.scale = signedInteger();
}
}
- State = 482;
+ State = 517;
Match(RPAREN);
}
}
@@ -2765,17 +2907,17 @@ public SignedIntegerContext signedInteger() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 487;
+ State = 522;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==MINUS) {
{
- State = 486;
+ State = 521;
Match(MINUS);
}
}
- State = 489;
+ State = 524;
Match(INTEGER_LITERAL);
}
}
@@ -2890,6 +3032,27 @@ public override TResult Accept(IParseTreeVisitor visitor) {
else return visitor.VisitChildren(this);
}
}
+ public partial class IdentityConstraintContext : ColumnConstraintContext {
+ public SignedIntegerContext seed;
+ public SignedIntegerContext increment;
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode IDENTITY() { return GetToken(AccessSqlParser.IDENTITY, 0); }
+ [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 SignedIntegerContext[] signedInteger() {
+ return GetRuleContexts();
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public SignedIntegerContext signedInteger(int i) {
+ return GetRuleContext(i);
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMMA() { return GetToken(AccessSqlParser.COMMA, 0); }
+ public IdentityConstraintContext(ColumnConstraintContext context) { CopyFrom(context); }
+ [System.Diagnostics.DebuggerNonUserCode]
+ public override TResult Accept(IParseTreeVisitor visitor) {
+ IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor;
+ if (typedVisitor != null) return typedVisitor.VisitIdentityConstraint(this);
+ else return visitor.VisitChildren(this);
+ }
+ }
public partial class NotNullConstraintContext : ColumnConstraintContext {
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NOT() { return GetToken(AccessSqlParser.NOT, 0); }
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode NULL() { return GetToken(AccessSqlParser.NULL, 0); }
@@ -2906,6 +3069,9 @@ public partial class PrimaryKeyConstraintContext : ColumnConstraintContext {
[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 ClusteredOptionContext clusteredOption() {
+ return GetRuleContext(0);
+ }
[System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() {
return GetRuleContext(0);
}
@@ -2921,6 +3087,9 @@ public partial class UniqueColumnConstraintContext : ColumnConstraintContext {
public IdentifierContext cname;
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode UNIQUE() { return GetToken(AccessSqlParser.UNIQUE, 0); }
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CONSTRAINT() { return GetToken(AccessSqlParser.CONSTRAINT, 0); }
+ [System.Diagnostics.DebuggerNonUserCode] public ClusteredOptionContext clusteredOption() {
+ return GetRuleContext(0);
+ }
[System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier() {
return GetRuleContext(0);
}
@@ -2939,16 +3108,16 @@ public ColumnConstraintContext columnConstraint() {
EnterRule(_localctx, 54, RULE_columnConstraint);
int _la;
try {
- State = 543;
+ State = 595;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,52,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,59,Context) ) {
case 1:
_localctx = new NotNullConstraintContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 491;
+ State = 526;
Match(NOT);
- State = 492;
+ State = 527;
Match(NULL);
}
break;
@@ -2956,7 +3125,7 @@ public ColumnConstraintContext columnConstraint() {
_localctx = new NullableConstraintContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 493;
+ State = 528;
Match(NULL);
}
break;
@@ -2964,9 +3133,9 @@ public ColumnConstraintContext columnConstraint() {
_localctx = new DefaultConstraintContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 494;
+ State = 529;
Match(DEFAULT);
- State = 495;
+ State = 530;
expression(0);
}
break;
@@ -2974,9 +3143,9 @@ public ColumnConstraintContext columnConstraint() {
_localctx = new CompressionConstraintContext(_localctx);
EnterOuterAlt(_localctx, 4);
{
- State = 496;
+ State = 531;
Match(WITH);
- State = 497;
+ State = 532;
_la = TokenStream.LA(1);
if ( !(_la==COMPRESSION || _la==COMP) ) {
ErrorHandler.RecoverInline(this);
@@ -2991,25 +3160,25 @@ public ColumnConstraintContext columnConstraint() {
_localctx = new CheckColumnConstraintContext(_localctx);
EnterOuterAlt(_localctx, 5);
{
- State = 500;
+ State = 535;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==CONSTRAINT) {
{
- State = 498;
+ State = 533;
Match(CONSTRAINT);
- State = 499;
+ State = 534;
((CheckColumnConstraintContext)_localctx).cname = identifier();
}
}
- State = 502;
+ State = 537;
Match(CHECK);
- State = 503;
+ State = 538;
Match(LPAREN);
- State = 504;
+ State = 539;
checkBody();
- State = 505;
+ State = 540;
Match(RPAREN);
}
break;
@@ -3017,110 +3186,164 @@ public ColumnConstraintContext columnConstraint() {
_localctx = new PrimaryKeyConstraintContext(_localctx);
EnterOuterAlt(_localctx, 6);
{
- State = 509;
+ State = 544;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==CONSTRAINT) {
{
- State = 507;
+ State = 542;
Match(CONSTRAINT);
- State = 508;
+ State = 543;
((PrimaryKeyConstraintContext)_localctx).cname = identifier();
}
}
- State = 511;
+ State = 546;
Match(PRIMARY);
- State = 512;
+ State = 547;
Match(KEY);
+ State = 549;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ if (_la==CLUSTERED || _la==NONCLUSTERED) {
+ {
+ State = 548;
+ clusteredOption();
+ }
+ }
+
}
break;
case 7:
_localctx = new UniqueColumnConstraintContext(_localctx);
EnterOuterAlt(_localctx, 7);
{
- State = 515;
+ State = 553;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==CONSTRAINT) {
{
- State = 513;
+ State = 551;
Match(CONSTRAINT);
- State = 514;
+ State = 552;
((UniqueColumnConstraintContext)_localctx).cname = identifier();
}
}
- State = 517;
+ State = 555;
Match(UNIQUE);
+ State = 557;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ if (_la==CLUSTERED || _la==NONCLUSTERED) {
+ {
+ State = 556;
+ clusteredOption();
+ }
}
- break;
+
+ }
+ break;
case 8:
_localctx = new ColumnReferencesConstraintContext(_localctx);
EnterOuterAlt(_localctx, 8);
{
- State = 520;
+ State = 561;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==CONSTRAINT) {
{
- State = 518;
+ State = 559;
Match(CONSTRAINT);
- State = 519;
+ State = 560;
((ColumnReferencesConstraintContext)_localctx).cname = identifier();
}
}
- State = 522;
+ State = 563;
Match(REFERENCES);
- State = 523;
+ State = 564;
((ColumnReferencesConstraintContext)_localctx).refTable = identifier();
- State = 535;
+ State = 576;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==LPAREN) {
{
- State = 524;
+ State = 565;
Match(LPAREN);
- State = 525;
+ State = 566;
((ColumnReferencesConstraintContext)_localctx)._identifier = identifier();
((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier);
- State = 530;
+ State = 571;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 526;
+ State = 567;
Match(COMMA);
- State = 527;
+ State = 568;
((ColumnReferencesConstraintContext)_localctx)._identifier = identifier();
((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier);
}
}
- State = 532;
+ State = 573;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 533;
+ State = 574;
Match(RPAREN);
}
}
- State = 540;
+ State = 581;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==ON) {
{
{
- State = 537;
+ State = 578;
foreignKeyAction();
}
}
- State = 542;
+ State = 583;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ }
+ }
+ break;
+ case 9:
+ _localctx = new IdentityConstraintContext(_localctx);
+ EnterOuterAlt(_localctx, 9);
+ {
+ State = 584;
+ Match(IDENTITY);
+ State = 593;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ if (_la==LPAREN) {
+ {
+ State = 585;
+ Match(LPAREN);
+ State = 586;
+ ((IdentityConstraintContext)_localctx).seed = signedInteger();
+ State = 589;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
+ if (_la==COMMA) {
+ {
+ State = 587;
+ Match(COMMA);
+ State = 588;
+ ((IdentityConstraintContext)_localctx).increment = signedInteger();
+ }
+ }
+
+ State = 591;
+ Match(RPAREN);
+ }
}
+
}
break;
}
@@ -3163,6 +3386,9 @@ [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier(int
return GetRuleContext(i);
}
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CONSTRAINT() { return GetToken(AccessSqlParser.CONSTRAINT, 0); }
+ [System.Diagnostics.DebuggerNonUserCode] public ClusteredOptionContext clusteredOption() {
+ return GetRuleContext(0);
+ }
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode[] COMMA() { return GetTokens(AccessSqlParser.COMMA); }
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMMA(int i) {
return GetToken(AccessSqlParser.COMMA, i);
@@ -3234,6 +3460,9 @@ [System.Diagnostics.DebuggerNonUserCode] public IdentifierContext identifier(int
return GetRuleContext(i);
}
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode CONSTRAINT() { return GetToken(AccessSqlParser.CONSTRAINT, 0); }
+ [System.Diagnostics.DebuggerNonUserCode] public ClusteredOptionContext clusteredOption() {
+ return GetRuleContext(0);
+ }
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode[] COMMA() { return GetTokens(AccessSqlParser.COMMA); }
[System.Diagnostics.DebuggerNonUserCode] public ITerminalNode COMMA(int i) {
return GetToken(AccessSqlParser.COMMA, i);
@@ -3273,52 +3502,62 @@ public TableConstraintContext tableConstraint() {
EnterRule(_localctx, 56, RULE_tableConstraint);
int _la;
try {
- State = 628;
+ State = 686;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,64,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,73,Context) ) {
case 1:
_localctx = new PrimaryKeyTableConstraintContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 547;
+ State = 599;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==CONSTRAINT) {
{
- State = 545;
+ State = 597;
Match(CONSTRAINT);
- State = 546;
+ State = 598;
((PrimaryKeyTableConstraintContext)_localctx).name = identifier();
}
}
- State = 549;
+ State = 601;
Match(PRIMARY);
- State = 550;
+ State = 602;
Match(KEY);
- State = 551;
+ State = 604;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ if (_la==CLUSTERED || _la==NONCLUSTERED) {
+ {
+ State = 603;
+ clusteredOption();
+ }
+ }
+
+ State = 606;
Match(LPAREN);
- State = 552;
+ State = 607;
((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier();
((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier);
- State = 557;
+ State = 612;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 553;
+ State = 608;
Match(COMMA);
- State = 554;
+ State = 609;
((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier();
((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier);
}
}
- State = 559;
+ State = 614;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 560;
+ State = 615;
Match(RPAREN);
}
break;
@@ -3326,43 +3565,53 @@ public TableConstraintContext tableConstraint() {
_localctx = new UniqueTableConstraintContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 564;
+ State = 619;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==CONSTRAINT) {
{
- State = 562;
+ State = 617;
Match(CONSTRAINT);
- State = 563;
+ State = 618;
((UniqueTableConstraintContext)_localctx).name = identifier();
}
}
- State = 566;
+ State = 621;
Match(UNIQUE);
- State = 567;
+ State = 623;
+ ErrorHandler.Sync(this);
+ _la = TokenStream.LA(1);
+ if (_la==CLUSTERED || _la==NONCLUSTERED) {
+ {
+ State = 622;
+ clusteredOption();
+ }
+ }
+
+ State = 625;
Match(LPAREN);
- State = 568;
+ State = 626;
((UniqueTableConstraintContext)_localctx)._identifier = identifier();
((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier);
- State = 573;
+ State = 631;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 569;
+ State = 627;
Match(COMMA);
- State = 570;
+ State = 628;
((UniqueTableConstraintContext)_localctx)._identifier = identifier();
((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier);
}
}
- State = 575;
+ State = 633;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 576;
+ State = 634;
Match(RPAREN);
}
break;
@@ -3370,105 +3619,105 @@ public TableConstraintContext tableConstraint() {
_localctx = new ForeignKeyTableConstraintContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 580;
+ State = 638;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==CONSTRAINT) {
{
- State = 578;
+ State = 636;
Match(CONSTRAINT);
- State = 579;
+ State = 637;
((ForeignKeyTableConstraintContext)_localctx).name = identifier();
}
}
- State = 582;
+ State = 640;
Match(FOREIGN);
- State = 583;
+ State = 641;
Match(KEY);
- State = 586;
+ State = 644;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==NO) {
{
- State = 584;
+ State = 642;
((ForeignKeyTableConstraintContext)_localctx).noIndex = Match(NO);
- State = 585;
+ State = 643;
Match(INDEX);
}
}
- State = 588;
+ State = 646;
Match(LPAREN);
- State = 589;
+ State = 647;
((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier();
((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier);
- State = 594;
+ State = 652;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 590;
+ State = 648;
Match(COMMA);
- State = 591;
+ State = 649;
((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier();
((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier);
}
}
- State = 596;
+ State = 654;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 597;
+ State = 655;
Match(RPAREN);
- State = 598;
+ State = 656;
Match(REFERENCES);
- State = 599;
+ State = 657;
((ForeignKeyTableConstraintContext)_localctx).refTable = identifier();
- State = 611;
+ State = 669;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==LPAREN) {
{
- State = 600;
+ State = 658;
Match(LPAREN);
- State = 601;
+ State = 659;
((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier();
((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier);
- State = 606;
+ State = 664;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 602;
+ State = 660;
Match(COMMA);
- State = 603;
+ State = 661;
((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier();
((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier);
}
}
- State = 608;
+ State = 666;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 609;
+ State = 667;
Match(RPAREN);
}
}
- State = 616;
+ State = 674;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==ON) {
{
{
- State = 613;
+ State = 671;
foreignKeyAction();
}
}
- State = 618;
+ State = 676;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -3478,25 +3727,25 @@ public TableConstraintContext tableConstraint() {
_localctx = new CheckTableConstraintContext(_localctx);
EnterOuterAlt(_localctx, 4);
{
- State = 621;
+ State = 679;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==CONSTRAINT) {
{
- State = 619;
+ State = 677;
Match(CONSTRAINT);
- State = 620;
+ State = 678;
((CheckTableConstraintContext)_localctx).name = identifier();
}
}
- State = 623;
+ State = 681;
Match(CHECK);
- State = 624;
+ State = 682;
Match(LPAREN);
- State = 625;
+ State = 683;
checkBody();
- State = 626;
+ State = 684;
Match(RPAREN);
}
break;
@@ -3549,12 +3798,12 @@ public CheckBodyContext checkBody() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 637;
+ State = 695;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- while ((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & -4503599627370497L) != 0) || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 63L) != 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)) & 67108607L) != 0)) {
{
- State = 635;
+ State = 693;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case SELECT:
@@ -3565,6 +3814,9 @@ public CheckBodyContext checkBody() {
case AND:
case OR:
case NOT:
+ case XOR:
+ case EQV:
+ case IMP:
case BAND:
case BOR:
case BXOR:
@@ -3640,6 +3892,9 @@ public CheckBodyContext checkBody() {
case DEFAULT:
case NO:
case UNIQUE:
+ case CLUSTERED:
+ case IDENTITY:
+ case NONCLUSTERED:
case INDEX:
case TEMPORARY:
case WITH:
@@ -3658,6 +3913,20 @@ public CheckBodyContext checkBody() {
case TRUE:
case FALSE:
case NULL:
+ 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 STAR:
case SLASH:
case BACKSLASH:
@@ -3689,7 +3958,7 @@ public CheckBodyContext checkBody() {
case LINE_COMMENT:
case BLOCK_COMMENT:
{
- State = 630;
+ State = 688;
_la = TokenStream.LA(1);
if ( _la <= 0 || (_la==LPAREN || _la==RPAREN) ) {
ErrorHandler.RecoverInline(this);
@@ -3702,11 +3971,11 @@ public CheckBodyContext checkBody() {
break;
case LPAREN:
{
- State = 631;
+ State = 689;
Match(LPAREN);
- State = 632;
+ State = 690;
checkBody();
- State = 633;
+ State = 691;
Match(RPAREN);
}
break;
@@ -3714,7 +3983,7 @@ public CheckBodyContext checkBody() {
throw new NoViableAltException(this);
}
}
- State = 639;
+ State = 697;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -3777,18 +4046,18 @@ public ForeignKeyActionContext foreignKeyAction() {
ForeignKeyActionContext _localctx = new ForeignKeyActionContext(Context, State);
EnterRule(_localctx, 60, RULE_foreignKeyAction);
try {
- State = 646;
+ State = 704;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,67,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,76,Context) ) {
case 1:
_localctx = new OnUpdateActionContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 640;
+ State = 698;
Match(ON);
- State = 641;
+ State = 699;
Match(UPDATE);
- State = 642;
+ State = 700;
referentialAction();
}
break;
@@ -3796,11 +4065,11 @@ public ForeignKeyActionContext foreignKeyAction() {
_localctx = new OnDeleteActionContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 643;
+ State = 701;
Match(ON);
- State = 644;
+ State = 702;
Match(DELETE);
- State = 645;
+ State = 703;
referentialAction();
}
break;
@@ -3888,14 +4157,14 @@ public ReferentialActionContext referentialAction() {
ReferentialActionContext _localctx = new ReferentialActionContext(Context, State);
EnterRule(_localctx, 62, RULE_referentialAction);
try {
- State = 656;
+ State = 714;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,68,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,77,Context) ) {
case 1:
_localctx = new CascadeActionContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 648;
+ State = 706;
Match(CASCADE);
}
break;
@@ -3903,9 +4172,9 @@ public ReferentialActionContext referentialAction() {
_localctx = new NoActionActionContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 649;
+ State = 707;
Match(NO);
- State = 650;
+ State = 708;
Match(ACTION);
}
break;
@@ -3913,7 +4182,7 @@ public ReferentialActionContext referentialAction() {
_localctx = new RestrictActionContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 651;
+ State = 709;
Match(RESTRICT);
}
break;
@@ -3921,9 +4190,9 @@ public ReferentialActionContext referentialAction() {
_localctx = new SetNullActionContext(_localctx);
EnterOuterAlt(_localctx, 4);
{
- State = 652;
+ State = 710;
Match(SET);
- State = 653;
+ State = 711;
Match(NULL);
}
break;
@@ -3931,9 +4200,9 @@ public ReferentialActionContext referentialAction() {
_localctx = new SetDefaultActionContext(_localctx);
EnterOuterAlt(_localctx, 5);
{
- State = 654;
+ State = 712;
Match(SET);
- State = 655;
+ State = 713;
Match(DEFAULT);
}
break;
@@ -4001,73 +4270,73 @@ public InsertStatementContext insertStatement() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 658;
+ State = 716;
Match(INSERT);
- State = 659;
+ State = 717;
Match(INTO);
- State = 660;
+ State = 718;
_localctx.table = identifier();
- State = 688;
+ State = 746;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case SELECT:
case VALUES:
case LPAREN:
{
- State = 672;
+ State = 730;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,70,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,79,Context) ) {
case 1:
{
- State = 661;
+ State = 719;
Match(LPAREN);
- State = 662;
+ State = 720;
_localctx._identifier = identifier();
_localctx._columns.Add(_localctx._identifier);
- State = 667;
+ State = 725;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 663;
+ State = 721;
Match(COMMA);
- State = 664;
+ State = 722;
_localctx._identifier = identifier();
_localctx._columns.Add(_localctx._identifier);
}
}
- State = 669;
+ State = 727;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 670;
+ State = 728;
Match(RPAREN);
}
break;
}
- State = 684;
+ State = 742;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,72,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,81,Context) ) {
case 1:
{
- State = 674;
+ State = 732;
Match(VALUES);
- State = 675;
+ State = 733;
rowValues();
- State = 680;
+ State = 738;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 676;
+ State = 734;
Match(COMMA);
- State = 677;
+ State = 735;
rowValues();
}
}
- State = 682;
+ State = 740;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -4075,7 +4344,7 @@ public InsertStatementContext insertStatement() {
break;
case 2:
{
- State = 683;
+ State = 741;
_localctx.source = queryExpression();
}
break;
@@ -4084,9 +4353,9 @@ public InsertStatementContext insertStatement() {
break;
case DEFAULT:
{
- State = 686;
+ State = 744;
Match(DEFAULT);
- State = 687;
+ State = 745;
Match(VALUES);
}
break;
@@ -4140,27 +4409,27 @@ public RowValuesContext rowValues() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 690;
+ State = 748;
Match(LPAREN);
- State = 691;
+ State = 749;
rowValue();
- State = 696;
+ State = 754;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 692;
+ State = 750;
Match(COMMA);
- State = 693;
+ State = 751;
rowValue();
}
}
- State = 698;
+ State = 756;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 699;
+ State = 757;
Match(RPAREN);
}
}
@@ -4198,13 +4467,13 @@ public RowValueContext rowValue() {
RowValueContext _localctx = new RowValueContext(Context, State);
EnterRule(_localctx, 68, RULE_rowValue);
try {
- State = 703;
+ State = 761;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case DEFAULT:
EnterOuterAlt(_localctx, 1);
{
- State = 701;
+ State = 759;
Match(DEFAULT);
}
break;
@@ -4220,6 +4489,21 @@ public RowValueContext rowValue() {
case TRUE:
case FALSE:
case NULL:
+ 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 PLUS:
case MINUS:
case LPAREN:
case SYSVAR:
@@ -4235,7 +4519,7 @@ public RowValueContext rowValue() {
case IDENTIFIER:
EnterOuterAlt(_localctx, 2);
{
- State = 702;
+ State = 760;
expression(0);
}
break;
@@ -4294,40 +4578,40 @@ public QueryExpressionContext queryExpression() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 705;
+ State = 763;
queryTerm();
- State = 711;
+ State = 769;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 14636698788954112L) != 0)) {
+ while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 117093590311632896L) != 0)) {
{
{
- State = 706;
+ State = 764;
setOperator();
- State = 707;
+ State = 765;
queryTerm();
}
}
- State = 713;
+ State = 771;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
- State = 715;
+ State = 773;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==ORDER) {
{
- State = 714;
+ State = 772;
orderByClause();
}
}
- State = 718;
+ State = 776;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==OFFSET || _la==FETCH) {
{
- State = 717;
+ State = 775;
offsetFetchClause();
}
}
@@ -4410,14 +4694,14 @@ public QueryTermContext queryTerm() {
EnterRule(_localctx, 72, RULE_queryTerm);
int _la;
try {
- State = 734;
+ State = 792;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case SELECT:
_localctx = new SelectTermContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 720;
+ State = 778;
querySpecification();
}
break;
@@ -4425,11 +4709,11 @@ public QueryTermContext queryTerm() {
_localctx = new ParenTermContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 721;
+ State = 779;
Match(LPAREN);
- State = 722;
+ State = 780;
queryExpression();
- State = 723;
+ State = 781;
Match(RPAREN);
}
break;
@@ -4437,23 +4721,23 @@ public QueryTermContext queryTerm() {
_localctx = new ValuesTermContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 725;
+ State = 783;
Match(VALUES);
- State = 726;
+ State = 784;
rowValues();
- State = 731;
+ State = 789;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 727;
+ State = 785;
Match(COMMA);
- State = 728;
+ State = 786;
rowValues();
}
}
- State = 733;
+ State = 791;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -4498,20 +4782,20 @@ public SetOperatorContext setOperator() {
EnterRule(_localctx, 74, RULE_setOperator);
int _la;
try {
- State = 742;
+ State = 800;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case UNION:
EnterOuterAlt(_localctx, 1);
{
- State = 736;
+ State = 794;
Match(UNION);
- State = 738;
+ State = 796;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==ALL) {
{
- State = 737;
+ State = 795;
Match(ALL);
}
}
@@ -4521,14 +4805,14 @@ public SetOperatorContext setOperator() {
case INTERSECT:
EnterOuterAlt(_localctx, 2);
{
- State = 740;
+ State = 798;
Match(INTERSECT);
}
break;
case EXCEPT:
EnterOuterAlt(_localctx, 3);
{
- State = 741;
+ State = 799;
Match(EXCEPT);
}
break;
@@ -4597,78 +4881,78 @@ public QuerySpecificationContext querySpecification() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 744;
+ State = 802;
Match(SELECT);
- State = 746;
+ State = 804;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 2251806256136192L) != 0)) {
+ if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) {
{
- State = 745;
+ State = 803;
_localctx.predicate = selectPredicate();
}
}
- State = 749;
+ State = 807;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==TOP) {
{
- State = 748;
+ State = 806;
topClause();
}
}
- State = 751;
+ State = 809;
selectList();
- State = 754;
+ State = 812;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==INTO) {
{
- State = 752;
+ State = 810;
Match(INTO);
- State = 753;
+ State = 811;
_localctx.into = identifier();
}
}
- State = 757;
+ State = 815;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==FROM) {
{
- State = 756;
+ State = 814;
fromClause();
}
}
- State = 760;
+ State = 818;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==WHERE) {
{
- State = 759;
+ State = 817;
whereClause();
}
}
- State = 763;
+ State = 821;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==GROUP) {
{
- State = 762;
+ State = 820;
groupByClause();
}
}
- State = 766;
+ State = 824;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==HAVING) {
{
- State = 765;
+ State = 823;
havingClause();
}
}
@@ -4711,9 +4995,9 @@ public SelectPredicateContext selectPredicate() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 768;
+ State = 826;
_la = TokenStream.LA(1);
- if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 2251806256136192L) != 0)) ) {
+ if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) ) {
ErrorHandler.RecoverInline(this);
}
else {
@@ -4767,25 +5051,25 @@ public GroupByClauseContext groupByClause() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 770;
+ State = 828;
Match(GROUP);
- State = 771;
+ State = 829;
Match(BY);
- State = 772;
+ State = 830;
expression(0);
- State = 777;
+ State = 835;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 773;
+ State = 831;
Match(COMMA);
- State = 774;
+ State = 832;
expression(0);
}
}
- State = 779;
+ State = 837;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -4827,9 +5111,9 @@ public HavingClauseContext havingClause() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 780;
+ State = 838;
Match(HAVING);
- State = 781;
+ State = 839;
expression(0);
}
}
@@ -4884,18 +5168,18 @@ public TopClauseContext topClause() {
int _alt;
EnterOuterAlt(_localctx, 1);
{
- State = 783;
+ State = 841;
Match(TOP);
- State = 784;
+ State = 842;
topOperand();
- State = 789;
+ State = 847;
ErrorHandler.Sync(this);
- _alt = Interpreter.AdaptivePredict(TokenStream,91,Context);
+ _alt = Interpreter.AdaptivePredict(TokenStream,100,Context);
while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) {
if ( _alt==1 ) {
{
{
- State = 785;
+ State = 843;
_la = TokenStream.LA(1);
if ( !(_la==PLUS || _la==MINUS) ) {
ErrorHandler.RecoverInline(this);
@@ -4904,21 +5188,21 @@ public TopClauseContext topClause() {
ErrorHandler.ReportMatch(this);
Consume();
}
- State = 786;
+ State = 844;
topOperand();
}
}
}
- State = 791;
+ State = 849;
ErrorHandler.Sync(this);
- _alt = Interpreter.AdaptivePredict(TokenStream,91,Context);
+ _alt = Interpreter.AdaptivePredict(TokenStream,100,Context);
}
- State = 793;
+ State = 851;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==PERCENT) {
{
- State = 792;
+ State = 850;
_localctx.percent = Match(PERCENT);
}
}
@@ -4962,31 +5246,31 @@ public TopOperandContext topOperand() {
TopOperandContext _localctx = new TopOperandContext(Context, State);
EnterRule(_localctx, 86, RULE_topOperand);
try {
- State = 801;
+ State = 859;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case INTEGER_LITERAL:
EnterOuterAlt(_localctx, 1);
{
- State = 795;
+ State = 853;
Match(INTEGER_LITERAL);
}
break;
case PARAM:
EnterOuterAlt(_localctx, 2);
{
- State = 796;
+ State = 854;
Match(PARAM);
}
break;
case LPAREN:
EnterOuterAlt(_localctx, 3);
{
- State = 797;
+ State = 855;
Match(LPAREN);
- State = 798;
+ State = 856;
expression(0);
- State = 799;
+ State = 857;
Match(RPAREN);
}
break;
@@ -5044,26 +5328,26 @@ public OffsetFetchClauseContext offsetFetchClause() {
EnterRule(_localctx, 88, RULE_offsetFetchClause);
int _la;
try {
- State = 820;
+ State = 878;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case OFFSET:
EnterOuterAlt(_localctx, 1);
{
- State = 803;
+ State = 861;
Match(OFFSET);
- State = 804;
+ State = 862;
_localctx.offset = expression(0);
- State = 805;
+ State = 863;
rowKeyword();
- State = 812;
+ State = 870;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==FETCH) {
{
- State = 806;
+ State = 864;
Match(FETCH);
- State = 807;
+ State = 865;
_la = TokenStream.LA(1);
if ( !(_la==NEXT || _la==FIRST) ) {
ErrorHandler.RecoverInline(this);
@@ -5072,11 +5356,11 @@ public OffsetFetchClauseContext offsetFetchClause() {
ErrorHandler.ReportMatch(this);
Consume();
}
- State = 808;
+ State = 866;
_localctx.limit = expression(0);
- State = 809;
+ State = 867;
rowKeyword();
- State = 810;
+ State = 868;
Match(ONLY);
}
}
@@ -5086,9 +5370,9 @@ public OffsetFetchClauseContext offsetFetchClause() {
case FETCH:
EnterOuterAlt(_localctx, 2);
{
- State = 814;
+ State = 872;
Match(FETCH);
- State = 815;
+ State = 873;
_la = TokenStream.LA(1);
if ( !(_la==NEXT || _la==FIRST) ) {
ErrorHandler.RecoverInline(this);
@@ -5097,11 +5381,11 @@ public OffsetFetchClauseContext offsetFetchClause() {
ErrorHandler.ReportMatch(this);
Consume();
}
- State = 816;
+ State = 874;
_localctx.limit = expression(0);
- State = 817;
+ State = 875;
rowKeyword();
- State = 818;
+ State = 876;
Match(ONLY);
}
break;
@@ -5144,7 +5428,7 @@ public RowKeywordContext rowKeyword() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 822;
+ State = 880;
_la = TokenStream.LA(1);
if ( !(_la==ROWS || _la==ROW) ) {
ErrorHandler.RecoverInline(this);
@@ -5197,13 +5481,13 @@ public SelectListContext selectList() {
EnterRule(_localctx, 92, RULE_selectList);
int _la;
try {
- State = 833;
+ State = 891;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case STAR:
EnterOuterAlt(_localctx, 1);
{
- State = 824;
+ State = 882;
Match(STAR);
}
break;
@@ -5219,6 +5503,21 @@ public SelectListContext selectList() {
case TRUE:
case FALSE:
case NULL:
+ 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 PLUS:
case MINUS:
case LPAREN:
case SYSVAR:
@@ -5234,21 +5533,21 @@ public SelectListContext selectList() {
case IDENTIFIER:
EnterOuterAlt(_localctx, 2);
{
- State = 825;
+ State = 883;
selectItem();
- State = 830;
+ State = 888;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 826;
+ State = 884;
Match(COMMA);
- State = 827;
+ State = 885;
selectItem();
}
}
- State = 832;
+ State = 890;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -5320,18 +5619,18 @@ public SelectItemContext selectItem() {
EnterRule(_localctx, 94, RULE_selectItem);
int _la;
try {
- State = 846;
+ State = 904;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,100,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,109,Context) ) {
case 1:
_localctx = new QualifiedStarSelectItemContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 835;
+ State = 893;
((QualifiedStarSelectItemContext)_localctx).qualifier = identifier();
- State = 836;
+ State = 894;
Match(DOT);
- State = 837;
+ State = 895;
Match(STAR);
}
break;
@@ -5339,24 +5638,24 @@ public SelectItemContext selectItem() {
_localctx = new ExpressionSelectItemContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 839;
+ State = 897;
expression(0);
- State = 844;
+ State = 902;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- if (_la==AS || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 7L) != 0)) {
+ if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) {
{
- State = 841;
+ State = 899;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==AS) {
{
- State = 840;
+ State = 898;
Match(AS);
}
}
- State = 843;
+ State = 901;
((ExpressionSelectItemContext)_localctx).alias = identifier();
}
}
@@ -5409,23 +5708,23 @@ public FromClauseContext fromClause() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 848;
+ State = 906;
Match(FROM);
- State = 849;
+ State = 907;
tableSource();
- State = 854;
+ State = 912;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 850;
+ State = 908;
Match(COMMA);
- State = 851;
+ State = 909;
tableSource();
}
}
- State = 856;
+ State = 914;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -5473,19 +5772,19 @@ public TableSourceContext tableSource() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 857;
+ State = 915;
tablePrimary();
- State = 861;
+ State = 919;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 17181933568L) != 0)) {
+ while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 137455468544L) != 0)) {
{
{
- State = 858;
+ State = 916;
joinClause();
}
}
- State = 863;
+ State = 921;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -5572,31 +5871,31 @@ public TablePrimaryContext tablePrimary() {
EnterRule(_localctx, 100, RULE_tablePrimary);
int _la;
try {
- State = 884;
+ State = 942;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,107,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,116,Context) ) {
case 1:
_localctx = new NamedTablePrimaryContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 864;
+ State = 922;
((NamedTablePrimaryContext)_localctx).table = identifier();
- State = 869;
+ State = 927;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- if (_la==AS || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 7L) != 0)) {
+ if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) {
{
- State = 866;
+ State = 924;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==AS) {
{
- State = 865;
+ State = 923;
Match(AS);
}
}
- State = 868;
+ State = 926;
((NamedTablePrimaryContext)_localctx).alias = identifier();
}
}
@@ -5607,28 +5906,28 @@ public TablePrimaryContext tablePrimary() {
_localctx = new SubqueryPrimaryContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 871;
+ State = 929;
Match(LPAREN);
- State = 872;
+ State = 930;
queryExpression();
- State = 873;
+ State = 931;
Match(RPAREN);
- State = 878;
+ State = 936;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
- if (_la==AS || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 7L) != 0)) {
+ if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) {
{
- State = 875;
+ State = 933;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==AS) {
{
- State = 874;
+ State = 932;
Match(AS);
}
}
- State = 877;
+ State = 935;
((SubqueryPrimaryContext)_localctx).alias = identifier();
}
}
@@ -5639,11 +5938,11 @@ public TablePrimaryContext tablePrimary() {
_localctx = new ParenJoinPrimaryContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 880;
+ State = 938;
Match(LPAREN);
- State = 881;
+ State = 939;
tableSource();
- State = 882;
+ State = 940;
Match(RPAREN);
}
break;
@@ -5740,22 +6039,22 @@ public JoinClauseContext joinClause() {
JoinClauseContext _localctx = new JoinClauseContext(Context, State);
EnterRule(_localctx, 102, RULE_joinClause);
try {
- State = 901;
+ State = 959;
ErrorHandler.Sync(this);
- switch ( Interpreter.AdaptivePredict(TokenStream,108,Context) ) {
+ switch ( Interpreter.AdaptivePredict(TokenStream,117,Context) ) {
case 1:
_localctx = new ConditionalJoinContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 886;
+ State = 944;
joinType();
- State = 887;
+ State = 945;
Match(JOIN);
- State = 888;
+ State = 946;
tablePrimary();
- State = 889;
+ State = 947;
Match(ON);
- State = 890;
+ State = 948;
expression(0);
}
break;
@@ -5763,11 +6062,11 @@ public JoinClauseContext joinClause() {
_localctx = new CrossJoinContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 892;
+ State = 950;
Match(CROSS);
- State = 893;
+ State = 951;
Match(JOIN);
- State = 894;
+ State = 952;
tablePrimary();
}
break;
@@ -5775,11 +6074,11 @@ public JoinClauseContext joinClause() {
_localctx = new CrossApplyContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 895;
+ State = 953;
Match(CROSS);
- State = 896;
+ State = 954;
Match(APPLY);
- State = 897;
+ State = 955;
tablePrimary();
}
break;
@@ -5787,11 +6086,11 @@ public JoinClauseContext joinClause() {
_localctx = new OuterApplyContext(_localctx);
EnterOuterAlt(_localctx, 4);
{
- State = 898;
+ State = 956;
Match(OUTER);
- State = 899;
+ State = 957;
Match(APPLY);
- State = 900;
+ State = 958;
tablePrimary();
}
break;
@@ -5870,7 +6169,7 @@ public JoinTypeContext joinType() {
EnterRule(_localctx, 104, RULE_joinType);
int _la;
try {
- State = 918;
+ State = 976;
ErrorHandler.Sync(this);
switch (TokenStream.LA(1)) {
case INNER:
@@ -5878,12 +6177,12 @@ public JoinTypeContext joinType() {
_localctx = new InnerJoinContext(_localctx);
EnterOuterAlt(_localctx, 1);
{
- State = 904;
+ State = 962;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==INNER) {
{
- State = 903;
+ State = 961;
Match(INNER);
}
}
@@ -5894,14 +6193,14 @@ public JoinTypeContext joinType() {
_localctx = new LeftJoinContext(_localctx);
EnterOuterAlt(_localctx, 2);
{
- State = 906;
+ State = 964;
Match(LEFT);
- State = 908;
+ State = 966;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==OUTER) {
{
- State = 907;
+ State = 965;
Match(OUTER);
}
}
@@ -5912,14 +6211,14 @@ public JoinTypeContext joinType() {
_localctx = new RightJoinContext(_localctx);
EnterOuterAlt(_localctx, 3);
{
- State = 910;
+ State = 968;
Match(RIGHT);
- State = 912;
+ State = 970;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==OUTER) {
{
- State = 911;
+ State = 969;
Match(OUTER);
}
}
@@ -5930,14 +6229,14 @@ public JoinTypeContext joinType() {
_localctx = new FullJoinContext(_localctx);
EnterOuterAlt(_localctx, 4);
{
- State = 914;
+ State = 972;
Match(FULL);
- State = 916;
+ State = 974;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==OUTER) {
{
- State = 915;
+ State = 973;
Match(OUTER);
}
}
@@ -5984,9 +6283,9 @@ public WhereClauseContext whereClause() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 920;
+ State = 978;
Match(WHERE);
- State = 921;
+ State = 979;
expression(0);
}
}
@@ -6035,25 +6334,25 @@ public OrderByClauseContext orderByClause() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 923;
+ State = 981;
Match(ORDER);
- State = 924;
+ State = 982;
Match(BY);
- State = 925;
+ State = 983;
orderByItem();
- State = 930;
+ State = 988;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
while (_la==COMMA) {
{
{
- State = 926;
+ State = 984;
Match(COMMA);
- State = 927;
+ State = 985;
orderByItem();
}
}
- State = 932;
+ State = 990;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
}
@@ -6098,14 +6397,14 @@ public OrderByItemContext orderByItem() {
try {
EnterOuterAlt(_localctx, 1);
{
- State = 933;
+ State = 991;
expression(0);
- State = 935;
+ State = 993;
ErrorHandler.Sync(this);
_la = TokenStream.LA(1);
if (_la==ASC || _la==DESC) {
{
- State = 934;
+ State = 992;
_localctx.dir = TokenStream.LT(1);
_la = TokenStream.LA(1);
if ( !(_la==ASC || _la==DESC) ) {
@@ -6145,14 +6444,16 @@ public virtual void CopyFrom(ExpressionContext context) {
}
public partial class AndExprContext : ExpressionContext {
public ExpressionContext left;
+ public IToken op;
public ExpressionContext right;
- [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode AND() { return GetToken(AccessSqlParser.AND, 0); }
[System.Diagnostics.DebuggerNonUserCode] public ExpressionContext[] expression() {
return GetRuleContexts();
}
[System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression(int i) {
return GetRuleContext(i);
}
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode AND() { return GetToken(AccessSqlParser.AND, 0); }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BAND() { return GetToken(AccessSqlParser.BAND, 0); }
public AndExprContext(ExpressionContext context) { CopyFrom(context); }
[System.Diagnostics.DebuggerNonUserCode]
public override TResult Accept(IParseTreeVisitor visitor) {
@@ -6161,6 +6462,25 @@ public override TResult Accept(IParseTreeVisitor visitor) {
else return visitor.VisitChildren(this);
}
}
+ public partial class IntDivExprContext : ExpressionContext {
+ public ExpressionContext left;
+ public IToken op;
+ public ExpressionContext right;
+ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext[] expression() {
+ return GetRuleContexts();
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression(int i) {
+ return GetRuleContext(i);
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BACKSLASH() { return GetToken(AccessSqlParser.BACKSLASH, 0); }
+ public IntDivExprContext(ExpressionContext context) { CopyFrom(context); }
+ [System.Diagnostics.DebuggerNonUserCode]
+ public override TResult Accept(IParseTreeVisitor visitor) {
+ IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor;
+ if (typedVisitor != null) return typedVisitor.VisitIntDivExpr(this);
+ else return visitor.VisitChildren(this);
+ }
+ }
public partial class LikeExprContext : ExpressionContext {
public ExpressionContext left;
public IToken not;
@@ -6222,6 +6542,26 @@ public override TResult Accept(IParseTreeVisitor visitor) {
else return visitor.VisitChildren(this);
}
}
+ public partial class XorExprContext : ExpressionContext {
+ public ExpressionContext left;
+ public IToken op;
+ public ExpressionContext right;
+ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext[] expression() {
+ return GetRuleContexts();
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression(int i) {
+ return GetRuleContext(i);
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode XOR() { return GetToken(AccessSqlParser.XOR, 0); }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BXOR() { return GetToken(AccessSqlParser.BXOR, 0); }
+ public XorExprContext(ExpressionContext context) { CopyFrom(context); }
+ [System.Diagnostics.DebuggerNonUserCode]
+ public override TResult Accept(IParseTreeVisitor visitor) {
+ IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor;
+ if (typedVisitor != null) return typedVisitor.VisitXorExpr(this);
+ else return visitor.VisitChildren(this);
+ }
+ }
public partial class BetweenExprContext : ExpressionContext {
public ExpressionContext val;
public IToken not;
@@ -6244,19 +6584,6 @@ public override TResult Accept(IParseTreeVisitor visitor) {
else return visitor.VisitChildren(this);
}
}
- public partial class BitNotExprContext : ExpressionContext {
- [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BNOT() { return GetToken(AccessSqlParser.BNOT, 0); }
- [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression() {
- return GetRuleContext(0);
- }
- public BitNotExprContext(ExpressionContext context) { CopyFrom(context); }
- [System.Diagnostics.DebuggerNonUserCode]
- public override TResult Accept(IParseTreeVisitor visitor) {
- IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor;
- if (typedVisitor != null) return typedVisitor.VisitBitNotExpr(this);
- else return visitor.VisitChildren(this);
- }
- }
public partial class PowExprContext : ExpressionContext {
public ExpressionContext left;
public ExpressionContext right;
@@ -6277,14 +6604,16 @@ public override TResult Accept(IParseTreeVisitor visitor) {
}
public partial class OrExprContext : ExpressionContext {
public ExpressionContext left;
+ public IToken op;
public ExpressionContext right;
- [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OR() { return GetToken(AccessSqlParser.OR, 0); }
[System.Diagnostics.DebuggerNonUserCode] public ExpressionContext[] expression() {
return GetRuleContexts();
}
[System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression(int i) {
return GetRuleContext(i);
}
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode OR() { return GetToken(AccessSqlParser.OR, 0); }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BOR() { return GetToken(AccessSqlParser.BOR, 0); }
public OrExprContext(ExpressionContext context) { CopyFrom(context); }
[System.Diagnostics.DebuggerNonUserCode]
public override TResult Accept(IParseTreeVisitor visitor) {
@@ -6294,10 +6623,12 @@ public override TResult Accept(IParseTreeVisitor visitor) {
}
}
public partial class NegateExprContext : ExpressionContext {
- [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode MINUS() { return GetToken(AccessSqlParser.MINUS, 0); }
+ public IToken op;
[System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression() {
return GetRuleContext(0);
}
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode MINUS() { return GetToken(AccessSqlParser.MINUS, 0); }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode PLUS() { return GetToken(AccessSqlParser.PLUS, 0); }
public NegateExprContext(ExpressionContext context) { CopyFrom(context); }
[System.Diagnostics.DebuggerNonUserCode]
public override TResult Accept(IParseTreeVisitor visitor) {
@@ -6328,7 +6659,7 @@ public override TResult Accept(IParseTreeVisitor visitor) {
else return visitor.VisitChildren(this);
}
}
- public partial class MulDivExprContext : ExpressionContext {
+ public partial class ConcatExprContext : ExpressionContext {
public ExpressionContext left;
public IToken op;
public ExpressionContext right;
@@ -6338,19 +6669,16 @@ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext[] expression()
[System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression(int i) {
return GetRuleContext(i);
}
- [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode STAR() { return GetToken(AccessSqlParser.STAR, 0); }
- [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode SLASH() { return GetToken(AccessSqlParser.SLASH, 0); }
- [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode MOD() { return GetToken(AccessSqlParser.MOD, 0); }
- [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode BACKSLASH() { return GetToken(AccessSqlParser.BACKSLASH, 0); }
- public MulDivExprContext(ExpressionContext context) { CopyFrom(context); }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode AMP() { return GetToken(AccessSqlParser.AMP, 0); }
+ public ConcatExprContext(ExpressionContext context) { CopyFrom(context); }
[System.Diagnostics.DebuggerNonUserCode]
public override TResult Accept(IParseTreeVisitor visitor) {
IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor;
- if (typedVisitor != null) return typedVisitor.VisitMulDivExpr(this);
+ if (typedVisitor != null) return typedVisitor.VisitConcatExpr(this);
else return visitor.VisitChildren(this);
}
}
- public partial class BitwiseExprContext : ExpressionContext {
+ public partial class MulDivExprContext : ExpressionContext {
public ExpressionContext left;
public IToken op;
public ExpressionContext right;
@@ -6360,18 +6688,36 @@ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext[] expression()
[System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression(int i) {
return GetRuleContext(i);
}
- [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); }
- public BitwiseExprContext(ExpressionContext context) { CopyFrom(context); }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode STAR() { return GetToken(AccessSqlParser.STAR, 0); }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode SLASH() { return GetToken(AccessSqlParser.SLASH, 0); }
+ public MulDivExprContext(ExpressionContext context) { CopyFrom(context); }
[System.Diagnostics.DebuggerNonUserCode]
public override TResult Accept(IParseTreeVisitor visitor) {
IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor;
- if (typedVisitor != null) return typedVisitor.VisitBitwiseExpr(this);
+ if (typedVisitor != null) return typedVisitor.VisitMulDivExpr(this);
else return visitor.VisitChildren(this);
}
}
- public partial class PrimaryExprContext : ExpressionContext {
+ public partial class ImpExprContext : ExpressionContext {
+ public ExpressionContext left;
+ public IToken op;
+ public ExpressionContext right;
+ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext[] expression() {
+ return GetRuleContexts();
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public ExpressionContext expression(int i) {
+ return GetRuleContext(i);
+ }
+ [System.Diagnostics.DebuggerNonUserCode] public ITerminalNode IMP() { return GetToken(AccessSqlParser.IMP, 0); }
+ public ImpExprContext(ExpressionContext context) { CopyFrom(context); }
+ [System.Diagnostics.DebuggerNonUserCode]
+ public override TResult Accept(IParseTreeVisitor visitor) {
+ IAccessSqlVisitor typedVisitor = visitor as IAccessSqlVisitor;
+ if (typedVisitor != null) return typedVisitor.VisitImpExpr(this);
+ else return visitor.VisitChildren(this);
+ }
+ }
+ public partial class PrimaryExprContext : ExpressionContext {
[System.Diagnostics.DebuggerNonUserCode] public PrimaryContext primary() {
return GetRuleContext(0);
}
@@ -6384,10 +6730,12 @@ public override TResult Accept(IParseTreeVisitor