diff --git a/src/LibRed/LibRed.Ado/LibRedCommand.cs b/src/LibRed/LibRed.Ado/LibRedCommand.cs index cf38f1000..5b0e9517a 100644 --- a/src/LibRed/LibRed.Ado/LibRedCommand.cs +++ b/src/LibRed/LibRed.Ado/LibRedCommand.cs @@ -48,11 +48,16 @@ public override void Prepare() { } protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) { + // SchemaOnly asks what the command WOULD return: the reader carries the columns and no rows, and + // nothing runs — not the INSERT in a batch, and not the stored procedure a name stands for. + if (behavior.HasFlag(CommandBehavior.SchemaOnly)) + return new LibRedDataReader(DescribeBatch(), recordsAffected: -1, behavior, Connection); + // Route through Execute so the reader path also handles DML/DDL: EF Core runs inserts through // ExecuteReader and inspects RecordsAffected. A query yields rows (RecordsAffected -1); an // INSERT/CREATE runs and yields an empty result carrying its rows-affected count. Engine.CommandResult result = ExecuteBatch(); - return new LibRedDataReader(result.Rows, result.RecordsAffected); + return new LibRedDataReader(result.Rows, result.RecordsAffected, behavior, Connection); } /// @@ -70,7 +75,7 @@ private Engine.CommandResult ExecuteBatch() IReadOnlyDictionary parameters = BuildParameters(); Engine.CommandResult? last = null; - foreach (string statement in SplitStatements(CommandText)) + foreach (string statement in SplitStatements(StatementText())) { // A fragment holding no statement (only comments) is skipped rather than run: it must not become // the batch's last result, or `INSERT …; -- done` would report the comment's zero rows instead of @@ -104,15 +109,66 @@ private Engine.CommandResult ExecuteBatch() return last ?? new Engine.CommandResult(Engine.Execution.ResultSet.Empty, RecordsAffected: 0); } + /// + /// The shape the command's batch would return, running none of it. As in the + /// batch's result is its last statement's — and since nothing runs, that is the only one worth + /// describing. + /// + private Engine.Execution.ResultSet DescribeBatch() + { + ValidateTransaction(); + Engine.QueryEngine engine = RequireEngine(); + + string? last = SplitStatements(StatementText()).LastOrDefault(s => !engine.IsStatementless(s)); + return last is null + ? Engine.Execution.ResultSet.Empty + : engine.Describe(last, BuildParameters()); + } + + /// + /// The SQL this command runs, which for the two non-text command types is built from the name in + /// : a stored procedure — an Access stored query — is executed by name with each + /// of the command's parameters bound to the procedure's parameter of the same name, and a table is read + /// whole. A name is bracket-quoted, so one containing spaces works as it does in Access. + /// + private string StatementText() => CommandType switch + { + CommandType.Text => CommandText, + CommandType.TableDirect => $"SELECT * FROM {Quote(CommandText)}", + CommandType.StoredProcedure => BuildExecute(), + _ => throw new NotSupportedException($"CommandType.{CommandType} is not supported."), + }; + + /// An EXECUTE for the named stored query, naming each parameter so the order the caller + /// added them in does not matter. A procedure taking none runs bare. + private string BuildExecute() + { + var arguments = _parameters.Cast() + .Where(p => p.Direction is ParameterDirection.Input or ParameterDirection.InputOutput) + .Select(p => $"{Quote(p.ParameterName.TrimStart('@'))} = @{p.ParameterName.TrimStart('@')}") + .ToList(); + + return arguments.Count == 0 + ? $"EXECUTE {Quote(CommandText)}" + : $"EXECUTE {Quote(CommandText)} {string.Join(", ", arguments)}"; + } + + private static string Quote(string name) => $"[{name.Trim().Trim('[', ']')}]"; + /// /// Splits a batch on top-level ; separators, ignoring semicolons inside string literals /// ('…' / "…") and quoted identifiers ([…] / `…`). Blank statements /// (e.g. a trailing ;) are dropped. The single-statement common case returns one item. + /// An Access PARAMETERS …; clause is not a statement of its own: its semicolon + /// ends the clause, and the query it declares for follows. Such a fragment is carried onto the next one + /// so the pair reaches the engine as the one statement it is — the form every stored parameterized + /// query reads back as. /// public static IEnumerable SplitStatements(string sql) { int start = 0; char quote = '\0'; // the closing delimiter we're inside, or '\0' at top level + string prefix = string.Empty; // a PARAMETERS clause awaiting its query for (int i = 0; i < sql.Length; i++) { char c = sql[i]; @@ -125,15 +181,26 @@ public static IEnumerable SplitStatements(string sql) else if (c == ';') { string part = sql[start..i].Trim(); - if (part.Length > 0) yield return part; + if (part.Length > 0) + { + if (IsParametersClause(part)) prefix += part + "; "; + else { yield return prefix + part; prefix = string.Empty; } + } start = i + 1; } } string tail = sql[start..].Trim(); - if (tail.Length > 0) yield return tail; + // A clause with nothing after it is yielded as-is, so the engine reports it rather than the batch + // silently running nothing. + if (tail.Length > 0) yield return prefix + tail; + else if (prefix.Length > 0) yield return prefix.TrimEnd(' ', ';'); } + private static bool IsParametersClause(string statement) => + statement.StartsWith("PARAMETERS", StringComparison.OrdinalIgnoreCase) + && (statement.Length == "PARAMETERS".Length || char.IsWhiteSpace(statement["PARAMETERS".Length])); + private Engine.QueryEngine RequireEngine() => Connection?.Engine ?? throw new InvalidOperationException("Connection is not open."); diff --git a/src/LibRed/LibRed.Ado/LibRedConnection.cs b/src/LibRed/LibRed.Ado/LibRedConnection.cs index e7bde2f35..15d31cf12 100644 --- a/src/LibRed/LibRed.Ado/LibRedConnection.cs +++ b/src/LibRed/LibRed.Ado/LibRedConnection.cs @@ -239,6 +239,23 @@ public override void Close() OnStateChange(new StateChangeEventArgs(ConnectionState.Open, ConnectionState.Closed)); } + /// The names of the metadata collections this provider serves. + public override DataTable GetSchema() => GetSchema(LibRedSchema.MetaDataCollections, null); + + /// + public override DataTable GetSchema(string collectionName) => GetSchema(collectionName, null); + + /// A metadata collection, filtered by . The collections match + /// the ones ACE's OLE DB provider serves, column for column, so code written against that provider reads + /// the same metadata here. A restriction naming a catalog or schema matches everything: a Jet file holds + /// one nameless catalog and no schemas. + public override DataTable GetSchema(string collectionName, string?[]? restrictionValues) + { + if (_database is null || _state != ConnectionState.Open) + throw new InvalidOperationException("The connection must be open to read schema metadata."); + return LibRedSchema.Get(collectionName, restrictionValues, _database); + } + public override void ChangeDatabase(string databaseName) => throw new NotSupportedException("A Jet/ACE connection maps to a single file."); diff --git a/src/LibRed/LibRed.Ado/LibRedDataReader.cs b/src/LibRed/LibRed.Ado/LibRedDataReader.cs index fee4e8f64..74cce1292 100644 --- a/src/LibRed/LibRed.Ado/LibRedDataReader.cs +++ b/src/LibRed/LibRed.Ado/LibRedDataReader.cs @@ -1,26 +1,45 @@ using System.Collections; +using System.Collections.ObjectModel; +using System.Data; using System.Data.Common; using LibRed.Engine.Execution; namespace LibRed.Data; /// Forward-only reader projecting an engine as ADO.NET rows. -public sealed class LibRedDataReader : DbDataReader +public sealed class LibRedDataReader : DbDataReader, IDbColumnSchemaGenerator { private readonly ResultSet _result; private readonly IEnumerator _rows; private readonly int _recordsAffected; + private readonly bool _singleRow; + private readonly LibRedConnection? _ownedConnection; private object?[] _current = []; private bool _pendingFirst; private bool _hadRows; private bool _closed; /// Rows affected for a DML command; -1 for a query (ADO convention). - internal LibRedDataReader(ResultSet result, int recordsAffected = -1) + /// + /// The behavior the command was executed with. Two of its flags reach the reader: SingleRow caps the + /// result at the one row (the constructor has already buffered it, so nothing further is ever read), and + /// CloseConnection hands the reader 's lifetime — closing the reader + /// then closes it, as ACE does. SingleResult is already the only shape this reader has (a batch + /// returns its last statement's rows and NextResult is always false), SequentialAccess asks + /// for a restriction on a row that is already in memory, and the key and base-column information + /// KeyInfo asks for costs a catalog lookup that GetSchemaTable makes anyway — so those three + /// are accepted and need nothing. + /// + /// The connection to close when this reader closes, for CloseConnection. + internal LibRedDataReader( + ResultSet result, int recordsAffected = -1, + CommandBehavior behavior = CommandBehavior.Default, LibRedConnection? connection = null) { _result = result; _rows = result.Rows.GetEnumerator(); _recordsAffected = recordsAffected; + _singleRow = behavior.HasFlag(CommandBehavior.SingleRow); + _ownedConnection = behavior.HasFlag(CommandBehavior.CloseConnection) ? connection : null; // Buffer the first row eagerly so column types (GetFieldType/GetDataTypeName) are available // before the first Read — EF's BufferedDataReader reads that metadata before reading any rows. @@ -44,6 +63,7 @@ internal LibRedDataReader(ResultSet result, int recordsAffected = -1) public override bool Read() { if (_pendingFirst) { _pendingFirst = false; return true; } // yield the pre-buffered first row + if (_singleRow) return false; // that buffered row was the only one asked for if (!_rows.MoveNext()) return false; _current = _rows.Current; return true; @@ -109,7 +129,66 @@ public override Type GetFieldType(int ordinal) : ordinal < _current.Length && _current[ordinal] is { } value ? value.GetType() : typeof(object); } - public override string GetDataTypeName(int ordinal) => GetFieldType(ordinal).Name; + /// + /// The provider's name for the column's type — VarChar, Char, Long, Currency — + /// the same name , and the DataTypes + /// metadata collection give it, so one type has one name across the whole surface. (ACE's OLE DB provider + /// answers this with the OLE DB spelling, DBTYPE_WVARCHAR; its own schema rowsets use these names, + /// and matching them is what keeps this provider self-consistent.) A result with nothing described behind + /// it — a system-variable select, say — falls back to the CLR type's name. + /// + public override string GetDataTypeName(int ordinal) => + _result.Columns[ordinal].TypeName is { Length: > 0 } name ? name : GetFieldType(ordinal).Name; + + /// The result's columns as s: each column's type, and for one read straight + /// from a table the stored column behind it — its table, its own name, and whether it is a key, unique, + /// an AutoNumber or computed. + public ReadOnlyCollection GetColumnSchema() => + new(_result.Columns.Select((c, i) => (DbColumn)new LibRedDbColumn(c, i)).ToList()); + + /// The same description in the older DataTable form, with the column set ADO.NET + /// defines for it. + public override DataTable GetSchemaTable() + { + var table = new DataTable("SchemaTable") { Locale = System.Globalization.CultureInfo.InvariantCulture }; + foreach ((string name, Type type) in new (string, Type)[] + { + (SchemaTableColumn.ColumnName, typeof(string)), (SchemaTableColumn.ColumnOrdinal, typeof(int)), + (SchemaTableColumn.ColumnSize, typeof(int)), (SchemaTableColumn.NumericPrecision, typeof(short)), + (SchemaTableColumn.NumericScale, typeof(short)), (SchemaTableColumn.IsUnique, typeof(bool)), + (SchemaTableColumn.IsKey, typeof(bool)), (SchemaTableOptionalColumn.BaseServerName, typeof(string)), + (SchemaTableOptionalColumn.BaseCatalogName, typeof(string)), (SchemaTableColumn.BaseColumnName, typeof(string)), + (SchemaTableColumn.BaseSchemaName, typeof(string)), (SchemaTableColumn.BaseTableName, typeof(string)), + (SchemaTableColumn.DataType, typeof(Type)), (SchemaTableColumn.AllowDBNull, typeof(bool)), + (SchemaTableColumn.ProviderType, typeof(int)), (SchemaTableColumn.IsAliased, typeof(bool)), + (SchemaTableColumn.IsExpression, typeof(bool)), (SchemaTableOptionalColumn.IsAutoIncrement, typeof(bool)), + (SchemaTableOptionalColumn.IsRowVersion, typeof(bool)), (SchemaTableOptionalColumn.IsHidden, typeof(bool)), + (SchemaTableColumn.IsLong, typeof(bool)), (SchemaTableOptionalColumn.IsReadOnly, typeof(bool)), + ("DataTypeName", typeof(string)), + }) + table.Columns.Add(name, type); + + for (int i = 0; i < _result.Columns.Count; i++) + { + ResultColumn c = _result.Columns[i]; + table.Rows.Add( + c.Name, i, (object?)c.Size ?? DBNull.Value, + c.Precision is { } p ? (short)p : DBNull.Value, c.Scale is { } s ? (short)s : DBNull.Value, + c.IsUnique, c.IsKey, + DBNull.Value, DBNull.Value, // a Jet file has no server or catalog + (object?)c.BaseColumnName ?? DBNull.Value, DBNull.Value, + (object?)c.BaseTableName ?? DBNull.Value, + c.ClrType, c.AllowNull, c.ProviderType, + // Aliased when the query renamed the stored column it reads. + c.BaseColumnName is not null && !string.Equals(c.BaseColumnName, c.Name, StringComparison.OrdinalIgnoreCase), + c.IsExpression, c.IsAutoIncrement, + false, false, // Jet has no rowversion, and hides no column + c.IsLong, c.IsReadOnly, + c.TypeName); + } + + return table; + } public override bool GetBoolean(int ordinal) { @@ -187,12 +266,19 @@ private static void ValidateCopyArguments(long dataOffset, int bufferLength, int public override IEnumerator GetEnumerator() => new DbEnumerator(this, closeReader: false); - public override void Close() => _closed = true; + /// Releases the cursors the rows are read through, and — under + /// — closes the connection they came from. + public override void Close() + { + if (_closed) return; + _closed = true; + _rows.Dispose(); + _ownedConnection?.Close(); + } protected override void Dispose(bool disposing) { - if (disposing) _rows.Dispose(); - Close(); + if (disposing) Close(); base.Dispose(disposing); } } diff --git a/src/LibRed/LibRed.Ado/LibRedDbColumn.cs b/src/LibRed/LibRed.Ado/LibRedDbColumn.cs new file mode 100644 index 000000000..d24be241f --- /dev/null +++ b/src/LibRed/LibRed.Ado/LibRedDbColumn.cs @@ -0,0 +1,49 @@ +using System.Data.Common; +using LibRed.Engine.Execution; + +namespace LibRed.Data; + +/// +/// One column of a result as — the modern half of GetSchemaTable, from the same +/// description. What the engine could not know is left null rather than guessed: a Jet file has no server, +/// catalog or schema, and nothing in it is a row version or a hidden column. +/// +internal sealed class LibRedDbColumn : DbColumn +{ + internal LibRedDbColumn(ResultColumn column, int ordinal) + { + ColumnName = column.Name; + ColumnOrdinal = ordinal; + ColumnSize = column.Size; + NumericPrecision = column.Precision; + NumericScale = column.Scale; + DataType = column.ClrType; + DataTypeName = column.TypeName; + AllowDBNull = column.AllowNull; + BaseColumnName = column.BaseColumnName; + BaseTableName = column.BaseTableName; + IsAliased = column.BaseColumnName is not null + && !string.Equals(column.BaseColumnName, column.Name, StringComparison.OrdinalIgnoreCase); + IsExpression = column.IsExpression; + IsAutoIncrement = column.IsAutoIncrement; + IsKey = column.IsKey; + IsUnique = column.IsUnique; + IsLong = column.IsLong; + IsReadOnly = column.IsReadOnly; + IsIdentity = column.IsAutoIncrement; + // Not applicable to a Jet file rather than unknown: no server, catalog or schema, no row versions, + // and every column a query returns is one the caller asked for. + BaseServerName = null; + BaseCatalogName = null; + BaseSchemaName = null; + IsHidden = false; + ProviderType = column.ProviderType; + } + + /// The OLE DB type code, the same one the Columns schema collection reports. Named as + /// GetSchemaTable names it, and reachable through the indexer as well as directly. + public int ProviderType { get; } + + public override object? this[string property] => + property == nameof(ProviderType) ? ProviderType : base[property]; +} diff --git a/src/LibRed/LibRed.Ado/LibRedSchema.cs b/src/LibRed/LibRed.Ado/LibRedSchema.cs new file mode 100644 index 000000000..e4892c4a1 --- /dev/null +++ b/src/LibRed/LibRed.Ado/LibRedSchema.cs @@ -0,0 +1,299 @@ +using System.Data; +using System.Data.Common; +using LibRed.Engine.Schema; +using LibRed.Sql; + +namespace LibRed.Data; + +/// +/// ADO.NET's metadata collections for an open connection. The catalog collections come from +/// , shaped as ACE's OLE DB provider serves them; the five collections the +/// framework defines (MetaDataCollections, DataSourceInformation, DataTypes, +/// Restrictions, ReservedWords) are built here, describing LibRed itself. +/// +internal static class LibRedSchema +{ + public const string MetaDataCollections = "MetaDataCollections"; + public const string DataSourceInformation = "DataSourceInformation"; + public const string DataTypes = "DataTypes"; + public const string Restrictions = "Restrictions"; + public const string ReservedWords = "ReservedWords"; + + /// Every collection served, the framework's own first, then the catalog ones — the order ACE + /// lists them in. + public static IReadOnlyList Names { get; } = + [MetaDataCollections, DataSourceInformation, DataTypes, Restrictions, ReservedWords, .. SchemaRowsets.Names]; + + /// How many identifier parts a collection's rows are named by, as ACE reports: a column takes + /// catalog, schema, table and column; a table one fewer. + private static readonly Dictionary IdentifierParts = new(StringComparer.OrdinalIgnoreCase) + { + ["Tables"] = 3, ["Columns"] = 4, ["Indexes"] = 4, ["Views"] = 3, ["Procedures"] = 3, + ["ForeignKeys"] = 3, ["PrimaryKeys"] = 3, ["TableConstraints"] = 3, ["KeyColumnUsage"] = 4, + ["ConstraintColumnUsage"] = 4, ["ReferentialConstraints"] = 3, ["CheckConstraints"] = 3, + ["Statistics"] = 3, ["ProcedureParameters"] = 4, ["ViewColumns"] = 4, + }; + + /// Each collection's restrictions, in ACE's order — note that Indexes takes the table name last, + /// after the index name and type. A restriction names the column its value filters. + private static readonly (string Collection, string Restriction)[] RestrictionList = + [ + ("Columns", "TABLE_CATALOG"), ("Columns", "TABLE_SCHEMA"), ("Columns", "TABLE_NAME"), ("Columns", "COLUMN_NAME"), + ("Indexes", "TABLE_CATALOG"), ("Indexes", "TABLE_SCHEMA"), ("Indexes", "INDEX_NAME"), ("Indexes", "TYPE"), + ("Indexes", "TABLE_NAME"), + ("Procedures", "PROCEDURE_CATALOG"), ("Procedures", "PROCEDURE_SCHEMA"), ("Procedures", "PROCEDURE_NAME"), + ("Procedures", "PROCEDURE_TYPE"), + ("Tables", "TABLE_CATALOG"), ("Tables", "TABLE_SCHEMA"), ("Tables", "TABLE_NAME"), ("Tables", "TABLE_TYPE"), + ("Views", "TABLE_CATALOG"), ("Views", "TABLE_SCHEMA"), ("Views", "TABLE_NAME"), + // The relational rowsets, restricted as OLE DB restricts them. + ("ForeignKeys", "PK_TABLE_CATALOG"), ("ForeignKeys", "PK_TABLE_SCHEMA"), ("ForeignKeys", "PK_TABLE_NAME"), + ("ForeignKeys", "FK_TABLE_CATALOG"), ("ForeignKeys", "FK_TABLE_SCHEMA"), ("ForeignKeys", "FK_TABLE_NAME"), + ("PrimaryKeys", "TABLE_CATALOG"), ("PrimaryKeys", "TABLE_SCHEMA"), ("PrimaryKeys", "TABLE_NAME"), + ("TableConstraints", "CONSTRAINT_CATALOG"), ("TableConstraints", "CONSTRAINT_SCHEMA"), + ("TableConstraints", "CONSTRAINT_NAME"), ("TableConstraints", "TABLE_CATALOG"), + ("TableConstraints", "TABLE_SCHEMA"), ("TableConstraints", "TABLE_NAME"), ("TableConstraints", "CONSTRAINT_TYPE"), + ("KeyColumnUsage", "CONSTRAINT_CATALOG"), ("KeyColumnUsage", "CONSTRAINT_SCHEMA"), + ("KeyColumnUsage", "CONSTRAINT_NAME"), ("KeyColumnUsage", "TABLE_CATALOG"), + ("KeyColumnUsage", "TABLE_SCHEMA"), ("KeyColumnUsage", "TABLE_NAME"), ("KeyColumnUsage", "COLUMN_NAME"), + ("ConstraintColumnUsage", "TABLE_CATALOG"), ("ConstraintColumnUsage", "TABLE_SCHEMA"), + ("ConstraintColumnUsage", "TABLE_NAME"), ("ConstraintColumnUsage", "COLUMN_NAME"), + ("ConstraintColumnUsage", "CONSTRAINT_CATALOG"), ("ConstraintColumnUsage", "CONSTRAINT_SCHEMA"), + ("ConstraintColumnUsage", "CONSTRAINT_NAME"), + ("ReferentialConstraints", "CONSTRAINT_CATALOG"), ("ReferentialConstraints", "CONSTRAINT_SCHEMA"), + ("ReferentialConstraints", "CONSTRAINT_NAME"), + ("CheckConstraints", "CONSTRAINT_CATALOG"), ("CheckConstraints", "CONSTRAINT_SCHEMA"), + ("CheckConstraints", "CONSTRAINT_NAME"), + ("Statistics", "TABLE_CATALOG"), ("Statistics", "TABLE_SCHEMA"), ("Statistics", "TABLE_NAME"), + ("ProcedureParameters", "PROCEDURE_CATALOG"), ("ProcedureParameters", "PROCEDURE_SCHEMA"), + ("ProcedureParameters", "PROCEDURE_NAME"), ("ProcedureParameters", "PARAMETER_NAME"), + ("ViewColumns", "VIEW_CATALOG"), ("ViewColumns", "VIEW_SCHEMA"), ("ViewColumns", "VIEW_NAME"), + ("ViewColumns", "COLUMN_NAME"), + ]; + + /// A Jet file holds one nameless catalog and no schemas, so these restrictions match everything + /// rather than filtering on the null every row carries. + private static bool IsCatalogOrSchema(string restriction) => + restriction.EndsWith("_CATALOG", StringComparison.Ordinal) || restriction.EndsWith("_SCHEMA", StringComparison.Ordinal); + + /// Builds a collection, filtered by (a null entry, or none at + /// all, matches everything). + public static DataTable Get(string collection, string?[]? restrictions, JetDatabase database) + { + string name = Names.FirstOrDefault(n => n.Equals(collection, StringComparison.OrdinalIgnoreCase)) + ?? throw new ArgumentException($"There is no metadata collection named '{collection}'.", nameof(collection)); + + var allowed = RestrictionList.Where(r => r.Collection.Equals(name, StringComparison.OrdinalIgnoreCase)).ToList(); + if (restrictions is not null && restrictions.Length > allowed.Count) + throw new ArgumentException( + $"The '{name}' collection takes {allowed.Count} restrictions; {restrictions.Length} were supplied.", + nameof(restrictions)); + + return name switch + { + MetaDataCollections => BuildMetaDataCollections(), + DataSourceInformation => BuildDataSourceInformation(database), + DataTypes => BuildDataTypes(database), + Restrictions => BuildRestrictions(), + ReservedWords => BuildReservedWords(), + _ => BuildCatalogCollection(name, restrictions, database), + }; + } + + private static DataTable BuildCatalogCollection(string name, string?[]? restrictions, JetDatabase database) + { + var table = new DataTable(name) { Locale = System.Globalization.CultureInfo.InvariantCulture }; + IReadOnlyList columns = SchemaRowsets.ColumnsOf(name); + IReadOnlyList types = SchemaRowsets.ColumnTypesOf(name); + for (int i = 0; i < columns.Count; i++) + table.Columns.Add(columns[i], types[i]); + + var allowed = RestrictionList.Where(r => r.Collection.Equals(name, StringComparison.OrdinalIgnoreCase)).ToList(); + foreach (object?[] row in SchemaRowsets.Rows(name, database)) + { + if (!Matches(row, columns, allowed, restrictions)) continue; + table.Rows.Add(row.Select(v => v ?? DBNull.Value).ToArray()); + } + + return table; + } + + /// Whether a row passes every supplied restriction. Values compare as text, case-insensitively, + /// because Jet identifiers are. + private static bool Matches( + object?[] row, IReadOnlyList columns, List<(string Collection, string Restriction)> allowed, string?[]? restrictions) + { + if (restrictions is null) return true; + + for (int i = 0; i < restrictions.Length; i++) + { + if (restrictions[i] is not { } wanted) continue; + string restriction = allowed[i].Restriction; + if (IsCatalogOrSchema(restriction)) continue; + + int column = IndexOf(columns, restriction); + if (column < 0) continue; + if (!string.Equals(row[column]?.ToString(), wanted, StringComparison.OrdinalIgnoreCase)) return false; + } + + return true; + } + + private static int IndexOf(IReadOnlyList columns, string name) + { + for (int i = 0; i < columns.Count; i++) + if (columns[i].Equals(name, StringComparison.OrdinalIgnoreCase)) return i; + return -1; + } + + private static DataTable BuildMetaDataCollections() + { + var table = Empty(MetaDataCollections, + ("CollectionName", typeof(string)), ("NumberOfRestrictions", typeof(int)), ("NumberOfIdentifierParts", typeof(int))); + foreach (string name in Names) + table.Rows.Add(name, + RestrictionList.Count(r => r.Collection.Equals(name, StringComparison.OrdinalIgnoreCase)), + IdentifierParts.GetValueOrDefault(name)); + return table; + } + + private static DataTable BuildRestrictions() + { + var table = Empty(Restrictions, + ("CollectionName", typeof(string)), ("RestrictionName", typeof(string)), + ("RestrictionDefault", typeof(string)), ("RestrictionNumber", typeof(int))); + string? collection = null; + int number = 0; + foreach ((string name, string restriction) in RestrictionList) + { + number = name == collection ? number + 1 : 1; + collection = name; + table.Rows.Add(name, restriction, DBNull.Value, number); + } + return table; + } + + private static DataTable BuildReservedWords() + { + var table = Empty(ReservedWords, ("ReservedWord", typeof(string))); + foreach (string word in SqlKeywords.Reserved) + table.Rows.Add(word); + return table; + } + + private static DataTable BuildDataSourceInformation(JetDatabase database) + { + var table = Empty(DataSourceInformation, + ("CompositeIdentifierSeparatorPattern", typeof(string)), ("DataSourceProductName", typeof(string)), + ("DataSourceProductVersion", typeof(string)), ("DataSourceProductVersionNormalized", typeof(string)), + ("GroupByBehavior", typeof(GroupByBehavior)), ("IdentifierPattern", typeof(string)), + ("IdentifierCase", typeof(IdentifierCase)), ("OrderByColumnsInSelect", typeof(bool)), + ("ParameterMarkerFormat", typeof(string)), ("ParameterMarkerPattern", typeof(string)), + ("ParameterNameMaxLength", typeof(int)), ("ParameterNamePattern", typeof(string)), + ("QuotedIdentifierPattern", typeof(string)), ("QuotedIdentifierCase", typeof(IdentifierCase)), + ("StatementSeparatorPattern", typeof(string)), ("StringLiteralPattern", typeof(string)), + ("SupportedJoinOperators", typeof(SupportedJoinOperators))); + + table.Rows.Add( + DBNull.Value, // no catalog or schema, so nothing separates identifier parts + "LibRed", + FormatName(database.Format.Version), + // Sortable and fixed-width, as the collection requires: the file's format version byte, so a later + // format never sorts below an earlier one. + $"{(byte)database.Format.Version:00}.00.0000", + GroupByBehavior.MustContainAll, + @"[^ ][^\.!`\[\]]*", // as ACE describes a Jet identifier + IdentifierCase.Insensitive, + false, // ORDER BY may name a column the SELECT does not + "?", + @"\?", + 0, // parameters are positional, so a name has no length + DBNull.Value, + "`(([^`]|``)*)`", // backticks, doubled to escape one + IdentifierCase.Insensitive, + ";", + "'(([^']|'')*)'", + SupportedJoinOperators.Inner | SupportedJoinOperators.LeftOuter + | SupportedJoinOperators.RightOuter | SupportedJoinOperators.FullOuter); + return table; + } + + /// The types a column can have, as ACE lists them — its own type names, each with the OLE DB type + /// code, the CLR type it reads as, and the literal syntax — plus the two types ACE's own list never learned. + /// + private static DataTable BuildDataTypes(JetDatabase database) + { + var table = Empty(DataTypes, + ("TypeName", typeof(string)), ("ProviderDbType", typeof(int)), ("ColumnSize", typeof(long)), + ("CreateFormat", typeof(string)), ("CreateParameters", typeof(string)), ("DataType", typeof(string)), + ("IsAutoIncrementable", typeof(bool)), ("IsBestMatch", typeof(bool)), ("IsCaseSensitive", typeof(bool)), + ("IsFixedLength", typeof(bool)), ("IsFixedPrecisionScale", typeof(bool)), ("IsLong", typeof(bool)), + ("IsNullable", typeof(bool)), ("IsSearchable", typeof(bool)), ("IsSearchableWithLike", typeof(bool)), + ("IsUnsigned", typeof(bool)), ("MaximumScale", typeof(short)), ("MinimumScale", typeof(short)), + ("IsConcurrencyType", typeof(bool)), ("IsLiteralSupported", typeof(bool)), + ("LiteralPrefix", typeof(string)), ("LiteralSuffix", typeof(string)), ("NativeDataType", typeof(short))); + + void Add(string name, int providerType, long size, string? createParameters, Type clr, bool autoIncrement, + bool fixedLength, bool fixedPrecisionScale, bool isLong, bool nullable, bool unsigned, + short? maximumScale, short? minimumScale, string? prefix, string? suffix, short nativeType) => + table.Rows.Add(name, providerType, size, DBNull.Value, (object?)createParameters ?? DBNull.Value, + clr.FullName, autoIncrement, DBNull.Value, false, fixedLength, fixedPrecisionScale, isLong, + nullable, true, true, unsigned, + (object?)maximumScale ?? DBNull.Value, (object?)minimumScale ?? DBNull.Value, + DBNull.Value, DBNull.Value, (object?)prefix ?? DBNull.Value, (object?)suffix ?? DBNull.Value, nativeType); + + Add("Short", 2, 5, null, typeof(short), false, true, true, false, true, false, null, null, null, null, 2); + Add("Long", 3, 10, null, typeof(int), true, true, true, false, true, false, null, null, null, null, 3); + Add("Single", 4, 7, null, typeof(float), false, true, false, false, true, false, null, null, null, null, 4); + Add("Double", 5, 15, null, typeof(double), false, true, false, false, true, false, null, null, null, null, 5); + Add("Currency", 6, 19, null, typeof(decimal), false, true, true, false, true, false, null, null, null, null, 6); + Add("DateTime", 7, 8, null, typeof(DateTime), false, true, true, false, true, true, null, null, "#", "#", 7); + Add("Bit", 11, 2, null, typeof(bool), false, true, true, false, false, true, null, null, null, null, 11); + Add("Byte", 17, 3, null, typeof(byte), false, true, true, false, true, true, null, null, null, null, 17); + Add("GUID", 72, 16, null, typeof(Guid), false, true, true, false, true, true, null, null, null, null, 72); + Add("BigBinary", 204, 4000, null, typeof(byte[]), false, false, false, false, true, true, null, null, "0x", null, 128); + Add("LongBinary", 205, 1073741823, null, typeof(byte[]), false, false, true, true, true, true, null, null, "0x", null, 128); + Add("VarBinary", 204, 510, "max length", typeof(byte[]), false, false, true, false, true, true, null, null, "0x", null, 128); + Add("LongText", 203, 536870910, null, typeof(string), false, false, true, true, true, true, null, null, "'", "'", 130); + Add("VarChar", 202, 255, "max length", typeof(string), false, false, true, false, true, true, null, null, "'", "'", 130); + Add("Decimal", 131, 28, "precision,scale", typeof(decimal), false, true, true, false, true, false, 28, 0, null, null, 131); + + // The fixed-length text and binary forms, which ACE's list omits although the engine has both: a + // CHAR(n)/NCHAR(n) column is stored fixed, and so is BINARY(n) — even though ACE then reports the + // binary one as variable. Without these rows a fixed column's TYPE_NAME would have nothing to join to. + Add("Char", 130, 255, "max length", typeof(string), false, true, true, false, true, true, null, null, "'", "'", 130); + Add("Binary", 128, 510, "max length", typeof(byte[]), false, true, true, false, true, true, null, null, "0x", null, 128); + + // BIGINT and DATETIME2 arrived after ACE's OLE DB provider, whose list still omits them although it + // reports columns of both. Any ACCDB can hold them: a file too old for one is raised to the format it + // needs when the column is created, which is what Access itself does. A Jet 3/4 .mdb cannot, so an + // .mdb lists exactly the types ACE does. + if (database.Format.Version >= LibRed.Formats.JetVersion.Version12_2007) + { + Add("BigInt", 20, 19, null, typeof(long), false, true, true, false, true, false, null, null, null, null, 20); + Add("DateTime2", 135, 42, null, typeof(DateTime), false, true, true, false, true, true, null, null, "#", "#", 135); + } + + return table; + } + + /// The engine version the open file's format belongs to, named as Access names it. + private static string FormatName(LibRed.Formats.JetVersion version) => version switch + { + LibRed.Formats.JetVersion.Version3 => "Jet 3 (Access 97)", + LibRed.Formats.JetVersion.Version4 => "Jet 4 (Access 2000-2003)", + LibRed.Formats.JetVersion.Version12_2007 => "ACE 12 (Access 2007)", + LibRed.Formats.JetVersion.Version14_2010 => "ACE 14 (Access 2010)", + LibRed.Formats.JetVersion.Version15_2013 => "ACE 15 (Access 2013)", + LibRed.Formats.JetVersion.Version16_2016 => "ACE 16 (Access 2016)", + LibRed.Formats.JetVersion.Version17_2019 => "ACE 17 (Access 2019)", + _ => version.ToString(), + }; + + private static DataTable Empty(string name, params (string Name, Type Type)[] columns) + { + var table = new DataTable(name) { Locale = System.Globalization.CultureInfo.InvariantCulture }; + foreach ((string column, Type type) in columns) + table.Columns.Add(column, type); + return table; + } +} diff --git a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs index 33dedf897..fbe125d47 100644 --- a/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs +++ b/src/LibRed/LibRed.Core/Catalog/JetCatalog.cs @@ -36,7 +36,7 @@ public sealed class JetCatalog(PageChannel channel, int catalogPage = 2) private List? _relationships; private Dictionary? _views; private Dictionary? _actionQueries; - private Dictionary>? _queryParameters; + private Dictionary>? _queryParameters; private long _seenSchemaGeneration = channel.SchemaGeneration; /// All tables in the database (user and system). @@ -54,7 +54,7 @@ public sealed class JetCatalog(PageChannel channel, int catalogPage = 2) /// A stored query's declared parameter names in declaration order (its Attribute=2 rows). /// Used to bind an EXECUTE proc a, b's positional arguments to the procedure's named parameters. /// Empty for a query with no parameters. - public IReadOnlyDictionary> QueryParameters { get { EnsureFresh(); EnsureStoredQueries(); return _queryParameters!; } } + public IReadOnlyDictionary> QueryParameters { get { EnsureFresh(); EnsureStoredQueries(); return _queryParameters!; } } /// Drops the cached catalog so a freshly created table is picked up on next read. public void Invalidate(bool markChanged = true) @@ -120,6 +120,7 @@ private List LoadTables() || name.StartsWith('#'); TableDef definition = ReadTableDefinition(definitionPage, name, isSystem); + definition.ObjectFlags = flags; // Attach column DefaultValue and table CHECK properties from the extended-properties (LvProp) blob. if (row[lvpropIndex] is byte[] { Length: > 0 } blob) { @@ -199,7 +200,7 @@ private void EnsureStoredQueries() if (_views is not null) return; _views = new Dictionary(StringComparer.OrdinalIgnoreCase); _actionQueries = new Dictionary(StringComparer.OrdinalIgnoreCase); - _queryParameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); + _queryParameters = new Dictionary>(StringComparer.OrdinalIgnoreCase); TableDef? mqDef = FindTable("MSysQueries"); TableDef? objDef = FindTable("MSysObjects"); @@ -208,7 +209,8 @@ private void EnsureStoredQueries() // Group MSysQueries rows by ObjectId. var mq = mqDef.Columns; int oid = ColumnIndex(mq, "ObjectId"), attr = ColumnIndex(mq, "Attribute"), expr = ColumnIndex(mq, "Expression"), - flag = ColumnIndex(mq, "Flag"), n1 = ColumnIndex(mq, "Name1"), n2 = ColumnIndex(mq, "Name2"), order = ColumnIndex(mq, "Order"); + flag = ColumnIndex(mq, "Flag"), n1 = ColumnIndex(mq, "Name1"), n2 = ColumnIndex(mq, "Name2"), order = ColumnIndex(mq, "Order"), + lvExtra = ColumnIndex(mq, "LvExtra"); var byObject = new Dictionary>(); foreach (object?[] row in new Table(_channel, mqDef).Rows()) if (row[oid] is int id) @@ -230,24 +232,40 @@ private void EnsureStoredQueries() object?[]? operation = rows.FirstOrDefault(r => r[attr] is byte b && b == StoredQueryFormat.AttrOperation); short kind = operation?[flag] is short k ? k : StoredQueryFormat.OperationSelect; if (operation is not null && kind != StoredQueryFormat.OperationSelect) - _actionQueries[name] = ReconstructAction(rows, attr, expr, flag, n1, n2, order); - else if (Reconstruct(rows, attr, expr, flag, n1, n2, order) is { } sql) + _actionQueries[name] = ReconstructAction(rows, attr, expr, flag, n1, n2, order, lvExtra); + else if (Reconstruct(rows, attr, expr, flag, n1, n2, order, lvExtra) is { } sql) _views[name] = sql; // The declared parameters (Attribute=2 rows), in declaration order, for EXECUTE positional binding. - var paramNames = rows.Where(r => r[attr] is byte b && b == StoredQueryFormat.AttrParameter) + // Each row's Flag is the parameter's Jet type code (0 for Access's untyped parameter). + var parameters = rows.Where(r => r[attr] is byte b && b == StoredQueryFormat.AttrParameter) .OrderBy(r => r[order] is byte[] ob && ob.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(ob) : 0) - .Select(r => r[n1] as string).Where(s => s is not null).Select(s => s!).ToList(); - if (paramNames.Count > 0) _queryParameters[name] = paramNames; + .Where(r => r[n1] is string) + .Select(r => + { + JetDataType? type = r[flag] is short f and not 0 ? (JetDataType)(byte)f : null; + // The declared facets ride in LvExtra: a length for text, precision and scale packed + // together for a decimal. + var (size, precision, scale) = type is { } t + ? StoredQueryFormat.UnpackParameterFacets(t, r[lvExtra] as int?) + : (null, null, null); + return new StoredQueryParameter((string)r[n1]!, type, size, precision, scale); + }) + .ToList(); + if (parameters.Count > 0) _queryParameters[name] = parameters; } } - /// Rebuilds a stored action query's executable SQL from its MSysQueries rows. Handles the kinds - /// LibRed can execute (CREATE/DROP TABLE, INSERT … VALUES); other kinds return an unsupported reason. - private static StoredActionQuery ReconstructAction(List rows, int attr, int expr, int flag, int n1, int n2, int order) + /// Rebuilds a stored action query's executable SQL from its MSysQueries rows. Handles every kind + /// whose statement LibRed's engine can run — CREATE/DROP TABLE, INSERT (from VALUES or from a SELECT), + /// UPDATE, DELETE and make-table; the rest return an unsupported reason. + private static StoredActionQuery ReconstructAction( + List rows, int attr, int expr, int flag, int n1, int n2, int order, int lvExtra) { static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; + List OfAttr(byte a) => rows.Where(r => r[attr] is byte b && b == a).OrderBy(r => Ord(r[order])).ToList(); + object?[]? action = rows.FirstOrDefault(r => r[attr] is byte b && b == StoredQueryFormat.AttrOperation); if (action is null) return new StoredActionQuery(null, "The stored action query has no action row."); @@ -256,94 +274,119 @@ private static StoredActionQuery ReconstructAction(List rows, int att // The whole DDL statement is in Expression (Access stored it with a leading space). return new StoredActionQuery((action[expr] as string)?.TrimStart(), null); - if (kind == StoredQueryFormat.ActionAppend) + // Every remaining kind keeps its sources, predicate and declared parameters where a SELECT keeps them, + // so they are read the same way. A query with no FROM source at all is an INSERT … VALUES, which has + // none by definition. + var source = BuildFromClause(rows, attr, expr, flag, n1, n2, order); + string? where = source is { } s ? WhereClause(rows, attr, expr, order, s.Extra) : null; + string Where() => where is null ? "" : $" WHERE {where}"; + var columns = OfAttr(StoredQueryFormat.AttrColumn); + string declared = ParametersClause(rows, attr, flag, n1, order, lvExtra); + + switch (kind) { - // INSERT … VALUES: only literal-value columns (Flag 0x8000) and no FROM source. An INSERT … - // SELECT (table rows present / Flag-0 columns) is stored but LibRed does not execute it yet. - if (rows.Any(r => r[attr] is byte b && b == StoredQueryFormat.AttrTable)) - return new StoredActionQuery(null, "INSERT … SELECT stored queries are not executed by LibRed yet."); - var cols = rows.Where(r => r[attr] is byte b && b == StoredQueryFormat.AttrColumn).OrderBy(r => Ord(r[order])).ToList(); - if (cols.Count == 0 || cols.Any(r => r[flag] is short cf && cf != StoredQueryFormat.AppendValueFlag)) - return new StoredActionQuery(null, "This append query shape is not executed by LibRed yet."); - - string target = action[n1] as string ?? ""; - string columns = string.Join(", ", cols.Select(r => $"[{r[n2] as string}]")); - string values = string.Join(", ", cols.Select(r => r[expr] as string ?? "NULL")); - return new StoredActionQuery($"INSERT INTO [{target}] ({columns}) VALUES ({values})", null); + case StoredQueryFormat.ActionAppend: + { + string target = Quote(action[n1] as string ?? ""); + // A literal-value column (Flag 0x8000) is an INSERT … VALUES; a Flag-0 one reads its value + // from the query's own FROM source, which makes it an INSERT … SELECT. Both name the target + // column in Name2 and hold the value's text in Expression. + if (columns.Count == 0) + return new StoredActionQuery(null, "An append query with no columns is not executed by LibRed."); + + string targetColumns = string.Join(", ", columns.Select(r => Quote(r[n2] as string ?? ""))); + string values = string.Join(", ", columns.Select(r => r[expr] as string ?? "NULL")); + + if (columns.All(r => r[flag] is short cf && cf == StoredQueryFormat.AppendValueFlag)) + return source is null + ? new StoredActionQuery($"{declared}INSERT INTO {target} ({targetColumns}) VALUES ({values})", null) + : new StoredActionQuery(null, "An append query cannot take both literal values and a source."); + + return source is { } appendSource + ? new StoredActionQuery( + $"{declared}INSERT INTO {target} ({targetColumns}) SELECT {values} FROM {appendSource.From}{Where()}", null) + : new StoredActionQuery(null, "An append query with no values and no source is not executed by LibRed."); + } + + case StoredQueryFormat.ActionUpdate when source is { } updateSource: + { + // One column row per assignment: Name2 is the target column — qualified when the update runs + // over a join — and Expression is the new value. + if (columns.Count == 0) + return new StoredActionQuery(null, "An update query with no assignments is not executed by LibRed."); + string assignments = string.Join(", ", + columns.Select(r => $"{Qualified(r[n2] as string ?? "")} = {r[expr] as string ?? "NULL"}")); + return new StoredActionQuery($"{declared}UPDATE {updateSource.From} SET {assignments}{Where()}", null); + } + + case StoredQueryFormat.ActionDelete when source is { } deleteSource: + { + // Access writes `DELETE .* FROM …` when the query names the table's columns and + // `DELETE * FROM …` when it doesn't; the column row holds that `
.*` verbatim. + string what = columns.Count > 0 ? columns[0][expr] as string ?? "*" : "*"; + return new StoredActionQuery($"{declared}DELETE {what} FROM {deleteSource.From}{Where()}", null); + } + + case StoredQueryFormat.ActionMakeTable when source is { } intoSource: + { + // The target is on the action row; a target in ANOTHER database file (Name2) is a shape + // LibRed has no statement for. + if (action[n2] is string external && external.Length > 0) + return new StoredActionQuery(null, $"A make-table query writing into '{external}' is not executed by LibRed."); + + string selected = columns.Count == 0 + ? "*" + : string.Join(", ", columns.Select(r => + (r[n1] as string) is { } alias ? $"{r[expr] as string} AS {Quote(alias)}" : r[expr] as string ?? "")); + var groupBy = OfAttr(StoredQueryFormat.AttrGroupBy).Select(r => r[expr] as string ?? "").ToList(); + string grouping = groupBy.Count > 0 ? $" GROUP BY {string.Join(", ", groupBy)}" : ""; + return new StoredActionQuery( + $"{declared}SELECT {selected} INTO {Quote(action[n1] as string ?? "")} FROM {intoSource.From}{Where()}{grouping}", null); + } } // Everything else is stored but not executed. Name the kind: "not supported" that doesn't say what it // is leaves a caller no way to tell an unimplemented feature from an unreadable file. - string what = kind switch + string reason = kind switch { - StoredQueryFormat.ActionUpdate => "UPDATE", - StoredQueryFormat.ActionDelete => "DELETE", - StoredQueryFormat.ActionMakeTable => "Make-table (SELECT … INTO)", - StoredQueryFormat.ActionCrosstab => "Crosstab (TRANSFORM)", - StoredQueryFormat.ActionPassThrough => "Pass-through", - StoredQueryFormat.ActionUnion => "UNION", - _ => $"Kind-{kind}", + StoredQueryFormat.ActionUpdate or StoredQueryFormat.ActionDelete or StoredQueryFormat.ActionMakeTable => + "The stored action query names no table.", + StoredQueryFormat.ActionCrosstab => "Crosstab (TRANSFORM) stored queries are not executed by LibRed yet.", + StoredQueryFormat.ActionPassThrough => "Pass-through stored queries are not executed by LibRed yet.", + StoredQueryFormat.ActionUnion => "UNION stored queries are not executed by LibRed yet.", + _ => $"Kind-{kind} stored queries are not executed by LibRed yet.", }; - return new StoredActionQuery(null, $"{what} stored queries are not executed by LibRed yet."); + return new StoredActionQuery(null, reason); } - /// Rebuilds a simple-SELECT view's SQL from its MSysQueries rows; null if it uses an - /// attribute we don't reconstruct (a non-simple query). - private static string? Reconstruct(List rows, int attr, int expr, int flag, int n1, int n2, int order) + /// Bracket-quotes a stored name, so one with a space in it survives. + private static string Quote(string name) => $"[{name}]"; + + /// Bracket-quotes a name that may already be table-qualified, quoting each part — a stored + /// assignment names its target as Orders.ShipCountry when the update runs over a join, and + /// quoting that whole string would make it one nonexistent column. + private static string Qualified(string name) => + name.Contains('.') ? string.Join(".", name.Split('.').Select(Quote)) : Quote(name); + + /// + /// A stored query's FROM clause, and the join conditions that could not go in it. Access stores a query's + /// sources the same way whatever its kind — one 0x05 row per table and one 0x07 per join + /// condition, flat, with no grouping — so a SELECT, an UPDATE, a DELETE and an append-from-SELECT all + /// rebuild their sources through here. Null when the query names no table at all. + /// + private static (string From, List Extra)? BuildFromClause( + List rows, int attr, int expr, int flag, int n1, int n2, int order) { static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; IEnumerable OfAttr(byte a) => rows.Where(r => r[attr] is byte b && b == a).OrderBy(r => Ord(r[order])); - // Bail out if the query uses attributes beyond a simple SELECT: a HAVING clause (0x0A), a pass-through - // connection string (0x04) or complex-type data (0x0C) all mean this is not a shape we can render. - // AttrOperation is in the list because a SELECT may carry one — the caller has already checked that - // its kind IS SELECT, so reaching here with any other kind is impossible. - var known = new byte[] { StoredQueryFormat.AttrType, StoredQueryFormat.AttrOperation, StoredQueryFormat.AttrParameter, StoredQueryFormat.AttrOption, StoredQueryFormat.AttrTable, StoredQueryFormat.AttrColumn, StoredQueryFormat.AttrJoin, StoredQueryFormat.AttrWhere, StoredQueryFormat.AttrGroupBy, StoredQueryFormat.AttrOrderBy, 0xFF }; - if (rows.Any(r => r[attr] is byte b && !known.Contains(b))) return null; - - // Declared parameters (a stored procedure): each Attribute=2 row is Name1=name, Flag=Jet type code. - // Emitted as a leading PARAMETERS clause so the parser lowers body references to them as parameters. - var parameters = OfAttr(StoredQueryFormat.AttrParameter) - .Select(r => (Name: r[n1] as string, Code: r[flag] is short f ? (byte)f : (byte)0)) - .Where(p => p.Name is not null) - .Select(p => $"[{p.Name}] {AccessTypeName(p.Code)}") - .ToList(); - - // A column row's Name1 (when present) is its output alias. - var columns = OfAttr(StoredQueryFormat.AttrColumn) - .Select(r => (r[n1] as string) is { } a ? $"{r[expr] as string} AS [{a}]" : r[expr] as string ?? "").ToList(); // A derived-table source has its subquery SQL in Expression and no Name1; a named table uses Name1. var tables = OfAttr(StoredQueryFormat.AttrTable) .Select(r => (Table: r[n1] as string ?? "", Alias: r[n2] as string, Sub: r[n1] is null ? r[expr] as string : null)).ToList(); if (tables.Count == 0) return null; - // No column rows at all is Access's "SELECT *" -- the shape every auto-generated form/report - // record-source query takes. Treating it as unreconstructable dropped those queries silently. - if (columns.Count == 0) columns.Add("*"); - - // The option bits are cumulative and can share one row, so test each as a bit across all of them. - short options = 0; - foreach (object?[] r in OfAttr(StoredQueryFormat.AttrOption)) - if (r[flag] is short f) options |= f; - - // DISTINCT and DISTINCTROW are separate bits and separate keywords: DISTINCT dedupes output rows, - // DISTINCTROW dedupes by the underlying contributing rows. Emitting one for the other changes results. - bool distinct = (options & StoredQueryFormat.FlagDistinct) != 0; - bool distinctRow = (options & StoredQueryFormat.FlagDistinctRow) != 0; - // TOP n: an AttrOption row with the TOP bit; the count is in Name1. The PERCENT bit rides alongside - // it (48 = TOP PERCENT), and dropping it turns "TOP 10 PERCENT" into "TOP 10" -- silently wrong. - string? top = OfAttr(StoredQueryFormat.AttrOption) - .Where(r => r[flag] is short f && (f & StoredQueryFormat.FlagTop) != 0) - .Select(r => r[n1] as string).FirstOrDefault(); - bool percent = (options & StoredQueryFormat.FlagPercent) != 0; - // ORDER BY: one AttrOrderBy row per key (Expression = column, Name1 = "d" for descending). - var orderBy = OfAttr(StoredQueryFormat.AttrOrderBy) - .Select(r => (r[expr] as string ?? "") + (string.Equals(r[n1] as string, "d", StringComparison.OrdinalIgnoreCase) ? " DESC" : "")) - .Where(s => s.Length > 0).ToList(); var joins = OfAttr(StoredQueryFormat.AttrJoin) .Select(r => (Cond: r[expr] as string ?? "", Kind: r[flag] is short f ? f : (short)1, Left: r[n1] as string ?? "", Right: r[n2] as string ?? "")).ToList(); - string? where = OfAttr(StoredQueryFormat.AttrWhere).Select(r => r[expr] as string).FirstOrDefault(); - var groupBy = OfAttr(StoredQueryFormat.AttrGroupBy).Select(r => r[expr] as string ?? "").ToList(); static string Ident(string s) => $"[{s}]"; static string Render((string Table, string? Alias, string? Sub) t) => @@ -384,21 +427,111 @@ static string Render((string Table, string? Alias, string? Sub) t) => from.Append($", {Render(t)}"); // Joins whose two tables were both already in scope become extra WHERE conditions (a cyclic graph). - // The stored predicate is only parenthesised when something is being ANDed onto it — wrapping a lone - // predicate adds a paren pair Access never wrote, which is a needless difference from its own SQL. - var extra = pending.Select(j => j.Cond).ToList(); - string? whereClause = - where is null ? (extra.Count > 0 ? string.Join(" AND ", extra) : null) + return (from.ToString(), pending.Select(j => j.Cond).ToList()); + } + + /// The leading PARAMETERS name Type, …; clause a query with declared parameters (its + /// 0x02 rows: Name1=name, Flag=Jet type code) is rebuilt with, so the parser lowers references to + /// those names in the body into engine parameters; empty for a query declaring none. An action query + /// declares them exactly as a SELECT does. + private static string ParametersClause(List rows, int attr, int flag, int n1, int order, int lvExtra) + { + static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; + var parameters = rows + .Where(r => r[attr] is byte b && b == StoredQueryFormat.AttrParameter) + .OrderBy(r => Ord(r[order])) + .Select(r => (Name: r[n1] as string, Code: r[flag] is short f ? (byte)f : (byte)0, Extra: r[lvExtra] as int?)) + .Where(p => p.Name is not null) + .Select(p => $"[{p.Name}] {AccessTypeName(p.Code)}{Facets(p.Code, p.Extra)}") + .ToList(); + + return parameters.Count == 0 ? "" : $"PARAMETERS {string.Join(", ", parameters)}; "; + } + + /// A declared parameter's facets as the type name's suffix — (50) for a text length, + /// (18,4) for a decimal's precision and scale — so the rebuilt clause declares what was declared. + /// Empty where the row records none. + private static string Facets(byte code, int? lvExtra) + { + if (code == 0) return ""; // the untyped parameter, rendered as the keyword Value + var (size, precision, scale) = StoredQueryFormat.UnpackParameterFacets((JetDataType)code, lvExtra); + return size is { } length ? $"({length})" + : precision is { } p ? $"({p},{scale ?? 0})" + : ""; + } + + /// The WHERE clause: the query's stored predicate (the 0x08 row) and any join condition + /// could not place, ANDed together. The stored predicate is only + /// parenthesised when something is being ANDed onto it — wrapping a lone predicate adds a paren pair + /// Access never wrote, which is a needless difference from its own SQL. + private static string? WhereClause(List rows, int attr, int expr, int order, List extra) + { + static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; + string? where = rows + .Where(r => r[attr] is byte b && b == StoredQueryFormat.AttrWhere) + .OrderBy(r => Ord(r[order])) + .Select(r => r[expr] as string).FirstOrDefault(); + + return where is null ? (extra.Count > 0 ? string.Join(" AND ", extra) : null) : extra.Count == 0 ? where : string.Join(" AND ", extra.Prepend($"({where})")); + } + + /// Rebuilds a simple-SELECT view's SQL from its MSysQueries rows; null if it uses an + /// attribute we don't reconstruct (a non-simple query). + private static string? Reconstruct( + List rows, int attr, int expr, int flag, int n1, int n2, int order, int lvExtra) + { + static int Ord(object? v) => v is byte[] b && b.Length >= 4 ? System.Buffers.Binary.BinaryPrimitives.ReadInt32BigEndian(b) : 0; + IEnumerable OfAttr(byte a) => rows.Where(r => r[attr] is byte b && b == a).OrderBy(r => Ord(r[order])); + + // Bail out if the query uses attributes beyond a simple SELECT: a HAVING clause (0x0A), a pass-through + // connection string (0x04) or complex-type data (0x0C) all mean this is not a shape we can render. + // AttrOperation is in the list because a SELECT may carry one — the caller has already checked that + // its kind IS SELECT, so reaching here with any other kind is impossible. + var known = new byte[] { StoredQueryFormat.AttrType, StoredQueryFormat.AttrOperation, StoredQueryFormat.AttrParameter, StoredQueryFormat.AttrOption, StoredQueryFormat.AttrTable, StoredQueryFormat.AttrColumn, StoredQueryFormat.AttrJoin, StoredQueryFormat.AttrWhere, StoredQueryFormat.AttrGroupBy, StoredQueryFormat.AttrOrderBy, 0xFF }; + if (rows.Any(r => r[attr] is byte b && !known.Contains(b))) return null; + + // A column row's Name1 (when present) is its output alias. + var columns = OfAttr(StoredQueryFormat.AttrColumn) + .Select(r => (r[n1] as string) is { } a ? $"{r[expr] as string} AS [{a}]" : r[expr] as string ?? "").ToList(); + // A query with no table rows has no FROM clause, which Access allows and stores this way (`SELECT 1 + // AS n`). One with neither tables nor columns says nothing at all, and is not a query we can rebuild. + var source = BuildFromClause(rows, attr, expr, flag, n1, n2, order); + if (source is null && columns.Count == 0) return null; + // No column rows at all is Access's "SELECT *" -- the shape every auto-generated form/report + // record-source query takes. Treating it as unreconstructable dropped those queries silently. + if (columns.Count == 0) columns.Add("*"); + + // The option bits are cumulative and can share one row, so test each as a bit across all of them. + short options = 0; + foreach (object?[] r in OfAttr(StoredQueryFormat.AttrOption)) + if (r[flag] is short f) options |= f; + + // DISTINCT and DISTINCTROW are separate bits and separate keywords: DISTINCT dedupes output rows, + // DISTINCTROW dedupes by the underlying contributing rows. Emitting one for the other changes results. + bool distinct = (options & StoredQueryFormat.FlagDistinct) != 0; + bool distinctRow = (options & StoredQueryFormat.FlagDistinctRow) != 0; + // TOP n: an AttrOption row with the TOP bit; the count is in Name1. The PERCENT bit rides alongside + // it (48 = TOP PERCENT), and dropping it turns "TOP 10 PERCENT" into "TOP 10" -- silently wrong. + string? top = OfAttr(StoredQueryFormat.AttrOption) + .Where(r => r[flag] is short f && (f & StoredQueryFormat.FlagTop) != 0) + .Select(r => r[n1] as string).FirstOrDefault(); + bool percent = (options & StoredQueryFormat.FlagPercent) != 0; + // ORDER BY: one AttrOrderBy row per key (Expression = column, Name1 = "d" for descending). + var orderBy = OfAttr(StoredQueryFormat.AttrOrderBy) + .Select(r => (r[expr] as string ?? "") + (string.Equals(r[n1] as string, "d", StringComparison.OrdinalIgnoreCase) ? " DESC" : "")) + .Where(s => s.Length > 0).ToList(); + var groupBy = OfAttr(StoredQueryFormat.AttrGroupBy).Select(r => r[expr] as string ?? "").ToList(); + string? whereClause = WhereClause(rows, attr, expr, order, source?.Extra ?? []); - var sql = new System.Text.StringBuilder(); - if (parameters.Count > 0) sql.Append("PARAMETERS ").Append(string.Join(", ", parameters)).Append("; "); + var sql = new System.Text.StringBuilder(ParametersClause(rows, attr, flag, n1, order, lvExtra)); sql.Append("SELECT "); if (distinctRow) sql.Append("DISTINCTROW "); else if (distinct) sql.Append("DISTINCT "); if (top is not null) sql.Append("TOP ").Append(top).Append(percent ? " PERCENT " : " "); - sql.Append(string.Join(", ", columns)).Append(" FROM ").Append(from); + sql.Append(string.Join(", ", columns)); + if (source is { } from) sql.Append(" FROM ").Append(from.From); if (whereClause is not null) sql.Append(" WHERE ").Append(whereClause); if (groupBy.Count > 0) sql.Append(" GROUP BY ").Append(string.Join(", ", groupBy)); if (orderBy.Count > 0) sql.Append(" ORDER BY ").Append(string.Join(", ", orderBy)); @@ -444,6 +577,8 @@ private TableDef ReadTableDefinition(int definitionPage, string name, bool isSys DefinitionPage = definitionPage, Columns = tdef.Columns, Indexes = tdef.Indexes, + LogicalIndexes = tdef.LogicalIndexes, + RowCount = tdef.RowCount, VariableColumnCount = tdef.VariableColumnCount, ComplexAutoNumber = tdef.ComplexAutoNumber, IsSystem = isSystem, diff --git a/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs b/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs new file mode 100644 index 000000000..fa97f6b96 --- /dev/null +++ b/src/LibRed/LibRed.Core/Catalog/LogicalIndexDef.cs @@ -0,0 +1,28 @@ +namespace LibRed.Catalog; + +/// +/// One logical index: a name in the TDEF's logical-index list, pointing at the real index that stores its +/// entries. Several logical indexes share one real index — a foreign key's relationship name, the index the +/// designer named, and a primary key can all sit over the same B-tree — so (one per +/// real index) cannot represent them all. Access's own schema views list every logical index, which is why +/// the names are kept rather than collapsed to the one that wins. +/// +/// The logical index's name. +/// Which real index () stores it. +/// True when the entry is a foreign-key relationship rather than a named index. +/// True when this entry is the table's primary key. +/// The relationship's direction as the info block records it: 0 none, 1 incoming +/// (this table is the parent — the half Access names .r… and keeps out of its schema views), 2 outgoing +/// (this table holds the foreign key). +public sealed record LogicalIndexDef( + string Name, int RealIndexOrdinal, bool IsRelationship, bool IsPrimaryKey, byte ForeignKeyType) +{ + /// True for the parent side of a relationship, which Access hides. + public bool IsIncomingRelationship => ForeignKeyType == IncomingRelationship; + + /// of the parent side. + public const byte IncomingRelationship = 1; + + /// of the child side, the table holding the foreign key. + public const byte OutgoingRelationship = 2; +} diff --git a/src/LibRed/LibRed.Core/Catalog/TableDef.cs b/src/LibRed/LibRed.Core/Catalog/TableDef.cs index 5c6e4695b..36ec18a4a 100644 --- a/src/LibRed/LibRed.Core/Catalog/TableDef.cs +++ b/src/LibRed/LibRed.Core/Catalog/TableDef.cs @@ -15,6 +15,11 @@ public sealed class TableDef public IReadOnlyList Columns { get; init; } = []; public IReadOnlyList Indexes { get; init; } = []; + /// Every name the TDEF gives an index, including the relationship names that share a real index + /// with a named one — see . holds one entry per real + /// index and so carries only the name that won. + public IReadOnlyList LogicalIndexes { get; init; } = []; + /// /// The variable-column count from the TDEF header (0x2B) — a high-water mark, not a live /// count. It never decrements on DROP COLUMN, so it exceeds the number of variable columns still present @@ -45,9 +50,20 @@ public sealed class TableDef /// public string? ValidationText { get; internal set; } + /// The row count the TDEF header carries (0x10), which the engine keeps current as rows are + /// inserted and deleted. Reported as the table's cardinality in schema metadata, as Access reports it. + public int RowCount { get; init; } + /// True for the MSys* system tables. public bool IsSystem { get; init; } + /// The object's raw MSysObjects.Flags, kept as read so callers can classify an object the + /// way Access does rather than by name — the system bit (0x80000000), the hidden bit (0x08) + /// and the bits that keep an object out of the schema rowsets altogether are all in here. Set by the + /// catalog after the definition is decoded; zero for a table built without one (a test's synthetic + /// definition, say). + public uint ObjectFlags { get; internal set; } + public ColumnDef? FindColumn(string name) => Columns.FirstOrDefault(c => string.Equals(c.Name, name, StringComparison.OrdinalIgnoreCase)); } diff --git a/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs b/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs index 3570c63fd..0bed843fc 100644 --- a/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs +++ b/src/LibRed/LibRed.Core/Catalog/ViewSpec.cs @@ -16,9 +16,14 @@ public sealed record ViewJoinSpec(ViewJoinType Kind, string Condition, string Le /// row Expression + Name1). public sealed record ViewColumnSpec(string Expression, string? Alias); -/// A declared parameter of a stored (procedure) query: its name and Jet type code, stored as an -/// MSysQueries Attribute=2 row (Name1 = name, Flag = ). -public sealed record ViewParameterSpec(string Name, byte TypeCode); +/// +/// A declared parameter of a stored (procedure) query: its name and Jet type code, stored as an MSysQueries +/// Attribute=2 row (Name1 = name, Flag = ). The row's LvExtra carries +/// the declared facets, and Access renders the query's PARAMETERS clause from it — see +/// for which types have one and how a decimal's precision +/// and scale pack into the single value. +/// +public sealed record ViewParameterSpec(string Name, byte TypeCode, int? Size = null, int? Scale = null); /// An ORDER BY key: verbatim sort expression + direction, stored as an MSysQueries /// Attribute=0x0B row (Expression = the column, Name1 = "d" when ). @@ -30,29 +35,55 @@ public enum ActionQueryKind { /// CREATE TABLE / DROP TABLE etc. — the whole SQL text is stored verbatim. DataDefinition, - /// INSERT INTO … — a target table plus the appended columns. + /// INSERT INTO … — a target table plus the appended columns, from VALUES or from a SELECT. Append, + /// UPDATE … SET — the assignments in , over the + /// 's sources. + Update, + /// DELETE — the 's sources and WHERE, and optionally the + /// table.* target Access stores when the query names one. + Delete, + /// SELECT … INTO — a SELECT whose target table is on the action row. + MakeTable, } -/// One appended column of an INSERT query: the target column and the verbatim value/source -/// expression, stored as an Attribute=0x06 row (Name2 = column, Expression = value). +/// One column/value pair of an action query, stored as an Attribute=0x06 row (Name2 = the +/// column, Expression = the value): an appended column of an INSERT — whose value is a literal for the VALUES +/// form and a source expression for the SELECT form — or one assignment of an UPDATE, whose +/// is table-qualified when the update runs over a join. public sealed record AppendColumnSpec(string Column, string ValueExpression); -/// A stored action query (a CREATE PROCEDURE body that is not a SELECT). A -/// query carries its whole ; an -/// query carries a and its appended -/// (VALUES mode — literal expressions per column). +/// +/// A stored action query (a CREATE PROCEDURE body that is not a plain SELECT). A +/// query carries its whole and nothing +/// else — Access does not decompose it. Every other kind stores its sources, joins and WHERE exactly as a view +/// does, in , and differs only in the action row and in what its column rows mean: +/// holds an append's columns or an update's assignments, +/// the table an append or make-table writes into, and the body's own columns are a make-table's output list. +/// are declared exactly as a parameterized SELECT declares them. +/// public sealed record ActionQuerySpec( ActionQueryKind Kind, string? DdlSql = null, string? TargetTable = null, - IReadOnlyList? Values = null); + IReadOnlyList? Values = null, + ViewSpec? Body = null, + IReadOnlyList? Parameters = null, + string? DeleteTarget = null); /// A stored action query read back from the catalog. is the reconstructed, /// executable statement when LibRed supports the kind; otherwise it is null and /// explains why executing it throws. public sealed record StoredActionQuery(string? Sql, string? UnsupportedReason); +/// A parameter a stored query declares, in declaration order. is the Jet type +/// Access recorded for it, or null for its untyped parameter — the one Access renders as the keyword +/// Value. is a text parameter's declared length and +/// / a decimal's, all read back from the row's +/// LvExtra; they are null for a type that declares none. +public sealed record StoredQueryParameter( + string Name, JetDataType? Type, int? Size = null, int? Precision = null, int? Scale = null); + /// /// A view's decomposed "simple SELECT" — the columns, source tables, joins and WHERE (all verbatim text) — /// that Access stores as MSysQueries rows. Aggregates / GROUP BY / HAVING / ORDER BY are not permitted. diff --git a/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs b/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs index ebe768d04..20ef22fba 100644 --- a/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs +++ b/src/LibRed/LibRed.Core/Formats/StoredQueryFormat.cs @@ -1,3 +1,5 @@ +using LibRed.Catalog; + namespace LibRed.Formats; /// @@ -56,4 +58,27 @@ internal static class StoredQueryFormat /// AttrColumn Flag bit marking an appended literal value. public const short AppendValueFlag = unchecked((short)0x8000); + + /// + /// The LvExtra value for a declared parameter's facets, or null for a type that carries none. + /// Measured against ACE: a Text(50) parameter stores the length, 50; a Decimal(18,4) packs + /// both into one value, (scale << 16) | precision = 262162; and a sized Binary(10) + /// stores nothing at all, as every type without a declared size does. Access renders the query's + /// PARAMETERS clause from this, so a parameter with no value here reads back as Text(255). + /// + public static int? PackParameterFacets(JetDataType type, int? size, int? scale) => type switch + { + JetDataType.Text => size, + JetDataType.FixedPoint when size is { } precision => (scale ?? 0) << 16 | precision & 0xFFFF, + _ => null, + }; + + /// A declared parameter's facets back out of its LvExtra — the inverse of + /// . A text parameter reports its length as the size; a decimal reports + /// the precision and scale packed into the one value. + public static (int? Size, int? Precision, int? Scale) UnpackParameterFacets(JetDataType type, int? lvExtra) => + lvExtra is not { } packed ? (null, null, null) + : type == JetDataType.FixedPoint ? (null, packed & 0xFFFF, packed >> 16) + : type == JetDataType.Text ? (packed, null, null) + : (null, null, null); } diff --git a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs index 004715f80..452afc1de 100644 --- a/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs +++ b/src/LibRed/LibRed.Core/Pages/TableDefinitionPage.cs @@ -40,6 +40,11 @@ public sealed class TableDefinitionPage : Page private readonly List _indexes = []; public IReadOnlyList Indexes => _indexes; + private readonly List _logicalIndexes = []; + /// Every logical index, in the order the TDEF lists them — including the relationship names that + /// share a real index with a named one, which keeps only one of. + public IReadOnlyList LogicalIndexes => _logicalIndexes; + private readonly Dictionary _longValueOwnedMaps = []; /// Per long-value (memo/OLE) column id → its owned-pages usage-map pointer (record row + /// page), from the §3.3.2 list after the index names. Used to record a newly allocated LVAL page. @@ -236,25 +241,30 @@ private void ReadLongValueMaps(PageBuffer buffer, int pos) private int ResolveIndexNames(PageBuffer buffer, int infoStart) { int logicalCount = LogicalIndexCount; // 0x2F — the logical-index (slot) count - var info = new (int DataNumber, bool IsRelationship, byte Type)[logicalCount]; + var info = new (int DataNumber, bool IsRelationship, byte Type, byte FkType)[logicalCount]; for (int i = 0; i < logicalCount; i++) { int block = infoStart + i * IndexBlockFormat.InfoBlockSize; info[i] = ( buffer.ReadInt32(block + IndexBlockFormat.InfoDataNumberOffset), buffer.ReadInt32(block + IndexBlockFormat.InfoFkTablePageOffset) != 0, - buffer.ReadByte(block + IndexBlockFormat.InfoTypeOffset)); + buffer.ReadByte(block + IndexBlockFormat.InfoTypeOffset), + buffer.ReadByte(block + IndexBlockFormat.InfoFkTypeOffset)); } int namePos = infoStart + logicalCount * IndexBlockFormat.InfoBlockSize; var priority = new int[_indexes.Count]; + _logicalIndexes.Clear(); for (int i = 0; i < logicalCount; i++) { (string name, namePos) = ReadName(buffer, namePos, $"logical index {i}"); - (int dataNumber, bool isRelationship, byte type) = info[i]; + (int dataNumber, bool isRelationship, byte type, byte fkType) = info[i]; if (dataNumber < 0 || dataNumber >= _indexes.Count) continue; + _logicalIndexes.Add(new LogicalIndexDef(name, dataNumber, isRelationship, + !isRelationship && type == IndexBlockFormat.TypePrimary, fkType)); + // Prefer a real index name over a relationship's; prefer the primary among real ones. int p = isRelationship ? 1 : type == IndexBlockFormat.TypePrimary ? 3 : 2; if (p > priority[dataNumber]) diff --git a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs index be6b09454..fc125c4f2 100644 --- a/src/LibRed/LibRed.Core/Storage/ViewCreator.cs +++ b/src/LibRed/LibRed.Core/Storage/ViewCreator.cs @@ -13,8 +13,15 @@ namespace LibRed.Storage; /// public sealed class ViewCreator(PageChannel channel, JetCatalog catalog) { - private const int ViewFlags = 0x10000000; // a SELECT query / view - private const int AppendFlags = 0x10000040; // an INSERT (append) query + // MSysObjects.Flags for a stored query: 0x10000000 plus the kind, and the kind byte is DAO's own + // QueryDef.Type value (verified vs ACE for all six: crosstab 16, delete 32, update 48, append 64, + // make-table 80, data-definition 96) — not the Flag the MSysQueries action row carries, which numbers + // the kinds differently. + private const int ViewFlags = 0x10000000; // a SELECT query / view (DAO type 0) + private const int DeleteFlags = 0x10000020; + private const int UpdateFlags = 0x10000030; + private const int AppendFlags = 0x10000040; // an INSERT (append) query + private const int MakeTableFlags = 0x10000050; private const int DataDefinitionFlags = 0x10000060; // a CREATE/DROP TABLE (data-definition) query private static readonly byte[] DefaultOwner = [0x69, 0x0C]; private static readonly byte[] AdminSid = [0x68, 0x0C]; @@ -42,7 +49,15 @@ public void Create(string name, ViewSpec spec) /// Persists a stored action query (a non-SELECT CREATE PROCEDURE body) byte-faithfully. public void CreateAction(string name, ActionQuerySpec spec) { - int flags = spec.Kind == ActionQueryKind.DataDefinition ? DataDefinitionFlags : AppendFlags; + int flags = spec.Kind switch + { + ActionQueryKind.DataDefinition => DataDefinitionFlags, + ActionQueryKind.Append => AppendFlags, + ActionQueryKind.Update => UpdateFlags, + ActionQueryKind.Delete => DeleteFlags, + ActionQueryKind.MakeTable => MakeTableFlags, + _ => throw new NotSupportedException($"Action query kind {spec.Kind} is not stored yet."), + }; int objectId = AllocateQueryObject(name, flags); AddActionRows(objectId, spec); } @@ -135,21 +150,112 @@ private void AddActionRows(int objectId, ActionQuerySpec spec) Row(mq, objectId, StoredQueryFormat.AttrType, order: 1, flag: StoredQueryFormat.QueryTypeSelect); Row(mq, objectId, StoredQueryFormat.AttrEnd, order: 1); + AddParameterRows(mq, objectId, spec.Parameters); + if (spec.Kind == ActionQueryKind.DataDefinition) { // The whole DDL statement is stored verbatim in one row; Access records it with a leading space. - Row(mq, objectId, StoredQueryFormat.AttrOperation, order: 1, flag: StoredQueryFormat.ActionDdl, expression: " " + spec.DdlSql); + // Nothing is decomposed: a data-definition query has no sources, columns or predicate. + Row(mq, objectId, StoredQueryFormat.AttrOperation, order: 1, flag: StoredQueryFormat.ActionDdl, + expression: " " + spec.DdlSql); + return; } - else + + // The action row carries the kind, and the target table for the two kinds that write into one. + short kind = spec.Kind switch + { + ActionQueryKind.Append => StoredQueryFormat.ActionAppend, + ActionQueryKind.Update => StoredQueryFormat.ActionUpdate, + ActionQueryKind.Delete => StoredQueryFormat.ActionDelete, + ActionQueryKind.MakeTable => StoredQueryFormat.ActionMakeTable, + _ => throw new NotSupportedException($"Action query kind {spec.Kind} is not stored yet."), + }; + Row(mq, objectId, StoredQueryFormat.AttrOperation, order: 1, flag: kind, + name1: spec.Kind is ActionQueryKind.Append or ActionQueryKind.MakeTable ? spec.TargetTable : null); + + // Sources first: Access processes the rows in order, and a derived-table source defines an alias the + // column expressions reference (the same reason the view path writes tables before columns). + AddSourceRows(mq, objectId, spec.Body); + + var values = spec.Values ?? []; + switch (spec.Kind) { - Row(mq, objectId, StoredQueryFormat.AttrOperation, order: 1, flag: StoredQueryFormat.ActionAppend, name1: spec.TargetTable); - // Each appended column: Name2 = target column, Expression = the (literal) value; the 0x8000 flag - // marks a VALUES append (as opposed to an INSERT … SELECT, whose columns carry Flag 0). - var values = spec.Values ?? []; - for (int i = 0; i < values.Count; i++) - Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, flag: StoredQueryFormat.AppendValueFlag, - expression: values[i].ValueExpression, name2: values[i].Column); + case ActionQueryKind.Append: + // Name2 = target column, Expression = the value; the 0x8000 flag marks an INSERT … VALUES, + // where a column fed by the query's own source carries Flag 0. + for (int i = 0; i < values.Count; i++) + Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, + flag: spec.Body is null ? StoredQueryFormat.AppendValueFlag : (short)0, + expression: values[i].ValueExpression, name2: values[i].Column); + break; + + case ActionQueryKind.Update: + // One row per SET assignment, stored exactly as an append's columns are. + for (int i = 0; i < values.Count; i++) + Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, flag: 0, + expression: values[i].ValueExpression, name2: values[i].Column); + break; + + case ActionQueryKind.Delete: + // `DELETE t.* FROM …` keeps that target verbatim in a single column row; `DELETE FROM …` + // stores no column row at all, and Access renders it back as `DELETE * FROM …`. + if (spec.DeleteTarget is { } target) + Row(mq, objectId, StoredQueryFormat.AttrColumn, order: 1, flag: 0, expression: target); + break; + + case ActionQueryKind.MakeTable: + // An ordinary output list: Expression = the column, Name1 = its alias. + var columns = spec.Body?.Columns ?? []; + for (int i = 0; i < columns.Count; i++) + Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, flag: 0, + expression: columns[i].Expression, name1: columns[i].Alias); + break; } + + AddJoinAndWhereRows(mq, objectId, spec.Body); + } + + /// The 0x02 parameter rows, in declaration order — written identically for a view and for + /// an action query. + private void AddParameterRows(TableDef mq, int objectId, IReadOnlyList? parameters) + { + for (int i = 0; i < (parameters?.Count ?? 0); i++) + { + ViewParameterSpec p = parameters![i]; + Row(mq, objectId, StoredQueryFormat.AttrParameter, order: i + 1, + flag: p.TypeCode, name1: p.Name, + lvExtra: StoredQueryFormat.PackParameterFacets((JetDataType)p.TypeCode, p.Size, p.Scale)); + } + } + + /// The 0x05 FROM rows: a named table in Name1 (alias in Name2), or a derived table whose + /// subquery SQL goes in Expression with Name1 empty. + private void AddSourceRows(TableDef mq, int objectId, ViewSpec? body) + { + var tables = body?.Tables ?? []; + for (int i = 0; i < tables.Count; i++) + { + ViewTableSpec t = tables[i]; + if (t.SubquerySql is { } sub) + Row(mq, objectId, StoredQueryFormat.AttrTable, order: i + 1, expression: sub, name2: t.Alias); + else + Row(mq, objectId, StoredQueryFormat.AttrTable, order: i + 1, name1: t.Table, name2: t.Alias); + } + } + + /// The 0x07 join rows (condition, kind, and the two tables the condition names) and the + /// single 0x08 WHERE row. + private void AddJoinAndWhereRows(TableDef mq, int objectId, ViewSpec? body) + { + var joins = body?.Joins ?? []; + for (int i = 0; i < joins.Count; i++) + { + ViewJoinSpec j = joins[i]; + Row(mq, objectId, StoredQueryFormat.AttrJoin, order: i + 1, flag: (short)j.Kind, + expression: j.Condition, name1: j.LeftAlias, name2: j.RightAlias); + } + if (body?.Where is { } where) + Row(mq, objectId, StoredQueryFormat.AttrWhere, order: 1, expression: where); } private void AddQueryRows(int objectId, ViewSpec spec) @@ -165,9 +271,7 @@ private void AddQueryRows(int objectId, ViewSpec spec) Row(mq, objectId, StoredQueryFormat.AttrType, order: 1, flag: StoredQueryFormat.QueryTypeSelect); Row(mq, objectId, StoredQueryFormat.AttrEnd, order: 1); // Declared parameters (CREATE PROCEDURE) come right after the End row, before the tables. - for (int i = 0; i < (spec.Parameters?.Count ?? 0); i++) - Row(mq, objectId, StoredQueryFormat.AttrParameter, order: i + 1, - flag: spec.Parameters![i].TypeCode, name1: spec.Parameters[i].Name); + AddParameterRows(mq, objectId, spec.Parameters); // DISTINCT and TOP are both StoredQueryFormat.AttrOption (0x03) rows, distinguished by their Flag bits; a TOP row also // carries the count in Name1. The bits are cumulative, so Access can put both on one row -- writing // them separately is equally valid and keeps the two spec fields independent here. @@ -177,25 +281,11 @@ private void AddQueryRows(int objectId, ViewSpec spec) Row(mq, objectId, StoredQueryFormat.AttrOption, order: flagOrder++, flag: StoredQueryFormat.FlagDistinct); if (spec.Top is { } top) Row(mq, objectId, StoredQueryFormat.AttrOption, order: flagOrder++, flag: StoredQueryFormat.FlagTop, name1: top.ToString(System.Globalization.CultureInfo.InvariantCulture)); - for (int i = 0; i < spec.Tables.Count; i++) - { - ViewTableSpec t = spec.Tables[i]; - // A derived table stores its subquery SQL in Expression (Name1 empty); a named table uses Name1. - if (t.SubquerySql is { } sub) - Row(mq, objectId, StoredQueryFormat.AttrTable, order: i + 1, expression: sub, name2: t.Alias); - else - Row(mq, objectId, StoredQueryFormat.AttrTable, order: i + 1, name1: t.Table, name2: t.Alias); - } + AddSourceRows(mq, objectId, spec); for (int i = 0; i < spec.Columns.Count; i++) Row(mq, objectId, StoredQueryFormat.AttrColumn, order: i + 1, flag: 0, expression: spec.Columns[i].Expression, name1: spec.Columns[i].Alias); - for (int i = 0; i < spec.Joins.Count; i++) - { - ViewJoinSpec j = spec.Joins[i]; - Row(mq, objectId, StoredQueryFormat.AttrJoin, order: i + 1, flag: (short)j.Kind, expression: j.Condition, name1: j.LeftAlias, name2: j.RightAlias); - } - if (spec.Where is { } where) - Row(mq, objectId, StoredQueryFormat.AttrWhere, order: 1, expression: where); + AddJoinAndWhereRows(mq, objectId, spec); for (int i = 0; i < (spec.GroupBy?.Count ?? 0); i++) Row(mq, objectId, StoredQueryFormat.AttrGroupBy, order: i + 1, flag: 0, expression: spec.GroupBy![i]); for (int i = 0; i < (spec.OrderBy?.Count ?? 0); i++) @@ -204,7 +294,8 @@ private void AddQueryRows(int objectId, ViewSpec spec) } private void Row(TableDef mq, int objectId, byte attribute, int order, - short? flag = null, string? expression = null, string? name1 = null, string? name2 = null) + short? flag = null, string? expression = null, string? name1 = null, string? name2 = null, + int? lvExtra = null) { var values = new object?[mq.Columns.Count]; SetByName(mq, values, "ObjectId", objectId); @@ -216,6 +307,8 @@ private void Row(TableDef mq, int objectId, byte attribute, int order, if (expression is not null) SetByName(mq, values, "Expression", expression); if (name1 is not null) SetByName(mq, values, "Name1", name1); if (name2 is not null) SetByName(mq, values, "Name2", name2); + // A declared parameter's length; ACE writes it here and renders the PARAMETERS clause from it. + if (lvExtra is { } extra) SetByName(mq, values, "LvExtra", extra); new RowInserter(_channel, mq).Insert(values, updateIndexes: true); } diff --git a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs index 7091c49cc..78ada7d87 100644 --- a/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs +++ b/src/LibRed/LibRed.Engine/Execution/ExpressionEvaluator.cs @@ -251,8 +251,8 @@ private static bool TryNiladicFunction(ColumnReference c, out object? value) "LCASE" => Convert1(f, v => ConcatText(v).ToLowerInvariant()), "UCASE" => Convert1(f, v => ConcatText(v).ToUpperInvariant()), "TRIM" => Convert1(f, v => ConcatText(v).Trim(TrimmedSpaces)), - "LTRIM" => Convert1(f, v => ConcatText(v).TrimStart(TrimmedSpaces)), - "RTRIM" => Convert1(f, v => ConcatText(v).TrimEnd(TrimmedSpaces)), + "LTRIM" => Trim(f, static (s, chars) => s.TrimStart(chars)), + "RTRIM" => Trim(f, static (s, chars) => s.TrimEnd(chars)), "LEFT" => StringInt(f, static (s, n) => n <= 0 ? "" : n >= s.Length ? s : s[..n]), "RIGHT" => StringInt(f, static (s, n) => n <= 0 ? "" : n >= s.Length ? s : s[^n..]), "MID" => Mid(f), @@ -413,7 +413,7 @@ internal static void ValidateArity(string name, int count) or "TAN" or "ATN" or "FLOOR" or "CEILING" or "CEIL" or "SIGN" or "SQRT" or "LN" or "LOG10" or "ASIN" or "ACOS" or "ATAN" or "SINH" or "COSH" or "TANH" or "DEGREES" or "RADIANS" - or "LEN" or "LCASE" or "UCASE" or "TRIM" or "LTRIM" or "RTRIM" or "SPACE" + or "LEN" or "LCASE" or "UCASE" or "TRIM" or "SPACE" or "STRREVERSE" or "STR" or "VAL" or "CHR" or "ASC" or "HEX" or "OCT" or "DATEVALUE" or "TIMEVALUE" or "YEAR" or "MONTH" or "DAY" or "HOUR" or "MINUTE" or "SECOND" or "ISDATE" or "ISNULL" or "ISNUMERIC" or "ISERROR" or "TYPENAME" or "VARTYPE" @@ -445,6 +445,8 @@ internal static void ValidateArity(string name, int count) "RGB" => (3, 3), "ROUND" => (1, 2), "LOG" => (1, 2), + // Access takes one argument; the second, a set of characters to strip, is SQL Server 2022's. + "LTRIM" or "RTRIM" => (1, 2), "POWER" or "ATAN2" => (2, 2), "PI" => (0, 0), "RND" => (0, 1), @@ -1416,6 +1418,23 @@ static object RoundWritten(T original, int places, Func written, Fun } } + /// + /// LTrim / RTrim. With one argument they are Access's, stripping . + /// With two they are SQL Server 2022's: every leading (or trailing) character that appears anywhere in the + /// second argument is removed, so the second argument is a SET of characters and not a substring — a + /// LibRed extension, since ACE takes only the one argument. Either argument Null gives Null, and an empty + /// set of characters strips nothing. + /// + private object? Trim(FunctionCall f, Func trim) + { + object? value = Evaluate(f.Arguments[0]); + if (value is null) return null; + if (f.Arguments.Count == 1) return trim(ConcatText(value), TrimmedSpaces); + + object? characters = Evaluate(f.Arguments[1]); + return characters is null ? null : trim(ConcatText(value), ConcatText(characters).ToCharArray()); + } + /// Applies a conversion to a single argument, propagating NULL. private object? Convert1(FunctionCall f, Func convert) { diff --git a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs index da5724aa6..88dfa79b6 100644 --- a/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/QueryExecutor.cs @@ -8,19 +8,33 @@ namespace LibRed.Engine.Execution; /// 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. +/// Where a column's values come from, when no stored column stands behind them: an expression over one +/// row, an aggregate over many, or the arms of a set operation. Schema metadata describes each differently. +/// +internal enum ColumnOrigin { Expression, Aggregate, SetOperation } + +/// The stored column this output passes through unchanged, where it does — carried so a +/// caller describing a query (the schema rowsets, for a view's columns) can report the declared type and its +/// length rather than only the CLR type. Null for anything computed. +/// What computes the column, where does not stand behind it. internal readonly record struct OutputColumn( - string? Qualifier, string Name, Type? ClrType = null, bool Currency = false, int? Scale = null, bool Null = false) + string? Qualifier, string Name, Type? ClrType = null, bool Currency = false, int? Scale = null, + bool Null = false, LibRed.Catalog.ColumnDef? Source = null, ColumnOrigin Origin = ColumnOrigin.Expression) { /// 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); + column.Type == LibRed.Catalog.JetDataType.FixedPoint ? column.Scale : null, + Source: column); - /// A computed column of , computed by . - public static OutputColumn Computed(string name, Type? clrType, NumberType type, Expression expression) => + /// A computed column of , computed by . + /// is the stored column it merely renames, where it is one. + public static OutputColumn Computed( + string name, Type? clrType, NumberType type, Expression expression, LibRed.Catalog.ColumnDef? source = null) => new(null, name, clrType, type.Class == NumberClass.Currency, - type.Class == NumberClass.Decimal ? type.Places : null, expression is LiteralExpression { Value: null }); + type.Class == NumberClass.Decimal ? type.Places : null, expression is LiteralExpression { Value: null }, + source); /// The column names, or null when none or more than one does (execution /// reports the ambiguous reference). @@ -98,20 +112,33 @@ public sealed class QueryExecutor : IScalarSubqueryRunner private DecorrelationGate Gate(SqlStatement query) => _gates.TryGetValue(query, out DecorrelationGate? gate) ? gate : _gates[query] = new DecorrelationGate(); - public QueryExecutor(JetDatabase database, IReadOnlyDictionary? parameters = null, SessionState? session = null) + /// + /// Builds the plan for its column shape alone — ADO's CommandBehavior.SchemaOnly. Every leaf yields + /// no rows and no seek key, offset or count expression is evaluated, so nothing is read from the file and a + /// parameter with no value supplied is not an error: a shape never depended on one. The columns are the same + /// ones the query would return, because those come from the catalog and the plan, never from a row. + /// + public QueryExecutor( + JetDatabase database, IReadOnlyDictionary? parameters = null, SessionState? session = null, + bool describing = false) { _database = database; _parameters = new ParameterBag(parameters); _session = session; + _describing = describing; } + private readonly bool _describing; + public ResultSet ExecuteQuery(PlanNode plan) { var (columns, rows) = Execute(plan, null); return new ResultSet( columns.Select(c => c.Name).ToList(), rows, - columns.Select(c => c.ClrType ?? typeof(object)).ToList()); + columns.Select(c => c.ClrType ?? typeof(object)).ToList(), + // Only a caller that asks for the schema pays to build it. + () => Schema.SchemaRowsets.Describe(columns, _database.Catalog)); } /// @@ -130,6 +157,25 @@ public ResultSet ExecuteQuery(PlanNode plan) PlanNode plan, EvalScope outer) => Execute(plan, outer); + /// The columns a plan produces, without reading a row: columns come back eagerly and rows lazily, + /// so describing a query costs only the planning. Used to report a view's columns in schema metadata. + internal IReadOnlyList DescribeQuery(PlanNode plan) => Execute(plan, null).Columns; + + /// The columns a join publishes. Joining something already collapsed — an aggregate, a DISTINCT, + /// a union — makes the whole join non-updatable, as Access counts updatability, so a stored column reached + /// through one no longer stands for a row anybody can write back to. + private static List JoinedColumns(IReadOnlyList left, IReadOnlyList right) + { + var columns = left.Concat(right).ToList(); + if (columns.TrueForAll(c => c.Origin == ColumnOrigin.Expression)) return columns; + + return columns + .Select(c => c is { Source: not null, Origin: ColumnOrigin.Expression } + ? c with { Origin = ColumnOrigin.Aggregate } + : c) + .ToList(); + } + /// Runs a FROM-less SELECT @@IDENTITY / SELECT @@ROWCOUNT: evaluates each system /// variable against the session state and yields a single row. Each output column is named by its alias, /// or the variable name if unaliased. @@ -450,7 +496,7 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) { case SingleRowNode: // FROM-less SELECT: one row, no columns — the projection above evaluates its constants once. - return ([], [new object?[0]]); + return ([], _describing ? [] : [new object?[0]]); case ValuesNode values: { @@ -466,6 +512,8 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) .Select((expr, i) => new OutputColumn(null, $"Expr{i + 1}", DeclaredType(expr, []))) .ToList(); + if (_describing) return (valueColumns, []); + var valueRows = values.Rows .Select(row => row.Select(evaluator.Evaluate).ToArray()) .ToList(); @@ -480,7 +528,7 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) var columns = Schema.InformationSchema.ColumnsOf(scan.Table) .Zip(Schema.InformationSchema.ColumnTypesOf(scan.Table), (name, type) => new OutputColumn(alias, name, type)).ToList(); - return (columns, Schema.InformationSchema.Rows(scan.Table, _database.Catalog)); + return (columns, _describing ? [] : Schema.InformationSchema.Rows(scan.Table, _database.Catalog)); } case ScanNode scan: @@ -488,7 +536,7 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) var table = _database.OpenTable(scan.Table); string alias = scan.Alias ?? scan.Table; var columns = table.Definition.Columns.Select(c => OutputColumn.Of(alias, c)).ToList(); - return (columns, table.Rows()); + return (columns, _describing ? [] : table.Rows()); } case IndexSeekNode seek: @@ -496,6 +544,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 => OutputColumn.Of(alias, c)).ToList(); + if (_describing) return (columns, []); // 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. @@ -512,6 +561,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 => OutputColumn.Of(alias, c)).ToList(); + if (_describing) return (columns, []); var evaluator = new ExpressionEvaluator(new EvalScope([], [], outer), this, parameters: _parameters, session: _session); int col = range.Index.Columns[0].Column.Index; @@ -559,7 +609,8 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) { var (columns, rows) = Execute(sort.Input, outer); // As in LimitNode: the count is literal/parameter/arithmetic, so an empty row scope suffices. - int? bound = sort.Limit is { } lim + // Describing sorts nothing, so its count is not evaluated either. + int? bound = sort.Limit is { } lim && !_describing ? Convert.ToInt32(Eval([], [], outer).Evaluate(lim), System.Globalization.CultureInfo.InvariantCulture) : null; return (columns, SortRows(sort.Keys, columns, outer, rows, bound)); @@ -602,6 +653,9 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) case LimitNode limit: { var (columns, rows) = Execute(limit.Input, outer); + // A limit cannot change the shape, so describing leaves its counts unevaluated. + if (_describing) return (columns, rows); + // Counts are literal/parameter/arithmetic (no column refs), so an empty row scope suffices. var limitEval = new ExpressionEvaluator(new EvalScope([], [], outer), this, parameters: _parameters, session: _session); @@ -643,7 +697,9 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) case DistinctNode distinct: { var (columns, rows) = Execute(distinct.Input, outer); - return (columns, Distinct(rows)); + // A distinct row stands for however many rows shared it, so its columns are no longer single + // stored values that could be written back — the same standing an aggregate's have. + return (columns.Select(c => c with { Origin = ColumnOrigin.Aggregate }).ToList(), Distinct(rows)); } case DistinctRowNode distinctRow: @@ -681,10 +737,16 @@ private PlanNode SubqueryPlan(SqlStatement query, EvalScope outerScope) /// private static OutputColumn SetOperationColumn(OutputColumn left, OutputColumn right) { + // Whatever the arms hold, the combined column is no longer any one stored column, so it carries no + // source: a caller describing the query sees a computed column, which is what it is. if (right.Null) - return left; + return left with { Source = null, Origin = ColumnOrigin.SetOperation }; if (left.Null) - return left with { ClrType = right.ClrType, Currency = right.Currency, Scale = right.Scale, Null = false }; + return left with + { + ClrType = right.ClrType, Currency = right.Currency, Scale = right.Scale, Null = false, + Source = null, Origin = ColumnOrigin.SetOperation, + }; // 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; @@ -701,6 +763,8 @@ private static OutputColumn SetOperationColumn(OutputColumn left, OutputColumn r Scale = !isDecimal ? null : leftDecimal && rightDecimal ? (left.Scale == right.Scale ? left.Scale : null) : leftDecimal ? left.Scale : rightDecimal ? right.Scale : null, + Source = null, + Origin = ColumnOrigin.SetOperation, }; static Type AsInteger(Type type) => type == typeof(bool) ? typeof(short) : type; @@ -802,8 +866,16 @@ private ProjectionSchema ProjectionSchemaFor(ProjectNode project, IReadOnlyList< { string name = item.Alias ?? (item.Value is ColumnReference c ? c.Column : $"Expr{plan.Count + 1}"); 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))); + // A projection item that is just a column keeps what that column was: the stored column behind + // it, and where none stands behind it, what computed it — so a renamed union column still + // describes itself as one. + OutputColumn? referenced = item.Value is ColumnReference reference + ? OutputColumn.Find(columns, reference) + : null; + plan.Add(( + OutputColumn.Computed(name, DeclaredType(item.Value, columns), type, item.Value, referenced?.Source) + with { Origin = referenced?.Origin ?? ColumnOrigin.Expression }, + -1, item.Value, type, ChoiceConversion(item.Value, columns))); } } @@ -997,7 +1069,7 @@ private static bool IsNumeric(Type type) /// and . internal static Type? AggregateResultType(string name, Type? argument) => RunningAggregate.Canonical(name) switch { - "LISTAGG" => typeof(string), + "LISTAGG" or "STRING_AGG" => 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), @@ -1201,7 +1273,7 @@ private static HashSet ContributingQualifiers( // Execute resolves columns eagerly and rows lazily, so for almost every node this reads nothing at all. var probeScope = new EvalScope(leftColumns, new object?[leftColumns.Count], outer); var (rightColumns, _) = Execute(apply.Right, probeScope); - var columns = leftColumns.Concat(rightColumns).ToList(); + var columns = JoinedColumns(leftColumns, rightColumns); IEnumerable Rows() { @@ -1289,7 +1361,7 @@ private static HashSet ContributingQualifiers( var (rightColumns, rightRowsEnum) = Execute(join.Right, outer); - var columns = leftColumns.Concat(rightColumns).ToList(); + var columns = JoinedColumns(leftColumns, rightColumns); var rightRows = rightRowsEnum.ToList(); // re-iterated per left row if (on is null && join.Kind != JoinKind.Cross) throw new NotSupportedException("Joins require an ON condition."); @@ -1356,7 +1428,7 @@ private static HashSet ContributingQualifiers( { var (leftColumns, leftRows) = Execute(join.Left, outer); var (rightColumns, rightRowsEnum) = Execute(join.Right, outer); - var joinColumns = leftColumns.Concat(rightColumns).ToList(); + var joinColumns = JoinedColumns(leftColumns, rightColumns); int leftWidth = leftColumns.Count, rightWidth = rightColumns.Count; Expression on = join.On; @@ -1846,10 +1918,22 @@ private static void CheckFrame(WindowFunction fn, WindowFrame frame) .Select(item => ExpressionEvaluator.NumberTypeOf(item.Value, columns, e => DeclaredType(e, columns))).ToList(); var outColumns = node.Projection .Select((item, i) => OutputColumn.Computed( - item.Alias ?? (item.Value is ColumnReference c ? c.Column : $"Expr{i + 1}"), - DeclaredType(item.Value, columns), - outTypes[i], - item.Value)) + item.Alias ?? (item.Value is ColumnReference c ? c.Column : $"Expr{i + 1}"), + DeclaredType(item.Value, columns), + outTypes[i], + item.Value, + // A grouping key that is just a column still passes that stored column through, keeping its + // declared type and length; anything holding an aggregate is computed over the group. + item.Value is ColumnReference reference ? OutputColumn.Find(columns, reference)?.Source : null) + with + { + // The grouping produces both its keys and its aggregates, so both count as drawn from many + // rows. An expression that merely evaluates per group (a concatenation of the key, say) is + // still computed from one value, and describes itself as one. + Origin = Aggregates(item.Value).Any() || item.Value is ColumnReference + ? ColumnOrigin.Aggregate + : ColumnOrigin.Expression, + }) .ToList(); var conversions = node.Projection.Select(item => ChoiceConversion(item.Value, columns)).ToList(); @@ -2025,23 +2109,29 @@ private static int CountRows(IEnumerable rows) if (name == "LAST") return group.Count == 0 ? null : Eval(columns, group[^1], outer).Evaluate(arg!); + // A list aggregate, ordered or not: STRING_AGG may go without its WITHIN GROUP, in which case the + // values list in the order the rows arrive (no keys, no directions — the sort is stable). + if (FunctionCall.IsListAggregate(name)) + { + if (group.Count == 0) + return null; + IReadOnlyList order = call.WithinGroup ?? []; + 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! : "", + order, + call.Distinct); + } + 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])), diff --git a/src/LibRed/LibRed.Engine/Execution/ResultSet.cs b/src/LibRed/LibRed.Engine/Execution/ResultSet.cs index d3711afde..c5aa75382 100644 --- a/src/LibRed/LibRed.Engine/Execution/ResultSet.cs +++ b/src/LibRed/LibRed.Engine/Execution/ResultSet.cs @@ -7,10 +7,14 @@ namespace LibRed.Engine.Execution; /// public sealed class ResultSet { + private readonly Func>? _describe; + private IReadOnlyList? _columns; + public ResultSet( IReadOnlyList columnNames, IEnumerable rows, - IReadOnlyList? columnTypes = null) + IReadOnlyList? columnTypes = null, + Func>? describe = null) { if (columnTypes is not null && columnTypes.Count != columnNames.Count) throw new ArgumentException("The number of column types must match the number of column names.", nameof(columnTypes)); @@ -18,8 +22,16 @@ public ResultSet( ColumnNames = columnNames; Rows = rows; ColumnTypes = columnTypes ?? Enumerable.Repeat(typeof(object), columnNames.Count).ToArray(); + _describe = describe; } + /// Everything known about each output column — the stored column behind it where there is one, + /// with its declared type and constraints. Described on demand, not per query: only a caller asking for + /// schema (GetSchemaTable, GetColumnSchema) pays for it. + public IReadOnlyList Columns => + _columns ??= _describe?.Invoke() + ?? ColumnNames.Select((name, i) => new ResultColumn(name, ColumnTypes[i])).ToList(); + public IReadOnlyList ColumnNames { get; } /// Declared CLR type for each output column. Unlike row-value inference, this remains @@ -31,3 +43,30 @@ public ResultSet( public static ResultSet Empty { get; } = new([], [], []); } + +/// +/// What is known about one column of a result: its name and CLR type always, and — where the value comes +/// straight from a stored column rather than being computed — that column's table, name, declared type and +/// the constraints on it. The ADO layer turns these into GetSchemaTable rows and DbColumns. +/// +/// The table the value is read from, or null when nothing stored stands behind it. +/// Its name in that table, which an alias in the query does not change. +/// The OLE DB type code, as the schema collections report it. +/// The provider's name for the type, as the DataTypes collection spells it. +public sealed record ResultColumn( + string Name, + Type ClrType, + string? BaseTableName = null, + string? BaseColumnName = null, + bool AllowNull = true, + bool IsExpression = false, + bool IsAutoIncrement = false, + bool IsKey = false, + bool IsUnique = false, + bool IsLong = false, + bool IsReadOnly = false, + int? Size = null, + int? Precision = null, + int? Scale = null, + int ProviderType = 0, + string TypeName = ""); diff --git a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs index 0bad79747..b1ce53d63 100644 --- a/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs +++ b/src/LibRed/LibRed.Engine/Execution/StatementExecutor.cs @@ -420,16 +420,27 @@ private int ExecuteCreateProcedure(CreateProcedureStatement statement) // hand, and a BIGINT/DATETIME2 parameter on an older format is left to fail. Upgrading a whole // database for a saved query's parameter type is a bigger claim than anything measured — what ACE // does with a new-type parameter in MSysQueries has not been probed, unlike the column case. - var parameters = statement.Parameters - .Select(p => new ViewParameterSpec( - p.Name, - (byte)AccessTypeMapper.ToColumnSpec( - new ColumnDefinition(p.Name, p.TypeName, null, null, false, false), _database.Format.Version).Type)) - .ToList(); - _database.CreateView(statement.Name, BuildViewSpec(statement.Definition) with { Parameters = parameters }); + _database.CreateView( + statement.Name, + BuildViewSpec(statement.Definition) with { Parameters = BuildParameterSpecs(statement.Parameters) }); return 0; } + /// The declared parameters of a stored query as the rows that hold them: each name with the Jet + /// type code its declared Access type maps to. An action query declares them exactly as a SELECT does. + private IReadOnlyList? BuildParameterSpecs(IReadOnlyList? parameters) => + parameters is null or { Count: 0 } + ? null + : parameters + .Select(p => new ViewParameterSpec( + p.Name, + // The declared size decides the type code as well as being stored: Text(50) is a Text + // parameter (code 10) where a bare Text is a memo (12), which is what ACE records. + (byte)AccessTypeMapper.ToColumnSpec( + new ColumnDefinition(p.Name, p.TypeName, p.Size, p.Scale, false, false), _database.Format.Version).Type, + p.Size, p.Scale)) + .ToList(); + private int ExecuteAlterTable(AlterTableStatement statement) => statement.Action switch { // ADD CONSTRAINT … PRIMARY KEY (cols): a primary key is a unique, primary index named after the @@ -664,10 +675,30 @@ private int AddForeignKey(string table, ForeignKeyConstraint fk) private int ExecuteCreateActionProcedure(CreateActionProcedureStatement statement) { - ActionQuerySpec spec = statement.Kind == ProcedureActionKind.DataDefinition - ? new ActionQuerySpec(ActionQueryKind.DataDefinition, DdlSql: statement.DdlSql) - : new ActionQuerySpec(ActionQueryKind.Append, TargetTable: statement.TargetTable, - Values: statement.AppendColumns!.Select(c => new AppendColumnSpec(c.Column, c.ValueExpression)).ToList()); + if (statement.Kind == ProcedureActionKind.DataDefinition) + { + _database.CreateActionQuery( + statement.Name, new ActionQuerySpec(ActionQueryKind.DataDefinition, DdlSql: statement.DdlSql)); + return 0; + } + + // Every other kind stores its sources, joins and WHERE the way a view stores them, so the body maps + // through the same builder; only the action row and the meaning of the column rows differ. + var spec = new ActionQuerySpec( + statement.Kind switch + { + ProcedureActionKind.Append => ActionQueryKind.Append, + ProcedureActionKind.Update => ActionQueryKind.Update, + ProcedureActionKind.Delete => ActionQueryKind.Delete, + ProcedureActionKind.MakeTable => ActionQueryKind.MakeTable, + _ => throw new NotSupportedException($"A {statement.Kind} procedure body is not stored yet."), + }, + TargetTable: statement.TargetTable, + Values: statement.AppendColumns?.Select(c => new AppendColumnSpec(c.Column, c.ValueExpression)).ToList(), + Body: statement.Body is { } body ? BuildViewSpec(body) : null, + Parameters: BuildParameterSpecs(statement.Parameters), + DeleteTarget: statement.DeleteTarget); + _database.CreateActionQuery(statement.Name, spec); return 0; } diff --git a/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs b/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs index c44bdcafa..5ef8f82c3 100644 --- a/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs +++ b/src/LibRed/LibRed.Engine/Execution/WindowFunctions.cs @@ -101,6 +101,10 @@ internal static class WindowFunctions ["PERCENTILE_DISC"] = PercentileOf("PERCENTILE_DISC"), ["LISTAGG"] = new(2, int.MaxValue, static _ => typeof(string), ListAggOf, WindowOptions.Frame | WindowOptions.Distinct | WindowOptions.Filter), + // SQL Server's spelling of the same aggregate, whose WITHIN GROUP is optional — so over a window it + // can arrive with just the value and the separator. + ["STRING_AGG"] = 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. @@ -228,7 +232,8 @@ public void Add(int position) /// private static void ListAggOf(WindowPartition p, object?[] o) { - IReadOnlyList directions = p.Call.WithinGroup!; + // STRING_AGG may have no WITHIN GROUP at all, and then lists in window order with no keys of its own. + 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; diff --git a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs index b2722c431..e86beeaa0 100644 --- a/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs +++ b/src/LibRed/LibRed.Engine/Planning/QueryPlanner.cs @@ -222,7 +222,7 @@ private static PlanNode PushSort(PlanNode node, IReadOnlyList keys) /// and LISTAGG. internal static bool IsAggregate(string name) => name.ToUpperInvariant() is var upper - && (upper is "FIRST" or "LAST" or "PERCENTILE_CONT" or "PERCENTILE_DISC" or "LISTAGG" + && (upper is "FIRST" or "LAST" or "PERCENTILE_CONT" or "PERCENTILE_DISC" or "LISTAGG" or "STRING_AGG" || Execution.RunningAggregate.Supports(upper)); internal static bool HasAggregate(Expression e) => e switch diff --git a/src/LibRed/LibRed.Engine/QueryEngine.cs b/src/LibRed/LibRed.Engine/QueryEngine.cs index 4390b7d67..a9214c5ef 100644 --- a/src/LibRed/LibRed.Engine/QueryEngine.cs +++ b/src/LibRed/LibRed.Engine/QueryEngine.cs @@ -77,6 +77,52 @@ private ResultSet ExecuteQueryCore(SqlStatement parsed, IReadOnlyDictionary + /// The shape would return, without running it — what ADO's + /// CommandBehavior.SchemaOnly asks for. A query is parsed, bound and planned, and its columns come + /// back with no rows: planning is what knows the shape, and rows are lazy, so nothing is ever read. A + /// statement that writes is not executed and describes as nothing (measured: ACE leaves an + /// INSERT's table untouched under SchemaOnly), and a stored procedure describes as the query it holds + /// rather than running it. + /// + public ResultSet Describe(string sql, IReadOnlyDictionary? parameters = null) + { + if (_parser.IsStatementless(sql)) return ResultSet.Empty; + + SqlStatement parsed = _parser.ParseStatement(sql); + // The shared scope always: describing reads the catalog and writes nothing, whatever the statement + // would have done had it run. + return _database.ReadConsistent(() => DescribeCore(parsed, parameters)); + } + + private ResultSet DescribeCore(SqlStatement parsed, IReadOnlyDictionary? parameters) + { + if (parsed is ExecuteStatement exec) + { + // A stored SELECT describes as its own (parameterized) text; a stored action query would have + // written, so it describes as nothing. Its arguments are never evaluated — values cannot change + // a shape. + if (_database.Catalog.Views.TryGetValue(exec.Procedure, out string? viewSql)) + return DescribeCore(_parser.ParseStatement(viewSql), parameters); + if (_database.Catalog.ActionQueries.ContainsKey(exec.Procedure)) return ResultSet.Empty; + throw new InvalidOperationException($"No stored procedure or query named '{exec.Procedure}'."); + } + + // Everything that writes — DML, DDL, a make-table SELECT, transaction control — returns no rows and + // does not run. + if (parsed is not (SelectStatement { Into: null } or SetOperationStatement or SystemVariableSelectStatement)) + return ResultSet.Empty; + + BoundStatement bound = _binder.Bind(ViewExpander.Expand(parsed, _database.Catalog.Views, _parser)); + var executor = new QueryExecutor(_database, parameters, _session, describing: true); + ResultSet shape = bound.Statement is SystemVariableSelectStatement sysSelect + ? executor.ExecuteSystemVariableSelect(sysSelect) + : executor.ExecuteQuery(PlanWithIndexes(bound)); + + // Its rows are lazy and so far untouched; dropping them is what guarantees they stay that way. + return new ResultSet(shape.ColumnNames, [], shape.ColumnTypes, () => shape.Columns); + } + public int ExecuteNonQuery(string sql, IReadOnlyDictionary? parameters = null) => Execute(sql, parameters).RecordsAffected; @@ -243,8 +289,8 @@ private CommandResult ExecuteProcedure(ExecuteStatement exec, IReadOnlyDictionar var executor = new QueryExecutor(_database, parameters, _session); var evaluator = new ExpressionEvaluator(new EvalScope([], [], null), executor, bag, _session); - IReadOnlyList paramNames = catalog.QueryParameters.TryGetValue(exec.Procedure, out var ns) - ? ns : []; + IReadOnlyList paramNames = catalog.QueryParameters.TryGetValue(exec.Procedure, out var declared) + ? declared.Select(p => p.Name).ToList() : []; // Access EXEC arguments take three shapes (EF emits all of them): // procParam = value → a NAMED argument (bind the proc's named parameter to the value) diff --git a/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs b/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs new file mode 100644 index 000000000..ac5c0ae0a --- /dev/null +++ b/src/LibRed/LibRed.Engine/Schema/SchemaRowsets.cs @@ -0,0 +1,795 @@ +using LibRed.Catalog; + +namespace LibRed.Engine.Schema; + +/// +/// The catalog as the ADO.NET metadata collections ACE's OLE DB provider serves from GetSchema, in the +/// same shapes: the same collection names, the same columns in the same order, and the same values — measured +/// against ACE 16 — so code written for that provider reads what it expects from LibRed. +/// +/// is a different surface: the Jet-dialect INFORMATION_SCHEMA.* +/// views EF's migrations query as SQL, with EFCore.Jet's ADOX-derived column sets. Both read the same +/// . LibRed.Ado shapes these rows into DataTables and applies each collection's +/// restrictions. +/// +public static class SchemaRowsets +{ + /// The collections served here. The first five are the ones ACE advertises through + /// GetSchema, spelled as it spells them; the rest are its relational rowsets, which ACE serves only + /// through GetOleDbSchemaTable — LibRed has no equivalent of that call, so they are named + /// collections here, in ACE's shapes. + public static IReadOnlyList Names { get; } = + [ + "Tables", "Columns", "Indexes", "Views", "Procedures", + "ForeignKeys", "PrimaryKeys", "TableConstraints", "KeyColumnUsage", "ConstraintColumnUsage", + "ReferentialConstraints", "CheckConstraints", "Statistics", + "ProcedureParameters", "ViewColumns", + ]; + + /// True if is one of (case-insensitively, as + /// ADO.NET matches collection names). + public static bool IsKnown(string collection) => + Names.Any(n => n.Equals(collection, StringComparison.OrdinalIgnoreCase)); + + /// The column names of a collection, in order. + public static IReadOnlyList ColumnsOf(string collection) => Canonical(collection) switch + { + "Tables" => ["TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "TABLE_TYPE", "TABLE_GUID", "DESCRIPTION", + "TABLE_PROPID", "DATE_CREATED", "DATE_MODIFIED"], + "Columns" => ["TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME", "COLUMN_GUID", "COLUMN_PROPID", + "ORDINAL_POSITION", "COLUMN_HASDEFAULT", "COLUMN_DEFAULT", "COLUMN_FLAGS", "IS_NULLABLE", "DATA_TYPE", + "TYPE_GUID", "CHARACTER_MAXIMUM_LENGTH", "CHARACTER_OCTET_LENGTH", "NUMERIC_PRECISION", "NUMERIC_SCALE", + "DATETIME_PRECISION", "CHARACTER_SET_CATALOG", "CHARACTER_SET_SCHEMA", "CHARACTER_SET_NAME", + "COLLATION_CATALOG", "COLLATION_SCHEMA", "COLLATION_NAME", "DOMAIN_CATALOG", "DOMAIN_SCHEMA", + "DOMAIN_NAME", "DESCRIPTION", + // Past ACE's own shape. A provider may add columns to a collection (the SQL Server OLE DB provider + // adds IS_COMPUTED to this one, spelled exactly so). The rest are things no ACE rowset carries: the + // expression behind a calculated column (ACE has it only in DAO), a readable type name — this + // rowset otherwise names types only by code — and the AutoNumber flag with its parameters, which + // OLE DB omits entirely and ODBC smuggles into the type name. + "IS_COMPUTED", "COLUMN_EXPRESSION", "TYPE_NAME", "IS_AUTOINCREMENT", "SEED", "INCREMENT"], + "Indexes" => ["TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "INDEX_CATALOG", "INDEX_SCHEMA", "INDEX_NAME", + "PRIMARY_KEY", "UNIQUE", "CLUSTERED", "TYPE", "FILL_FACTOR", "INITIAL_SIZE", "NULLS", "SORT_BOOKMARKS", + "AUTO_UPDATE", "NULL_COLLATION", "ORDINAL_POSITION", "COLUMN_NAME", "COLUMN_GUID", "COLUMN_PROPID", + "COLLATION", "CARDINALITY", "PAGES", "FILTER_CONDITION", "INTEGRATED"], + "Views" => ["TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "VIEW_DEFINITION", "CHECK_OPTION", + "IS_UPDATABLE", "DESCRIPTION", "DATE_CREATED", "DATE_MODIFIED"], + "Procedures" => ["PROCEDURE_CATALOG", "PROCEDURE_SCHEMA", "PROCEDURE_NAME", "PROCEDURE_TYPE", + "PROCEDURE_DEFINITION", "DESCRIPTION", "DATE_CREATED", "DATE_MODIFIED"], + "ForeignKeys" => ["PK_TABLE_CATALOG", "PK_TABLE_SCHEMA", "PK_TABLE_NAME", "PK_COLUMN_NAME", + "PK_COLUMN_GUID", "PK_COLUMN_PROPID", "FK_TABLE_CATALOG", "FK_TABLE_SCHEMA", "FK_TABLE_NAME", + "FK_COLUMN_NAME", "FK_COLUMN_GUID", "FK_COLUMN_PROPID", "ORDINAL", "UPDATE_RULE", "DELETE_RULE", + "PK_NAME", "FK_NAME", "DEFERRABILITY"], + "PrimaryKeys" => ["TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME", "COLUMN_GUID", + "COLUMN_PROPID", "ORDINAL", "PK_NAME"], + "TableConstraints" => ["CONSTRAINT_CATALOG", "CONSTRAINT_SCHEMA", "CONSTRAINT_NAME", "TABLE_CATALOG", + "TABLE_SCHEMA", "TABLE_NAME", "CONSTRAINT_TYPE", "IS_DEFERRABLE", "INITIALLY_DEFERRED", "DESCRIPTION"], + "KeyColumnUsage" => ["CONSTRAINT_CATALOG", "CONSTRAINT_SCHEMA", "CONSTRAINT_NAME", "TABLE_CATALOG", + "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME", "COLUMN_GUID", "COLUMN_PROPID", "ORDINAL_POSITION"], + "ConstraintColumnUsage" => ["TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME", "COLUMN_GUID", + "COLUMN_PROPID", "CONSTRAINT_CATALOG", "CONSTRAINT_SCHEMA", "CONSTRAINT_NAME"], + "ReferentialConstraints" => ["CONSTRAINT_CATALOG", "CONSTRAINT_SCHEMA", "CONSTRAINT_NAME", + "UNIQUE_CONSTRAINT_CATALOG", "UNIQUE_CONSTRAINT_SCHEMA", "UNIQUE_CONSTRAINT_NAME", "MATCH_OPTION", + "UPDATE_RULE", "DELETE_RULE", "DESCRIPTION"], + "CheckConstraints" => ["CONSTRAINT_CATALOG", "CONSTRAINT_SCHEMA", "CONSTRAINT_NAME", "CHECK_CLAUSE", + "DESCRIPTION"], + "Statistics" => ["TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "CARDINALITY"], + // ACE serves neither of these: it refuses the procedure-parameter rowset outright, and has no + // view-column one. Both follow the shapes the OLE DB and SQL Server providers document. + "ProcedureParameters" => ["PROCEDURE_CATALOG", "PROCEDURE_SCHEMA", "PROCEDURE_NAME", "PARAMETER_NAME", + "ORDINAL_POSITION", "PARAMETER_TYPE", "PARAMETER_HASDEFAULT", "PARAMETER_DEFAULT", "IS_NULLABLE", + "DATA_TYPE", "CHARACTER_MAXIMUM_LENGTH", "CHARACTER_OCTET_LENGTH", "NUMERIC_PRECISION", + "NUMERIC_SCALE", "DESCRIPTION", "TYPE_NAME", "LOCAL_TYPE_NAME"], + "ViewColumns" => ["VIEW_CATALOG", "VIEW_SCHEMA", "VIEW_NAME", "TABLE_CATALOG", "TABLE_SCHEMA", + "TABLE_NAME", "COLUMN_NAME"], + _ => throw new InvalidOperationException($"'{collection}' is not a schema collection."), + }; + + /// Declared CLR types corresponding to . A nullable value still reports its + /// underlying type, as ADO.NET metadata does; row nulls are represented by null. + public static IReadOnlyList ColumnTypesOf(string collection) => Canonical(collection) switch + { + "Tables" => [typeof(string), typeof(string), typeof(string), typeof(string), typeof(Guid), typeof(string), + typeof(long), typeof(DateTime), typeof(DateTime)], + "Columns" => [typeof(string), typeof(string), typeof(string), typeof(string), typeof(Guid), typeof(long), + typeof(long), typeof(bool), typeof(string), typeof(long), typeof(bool), typeof(int), + typeof(Guid), typeof(long), typeof(long), typeof(int), typeof(short), + typeof(long), typeof(string), typeof(string), typeof(string), + typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), + typeof(string), typeof(string), + typeof(bool), typeof(string), typeof(string), typeof(bool), typeof(int), typeof(int)], + "Indexes" => [typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), + typeof(bool), typeof(bool), typeof(bool), typeof(int), typeof(int), typeof(int), typeof(int), typeof(bool), + typeof(bool), typeof(int), typeof(long), typeof(string), typeof(Guid), typeof(long), + typeof(short), typeof(decimal), typeof(int), typeof(string), typeof(bool)], + "Views" => [typeof(string), typeof(string), typeof(string), typeof(string), typeof(bool), + typeof(bool), typeof(string), typeof(DateTime), typeof(DateTime)], + "Procedures" => [typeof(string), typeof(string), typeof(string), typeof(short), + typeof(string), typeof(string), typeof(DateTime), typeof(DateTime)], + "ForeignKeys" => [typeof(string), typeof(string), typeof(string), typeof(string), + typeof(Guid), typeof(long), typeof(string), typeof(string), typeof(string), + typeof(string), typeof(Guid), typeof(long), typeof(long), typeof(string), typeof(string), + typeof(string), typeof(string), typeof(short)], + "PrimaryKeys" => [typeof(string), typeof(string), typeof(string), typeof(string), typeof(Guid), + typeof(long), typeof(long), typeof(string)], + "TableConstraints" => [typeof(string), typeof(string), typeof(string), typeof(string), + typeof(string), typeof(string), typeof(string), typeof(bool), typeof(bool), typeof(string)], + "KeyColumnUsage" => [typeof(string), typeof(string), typeof(string), typeof(string), + typeof(string), typeof(string), typeof(string), typeof(Guid), typeof(long), typeof(long)], + "ConstraintColumnUsage" => [typeof(string), typeof(string), typeof(string), typeof(string), typeof(Guid), + typeof(long), typeof(string), typeof(string), typeof(string)], + "ReferentialConstraints" => [typeof(string), typeof(string), typeof(string), + typeof(string), typeof(string), typeof(string), typeof(string), + typeof(string), typeof(string), typeof(string)], + "CheckConstraints" => [typeof(string), typeof(string), typeof(string), typeof(string), typeof(string)], + "Statistics" => [typeof(string), typeof(string), typeof(string), typeof(decimal)], + "ProcedureParameters" => [typeof(string), typeof(string), typeof(string), typeof(string), + typeof(int), typeof(int), typeof(bool), typeof(string), typeof(bool), + typeof(int), typeof(long), typeof(long), typeof(int), + typeof(short), typeof(string), typeof(string), typeof(string)], + "ViewColumns" => [typeof(string), typeof(string), typeof(string), typeof(string), typeof(string), + typeof(string), typeof(string)], + _ => throw new InvalidOperationException($"'{collection}' is not a schema collection."), + }; + + /// The rows of a collection, in order. A Jet file has no catalog or schema, + /// so every *_CATALOG / *_SCHEMA value is null, as ACE reports them. + public static IReadOnlyList Rows(string collection, JetDatabase database) + { + JetCatalog catalog = database.Catalog; + var rows = new List(); + switch (Canonical(collection)) + { + case "Tables": + // Tables and views together, ordered by name, as ACE returns them. A stored query with declared + // parameters is a procedure rather than a view, so it appears in neither this rowset nor Views. + foreach (TableDef t in catalog.Tables) + if (TableType(t) is { } type) + rows.Add([null, null, t.Name, type, null, null, null, null, null]); + foreach (string name in ViewNames(catalog)) + rows.Add([null, null, name, "VIEW", null, null, null, null, null]); + rows.Sort(ByName(2)); + break; + + case "Columns": + // Every object ACE lists columns for except the system tables: user tables, the Access-owned + // hidden tables, and views. + foreach (TableDef t in catalog.Tables) + { + if (TableType(t) is not { } type || type == "SYSTEM TABLE") continue; + foreach (ColumnDef c in t.Columns) + rows.Add([null, null, t.Name, c.Name, null, null, (long)(c.Index + 1), + c.DefaultValue is not null, c.DefaultValue, ColumnFlags(c), Nullable(c), + DataTypeCode(c), null, CharacterMaxLength(c), CharacterOctetLength(c), + NumericPrecision(c), NumericScale(c), DateTimePrecision(c), + null, null, null, null, null, null, null, null, null, null, + c.IsCalculated, c.CalculatedExpression, ProviderTypeName(c), + c.IsAutoNumber, c.IsAutoNumber ? c.Seed : null, c.IsAutoNumber ? c.Increment : null]); + } + + // A view's columns are the shape its query produces, which only planning the query can say. + // A column the query passes through unchanged reports the stored column behind it, facets and + // all; a computed one has only the type the expression yields. Either way the writability flag + // is WRITE rather than the WRITEUNKNOWN a stored column carries, as ACE reports a view's. + foreach (string view in ViewNames(catalog)) + { + var described = ViewColumns(database, view); + for (int i = 0; i < described.Count; i++) + { + var column = described[i]; + ColumnDef? c = column.Source; + bool nullable = c is not null ? ViewNullable(c) : column.ClrType != typeof(bool); + rows.Add(c is not null + ? [null, null, view, column.Name, null, null, (long)(i + 1), + false, null, ViewColumnFlags(c, column.Origin), nullable, + DataTypeCode(c), null, CharacterMaxLength(c), CharacterOctetLength(c), + NumericPrecision(c), NumericScale(c), DateTimePrecision(c), + null, null, null, null, null, null, null, null, null, null, + // A view column is computed when the view computes it, and also when it passes + // through a table's calculated column — which keeps its expression. An + // AutoNumber keeps its flag through a view, but not its seed: nothing is + // assigned through a view, so the next value belongs to the table. + c.IsCalculated, c.CalculatedExpression, ProviderTypeName(c), + c.IsAutoNumber, null, null] + : [null, null, view, column.Name, null, null, (long)(i + 1), + false, null, ComputedColumnFlags(column.ClrType, nullable, column.Origin), nullable, + ComputedDataTypeCode(column.ClrType, column.Currency), null, + ComputedTextLength(column.ClrType, column.Origin), + ComputedTextLength(column.ClrType, column.Origin) * 2, + ComputedPrecision(column.ClrType, column.Currency), (short?)column.Scale, + column.ClrType == typeof(DateTime) ? 0L : null, + null, null, null, null, null, null, null, null, null, null, + // Computed by the view; the expression's text is not reconstructed here. + true, null, ComputedTypeName(column.ClrType, column.Currency), false, null, null]); + } + } + break; + + case "Indexes": + // One row per index column, for the same objects as Columns minus the views (an index belongs + // to a table). Every LOGICAL index is listed, so a column covered by a named index, a primary + // key and a relationship appears once under each name, all reporting the one real index's + // columns and statistics — as ACE lists them. CARDINALITY is the distinct-entry count. + foreach (TableDef t in catalog.Tables) + { + if (TableType(t) is not { } type || type == "SYSTEM TABLE") continue; + foreach ((string name, IndexDef ix, bool isPrimaryKey) in LogicalIndexes(t)) + for (int i = 0; i < ix.Columns.Count; i++) + rows.Add([null, null, t.Name, null, null, name, isPrimaryKey, ix.IsUnique, false, + IndexTypeBtree, FillFactor, InitialSize, Nulls(ix), false, true, NullCollationLow, + (long)(i + 1), ix.Columns[i].Column.Name, null, null, + ix.Columns[i].Ascending ? CollationAscending : CollationDescending, + (decimal)ix.UniqueEntryCount, null, null, true]); + } + break; + + case "Views": + foreach (string name in ViewNames(catalog)) + rows.Add([null, null, name, catalog.Views[name], null, true, null, null, null]); + rows.Sort(ByName(2)); + break; + + case "Procedures": + // A stored query is a procedure when it declares parameters, and an action query always is. + // ACE's PROCEDURE_DEFINITION carries the PARAMETERS clause ahead of the statement; LibRed does + // not reconstruct the parameters' declared types yet, so the statement alone is reported. + foreach ((string name, string sql) in catalog.Views) + if (catalog.QueryParameters.ContainsKey(name)) + rows.Add([null, null, name, ProcedureTypeReturnsRows, sql, null, null, null]); + foreach ((string name, StoredActionQuery query) in catalog.ActionQueries) + rows.Add([null, null, name, ProcedureTypeReturnsRows, query.Sql, null, null, null]); + rows.Sort(ByName(2)); + break; + + case "ForeignKeys": + // One row per column pair, naming the parent's key on one side and the foreign key on the + // other. PK_NAME is the constraint the parent side is keyed on, which is its primary key + // unless the relationship references some other unique index. + foreach (ForeignKey fk in catalog.Relationships) + { + string? parentKey = ParentKeyName(fk, catalog); + for (int i = 0; i < fk.Columns.Count; i++) + rows.Add([null, null, fk.ReferencedTable, fk.Columns[i].ReferencedColumn, null, null, + null, null, fk.Table, fk.Columns[i].Column, null, null, (long)(i + 1), + RefAction(fk.CascadeUpdate, fk.UpdateSetNull), RefAction(fk.CascadeDelete, fk.DeleteSetNull), + parentKey, fk.Name, null]); + } + break; + + case "PrimaryKeys": + foreach (TableDef t in ConstraintTables(catalog)) + if (t.Indexes.FirstOrDefault(ix => ix.IsPrimaryKey) is { } pk) + for (int i = 0; i < pk.Columns.Count; i++) + rows.Add([null, null, t.Name, pk.Columns[i].Column.Name, null, null, (long)(i + 1), pk.Name]); + break; + + case "TableConstraints": + foreach ((TableDef table, string name, string kind, IndexDef? _, ForeignKey? __) in Constraints(catalog)) + rows.Add([null, null, name, null, null, table.Name, kind, false, false, null]); + break; + + case "KeyColumnUsage": + // Each constraint's own key columns: a foreign key's are the columns it constrains on this + // table. ACE agrees on all but two relationships in the Northwind corpus, where it reports the + // table's primary-key column instead of the one the relationship's index actually holds + // (Employees.ReportsTo read back as EmployeeID); the rule behind those two is not established, + // and the key's own columns are what the rowset is defined to carry. + foreach ((TableDef table, string name, string _, IndexDef? index, ForeignKey? fk) in Constraints(catalog)) + { + if (fk is not null) + for (int i = 0; i < fk.Columns.Count; i++) + rows.Add([null, null, name, null, null, table.Name, fk.Columns[i].Column, + null, null, (long)(i + 1)]); + else if (index is not null) + for (int i = 0; i < index.Columns.Count; i++) + rows.Add([null, null, name, null, null, table.Name, index.Columns[i].Column.Name, + null, null, (long)(i + 1)]); + } + break; + + case "ConstraintColumnUsage": + foreach ((TableDef table, string name, string _, IndexDef? index, ForeignKey? __) in Constraints(catalog)) + if (index is not null) + foreach (var column in index.Columns) + rows.Add([null, null, table.Name, column.Column.Name, null, null, null, null, name]); + break; + + case "ReferentialConstraints": + // ACE repeats the relationship's own name as the unique constraint it references, rather than + // naming the parent's key, and reports every Jet relationship as MATCH FULL — a row with some + // key columns null and others not is rejected rather than ignored. + foreach (ForeignKey fk in catalog.Relationships) + rows.Add([null, null, fk.Name, null, null, fk.Name, "FULL", + RefAction(fk.CascadeUpdate, fk.UpdateSetNull), RefAction(fk.CascadeDelete, fk.DeleteSetNull), null]); + break; + + case "CheckConstraints": + foreach (TableDef t in catalog.Tables) + { + if (TableType(t) is not { } type || type == "SYSTEM TABLE") continue; + foreach ((string name, string expression) in t.CheckConstraints) + rows.Add([null, null, name, expression, null]); + } + break; + + case "Statistics": + // Every table, system ones included, with the row count the TDEF carries. Views have no + // cardinality to report and ACE lists none. + foreach (TableDef t in catalog.Tables) + if (TableType(t) is not null) + rows.Add([null, null, t.Name, (decimal)t.RowCount]); + rows.Sort(ByName(2)); + break; + + case "ProcedureParameters": + // Each stored query's declared parameters, in declaration order. Access declares a parameter's + // type but neither a default nor whether it takes null, so those report as the rowset's + // "no default" and nullable. An untyped parameter — Access's `Value` — reports no type. + foreach ((string name, IReadOnlyList parameters) in catalog.QueryParameters) + for (int i = 0; i < parameters.Count; i++) + { + StoredQueryParameter p = parameters[i]; + // The declared facets where the parameter row records them: a text length (reported + // in characters and in bytes, two per character as the Columns collection reports a + // column's), and a decimal's precision and scale. A type that records none falls back + // to the precision its type implies, as a column of it would report. + rows.Add([null, null, name, p.Name, i + 1, ParameterTypeInput, false, null, true, + p.Type is { } type ? DataTypeCodeOf(type) : null, + p.Size is { } size ? (long)size : null, + p.Size is { } octets ? (long)octets * 2 : null, + p.Precision ?? (p.Type is { } precisionType ? NumericPrecisionOf(precisionType) : null), + p.Scale is { } scale ? (short)scale : null, null, + p.Type is { } named ? ProviderTypeName(named) : null, + p.Type is { } local ? ProviderTypeName(local) : null]); + } + rows.Sort(ByName(2)); + break; + + case "ViewColumns": + // Which stored column each of a view's columns comes from, where one does — the provenance the + // planner already tracks. A computed column has no base column and so no row here. + var owners = ColumnOwners(catalog); + foreach (string view in ViewNames(catalog)) + foreach (var column in ViewColumns(database, view)) + if (column.Source is { } source && owners.TryGetValue(source, out string? owner)) + rows.Add([null, null, view, null, null, owner, source.Name]); + break; + } + return rows; + } + + /// The tables whose constraints are reported: the same set whose columns are, so a system table's + /// constraints stay out of the rowsets as ACE keeps them. + private static IEnumerable ConstraintTables(JetCatalog catalog) => + catalog.Tables.Where(t => TableType(t) is { } type && type != "SYSTEM TABLE"); + + /// Every constraint on those tables: a primary key, each other unique index, and each foreign key + /// the table declares. A foreign key's columns come from the index backing it, so it reports the columns + /// it constrains. + private static IEnumerable<(TableDef Table, string Name, string Kind, IndexDef? Index, ForeignKey? ForeignKey)> + Constraints(JetCatalog catalog) + { + foreach (TableDef t in ConstraintTables(catalog)) + { + foreach (IndexDef ix in t.Indexes.Where(ix => ix.IsPrimaryKey)) + yield return (t, ix.Name, "PRIMARY KEY", ix, null); + foreach (IndexDef ix in t.Indexes.Where(ix => ix.IsUnique && !ix.IsPrimaryKey)) + yield return (t, ix.Name, "UNIQUE", ix, null); + + foreach (ForeignKey fk in catalog.Relationships.Where(fk => + fk.Table.Equals(t.Name, StringComparison.OrdinalIgnoreCase))) + yield return (t, fk.Name, "FOREIGN KEY", BackingIndex(fk, t), fk); + } + } + + /// The index on the child table that stores a relationship's key columns. + private static IndexDef? BackingIndex(ForeignKey fk, TableDef child) + { + var columns = fk.Columns.Select(c => c.Column).ToHashSet(StringComparer.OrdinalIgnoreCase); + return child.Indexes.FirstOrDefault(ix => + ix.Columns.Count == columns.Count && ix.Columns.All(c => columns.Contains(c.Column.Name))); + } + + /// The name of the parent-side key a relationship references — its primary key unless the + /// relationship names some other unique index. + private static string? ParentKeyName(ForeignKey fk, JetCatalog catalog) + { + TableDef? parent = catalog.Tables.FirstOrDefault(t => + t.Name.Equals(fk.ReferencedTable, StringComparison.OrdinalIgnoreCase)); + if (parent is null) return null; + + var columns = fk.Columns.Select(c => c.ReferencedColumn).ToHashSet(StringComparer.OrdinalIgnoreCase); + bool Covers(IndexDef ix) => ix.Columns.Count == columns.Count && ix.Columns.All(c => columns.Contains(c.Column.Name)); + return (parent.Indexes.FirstOrDefault(ix => ix.IsPrimaryKey && Covers(ix)) + ?? parent.Indexes.FirstOrDefault(ix => ix.IsUnique && Covers(ix)))?.Name; + } + + private static string RefAction(bool cascade, bool setNull) => + cascade ? "CASCADE" : setNull ? "SET NULL" : "NO ACTION"; + + // ACE's fixed index-rowset values, named rather than repeated: a Jet index is a non-clustered B-tree that + // sorts nulls low, is maintained by the engine, and reports the page-size initial allocation. + private const int IndexTypeBtree = 1; // DBPROPVAL_IT_BTREE + private const int FillFactor = 100; + private const int InitialSize = 4096; + private const int NullCollationLow = 4; // DBPROPVAL_NC_LOW + private const short CollationAscending = 1; // DB_COLLATION_ASC + private const short CollationDescending = 2; // DB_COLLATION_DESC + private const short ProcedureTypeReturnsRows = 3; // DB_PT_FUNCTION + private const int ParameterTypeInput = 1; // DBPARAMTYPE_INPUT — Access declares no other kind + + /// Describes a query's output columns for the ADO layer's GetSchemaTable / + /// GetColumnSchema: each column's type and, where a stored column stands behind it, that column's + /// table, declared facets and constraints. Same rules as the Columns collection, so a caller reading + /// a query's schema and one reading the table's metadata are told the same thing. + internal static IReadOnlyList Describe( + IReadOnlyList columns, JetCatalog catalog) + { + var owners = ColumnOwners(catalog); + var described = new List(columns.Count); + + foreach (Execution.OutputColumn column in columns) + { + Type clrType = column.ClrType ?? typeof(object); + if (column.Source is not { } c || !owners.TryGetValue(c, out string? table)) + { + described.Add(new Execution.ResultColumn( + column.Name, clrType, AllowNull: clrType != typeof(bool), IsExpression: true, IsReadOnly: true, + Size: (int?)ComputedTextLength(column.ClrType, column.Origin), + Precision: ComputedPrecision(column.ClrType, column.Currency), Scale: column.Scale, + ProviderType: ComputedDataTypeCode(column.ClrType, column.Currency), + TypeName: ComputedTypeName(column.ClrType, column.Currency))); + continue; + } + + TableDef? owner = catalog.Tables.FirstOrDefault(t => t.Name == table); + described.Add(new Execution.ResultColumn( + column.Name, clrType, table, c.Name, + AllowNull: Nullable(c), + IsExpression: false, + IsAutoIncrement: c.IsAutoNumber, + IsKey: owner is not null && owner.Indexes.Any(ix => ix.IsPrimaryKey && Covers(ix, c)), + IsUnique: owner is not null && owner.Indexes.Any(ix => ix.IsUnique && ix.Columns.Count == 1 && Covers(ix, c)), + IsLong: IsLong(c), + // A calculated column cannot be written, and neither can a value a query computed from one. + IsReadOnly: c.IsCalculated, + Size: (int?)CharacterMaxLength(c), + Precision: NumericPrecision(c), + Scale: NumericScale(c), + ProviderType: DataTypeCode(c), + TypeName: ProviderTypeName(c))); + } + + return described; + + static bool Covers(IndexDef index, ColumnDef column) => + index.Columns.Any(c => ReferenceEquals(c.Column, column)); + } + + /// Which table owns each column, by the column's own identity, so a query's output column can be + /// traced back to the table it came from. + private static Dictionary ColumnOwners(JetCatalog catalog) + { + var owners = new Dictionary(ReferenceEqualityComparer.Instance as IEqualityComparer + ?? EqualityComparer.Default); + foreach (TableDef t in catalog.Tables) + foreach (ColumnDef c in t.Columns) + owners[c] = t.Name; + return owners; + } + + /// The provider's name for a column's type, as the DataTypes collection spells it — which for text + /// and binary means naming the fixed-length forms apart from the variable ones, since the two are different + /// types there (Char/VarChar, Binary/VarBinary) even though ACE's own flags collapse binary into one. + /// + private static string ProviderTypeName(ColumnDef c) => EffectiveType(c) switch + { + JetDataType.Text => c.IsFixedLength ? "Char" : "VarChar", + JetDataType.Binary => c.IsFixedLength ? "Binary" : "VarBinary", + var type => ProviderTypeName(type), + }; + + /// The provider's own name for a type, as the DataTypes collection spells it. + private static string ProviderTypeName(JetDataType type) => type switch + { + JetDataType.Boolean => "Bit", + JetDataType.Byte => "Byte", + JetDataType.Int16 => "Short", + JetDataType.Int32 => "Long", + JetDataType.Int64 => "BigInt", + JetDataType.Single => "Single", + JetDataType.Double => "Double", + JetDataType.Currency => "Currency", + JetDataType.DateTime => "DateTime", + JetDataType.DateTimeExtended => "DateTime2", + JetDataType.Guid => "GUID", + JetDataType.FixedPoint => "Decimal", + JetDataType.Text => "VarChar", + JetDataType.Memo or JetDataType.Complex => "LongText", + JetDataType.Binary => "VarBinary", + JetDataType.Ole => "LongBinary", + _ => "VarBinary", + }; + + // MSysObjects.Flags decides what an object is called in the schema rowsets, the way Access classifies it — + // not its name (measured on ACE 16): the system bit makes it a SYSTEM TABLE, the hidden bit an ACCESS TABLE + // (the navigation-pane and resource tables), and an object carrying ExcludedFlags is not listed at all — + // the MSysComplexType_* tables, flags 0x80030000. + private const uint SystemFlag = 0x80000000; + private const uint HiddenFlag = 0x00000008; + private const uint ExcludedFlags = 0x00030000; + + /// What ACE calls this object in the schema rowsets, or null when it lists the object nowhere. + private static string? TableType(TableDef t) => + (t.ObjectFlags & ExcludedFlags) != 0 ? null + : (t.ObjectFlags & SystemFlag) != 0 ? "SYSTEM TABLE" + : (t.ObjectFlags & HiddenFlag) != 0 ? "ACCESS TABLE" + : "TABLE"; + + /// Each of the table's logical indexes paired with the real index that stores it, primary key + /// first and the rest by name, as ACE orders them. The parent half of a relationship is left out: Access + /// names it .r… and keeps it out of its own schema views. A table read from a definition that + /// carries no logical list (one LibRed built itself, say) falls back to its real indexes, one name each. + private static IEnumerable<(string Name, IndexDef Index, bool IsPrimaryKey)> LogicalIndexes(TableDef t) + { + IEnumerable<(string Name, IndexDef Index, bool IsPrimaryKey)> all = t.LogicalIndexes.Count > 0 + ? t.LogicalIndexes + .Where(l => !l.IsIncomingRelationship) + .Select(l => (l.Name, Index: t.Indexes.FirstOrDefault(ix => ix.RealIndexOrdinal == l.RealIndexOrdinal), l.IsPrimaryKey)) + .Where(x => x.Index is not null) + .Select(x => (x.Name, x.Index!, x.IsPrimaryKey)) + : t.Indexes.Select(ix => (ix.Name, ix, ix.IsPrimaryKey)); + + return all + .OrderByDescending(x => x.IsPrimaryKey) + .ThenBy(x => x.Name, StringComparer.OrdinalIgnoreCase); + } + + /// The stored SELECT queries that are views: a query declaring parameters is a procedure. + private static IEnumerable ViewNames(JetCatalog catalog) => + catalog.Views.Keys.Where(name => !catalog.QueryParameters.ContainsKey(name)); + + private static Comparison ByName(int column) => + (left, right) => string.Compare((string?)left[column], (string?)right[column], StringComparison.OrdinalIgnoreCase); + + /// The OLE DB type code ACE reports for a column. A memo and a text column share one code, as do an + /// OLE and a binary column; what separates them is the long-value flag in . + private static int DataTypeCode(ColumnDef c) => DataTypeCodeOf(EffectiveType(c)); + + /// + private static int DataTypeCodeOf(JetDataType type) => type switch + { + JetDataType.Boolean => 11, // DBTYPE_BOOL + JetDataType.Byte => 17, // DBTYPE_UI1 + JetDataType.Int16 => 2, // DBTYPE_I2 + JetDataType.Int32 => 3, // DBTYPE_I4 — an AutoNumber reports this too + JetDataType.Single => 4, // DBTYPE_R4 + JetDataType.Double => 5, // DBTYPE_R8 + JetDataType.Currency => 6, // DBTYPE_CY + JetDataType.DateTime => 7, // DBTYPE_DATE + JetDataType.Guid => 72, // DBTYPE_GUID + JetDataType.FixedPoint => 131, // DBTYPE_NUMERIC + // DBTYPE_WSTR. A complex (multi-value / attachment) column reports as long text too — measured on + // MSysResources.Data, the only complex column in the corpus. + JetDataType.Text or JetDataType.Memo or JetDataType.Complex => 130, + // Measured against ACE 16, which reports these two through the codes their CLR types map to even + // though its own DataTypes list never learned them: DBTYPE_I8 and DBTYPE_DBTIMESTAMP. + JetDataType.Int64 => 20, + JetDataType.DateTimeExtended => 135, + _ => 128, // DBTYPE_BYTES — binary, OLE, and anything unmodelled + }; + + // DBCOLUMNFLAGS as ACE sets them: every column is deferrable, of unknown writability and may hold null; + // a fixed-length one adds ISFIXEDLENGTH, a nullable one ISNULLABLE, and a memo/OLE column ISLONG. + private const long FlagsEveryColumn = 0x02 | 0x40; + private const long FlagIsFixedLength = 0x10; + private const long FlagIsNullable = 0x20; + private const long FlagIsLong = 0x80; + + // A stored column reports its writability as unknown; a view's column reports it as writable, which is + // what ACE puts in the flags for one. + private const long FlagWriteUnknown = 0x08; + private const long FlagWrite = 0x04; + + /// The columns a view produces, from planning its query: each carries the stored column behind it + /// where it passes one through unchanged, and otherwise the type the expression yields. A view LibRed + /// cannot plan — one written in SQL it does not accept — reports no columns rather than failing the whole + /// collection. + private static IReadOnlyList ViewColumns(JetDatabase database, string view) + { + try + { + var plan = new QueryEngine(database).PlanFor($"SELECT * FROM [{view}]"); + // Describing: the plan is built for its shape alone, so reporting a view's columns reads no rows — + // it would otherwise sort and buffer the whole of any view with an ORDER BY — and a view with + // declared parameters describes without values nobody supplied. + return new Execution.QueryExecutor(database, describing: true).DescribeQuery(plan); + } + catch (Exception) + { + return []; + } + } + + /// A view's column is writable, where a stored one's writability is unknown — except an + /// AutoNumber, which no one writes, and except a column a grouping produces, which cannot be written back + /// through the view either; ACE reports both as unknown. + private static long ViewColumnFlags(ColumnDef c, Execution.ColumnOrigin origin) => + FlagsEveryColumn + | (c.IsAutoNumber || origin != Execution.ColumnOrigin.Expression ? FlagWriteUnknown : FlagWrite) + | (IsFixedLength(c) ? FlagIsFixedLength : 0) + | (ViewNullable(c) ? FlagIsNullable : 0) + | (IsLong(c) ? FlagIsLong : 0); + + /// Whether a view reports the column as nullable. A view carries the type's own nullability but + /// not the table's Required property, so a required text column reads as nullable through a view while an + /// AutoNumber or Yes/No column — which the type itself forbids nulls in — still does not. + private static bool ViewNullable(ColumnDef c) => !c.IsAutoNumber && c.Type != JetDataType.Boolean; + + /// A computed view column's flags. What a column is computed from changes them: a value read out + /// of one row is not writable at all and, when it is text, is reported as a long one of no declared length; + /// a value drawn from several rows — an aggregate, or the arms of a union — is text of Access's default + /// width whose writability is merely unknown. + private static long ComputedColumnFlags(Type? clrType, bool nullable, Execution.ColumnOrigin origin) + { + bool text = clrType == typeof(string); + bool fromManyRows = origin != Execution.ColumnOrigin.Expression; + return FlagsEveryColumn + | (fromManyRows ? FlagWriteUnknown : 0) + | (nullable ? FlagIsNullable : 0) + | (text || clrType == typeof(byte[]) ? 0 : FlagIsFixedLength) + | (text && !fromManyRows ? FlagIsLong : 0); + } + + /// The OLE DB type code for a computed column. Currency and Decimal share , + /// so the planner's own Currency marking picks between them. + private static int ComputedDataTypeCode(Type? clrType, bool currency) => clrType switch + { + null => 130, + _ when clrType == typeof(bool) => 11, + _ when clrType == typeof(byte) => 17, + _ when clrType == typeof(short) => 2, + _ when clrType == typeof(int) => 3, + _ when clrType == typeof(long) => 20, + _ when clrType == typeof(float) => 4, + _ when clrType == typeof(double) => 5, + _ when clrType == typeof(decimal) => currency ? 6 : 131, + _ when clrType == typeof(DateTime) => 7, + _ when clrType == typeof(Guid) => 72, + _ when clrType == typeof(byte[]) => 128, + _ => 130, + }; + + /// The provider type name for a computed column, from the type its expression yields. + private static string ComputedTypeName(Type? clrType, bool currency) => clrType switch + { + null => "VarChar", + _ when clrType == typeof(bool) => "Bit", + _ when clrType == typeof(byte) => "Byte", + _ when clrType == typeof(short) => "Short", + _ when clrType == typeof(int) => "Long", + _ when clrType == typeof(long) => "BigInt", + _ when clrType == typeof(float) => "Single", + _ when clrType == typeof(double) => "Double", + _ when clrType == typeof(decimal) => currency ? "Currency" : "Decimal", + _ when clrType == typeof(DateTime) => "DateTime", + _ when clrType == typeof(Guid) => "GUID", + _ when clrType == typeof(byte[]) => "VarBinary", + _ => "VarChar", + }; + + /// A computed text column's length: Access's default width where the value is drawn from several + /// rows, and none at all where an expression computed it from one — that column is reported as long text, + /// which has no declared length. + private static long? ComputedTextLength(Type? clrType, Execution.ColumnOrigin origin) => + clrType != typeof(string) ? null + : origin == Execution.ColumnOrigin.Expression ? 0L + : 255L; + + /// A computed column's numeric precision — the same per-type value a stored column of that type + /// reports. + private static int? ComputedPrecision(Type? clrType, bool currency) => clrType switch + { + _ when clrType == typeof(byte) => 3, + _ when clrType == typeof(short) => 5, + _ when clrType == typeof(int) => 10, + _ when clrType == typeof(long) => 19, + _ when clrType == typeof(float) => 7, + _ when clrType == typeof(double) => 15, + _ when clrType == typeof(decimal) => currency ? 19 : 28, + _ => null, + }; + + /// A stored column's flags. A calculated one carries no write bit at all — nothing writes it, as + /// with an expression in a view — where an ordinary column's writability is merely unknown. + private static long ColumnFlags(ColumnDef c) => + FlagsEveryColumn | (c.IsCalculated ? 0 : FlagWriteUnknown) + | (IsFixedLength(c) ? FlagIsFixedLength : 0) + | (Nullable(c) ? FlagIsNullable : 0) + | (IsLong(c) ? FlagIsLong : 0); + + /// Whether ACE calls the column fixed-length: a scalar type always is, whatever its descriptor + /// says (Access's own system tables carry Long columns the descriptor marks variable, and ACE still + /// reports them fixed); text and binary follow their descriptor, so a designer-made fixed-width text + /// column is fixed while one declared through SQL is not; and memo/OLE never are. + private static bool IsFixedLength(ColumnDef c) => EffectiveType(c) switch + { + JetDataType.Memo or JetDataType.Ole or JetDataType.Complex => false, + // A text column is fixed when its descriptor says so — CHAR(n) and NCHAR(n) are, TEXT(n)/VARCHAR(n) + // are not. A binary column never is here, whatever its descriptor: ACE reports BINARY(n) as variable, + // the same collapse ADOX makes in reporting every binary column as adVarBinary. + JetDataType.Text => c.IsFixedLength, + JetDataType.Binary => false, + // BIGINT and DATETIME2 postdate ACE's OLE DB provider, which describes both as variable-length with no + // precision although each is a fixed width on disk — measured, and matched here so the metadata agrees + // with what a caller reading through ACE would have been told. + JetDataType.Int64 or JetDataType.DateTimeExtended => false, + _ => true, + }; + + private static bool IsLong(ColumnDef c) => + EffectiveType(c) is JetDataType.Memo or JetDataType.Ole or JetDataType.Complex; + + /// The type a column reports as. A calculated column reports what its expression yields, which + /// need not be the type its descriptor declares for storage — Access keeps a Yes/No expression in a + /// two-byte integer, and reports it as Yes/No. + private static JetDataType EffectiveType(ColumnDef c) => + c.IsCalculated && c.CalculatedResultType is { } result ? result : c.Type; + + /// Whether a stored column reports as nullable. A complex column's values live outside the row, so + /// the AutoNumber flag its descriptor carries (for the complex id) says nothing about nulls; a calculated + /// one is as nullable as the type it yields. + private static bool Nullable(ColumnDef c) => + c.IsCalculated ? EffectiveType(c) != JetDataType.Boolean + : c.Type == JetDataType.Complex ? c.IsNullable + : JetStoreType.IsNullable(c); + + /// CHARACTER_MAXIMUM_LENGTH: the declared length in characters for text and bytes for binary, zero + /// for the unbounded memo/OLE types, and — as ACE reports it — two for a Yes/No column. + private static long? CharacterMaxLength(ColumnDef c) => EffectiveType(c) switch + { + JetDataType.Text or JetDataType.Binary => JetStoreType.MaxLength(c), + JetDataType.Memo or JetDataType.Ole or JetDataType.Complex => 0L, + JetDataType.Boolean => 2L, + _ => null, + }; + + /// CHARACTER_OCTET_LENGTH: the storage width of the same, so twice the character count for text. + private static long? CharacterOctetLength(ColumnDef c) => EffectiveType(c) switch + { + JetDataType.Text => JetStoreType.MaxLength(c) * 2L, + JetDataType.Binary => JetStoreType.MaxLength(c), + JetDataType.Memo or JetDataType.Ole or JetDataType.Complex => 0L, + _ => null, + }; + + private static int? NumericPrecision(ColumnDef c) => + EffectiveType(c) == JetDataType.FixedPoint ? c.Precision : NumericPrecisionOf(EffectiveType(c)); + + /// + private static int? NumericPrecisionOf(JetDataType type) => type switch + { + JetDataType.Byte => 3, + JetDataType.Int16 => 5, + JetDataType.Int32 => 10, + JetDataType.Single => 7, + JetDataType.Double => 15, + JetDataType.Currency => 19, + // No precision for BIGINT: ACE reports none, though the type holds 19 digits (see IsFixedLength). + // A DECIMAL's precision is declared per column, so the caller supplies it (see NumericPrecision); + // 28 is the type's own maximum, which is what a parameter of the type reports. + JetDataType.FixedPoint => 28, + _ => null, + }; + + private static short? NumericScale(ColumnDef c) => EffectiveType(c) == JetDataType.FixedPoint ? c.Scale : null; + + private static long? DateTimePrecision(ColumnDef c) => EffectiveType(c) == JetDataType.DateTime ? 0L : null; + + /// NULLS: whether the index refuses null keys, ignores them, or takes them as keys like any other. + private static int Nulls(IndexDef ix) => ix.Required ? 1 : ix.IgnoreNulls ? 2 : 0; + + private static string Canonical(string collection) => + Names.FirstOrDefault(n => n.Equals(collection, StringComparison.OrdinalIgnoreCase)) + ?? throw new InvalidOperationException($"'{collection}' is not a schema collection."); +} diff --git a/src/LibRed/LibRed.Sql/Ast/Expressions.cs b/src/LibRed/LibRed.Sql/Ast/Expressions.cs index f763cb258..5319d81b1 100644 --- a/src/LibRed/LibRed.Sql/Ast/Expressions.cs +++ b/src/LibRed/LibRed.Sql/Ast/Expressions.cs @@ -63,6 +63,20 @@ public static bool IsOrderedSetAggregate(string name) => || name.Equals("PERCENTILE_DISC", StringComparison.OrdinalIgnoreCase) || name.Equals("LISTAGG", StringComparison.OrdinalIgnoreCase); + /// Whether is a list aggregate — the standard's LISTAGG or SQL + /// Server's STRING_AGG, which compute the same thing and differ only in what they require: LISTAGG + /// needs its WITHIN GROUP and lets the separator go, STRING_AGG needs the separator and lets the order + /// go. + public static bool IsListAggregate(string name) => + name.Equals("LISTAGG", StringComparison.OrdinalIgnoreCase) + || name.Equals("STRING_AGG", StringComparison.OrdinalIgnoreCase); + + /// Whether accepts WITHIN GROUP — the ordered-set aggregates, which + /// require it, and STRING_AGG, for which it is optional (unordered, the values list in the order + /// the rows arrive). + public static bool AcceptsWithinGroup(string name) => + IsOrderedSetAggregate(name) || name.Equals("STRING_AGG", 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(); diff --git a/src/LibRed/LibRed.Sql/Ast/Statements.cs b/src/LibRed/LibRed.Sql/Ast/Statements.cs index 89f7137e6..736030917 100644 --- a/src/LibRed/LibRed.Sql/Ast/Statements.cs +++ b/src/LibRed/LibRed.Sql/Ast/Statements.cs @@ -232,8 +232,10 @@ public sealed record CreateViewStatement( ViewDefinition Definition, string QuerySql) : SqlStatement; -/// A CREATE PROCEDURE parameter: a name and its declared Access SQL type name. -public sealed record ProcedureParameter(string Name, string TypeName); +/// A CREATE PROCEDURE parameter: a name and its declared Access SQL type, with the +/// and it declares. The size also decides the type code — +/// Text(50) is a Text parameter where a bare Text is a memo — and both are stored alongside it. +public sealed record ProcedureParameter(string Name, string TypeName, int? Size = null, int? Scale = null); /// CREATE PROCEDURE name [param datatype, …] AS select — a parameterized stored query. Stored like /// a view (the decomposed ) plus a parameter row per declared parameter. @@ -244,21 +246,31 @@ public sealed record CreateProcedureStatement( string QuerySql) : SqlStatement; /// The kind of non-SELECT (action) CREATE PROCEDURE body. -public enum ProcedureActionKind { DataDefinition, Append } +public enum ProcedureActionKind { DataDefinition, Append, Update, Delete, MakeTable } -/// One appended column of an INSERT procedure body: the target column and the verbatim value text. +/// One column/value pair of an action procedure body: an appended column of an INSERT and its +/// verbatim value text, or one assignment of an UPDATE (whose is table-qualified +/// when the update runs over a join). Access stores both the same way. public sealed record AppendColumn(string Column, string ValueExpression); -/// A CREATE PROCEDURE whose body is an action query (not a SELECT). A +/// +/// A CREATE PROCEDURE whose body is an action query (not a plain SELECT). A /// body carries the whole -/// (CREATE/DROP TABLE); an body carries the -/// and its appended . +/// (CREATE/DROP TABLE) and nothing else — Access stores it verbatim. Every other kind carries its sources, +/// joins and WHERE in , exactly as a view carries them, plus what its own kind needs: +/// are an INSERT's columns or an UPDATE's assignments, +/// is the table an INSERT or a make-table writes into, and +/// is the table.* a DELETE names when it names one. +/// public sealed record CreateActionProcedureStatement( string Name, ProcedureActionKind Kind, string? DdlSql, string? TargetTable, - IReadOnlyList? AppendColumns) : SqlStatement; + IReadOnlyList? AppendColumns, + ViewDefinition? Body = null, + string? DeleteTarget = null, + IReadOnlyList? Parameters = null) : SqlStatement; /// One action of an ALTER TABLE statement (Access allows exactly one per statement). public abstract record AlterTableAction; diff --git a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 index 509fe49a4..7dd9ebb28 100644 --- a/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 +++ b/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 @@ -123,9 +123,11 @@ alterTableAction | RENAME INDEX index=identifier TO newName=identifier # RenameIndexAction ; -// A procedure body is any statement Access allows. We store/execute the ones we know (SELECT, INSERT, -// CREATE TABLE); other action queries (UPDATE/DELETE/DROP/…) are rejected by the builder. -procedureBody : queryExpression | insertStatement | createTableStatement ; +// A procedure body is any statement Access allows. We store the ones we can decompose into the MSysQueries +// rows Access writes for them — a SELECT (a plain view, or a make-table when it has an INTO), an INSERT from +// either VALUES or a SELECT, an UPDATE, a DELETE, and CREATE TABLE (stored verbatim). The kinds we cannot +// store (crosstab, pass-through, UNION) are rejected by the builder. +procedureBody : queryExpression | insertStatement | updateStatement | deleteStatement | createTableStatement ; // CREATE [UNIQUE] INDEX name ON table (field [ASC|DESC], …) [WITH {PRIMARY|DISALLOW NULL|IGNORE NULL}] createIndexStatement diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlBaseVisitor.cs index 0401ab57e..001285940 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 D:/toolkits/efcorejetlibred/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 by ANTLR 4.13.1 +// Generated from AccessSql.g4 by ANTLR 4.13.1 // Unreachable code detected #pragma warning disable 0162 diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlLexer.cs index a4257e173..af6909e02 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 D:/toolkits/efcorejetlibred/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 by ANTLR 4.13.1 +// Generated from AccessSql.g4 by ANTLR 4.13.1 // Unreachable code detected #pragma warning disable 0162 diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlParser.cs index d70f31e88..1bd40cd7b 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 D:/toolkits/efcorejetlibred/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 by ANTLR 4.13.1 +// Generated from AccessSql.g4 by ANTLR 4.13.1 // Unreachable code detected #pragma warning disable 0162 @@ -2130,6 +2130,12 @@ [System.Diagnostics.DebuggerNonUserCode] public QueryExpressionContext queryExpr [System.Diagnostics.DebuggerNonUserCode] public InsertStatementContext insertStatement() { return GetRuleContext(0); } + [System.Diagnostics.DebuggerNonUserCode] public UpdateStatementContext updateStatement() { + return GetRuleContext(0); + } + [System.Diagnostics.DebuggerNonUserCode] public DeleteStatementContext deleteStatement() { + return GetRuleContext(0); + } [System.Diagnostics.DebuggerNonUserCode] public CreateTableStatementContext createTableStatement() { return GetRuleContext(0); } @@ -2151,7 +2157,7 @@ public ProcedureBodyContext procedureBody() { ProcedureBodyContext _localctx = new ProcedureBodyContext(Context, State); EnterRule(_localctx, 36, RULE_procedureBody); try { - State = 433; + State = 435; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: @@ -2170,10 +2176,24 @@ public ProcedureBodyContext procedureBody() { insertStatement(); } break; - case CREATE: + case UPDATE: EnterOuterAlt(_localctx, 3); { State = 432; + updateStatement(); + } + break; + case DELETE: + EnterOuterAlt(_localctx, 4); + { + State = 433; + deleteStatement(); + } + break; + case CREATE: + EnterOuterAlt(_localctx, 5); + { + State = 434; createTableStatement(); } break; @@ -2243,56 +2263,56 @@ public CreateIndexStatementContext createIndexStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 435; - Match(CREATE); State = 437; + Match(CREATE); + State = 439; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==UNIQUE) { { - State = 436; + State = 438; _localctx.unique = Match(UNIQUE); } } - State = 439; + State = 441; Match(INDEX); - State = 440; + State = 442; _localctx.name = identifier(); - State = 441; + State = 443; Match(ON); - State = 442; + State = 444; _localctx.table = identifier(); - State = 443; + State = 445; Match(LPAREN); - State = 444; + State = 446; indexColumn(); - State = 449; + State = 451; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 445; + State = 447; Match(COMMA); - State = 446; + State = 448; indexColumn(); } } - State = 451; + State = 453; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 452; + State = 454; Match(RPAREN); - State = 455; + State = 457; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WITH) { { - State = 453; + State = 455; Match(WITH); - State = 454; + State = 456; withOption(); } } @@ -2393,18 +2413,18 @@ public DropStatementContext dropStatement() { DropStatementContext _localctx = new DropStatementContext(Context, State); EnterRule(_localctx, 40, RULE_dropStatement); try { - State = 472; + State = 474; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,37,Context) ) { case 1: _localctx = new DropTableStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 457; + State = 459; Match(DROP); - State = 458; + State = 460; Match(TABLE); - State = 459; + State = 461; ((DropTableStatementContext)_localctx).table = identifier(); } break; @@ -2412,15 +2432,15 @@ public DropStatementContext dropStatement() { _localctx = new DropIndexStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 460; + State = 462; Match(DROP); - State = 461; + State = 463; Match(INDEX); - State = 462; + State = 464; ((DropIndexStatementContext)_localctx).index = identifier(); - State = 463; + State = 465; Match(ON); - State = 464; + State = 466; ((DropIndexStatementContext)_localctx).table = identifier(); } break; @@ -2428,11 +2448,11 @@ public DropStatementContext dropStatement() { _localctx = new DropProcedureStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 466; + State = 468; Match(DROP); - State = 467; + State = 469; Match(PROCEDURE); - State = 468; + State = 470; ((DropProcedureStatementContext)_localctx).proc = identifier(); } break; @@ -2440,11 +2460,11 @@ public DropStatementContext dropStatement() { _localctx = new DropViewStatementContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 469; + State = 471; Match(DROP); - State = 470; + State = 472; Match(VIEW); - State = 471; + State = 473; ((DropViewStatementContext)_localctx).view = identifier(); } break; @@ -2490,14 +2510,14 @@ public IndexColumnContext indexColumn() { try { EnterOuterAlt(_localctx, 1); { - State = 474; - _localctx.col = identifier(); State = 476; + _localctx.col = identifier(); + State = 478; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 475; + State = 477; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -2573,14 +2593,14 @@ public WithOptionContext withOption() { WithOptionContext _localctx = new WithOptionContext(Context, State); EnterRule(_localctx, 44, RULE_withOption); try { - State = 483; + State = 485; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case PRIMARY: _localctx = new WithPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 478; + State = 480; Match(PRIMARY); } break; @@ -2588,9 +2608,9 @@ public WithOptionContext withOption() { _localctx = new WithDisallowNullContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 479; + State = 481; Match(DISALLOW); - State = 480; + State = 482; Match(NULL); } break; @@ -2598,9 +2618,9 @@ public WithOptionContext withOption() { _localctx = new WithIgnoreNullContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 481; + State = 483; Match(IGNORE); - State = 482; + State = 484; Match(NULL); } break; @@ -2657,31 +2677,31 @@ public ColumnDefinitionContext columnDefinition() { try { EnterOuterAlt(_localctx, 1); { - State = 485; + State = 487; _localctx.name = identifier(); - State = 486; - dataType(); State = 488; + dataType(); + State = 490; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 487; + State = 489; calculatedClause(); } } - State = 493; + State = 495; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==NOT || ((((_la - 73)) & ~0x3f) == 0 && ((1L << (_la - 73)) & 17197213717L) != 0)) { { { - State = 490; + State = 492; columnConstraint(); } } - State = 495; + State = 497; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -2725,13 +2745,13 @@ public CalculatedClauseContext calculatedClause() { try { EnterOuterAlt(_localctx, 1); { - State = 496; + State = 498; Match(AS); - State = 497; + State = 499; Match(LPAREN); - State = 498; + State = 500; expression(0); - State = 499; + State = 501; Match(RPAREN); } } @@ -2790,7 +2810,7 @@ public DataTypeContext dataType() { try { EnterOuterAlt(_localctx, 1); { - State = 503; + State = 505; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case RANGE: @@ -2811,61 +2831,61 @@ public DataTypeContext dataType() { case BACKTICK_ID: case IDENTIFIER: { - State = 501; + State = 503; _localctx.typeName = identifier(); } break; case IDENTITY: { - State = 502; + State = 504; _localctx.identityType = Match(IDENTITY); } break; default: throw new NoViableAltException(this); } - State = 506; + State = 508; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,43,Context) ) { case 1: { - State = 505; + State = 507; _localctx.extra = identifier(); } break; } - State = 509; + State = 511; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) { { - State = 508; + State = 510; _localctx.extra2 = identifier(); } } - State = 519; + State = 521; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 511; + State = 513; Match(LPAREN); - State = 512; + State = 514; _localctx.size = signedInteger(); - State = 515; + State = 517; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COMMA) { { - State = 513; + State = 515; Match(COMMA); - State = 514; + State = 516; _localctx.scale = signedInteger(); } } - State = 517; + State = 519; Match(RPAREN); } } @@ -2907,17 +2927,17 @@ public SignedIntegerContext signedInteger() { try { EnterOuterAlt(_localctx, 1); { - State = 522; + State = 524; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==MINUS) { { - State = 521; + State = 523; Match(MINUS); } } - State = 524; + State = 526; Match(INTEGER_LITERAL); } } @@ -3108,16 +3128,16 @@ public ColumnConstraintContext columnConstraint() { EnterRule(_localctx, 54, RULE_columnConstraint); int _la; try { - State = 595; + State = 597; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,59,Context) ) { case 1: _localctx = new NotNullConstraintContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 526; + State = 528; Match(NOT); - State = 527; + State = 529; Match(NULL); } break; @@ -3125,7 +3145,7 @@ public ColumnConstraintContext columnConstraint() { _localctx = new NullableConstraintContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 528; + State = 530; Match(NULL); } break; @@ -3133,9 +3153,9 @@ public ColumnConstraintContext columnConstraint() { _localctx = new DefaultConstraintContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 529; + State = 531; Match(DEFAULT); - State = 530; + State = 532; expression(0); } break; @@ -3143,9 +3163,9 @@ public ColumnConstraintContext columnConstraint() { _localctx = new CompressionConstraintContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 531; + State = 533; Match(WITH); - State = 532; + State = 534; _la = TokenStream.LA(1); if ( !(_la==COMPRESSION || _la==COMP) ) { ErrorHandler.RecoverInline(this); @@ -3160,25 +3180,25 @@ public ColumnConstraintContext columnConstraint() { _localctx = new CheckColumnConstraintContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 535; + State = 537; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 533; + State = 535; Match(CONSTRAINT); - State = 534; + State = 536; ((CheckColumnConstraintContext)_localctx).cname = identifier(); } } - State = 537; + State = 539; Match(CHECK); - State = 538; + State = 540; Match(LPAREN); - State = 539; + State = 541; checkBody(); - State = 540; + State = 542; Match(RPAREN); } break; @@ -3186,28 +3206,28 @@ public ColumnConstraintContext columnConstraint() { _localctx = new PrimaryKeyConstraintContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 544; + State = 546; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 542; + State = 544; Match(CONSTRAINT); - State = 543; + State = 545; ((PrimaryKeyConstraintContext)_localctx).cname = identifier(); } } - State = 546; + State = 548; Match(PRIMARY); - State = 547; - Match(KEY); State = 549; + Match(KEY); + State = 551; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 548; + State = 550; clusteredOption(); } } @@ -3218,26 +3238,26 @@ public ColumnConstraintContext columnConstraint() { _localctx = new UniqueColumnConstraintContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 553; + State = 555; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 551; + State = 553; Match(CONSTRAINT); - State = 552; + State = 554; ((UniqueColumnConstraintContext)_localctx).cname = identifier(); } } - State = 555; - Match(UNIQUE); State = 557; + Match(UNIQUE); + State = 559; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 556; + State = 558; clusteredOption(); } } @@ -3248,65 +3268,65 @@ public ColumnConstraintContext columnConstraint() { _localctx = new ColumnReferencesConstraintContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 561; + State = 563; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 559; + State = 561; Match(CONSTRAINT); - State = 560; + State = 562; ((ColumnReferencesConstraintContext)_localctx).cname = identifier(); } } - State = 563; + State = 565; Match(REFERENCES); - State = 564; + State = 566; ((ColumnReferencesConstraintContext)_localctx).refTable = identifier(); - State = 576; + State = 578; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 565; + State = 567; Match(LPAREN); - State = 566; + State = 568; ((ColumnReferencesConstraintContext)_localctx)._identifier = identifier(); ((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier); - State = 571; + State = 573; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 567; + State = 569; Match(COMMA); - State = 568; + State = 570; ((ColumnReferencesConstraintContext)_localctx)._identifier = identifier(); ((ColumnReferencesConstraintContext)_localctx)._refColumns.Add(((ColumnReferencesConstraintContext)_localctx)._identifier); } } - State = 573; + State = 575; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 574; + State = 576; Match(RPAREN); } } - State = 581; + State = 583; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==ON) { { { - State = 578; + State = 580; foreignKeyAction(); } } - State = 583; + State = 585; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -3316,30 +3336,30 @@ public ColumnConstraintContext columnConstraint() { _localctx = new IdentityConstraintContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 584; + State = 586; Match(IDENTITY); - State = 593; + State = 595; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 585; + State = 587; Match(LPAREN); - State = 586; + State = 588; ((IdentityConstraintContext)_localctx).seed = signedInteger(); - State = 589; + State = 591; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==COMMA) { { - State = 587; + State = 589; Match(COMMA); - State = 588; + State = 590; ((IdentityConstraintContext)_localctx).increment = signedInteger(); } } - State = 591; + State = 593; Match(RPAREN); } } @@ -3502,62 +3522,62 @@ public TableConstraintContext tableConstraint() { EnterRule(_localctx, 56, RULE_tableConstraint); int _la; try { - State = 686; + State = 688; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,73,Context) ) { case 1: _localctx = new PrimaryKeyTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 599; + State = 601; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 597; + State = 599; Match(CONSTRAINT); - State = 598; + State = 600; ((PrimaryKeyTableConstraintContext)_localctx).name = identifier(); } } - State = 601; + State = 603; Match(PRIMARY); - State = 602; - Match(KEY); State = 604; + Match(KEY); + State = 606; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 603; + State = 605; clusteredOption(); } } - State = 606; + State = 608; Match(LPAREN); - State = 607; + State = 609; ((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier(); ((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier); - State = 612; + State = 614; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 608; + State = 610; Match(COMMA); - State = 609; + State = 611; ((PrimaryKeyTableConstraintContext)_localctx)._identifier = identifier(); ((PrimaryKeyTableConstraintContext)_localctx)._columns.Add(((PrimaryKeyTableConstraintContext)_localctx)._identifier); } } - State = 614; + State = 616; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 615; + State = 617; Match(RPAREN); } break; @@ -3565,53 +3585,53 @@ public TableConstraintContext tableConstraint() { _localctx = new UniqueTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 619; + State = 621; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 617; + State = 619; Match(CONSTRAINT); - State = 618; + State = 620; ((UniqueTableConstraintContext)_localctx).name = identifier(); } } - State = 621; - Match(UNIQUE); State = 623; + Match(UNIQUE); + State = 625; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CLUSTERED || _la==NONCLUSTERED) { { - State = 622; + State = 624; clusteredOption(); } } - State = 625; + State = 627; Match(LPAREN); - State = 626; + State = 628; ((UniqueTableConstraintContext)_localctx)._identifier = identifier(); ((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier); - State = 631; + State = 633; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 627; + State = 629; Match(COMMA); - State = 628; + State = 630; ((UniqueTableConstraintContext)_localctx)._identifier = identifier(); ((UniqueTableConstraintContext)_localctx)._columns.Add(((UniqueTableConstraintContext)_localctx)._identifier); } } - State = 633; + State = 635; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 634; + State = 636; Match(RPAREN); } break; @@ -3619,105 +3639,105 @@ public TableConstraintContext tableConstraint() { _localctx = new ForeignKeyTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 638; + State = 640; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 636; + State = 638; Match(CONSTRAINT); - State = 637; + State = 639; ((ForeignKeyTableConstraintContext)_localctx).name = identifier(); } } - State = 640; + State = 642; Match(FOREIGN); - State = 641; + State = 643; Match(KEY); - State = 644; + State = 646; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NO) { { - State = 642; + State = 644; ((ForeignKeyTableConstraintContext)_localctx).noIndex = Match(NO); - State = 643; + State = 645; Match(INDEX); } } - State = 646; + State = 648; Match(LPAREN); - State = 647; + State = 649; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); - State = 652; + State = 654; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 648; + State = 650; Match(COMMA); - State = 649; + State = 651; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._columns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); } } - State = 654; + State = 656; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 655; + State = 657; Match(RPAREN); - State = 656; + State = 658; Match(REFERENCES); - State = 657; + State = 659; ((ForeignKeyTableConstraintContext)_localctx).refTable = identifier(); - State = 669; + State = 671; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==LPAREN) { { - State = 658; + State = 660; Match(LPAREN); - State = 659; + State = 661; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); - State = 664; + State = 666; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 660; + State = 662; Match(COMMA); - State = 661; + State = 663; ((ForeignKeyTableConstraintContext)_localctx)._identifier = identifier(); ((ForeignKeyTableConstraintContext)_localctx)._refColumns.Add(((ForeignKeyTableConstraintContext)_localctx)._identifier); } } - State = 666; + State = 668; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 667; + State = 669; Match(RPAREN); } } - State = 674; + State = 676; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==ON) { { { - State = 671; + State = 673; foreignKeyAction(); } } - State = 676; + State = 678; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -3727,25 +3747,25 @@ public TableConstraintContext tableConstraint() { _localctx = new CheckTableConstraintContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 679; + State = 681; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==CONSTRAINT) { { - State = 677; + State = 679; Match(CONSTRAINT); - State = 678; + State = 680; ((CheckTableConstraintContext)_localctx).name = identifier(); } } - State = 681; + State = 683; Match(CHECK); - State = 682; + State = 684; Match(LPAREN); - State = 683; + State = 685; checkBody(); - State = 684; + State = 686; Match(RPAREN); } break; @@ -3798,12 +3818,12 @@ public CheckBodyContext checkBody() { try { EnterOuterAlt(_localctx, 1); { - State = 695; + State = 697; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & -2L) != 0) || ((((_la - 64)) & ~0x3f) == 0 && ((1L << (_la - 64)) & -1L) != 0) || ((((_la - 128)) & ~0x3f) == 0 && ((1L << (_la - 128)) & 67108607L) != 0)) { { - State = 693; + State = 695; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: @@ -3958,7 +3978,7 @@ public CheckBodyContext checkBody() { case LINE_COMMENT: case BLOCK_COMMENT: { - State = 688; + State = 690; _la = TokenStream.LA(1); if ( _la <= 0 || (_la==LPAREN || _la==RPAREN) ) { ErrorHandler.RecoverInline(this); @@ -3971,11 +3991,11 @@ public CheckBodyContext checkBody() { break; case LPAREN: { - State = 689; + State = 691; Match(LPAREN); - State = 690; + State = 692; checkBody(); - State = 691; + State = 693; Match(RPAREN); } break; @@ -3983,7 +4003,7 @@ public CheckBodyContext checkBody() { throw new NoViableAltException(this); } } - State = 697; + State = 699; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4046,18 +4066,18 @@ public ForeignKeyActionContext foreignKeyAction() { ForeignKeyActionContext _localctx = new ForeignKeyActionContext(Context, State); EnterRule(_localctx, 60, RULE_foreignKeyAction); try { - State = 704; + State = 706; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,76,Context) ) { case 1: _localctx = new OnUpdateActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 698; + State = 700; Match(ON); - State = 699; + State = 701; Match(UPDATE); - State = 700; + State = 702; referentialAction(); } break; @@ -4065,11 +4085,11 @@ public ForeignKeyActionContext foreignKeyAction() { _localctx = new OnDeleteActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 701; + State = 703; Match(ON); - State = 702; + State = 704; Match(DELETE); - State = 703; + State = 705; referentialAction(); } break; @@ -4157,14 +4177,14 @@ public ReferentialActionContext referentialAction() { ReferentialActionContext _localctx = new ReferentialActionContext(Context, State); EnterRule(_localctx, 62, RULE_referentialAction); try { - State = 714; + State = 716; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,77,Context) ) { case 1: _localctx = new CascadeActionContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 706; + State = 708; Match(CASCADE); } break; @@ -4172,9 +4192,9 @@ public ReferentialActionContext referentialAction() { _localctx = new NoActionActionContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 707; + State = 709; Match(NO); - State = 708; + State = 710; Match(ACTION); } break; @@ -4182,7 +4202,7 @@ public ReferentialActionContext referentialAction() { _localctx = new RestrictActionContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 709; + State = 711; Match(RESTRICT); } break; @@ -4190,9 +4210,9 @@ public ReferentialActionContext referentialAction() { _localctx = new SetNullActionContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 710; + State = 712; Match(SET); - State = 711; + State = 713; Match(NULL); } break; @@ -4200,9 +4220,9 @@ public ReferentialActionContext referentialAction() { _localctx = new SetDefaultActionContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 712; + State = 714; Match(SET); - State = 713; + State = 715; Match(DEFAULT); } break; @@ -4270,73 +4290,73 @@ public InsertStatementContext insertStatement() { try { EnterOuterAlt(_localctx, 1); { - State = 716; + State = 718; Match(INSERT); - State = 717; + State = 719; Match(INTO); - State = 718; + State = 720; _localctx.table = identifier(); - State = 746; + State = 748; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: case VALUES: case LPAREN: { - State = 730; + State = 732; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,79,Context) ) { case 1: { - State = 719; + State = 721; Match(LPAREN); - State = 720; + State = 722; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); - State = 725; + State = 727; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 721; + State = 723; Match(COMMA); - State = 722; + State = 724; _localctx._identifier = identifier(); _localctx._columns.Add(_localctx._identifier); } } - State = 727; + State = 729; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 728; + State = 730; Match(RPAREN); } break; } - State = 742; + State = 744; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,81,Context) ) { case 1: { - State = 732; + State = 734; Match(VALUES); - State = 733; + State = 735; rowValues(); - State = 738; + State = 740; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 734; + State = 736; Match(COMMA); - State = 735; + State = 737; rowValues(); } } - State = 740; + State = 742; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4344,7 +4364,7 @@ public InsertStatementContext insertStatement() { break; case 2: { - State = 741; + State = 743; _localctx.source = queryExpression(); } break; @@ -4353,9 +4373,9 @@ public InsertStatementContext insertStatement() { break; case DEFAULT: { - State = 744; + State = 746; Match(DEFAULT); - State = 745; + State = 747; Match(VALUES); } break; @@ -4409,27 +4429,27 @@ public RowValuesContext rowValues() { try { EnterOuterAlt(_localctx, 1); { - State = 748; + State = 750; Match(LPAREN); - State = 749; + State = 751; rowValue(); - State = 754; + State = 756; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 750; + State = 752; Match(COMMA); - State = 751; + State = 753; rowValue(); } } - State = 756; + State = 758; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 757; + State = 759; Match(RPAREN); } } @@ -4467,13 +4487,13 @@ public RowValueContext rowValue() { RowValueContext _localctx = new RowValueContext(Context, State); EnterRule(_localctx, 68, RULE_rowValue); try { - State = 761; + State = 763; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case DEFAULT: EnterOuterAlt(_localctx, 1); { - State = 759; + State = 761; Match(DEFAULT); } break; @@ -4519,7 +4539,7 @@ public RowValueContext rowValue() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 760; + State = 762; expression(0); } break; @@ -4578,40 +4598,40 @@ public QueryExpressionContext queryExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 763; + State = 765; queryTerm(); - State = 769; + State = 771; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 117093590311632896L) != 0)) { { { - State = 764; + State = 766; setOperator(); - State = 765; + State = 767; queryTerm(); } } - State = 771; + State = 773; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 773; + State = 775; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 772; + State = 774; orderByClause(); } } - State = 776; + State = 778; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OFFSET || _la==FETCH) { { - State = 775; + State = 777; offsetFetchClause(); } } @@ -4694,14 +4714,14 @@ public QueryTermContext queryTerm() { EnterRule(_localctx, 72, RULE_queryTerm); int _la; try { - State = 792; + State = 794; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case SELECT: _localctx = new SelectTermContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 778; + State = 780; querySpecification(); } break; @@ -4709,11 +4729,11 @@ public QueryTermContext queryTerm() { _localctx = new ParenTermContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 779; + State = 781; Match(LPAREN); - State = 780; + State = 782; queryExpression(); - State = 781; + State = 783; Match(RPAREN); } break; @@ -4721,23 +4741,23 @@ public QueryTermContext queryTerm() { _localctx = new ValuesTermContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 783; + State = 785; Match(VALUES); - State = 784; + State = 786; rowValues(); - State = 789; + State = 791; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 785; + State = 787; Match(COMMA); - State = 786; + State = 788; rowValues(); } } - State = 791; + State = 793; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -4782,20 +4802,20 @@ public SetOperatorContext setOperator() { EnterRule(_localctx, 74, RULE_setOperator); int _la; try { - State = 800; + State = 802; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case UNION: EnterOuterAlt(_localctx, 1); { - State = 794; - Match(UNION); State = 796; + Match(UNION); + State = 798; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ALL) { { - State = 795; + State = 797; Match(ALL); } } @@ -4805,14 +4825,14 @@ public SetOperatorContext setOperator() { case INTERSECT: EnterOuterAlt(_localctx, 2); { - State = 798; + State = 800; Match(INTERSECT); } break; case EXCEPT: EnterOuterAlt(_localctx, 3); { - State = 799; + State = 801; Match(EXCEPT); } break; @@ -4881,78 +4901,78 @@ public QuerySpecificationContext querySpecification() { try { EnterOuterAlt(_localctx, 1); { - State = 802; - Match(SELECT); State = 804; + Match(SELECT); + State = 806; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) { { - State = 803; + State = 805; _localctx.predicate = selectPredicate(); } } - State = 807; + State = 809; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TOP) { { - State = 806; + State = 808; topClause(); } } - State = 809; + State = 811; selectList(); - State = 812; + State = 814; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INTO) { { - State = 810; + State = 812; Match(INTO); - State = 811; + State = 813; _localctx.into = identifier(); } } - State = 815; + State = 817; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 814; + State = 816; fromClause(); } } - State = 818; + State = 820; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==WHERE) { { - State = 817; + State = 819; whereClause(); } } - State = 821; + State = 823; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==GROUP) { { - State = 820; + State = 822; groupByClause(); } } - State = 824; + State = 826; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==HAVING) { { - State = 823; + State = 825; havingClause(); } } @@ -4995,7 +5015,7 @@ public SelectPredicateContext selectPredicate() { try { EnterOuterAlt(_localctx, 1); { - State = 826; + State = 828; _la = TokenStream.LA(1); if ( !((((_la) & ~0x3f) == 0 && ((1L << _la) & 18014450049089536L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -5051,25 +5071,25 @@ public GroupByClauseContext groupByClause() { try { EnterOuterAlt(_localctx, 1); { - State = 828; + State = 830; Match(GROUP); - State = 829; + State = 831; Match(BY); - State = 830; + State = 832; expression(0); - State = 835; + State = 837; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 831; + State = 833; Match(COMMA); - State = 832; + State = 834; expression(0); } } - State = 837; + State = 839; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5111,9 +5131,9 @@ public HavingClauseContext havingClause() { try { EnterOuterAlt(_localctx, 1); { - State = 838; + State = 840; Match(HAVING); - State = 839; + State = 841; expression(0); } } @@ -5168,18 +5188,18 @@ public TopClauseContext topClause() { int _alt; EnterOuterAlt(_localctx, 1); { - State = 841; + State = 843; Match(TOP); - State = 842; + State = 844; topOperand(); - State = 847; + State = 849; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,100,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { if ( _alt==1 ) { { { - State = 843; + State = 845; _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { ErrorHandler.RecoverInline(this); @@ -5188,21 +5208,21 @@ public TopClauseContext topClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 844; + State = 846; topOperand(); } } } - State = 849; + State = 851; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,100,Context); } - State = 851; + State = 853; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PERCENT) { { - State = 850; + State = 852; _localctx.percent = Match(PERCENT); } } @@ -5246,31 +5266,31 @@ public TopOperandContext topOperand() { TopOperandContext _localctx = new TopOperandContext(Context, State); EnterRule(_localctx, 86, RULE_topOperand); try { - State = 859; + State = 861; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: EnterOuterAlt(_localctx, 1); { - State = 853; + State = 855; Match(INTEGER_LITERAL); } break; case PARAM: EnterOuterAlt(_localctx, 2); { - State = 854; + State = 856; Match(PARAM); } break; case LPAREN: EnterOuterAlt(_localctx, 3); { - State = 855; + State = 857; Match(LPAREN); - State = 856; + State = 858; expression(0); - State = 857; + State = 859; Match(RPAREN); } break; @@ -5328,26 +5348,26 @@ public OffsetFetchClauseContext offsetFetchClause() { EnterRule(_localctx, 88, RULE_offsetFetchClause); int _la; try { - State = 878; + State = 880; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case OFFSET: EnterOuterAlt(_localctx, 1); { - State = 861; + State = 863; Match(OFFSET); - State = 862; + State = 864; _localctx.offset = expression(0); - State = 863; + State = 865; rowKeyword(); - State = 870; + State = 872; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FETCH) { { - State = 864; + State = 866; Match(FETCH); - State = 865; + State = 867; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5356,11 +5376,11 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 866; + State = 868; _localctx.limit = expression(0); - State = 867; + State = 869; rowKeyword(); - State = 868; + State = 870; Match(ONLY); } } @@ -5370,9 +5390,9 @@ public OffsetFetchClauseContext offsetFetchClause() { case FETCH: EnterOuterAlt(_localctx, 2); { - State = 872; + State = 874; Match(FETCH); - State = 873; + State = 875; _la = TokenStream.LA(1); if ( !(_la==NEXT || _la==FIRST) ) { ErrorHandler.RecoverInline(this); @@ -5381,11 +5401,11 @@ public OffsetFetchClauseContext offsetFetchClause() { ErrorHandler.ReportMatch(this); Consume(); } - State = 874; + State = 876; _localctx.limit = expression(0); - State = 875; + State = 877; rowKeyword(); - State = 876; + State = 878; Match(ONLY); } break; @@ -5428,7 +5448,7 @@ public RowKeywordContext rowKeyword() { try { EnterOuterAlt(_localctx, 1); { - State = 880; + State = 882; _la = TokenStream.LA(1); if ( !(_la==ROWS || _la==ROW) ) { ErrorHandler.RecoverInline(this); @@ -5481,13 +5501,13 @@ public SelectListContext selectList() { EnterRule(_localctx, 92, RULE_selectList); int _la; try { - State = 891; + State = 893; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: EnterOuterAlt(_localctx, 1); { - State = 882; + State = 884; Match(STAR); } break; @@ -5533,21 +5553,21 @@ public SelectListContext selectList() { case IDENTIFIER: EnterOuterAlt(_localctx, 2); { - State = 883; + State = 885; selectItem(); - State = 888; + State = 890; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 884; + State = 886; Match(COMMA); - State = 885; + State = 887; selectItem(); } } - State = 890; + State = 892; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5619,18 +5639,18 @@ public SelectItemContext selectItem() { EnterRule(_localctx, 94, RULE_selectItem); int _la; try { - State = 904; + State = 906; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,109,Context) ) { case 1: _localctx = new QualifiedStarSelectItemContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 893; + State = 895; ((QualifiedStarSelectItemContext)_localctx).qualifier = identifier(); - State = 894; + State = 896; Match(DOT); - State = 895; + State = 897; Match(STAR); } break; @@ -5638,24 +5658,24 @@ public SelectItemContext selectItem() { _localctx = new ExpressionSelectItemContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 897; + State = 899; expression(0); - State = 902; + State = 904; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) { { - State = 899; + State = 901; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 898; + State = 900; Match(AS); } } - State = 901; + State = 903; ((ExpressionSelectItemContext)_localctx).alias = identifier(); } } @@ -5708,23 +5728,23 @@ public FromClauseContext fromClause() { try { EnterOuterAlt(_localctx, 1); { - State = 906; + State = 908; Match(FROM); - State = 907; + State = 909; tableSource(); - State = 912; + State = 914; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 908; + State = 910; Match(COMMA); - State = 909; + State = 911; tableSource(); } } - State = 914; + State = 916; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5772,19 +5792,19 @@ public TableSourceContext tableSource() { try { EnterOuterAlt(_localctx, 1); { - State = 915; + State = 917; tablePrimary(); - State = 919; + State = 921; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while ((((_la) & ~0x3f) == 0 && ((1L << _la) & 137455468544L) != 0)) { { { - State = 916; + State = 918; joinClause(); } } - State = 921; + State = 923; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -5871,31 +5891,31 @@ public TablePrimaryContext tablePrimary() { EnterRule(_localctx, 100, RULE_tablePrimary); int _la; try { - State = 942; + State = 944; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,116,Context) ) { case 1: _localctx = new NamedTablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 922; + State = 924; ((NamedTablePrimaryContext)_localctx).table = identifier(); - State = 927; + State = 929; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) { { - State = 924; + State = 926; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 923; + State = 925; Match(AS); } } - State = 926; + State = 928; ((NamedTablePrimaryContext)_localctx).alias = identifier(); } } @@ -5906,28 +5926,28 @@ public TablePrimaryContext tablePrimary() { _localctx = new SubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 929; + State = 931; Match(LPAREN); - State = 930; + State = 932; queryExpression(); - State = 931; + State = 933; Match(RPAREN); - State = 936; + State = 938; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS || ((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 7696581410815L) != 0)) { { - State = 933; + State = 935; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==AS) { { - State = 932; + State = 934; Match(AS); } } - State = 935; + State = 937; ((SubqueryPrimaryContext)_localctx).alias = identifier(); } } @@ -5938,11 +5958,11 @@ public TablePrimaryContext tablePrimary() { _localctx = new ParenJoinPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 938; + State = 940; Match(LPAREN); - State = 939; + State = 941; tableSource(); - State = 940; + State = 942; Match(RPAREN); } break; @@ -6039,22 +6059,22 @@ public JoinClauseContext joinClause() { JoinClauseContext _localctx = new JoinClauseContext(Context, State); EnterRule(_localctx, 102, RULE_joinClause); try { - State = 959; + State = 961; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,117,Context) ) { case 1: _localctx = new ConditionalJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 944; + State = 946; joinType(); - State = 945; + State = 947; Match(JOIN); - State = 946; + State = 948; tablePrimary(); - State = 947; + State = 949; Match(ON); - State = 948; + State = 950; expression(0); } break; @@ -6062,11 +6082,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 950; + State = 952; Match(CROSS); - State = 951; + State = 953; Match(JOIN); - State = 952; + State = 954; tablePrimary(); } break; @@ -6074,11 +6094,11 @@ public JoinClauseContext joinClause() { _localctx = new CrossApplyContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 953; + State = 955; Match(CROSS); - State = 954; + State = 956; Match(APPLY); - State = 955; + State = 957; tablePrimary(); } break; @@ -6086,11 +6106,11 @@ public JoinClauseContext joinClause() { _localctx = new OuterApplyContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 956; + State = 958; Match(OUTER); - State = 957; + State = 959; Match(APPLY); - State = 958; + State = 960; tablePrimary(); } break; @@ -6169,7 +6189,7 @@ public JoinTypeContext joinType() { EnterRule(_localctx, 104, RULE_joinType); int _la; try { - State = 976; + State = 978; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INNER: @@ -6177,12 +6197,12 @@ public JoinTypeContext joinType() { _localctx = new InnerJoinContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 962; + State = 964; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==INNER) { { - State = 961; + State = 963; Match(INNER); } } @@ -6193,14 +6213,14 @@ public JoinTypeContext joinType() { _localctx = new LeftJoinContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 964; - Match(LEFT); State = 966; + Match(LEFT); + State = 968; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 965; + State = 967; Match(OUTER); } } @@ -6211,14 +6231,14 @@ public JoinTypeContext joinType() { _localctx = new RightJoinContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 968; - Match(RIGHT); State = 970; + Match(RIGHT); + State = 972; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 969; + State = 971; Match(OUTER); } } @@ -6229,14 +6249,14 @@ public JoinTypeContext joinType() { _localctx = new FullJoinContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 972; - Match(FULL); State = 974; + Match(FULL); + State = 976; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==OUTER) { { - State = 973; + State = 975; Match(OUTER); } } @@ -6283,9 +6303,9 @@ public WhereClauseContext whereClause() { try { EnterOuterAlt(_localctx, 1); { - State = 978; + State = 980; Match(WHERE); - State = 979; + State = 981; expression(0); } } @@ -6334,25 +6354,25 @@ public OrderByClauseContext orderByClause() { try { EnterOuterAlt(_localctx, 1); { - State = 981; + State = 983; Match(ORDER); - State = 982; + State = 984; Match(BY); - State = 983; + State = 985; orderByItem(); - State = 988; + State = 990; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 984; + State = 986; Match(COMMA); - State = 985; + State = 987; orderByItem(); } } - State = 990; + State = 992; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -6397,14 +6417,14 @@ public OrderByItemContext orderByItem() { try { EnterOuterAlt(_localctx, 1); { - State = 991; - expression(0); State = 993; + expression(0); + State = 995; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ASC || _la==DESC) { { - State = 992; + State = 994; _localctx.dir = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==ASC || _la==DESC) ) { @@ -6847,7 +6867,7 @@ private ExpressionContext expression(int _p) { int _alt; EnterOuterAlt(_localctx, 1); { - State = 1001; + State = 1003; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case PLUS: @@ -6857,7 +6877,7 @@ private ExpressionContext expression(int _p) { Context = _localctx; _prevctx = _localctx; - State = 996; + State = 998; ((NegateExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -6867,7 +6887,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 997; + State = 999; expression(19); } break; @@ -6877,7 +6897,7 @@ private ExpressionContext expression(int _p) { _localctx = new NotExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 998; + State = 1000; ((NotExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==NOT || _la==BNOT) ) { @@ -6887,7 +6907,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 999; + State = 1001; expression(7); } break; @@ -6931,7 +6951,7 @@ private ExpressionContext expression(int _p) { _localctx = new PrimaryExprContext(_localctx); Context = _localctx; _prevctx = _localctx; - State = 1000; + State = 1002; primary(); } break; @@ -6939,7 +6959,7 @@ private ExpressionContext expression(int _p) { throw new NoViableAltException(this); } Context.Stop = TokenStream.LT(-1); - State = 1087; + State = 1089; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,133,Context); while ( _alt!=2 && _alt!=global::Antlr4.Runtime.Atn.ATN.INVALID_ALT_NUMBER ) { @@ -6948,7 +6968,7 @@ private ExpressionContext expression(int _p) { TriggerExitRuleEvent(); _prevctx = _localctx; { - State = 1085; + State = 1087; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,132,Context) ) { case 1: @@ -6956,11 +6976,11 @@ private ExpressionContext expression(int _p) { _localctx = new PowExprContext(new ExpressionContext(_parentctx, _parentState)); ((PowExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1003; + State = 1005; if (!(Precpred(Context, 20))) throw new FailedPredicateException(this, "Precpred(Context, 20)"); - State = 1004; + State = 1006; Match(CARET); - State = 1005; + State = 1007; ((PowExprContext)_localctx).right = expression(21); } break; @@ -6969,9 +6989,9 @@ private ExpressionContext expression(int _p) { _localctx = new MulDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((MulDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1006; + State = 1008; if (!(Precpred(Context, 18))) throw new FailedPredicateException(this, "Precpred(Context, 18)"); - State = 1007; + State = 1009; ((MulDivExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==STAR || _la==SLASH) ) { @@ -6981,7 +7001,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1008; + State = 1010; ((MulDivExprContext)_localctx).right = expression(19); } break; @@ -6990,11 +7010,11 @@ private ExpressionContext expression(int _p) { _localctx = new IntDivExprContext(new ExpressionContext(_parentctx, _parentState)); ((IntDivExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1009; + State = 1011; if (!(Precpred(Context, 17))) throw new FailedPredicateException(this, "Precpred(Context, 17)"); - State = 1010; + State = 1012; ((IntDivExprContext)_localctx).op = Match(BACKSLASH); - State = 1011; + State = 1013; ((IntDivExprContext)_localctx).right = expression(18); } break; @@ -7003,11 +7023,11 @@ private ExpressionContext expression(int _p) { _localctx = new ModExprContext(new ExpressionContext(_parentctx, _parentState)); ((ModExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1012; + State = 1014; if (!(Precpred(Context, 16))) throw new FailedPredicateException(this, "Precpred(Context, 16)"); - State = 1013; + State = 1015; ((ModExprContext)_localctx).op = Match(MOD); - State = 1014; + State = 1016; ((ModExprContext)_localctx).right = expression(17); } break; @@ -7016,9 +7036,9 @@ private ExpressionContext expression(int _p) { _localctx = new AddSubExprContext(new ExpressionContext(_parentctx, _parentState)); ((AddSubExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1015; + State = 1017; if (!(Precpred(Context, 15))) throw new FailedPredicateException(this, "Precpred(Context, 15)"); - State = 1016; + State = 1018; ((AddSubExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PLUS || _la==MINUS) ) { @@ -7028,7 +7048,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1017; + State = 1019; ((AddSubExprContext)_localctx).right = expression(16); } break; @@ -7037,11 +7057,11 @@ private ExpressionContext expression(int _p) { _localctx = new ConcatExprContext(new ExpressionContext(_parentctx, _parentState)); ((ConcatExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1018; + State = 1020; if (!(Precpred(Context, 14))) throw new FailedPredicateException(this, "Precpred(Context, 14)"); - State = 1019; + State = 1021; ((ConcatExprContext)_localctx).op = Match(AMP); - State = 1020; + State = 1022; ((ConcatExprContext)_localctx).right = expression(15); } break; @@ -7050,9 +7070,9 @@ private ExpressionContext expression(int _p) { _localctx = new ComparisonExprContext(new ExpressionContext(_parentctx, _parentState)); ((ComparisonExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1021; + State = 1023; if (!(Precpred(Context, 13))) throw new FailedPredicateException(this, "Precpred(Context, 13)"); - State = 1022; + State = 1024; ((ComparisonExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(((((_la - 129)) & ~0x3f) == 0 && ((1L << (_la - 129)) & 63L) != 0)) ) { @@ -7062,7 +7082,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1023; + State = 1025; ((ComparisonExprContext)_localctx).right = expression(14); } break; @@ -7071,25 +7091,25 @@ private ExpressionContext expression(int _p) { _localctx = new BetweenExprContext(new ExpressionContext(_parentctx, _parentState)); ((BetweenExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1024; - if (!(Precpred(Context, 12))) throw new FailedPredicateException(this, "Precpred(Context, 12)"); State = 1026; + if (!(Precpred(Context, 12))) throw new FailedPredicateException(this, "Precpred(Context, 12)"); + State = 1028; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1025; + State = 1027; ((BetweenExprContext)_localctx).not = Match(NOT); } } - State = 1028; + State = 1030; Match(BETWEEN); - State = 1029; + State = 1031; ((BetweenExprContext)_localctx).lo = expression(0); - State = 1030; + State = 1032; Match(AND); - State = 1031; + State = 1033; ((BetweenExprContext)_localctx).hi = expression(13); } break; @@ -7098,21 +7118,21 @@ private ExpressionContext expression(int _p) { _localctx = new LikeExprContext(new ExpressionContext(_parentctx, _parentState)); ((LikeExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1033; - if (!(Precpred(Context, 11))) throw new FailedPredicateException(this, "Precpred(Context, 11)"); State = 1035; + if (!(Precpred(Context, 11))) throw new FailedPredicateException(this, "Precpred(Context, 11)"); + State = 1037; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1034; + State = 1036; ((LikeExprContext)_localctx).not = Match(NOT); } } - State = 1037; + State = 1039; Match(LIKE); - State = 1038; + State = 1040; ((LikeExprContext)_localctx).right = expression(12); } break; @@ -7121,9 +7141,9 @@ private ExpressionContext expression(int _p) { _localctx = new AndExprContext(new ExpressionContext(_parentctx, _parentState)); ((AndExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1039; + State = 1041; if (!(Precpred(Context, 6))) throw new FailedPredicateException(this, "Precpred(Context, 6)"); - State = 1040; + State = 1042; ((AndExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==AND || _la==BAND) ) { @@ -7133,7 +7153,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1041; + State = 1043; ((AndExprContext)_localctx).right = expression(7); } break; @@ -7142,9 +7162,9 @@ private ExpressionContext expression(int _p) { _localctx = new OrExprContext(new ExpressionContext(_parentctx, _parentState)); ((OrExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1042; + State = 1044; if (!(Precpred(Context, 5))) throw new FailedPredicateException(this, "Precpred(Context, 5)"); - State = 1043; + State = 1045; ((OrExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==OR || _la==BOR) ) { @@ -7154,7 +7174,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1044; + State = 1046; ((OrExprContext)_localctx).right = expression(6); } break; @@ -7163,9 +7183,9 @@ private ExpressionContext expression(int _p) { _localctx = new XorExprContext(new ExpressionContext(_parentctx, _parentState)); ((XorExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1045; + State = 1047; if (!(Precpred(Context, 4))) throw new FailedPredicateException(this, "Precpred(Context, 4)"); - State = 1046; + State = 1048; ((XorExprContext)_localctx).op = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==XOR || _la==BXOR) ) { @@ -7175,7 +7195,7 @@ private ExpressionContext expression(int _p) { ErrorHandler.ReportMatch(this); Consume(); } - State = 1047; + State = 1049; ((XorExprContext)_localctx).right = expression(5); } break; @@ -7184,11 +7204,11 @@ private ExpressionContext expression(int _p) { _localctx = new EqvExprContext(new ExpressionContext(_parentctx, _parentState)); ((EqvExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1048; + State = 1050; if (!(Precpred(Context, 3))) throw new FailedPredicateException(this, "Precpred(Context, 3)"); - State = 1049; + State = 1051; ((EqvExprContext)_localctx).op = Match(EQV); - State = 1050; + State = 1052; ((EqvExprContext)_localctx).right = expression(4); } break; @@ -7197,11 +7217,11 @@ private ExpressionContext expression(int _p) { _localctx = new ImpExprContext(new ExpressionContext(_parentctx, _parentState)); ((ImpExprContext)_localctx).left = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1051; + State = 1053; if (!(Precpred(Context, 2))) throw new FailedPredicateException(this, "Precpred(Context, 2)"); - State = 1052; + State = 1054; ((ImpExprContext)_localctx).op = Match(IMP); - State = 1053; + State = 1055; ((ImpExprContext)_localctx).right = expression(3); } break; @@ -7210,25 +7230,25 @@ private ExpressionContext expression(int _p) { _localctx = new InSubqueryExprContext(new ExpressionContext(_parentctx, _parentState)); ((InSubqueryExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1054; - if (!(Precpred(Context, 10))) throw new FailedPredicateException(this, "Precpred(Context, 10)"); State = 1056; + if (!(Precpred(Context, 10))) throw new FailedPredicateException(this, "Precpred(Context, 10)"); + State = 1058; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1055; + State = 1057; ((InSubqueryExprContext)_localctx).not = Match(NOT); } } - State = 1058; + State = 1060; Match(IN); - State = 1059; + State = 1061; Match(LPAREN); - State = 1060; + State = 1062; ((InSubqueryExprContext)_localctx).sub = queryExpression(); - State = 1061; + State = 1063; Match(RPAREN); } break; @@ -7237,43 +7257,43 @@ private ExpressionContext expression(int _p) { _localctx = new InExprContext(new ExpressionContext(_parentctx, _parentState)); ((InExprContext)_localctx).val = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1063; - if (!(Precpred(Context, 9))) throw new FailedPredicateException(this, "Precpred(Context, 9)"); State = 1065; + if (!(Precpred(Context, 9))) throw new FailedPredicateException(this, "Precpred(Context, 9)"); + State = 1067; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1064; + State = 1066; ((InExprContext)_localctx).not = Match(NOT); } } - State = 1067; + State = 1069; Match(IN); - State = 1068; + State = 1070; Match(LPAREN); - State = 1069; + State = 1071; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); - State = 1074; + State = 1076; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1070; + State = 1072; Match(COMMA); - State = 1071; + State = 1073; ((InExprContext)_localctx)._expression = expression(0); ((InExprContext)_localctx)._items.Add(((InExprContext)_localctx)._expression); } } - State = 1076; + State = 1078; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } - State = 1077; + State = 1079; Match(RPAREN); } break; @@ -7282,28 +7302,28 @@ private ExpressionContext expression(int _p) { _localctx = new IsNullExprContext(new ExpressionContext(_parentctx, _parentState)); ((IsNullExprContext)_localctx).operand = _prevctx; PushNewRecursionContext(_localctx, _startState, RULE_expression); - State = 1079; + State = 1081; if (!(Precpred(Context, 8))) throw new FailedPredicateException(this, "Precpred(Context, 8)"); - State = 1080; - Match(IS); State = 1082; + Match(IS); + State = 1084; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==NOT) { { - State = 1081; + State = 1083; ((IsNullExprContext)_localctx).not = Match(NOT); } } - State = 1084; + State = 1086; Match(NULL); } break; } } } - State = 1089; + State = 1091; ErrorHandler.Sync(this); _alt = Interpreter.AdaptivePredict(TokenStream,133,Context); } @@ -7449,14 +7469,14 @@ public PrimaryContext primary() { PrimaryContext _localctx = new PrimaryContext(Context, State); EnterRule(_localctx, 114, RULE_primary); try { - State = 1109; + State = 1111; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,134,Context) ) { case 1: _localctx = new LiteralPrimaryContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1090; + State = 1092; literal(); } break; @@ -7464,7 +7484,7 @@ public PrimaryContext primary() { _localctx = new CasePrimaryContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1091; + State = 1093; caseExpression(); } break; @@ -7472,7 +7492,7 @@ public PrimaryContext primary() { _localctx = new FunctionCallPrimaryContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1092; + State = 1094; functionCall(); } break; @@ -7480,7 +7500,7 @@ public PrimaryContext primary() { _localctx = new ColumnPrimaryContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1093; + State = 1095; columnRef(); } break; @@ -7488,7 +7508,7 @@ public PrimaryContext primary() { _localctx = new ParamPrimaryContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1094; + State = 1096; Match(PARAM); } break; @@ -7496,7 +7516,7 @@ public PrimaryContext primary() { _localctx = new SystemVariablePrimaryContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1095; + State = 1097; Match(SYSVAR); } break; @@ -7504,13 +7524,13 @@ public PrimaryContext primary() { _localctx = new ExistsPrimaryContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1096; + State = 1098; Match(EXISTS); - State = 1097; + State = 1099; Match(LPAREN); - State = 1098; + State = 1100; queryExpression(); - State = 1099; + State = 1101; Match(RPAREN); } break; @@ -7518,11 +7538,11 @@ public PrimaryContext primary() { _localctx = new ScalarSubqueryPrimaryContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1101; + State = 1103; Match(LPAREN); - State = 1102; + State = 1104; queryExpression(); - State = 1103; + State = 1105; Match(RPAREN); } break; @@ -7530,11 +7550,11 @@ public PrimaryContext primary() { _localctx = new ParenPrimaryContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1105; + State = 1107; Match(LPAREN); - State = 1106; + State = 1108; expression(0); - State = 1107; + State = 1109; Match(RPAREN); } break; @@ -7590,45 +7610,45 @@ public CaseExpressionContext caseExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 1111; - Match(CASE); State = 1113; + Match(CASE); + State = 1115; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if ((((_la) & ~0x3f) == 0 && ((1L << _la) & 284775660683520L) != 0) || ((((_la - 103)) & ~0x3f) == 0 && ((1L << (_la - 103)) & 281341858414589L) != 0)) { { - State = 1112; + State = 1114; _localctx.operand = expression(0); } } - State = 1116; + State = 1118; ErrorHandler.Sync(this); _la = TokenStream.LA(1); do { { { - State = 1115; + State = 1117; caseWhen(); } } - State = 1118; + State = 1120; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } while ( _la==WHEN ); - State = 1122; + State = 1124; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ELSE) { { - State = 1120; + State = 1122; Match(ELSE); - State = 1121; + State = 1123; _localctx.elseResult = expression(0); } } - State = 1124; + State = 1126; Match(END); } } @@ -7674,13 +7694,13 @@ public CaseWhenContext caseWhen() { try { EnterOuterAlt(_localctx, 1); { - State = 1126; + State = 1128; Match(WHEN); - State = 1127; + State = 1129; _localctx.condition = expression(0); - State = 1128; + State = 1130; Match(THEN); - State = 1129; + State = 1131; _localctx.result = expression(0); } } @@ -7753,16 +7773,16 @@ public FunctionCallContext functionCall() { try { EnterOuterAlt(_localctx, 1); { - State = 1131; + State = 1133; _localctx.name = functionName(); - State = 1132; + State = 1134; Match(LPAREN); - State = 1145; + State = 1147; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case STAR: { - State = 1133; + State = 1135; _localctx.star = Match(STAR); } break; @@ -7809,31 +7829,31 @@ public FunctionCallContext functionCall() { case IDENTIFIER: { { - State = 1135; + State = 1137; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==DISTINCT) { { - State = 1134; + State = 1136; _localctx.distinct = Match(DISTINCT); } } - State = 1137; + State = 1139; expression(0); - State = 1142; + State = 1144; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1138; + State = 1140; Match(COMMA); - State = 1139; + State = 1141; expression(0); } } - State = 1144; + State = 1146; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } @@ -7845,56 +7865,56 @@ public FunctionCallContext functionCall() { default: break; } - State = 1147; - Match(RPAREN); State = 1149; + Match(RPAREN); + State = 1151; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,141,Context) ) { case 1: { - State = 1148; + State = 1150; withinGroup(); } break; } - State = 1152; + State = 1154; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,142,Context) ) { case 1: { - State = 1151; + State = 1153; filterClause(); } break; } - State = 1162; + State = 1164; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,145,Context) ) { case 1: { - State = 1155; + State = 1157; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==FROM) { { - State = 1154; + State = 1156; nthRowFrom(); } } - State = 1158; + State = 1160; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==IGNORE || _la==RESPECT) { { - State = 1157; + State = 1159; nullTreatment(); } } - State = 1160; + State = 1162; Match(OVER); - State = 1161; + State = 1163; windowSpecification(); } break; @@ -7939,7 +7959,7 @@ public FunctionNameContext functionName() { FunctionNameContext _localctx = new FunctionNameContext(Context, State); EnterRule(_localctx, 122, RULE_functionName); try { - State = 1170; + State = 1172; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case RANGE: @@ -7961,42 +7981,42 @@ public FunctionNameContext functionName() { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1164; + State = 1166; identifier(); } break; case LEFT: EnterOuterAlt(_localctx, 2); { - State = 1165; + State = 1167; Match(LEFT); } break; case RIGHT: EnterOuterAlt(_localctx, 3); { - State = 1166; + State = 1168; Match(RIGHT); } break; case ASC: EnterOuterAlt(_localctx, 4); { - State = 1167; + State = 1169; Match(ASC); } break; case FIRST: EnterOuterAlt(_localctx, 5); { - State = 1168; + State = 1170; Match(FIRST); } break; case PARTITION: EnterOuterAlt(_localctx, 6); { - State = 1169; + State = 1171; Match(PARTITION); } break; @@ -8045,19 +8065,19 @@ public ColumnRefContext columnRef() { try { EnterOuterAlt(_localctx, 1); { - State = 1175; + State = 1177; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,147,Context) ) { case 1: { - State = 1172; + State = 1174; _localctx.qualifier = identifier(); - State = 1173; + State = 1175; Match(DOT); } break; } - State = 1177; + State = 1179; _localctx.name = identifier(); } } @@ -8097,27 +8117,27 @@ public IdentifierContext identifier() { IdentifierContext _localctx = new IdentifierContext(Context, State); EnterRule(_localctx, 126, RULE_identifier); try { - State = 1183; + State = 1185; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case IDENTIFIER: EnterOuterAlt(_localctx, 1); { - State = 1179; + State = 1181; Match(IDENTIFIER); } break; case BRACKET_ID: EnterOuterAlt(_localctx, 2); { - State = 1180; + State = 1182; Match(BRACKET_ID); } break; case BACKTICK_ID: EnterOuterAlt(_localctx, 3); { - State = 1181; + State = 1183; Match(BACKTICK_ID); } break; @@ -8137,7 +8157,7 @@ public IdentifierContext identifier() { case FILTER: EnterOuterAlt(_localctx, 4); { - State = 1182; + State = 1184; nonReservedKeyword(); } break; @@ -8264,14 +8284,14 @@ public LiteralContext literal() { LiteralContext _localctx = new LiteralContext(Context, State); EnterRule(_localctx, 128, RULE_literal); try { - State = 1194; + State = 1196; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case INTEGER_LITERAL: _localctx = new IntLiteralContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1185; + State = 1187; Match(INTEGER_LITERAL); } break; @@ -8279,7 +8299,7 @@ public LiteralContext literal() { _localctx = new NumberLiteralContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1186; + State = 1188; Match(NUMBER_LITERAL); } break; @@ -8287,7 +8307,7 @@ public LiteralContext literal() { _localctx = new HexLiteralContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1187; + State = 1189; Match(HEX_LITERAL); } break; @@ -8295,7 +8315,7 @@ public LiteralContext literal() { _localctx = new StringLiteralContext(_localctx); EnterOuterAlt(_localctx, 4); { - State = 1188; + State = 1190; Match(STRING_LITERAL); } break; @@ -8303,7 +8323,7 @@ public LiteralContext literal() { _localctx = new DateLiteralContext(_localctx); EnterOuterAlt(_localctx, 5); { - State = 1189; + State = 1191; Match(DATE_LITERAL); } break; @@ -8311,7 +8331,7 @@ public LiteralContext literal() { _localctx = new GuidLiteralContext(_localctx); EnterOuterAlt(_localctx, 6); { - State = 1190; + State = 1192; Match(GUID_LITERAL); } break; @@ -8319,7 +8339,7 @@ public LiteralContext literal() { _localctx = new TrueLiteralContext(_localctx); EnterOuterAlt(_localctx, 7); { - State = 1191; + State = 1193; Match(TRUE); } break; @@ -8327,7 +8347,7 @@ public LiteralContext literal() { _localctx = new FalseLiteralContext(_localctx); EnterOuterAlt(_localctx, 8); { - State = 1192; + State = 1194; Match(FALSE); } break; @@ -8335,7 +8355,7 @@ public LiteralContext literal() { _localctx = new NullLiteralContext(_localctx); EnterOuterAlt(_localctx, 9); { - State = 1193; + State = 1195; Match(NULL); } break; @@ -8409,21 +8429,21 @@ public TransactionStatementContext transactionStatement() { EnterRule(_localctx, 130, RULE_transactionStatement); int _la; try { - State = 1208; + State = 1210; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BEGIN: _localctx = new BeginTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 1); { - State = 1196; - Match(BEGIN); State = 1198; + Match(BEGIN); + State = 1200; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1197; + State = 1199; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8441,14 +8461,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new CommitTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 2); { - State = 1200; - Match(COMMIT); State = 1202; + Match(COMMIT); + State = 1204; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1201; + State = 1203; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8466,14 +8486,14 @@ public TransactionStatementContext transactionStatement() { _localctx = new RollbackTransactionStatementContext(_localctx); EnterOuterAlt(_localctx, 3); { - State = 1204; - Match(ROLLBACK); State = 1206; + Match(ROLLBACK); + State = 1208; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==TRANSACTION || _la==WORK) { { - State = 1205; + State = 1207; _la = TokenStream.LA(1); if ( !(_la==TRANSACTION || _la==WORK) ) { ErrorHandler.RecoverInline(this); @@ -8527,9 +8547,9 @@ public StandaloneExpressionContext standaloneExpression() { try { EnterOuterAlt(_localctx, 1); { - State = 1210; + State = 1212; expression(0); - State = 1211; + State = 1213; Match(Eof); } } @@ -8588,61 +8608,61 @@ public WindowSpecificationContext windowSpecification() { try { EnterOuterAlt(_localctx, 1); { - State = 1213; + State = 1215; Match(LPAREN); - State = 1224; + State = 1226; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==PARTITION) { { - State = 1214; + State = 1216; Match(PARTITION); - State = 1215; + State = 1217; Match(BY); - State = 1216; + State = 1218; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); - State = 1221; + State = 1223; ErrorHandler.Sync(this); _la = TokenStream.LA(1); while (_la==COMMA) { { { - State = 1217; + State = 1219; Match(COMMA); - State = 1218; + State = 1220; _localctx._expression = expression(0); _localctx._partition.Add(_localctx._expression); } } - State = 1223; + State = 1225; ErrorHandler.Sync(this); _la = TokenStream.LA(1); } } } - State = 1227; + State = 1229; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==ORDER) { { - State = 1226; + State = 1228; orderByClause(); } } - State = 1230; + State = 1232; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 1729382256910270465L) != 0)) { { - State = 1229; + State = 1231; windowFrame(); } } - State = 1232; + State = 1234; Match(RPAREN); } } @@ -8681,7 +8701,7 @@ public ClusteredOptionContext clusteredOption() { try { EnterOuterAlt(_localctx, 1); { - State = 1234; + State = 1236; _la = TokenStream.LA(1); if ( !(_la==CLUSTERED || _la==NONCLUSTERED) ) { ErrorHandler.RecoverInline(this); @@ -8744,7 +8764,7 @@ public WindowFrameContext windowFrame() { try { EnterOuterAlt(_localctx, 1); { - State = 1236; + State = 1238; _localctx.unit = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(((((_la - 49)) & ~0x3f) == 0 && ((1L << (_la - 49)) & 1729382256910270465L) != 0)) ) { @@ -8754,18 +8774,18 @@ public WindowFrameContext windowFrame() { ErrorHandler.ReportMatch(this); Consume(); } - State = 1243; + State = 1245; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case BETWEEN: { - State = 1237; + State = 1239; Match(BETWEEN); - State = 1238; + State = 1240; _localctx.start = frameBound(); - State = 1239; + State = 1241; Match(AND); - State = 1240; + State = 1242; _localctx.end = frameBound(); } break; @@ -8810,21 +8830,21 @@ public WindowFrameContext windowFrame() { case BACKTICK_ID: case IDENTIFIER: { - State = 1242; + State = 1244; _localctx.start = frameBound(); } break; default: throw new NoViableAltException(this); } - State = 1247; + State = 1249; ErrorHandler.Sync(this); _la = TokenStream.LA(1); if (_la==EXCLUDE) { { - State = 1245; + State = 1247; Match(EXCLUDE); - State = 1246; + State = 1248; _localctx.exclusion = frameExclusion(); } } @@ -8872,15 +8892,15 @@ public FrameBoundContext frameBound() { EnterRule(_localctx, 140, RULE_frameBound); int _la; try { - State = 1256; + State = 1258; ErrorHandler.Sync(this); switch ( Interpreter.AdaptivePredict(TokenStream,160,Context) ) { case 1: EnterOuterAlt(_localctx, 1); { - State = 1249; + State = 1251; Match(UNBOUNDED); - State = 1250; + State = 1252; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -8895,18 +8915,18 @@ public FrameBoundContext frameBound() { case 2: EnterOuterAlt(_localctx, 2); { - State = 1251; + State = 1253; Match(CURRENT); - State = 1252; + State = 1254; Match(ROW); } break; case 3: EnterOuterAlt(_localctx, 3); { - State = 1253; + State = 1255; _localctx.offset = expression(0); - State = 1254; + State = 1256; _localctx.direction = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==PRECEDING || _la==FOLLOWING) ) { @@ -8956,38 +8976,38 @@ public FrameExclusionContext frameExclusion() { FrameExclusionContext _localctx = new FrameExclusionContext(Context, State); EnterRule(_localctx, 142, RULE_frameExclusion); try { - State = 1264; + State = 1266; ErrorHandler.Sync(this); switch (TokenStream.LA(1)) { case CURRENT: EnterOuterAlt(_localctx, 1); { - State = 1258; + State = 1260; Match(CURRENT); - State = 1259; + State = 1261; Match(ROW); } break; case GROUP: EnterOuterAlt(_localctx, 2); { - State = 1260; + State = 1262; Match(GROUP); } break; case TIES: EnterOuterAlt(_localctx, 3); { - State = 1261; + State = 1263; Match(TIES); } break; case NO: EnterOuterAlt(_localctx, 4); { - State = 1262; + State = 1264; Match(NO); - State = 1263; + State = 1265; Match(OTHERS); } break; @@ -9042,7 +9062,7 @@ public NonReservedKeywordContext nonReservedKeyword() { try { EnterOuterAlt(_localctx, 1); { - State = 1266; + State = 1268; _la = TokenStream.LA(1); if ( !(((((_la - 108)) & ~0x3f) == 0 && ((1L << (_la - 108)) & 16383L) != 0)) ) { ErrorHandler.RecoverInline(this); @@ -9093,15 +9113,15 @@ public FilterClauseContext filterClause() { try { EnterOuterAlt(_localctx, 1); { - State = 1268; + State = 1270; Match(FILTER); - State = 1269; + State = 1271; Match(LPAREN); - State = 1270; + State = 1272; Match(WHERE); - State = 1271; + State = 1273; _localctx.condition = expression(0); - State = 1272; + State = 1274; Match(RPAREN); } } @@ -9144,15 +9164,15 @@ public WithinGroupContext withinGroup() { try { EnterOuterAlt(_localctx, 1); { - State = 1274; + State = 1276; Match(WITHIN); - State = 1275; + State = 1277; Match(GROUP); - State = 1276; + State = 1278; Match(LPAREN); - State = 1277; + State = 1279; orderByClause(); - State = 1278; + State = 1280; Match(RPAREN); } } @@ -9193,9 +9213,9 @@ public NthRowFromContext nthRowFrom() { try { EnterOuterAlt(_localctx, 1); { - State = 1280; + State = 1282; Match(FROM); - State = 1281; + State = 1283; _localctx.edge = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==FIRST || _la==LAST) ) { @@ -9244,7 +9264,7 @@ public NullTreatmentContext nullTreatment() { try { EnterOuterAlt(_localctx, 1); { - State = 1283; + State = 1285; _localctx.treatment = TokenStream.LT(1); _la = TokenStream.LA(1); if ( !(_la==IGNORE || _la==RESPECT) ) { @@ -9254,7 +9274,7 @@ public NullTreatmentContext nullTreatment() { ErrorHandler.ReportMatch(this); Consume(); } - State = 1284; + State = 1286; Match(NULLS); } } @@ -9299,7 +9319,7 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { } private static int[] _serializedATN = { - 4,1,153,1287,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2, + 4,1,153,1289,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2, 7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7,13,2,14,7,14, 2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2,20,7,20,2,21,7,21, 2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7,26,2,27,7,27,2,28,7,28, @@ -9330,449 +9350,451 @@ private bool expression_sempred(ExpressionContext _localctx, int predIndex) { 386,8,17,10,17,12,17,389,9,17,1,17,1,17,3,17,393,8,17,1,17,1,17,1,17,1, 17,1,17,1,17,1,17,3,17,402,8,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17, 1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17,1,17, - 1,17,1,17,1,17,3,17,429,8,17,1,18,1,18,1,18,3,18,434,8,18,1,19,1,19,3, - 19,438,8,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,5,19,448,8,19,10,19, - 12,19,451,9,19,1,19,1,19,1,19,3,19,456,8,19,1,20,1,20,1,20,1,20,1,20,1, - 20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,3,20,473,8,20,1,21,1,21, - 3,21,477,8,21,1,22,1,22,1,22,1,22,1,22,3,22,484,8,22,1,23,1,23,1,23,3, - 23,489,8,23,1,23,5,23,492,8,23,10,23,12,23,495,9,23,1,24,1,24,1,24,1,24, - 1,24,1,25,1,25,3,25,504,8,25,1,25,3,25,507,8,25,1,25,3,25,510,8,25,1,25, - 1,25,1,25,1,25,3,25,516,8,25,1,25,1,25,3,25,520,8,25,1,26,3,26,523,8,26, - 1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,536,8,27,1, - 27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,545,8,27,1,27,1,27,1,27,3,27,550, - 8,27,1,27,1,27,3,27,554,8,27,1,27,1,27,3,27,558,8,27,1,27,1,27,3,27,562, - 8,27,1,27,1,27,1,27,1,27,1,27,1,27,5,27,570,8,27,10,27,12,27,573,9,27, - 1,27,1,27,3,27,577,8,27,1,27,5,27,580,8,27,10,27,12,27,583,9,27,1,27,1, - 27,1,27,1,27,1,27,3,27,590,8,27,1,27,1,27,3,27,594,8,27,3,27,596,8,27, - 1,28,1,28,3,28,600,8,28,1,28,1,28,1,28,3,28,605,8,28,1,28,1,28,1,28,1, - 28,5,28,611,8,28,10,28,12,28,614,9,28,1,28,1,28,1,28,1,28,3,28,620,8,28, - 1,28,1,28,3,28,624,8,28,1,28,1,28,1,28,1,28,5,28,630,8,28,10,28,12,28, - 633,9,28,1,28,1,28,1,28,1,28,3,28,639,8,28,1,28,1,28,1,28,1,28,3,28,645, - 8,28,1,28,1,28,1,28,1,28,5,28,651,8,28,10,28,12,28,654,9,28,1,28,1,28, - 1,28,1,28,1,28,1,28,1,28,5,28,663,8,28,10,28,12,28,666,9,28,1,28,1,28, - 3,28,670,8,28,1,28,5,28,673,8,28,10,28,12,28,676,9,28,1,28,1,28,3,28,680, - 8,28,1,28,1,28,1,28,1,28,1,28,3,28,687,8,28,1,29,1,29,1,29,1,29,1,29,5, - 29,694,8,29,10,29,12,29,697,9,29,1,30,1,30,1,30,1,30,1,30,1,30,3,30,705, - 8,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,3,31,715,8,31,1,32,1,32,1, - 32,1,32,1,32,1,32,1,32,5,32,724,8,32,10,32,12,32,727,9,32,1,32,1,32,3, - 32,731,8,32,1,32,1,32,1,32,1,32,5,32,737,8,32,10,32,12,32,740,9,32,1,32, - 3,32,743,8,32,1,32,1,32,3,32,747,8,32,1,33,1,33,1,33,1,33,5,33,753,8,33, - 10,33,12,33,756,9,33,1,33,1,33,1,34,1,34,3,34,762,8,34,1,35,1,35,1,35, - 1,35,5,35,768,8,35,10,35,12,35,771,9,35,1,35,3,35,774,8,35,1,35,3,35,777, - 8,35,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,5,36,788,8,36,10,36, - 12,36,791,9,36,3,36,793,8,36,1,37,1,37,3,37,797,8,37,1,37,1,37,3,37,801, - 8,37,1,38,1,38,3,38,805,8,38,1,38,3,38,808,8,38,1,38,1,38,1,38,3,38,813, - 8,38,1,38,3,38,816,8,38,1,38,3,38,819,8,38,1,38,3,38,822,8,38,1,38,3,38, - 825,8,38,1,39,1,39,1,40,1,40,1,40,1,40,1,40,5,40,834,8,40,10,40,12,40, - 837,9,40,1,41,1,41,1,41,1,42,1,42,1,42,1,42,5,42,846,8,42,10,42,12,42, - 849,9,42,1,42,3,42,852,8,42,1,43,1,43,1,43,1,43,1,43,1,43,3,43,860,8,43, - 1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,871,8,44,1,44,1,44,1, - 44,1,44,1,44,1,44,3,44,879,8,44,1,45,1,45,1,46,1,46,1,46,1,46,5,46,887, - 8,46,10,46,12,46,890,9,46,3,46,892,8,46,1,47,1,47,1,47,1,47,1,47,1,47, - 3,47,900,8,47,1,47,3,47,903,8,47,3,47,905,8,47,1,48,1,48,1,48,1,48,5,48, - 911,8,48,10,48,12,48,914,9,48,1,49,1,49,5,49,918,8,49,10,49,12,49,921, - 9,49,1,50,1,50,3,50,925,8,50,1,50,3,50,928,8,50,1,50,1,50,1,50,1,50,3, - 50,934,8,50,1,50,3,50,937,8,50,1,50,1,50,1,50,1,50,3,50,943,8,50,1,51, - 1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51, - 3,51,960,8,51,1,52,3,52,963,8,52,1,52,1,52,3,52,967,8,52,1,52,1,52,3,52, - 971,8,52,1,52,1,52,3,52,975,8,52,3,52,977,8,52,1,53,1,53,1,53,1,54,1,54, - 1,54,1,54,1,54,5,54,987,8,54,10,54,12,54,990,9,54,1,55,1,55,3,55,994,8, - 55,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1002,8,56,1,56,1,56,1,56,1,56,1, + 1,17,1,17,1,17,3,17,429,8,17,1,18,1,18,1,18,1,18,1,18,3,18,436,8,18,1, + 19,1,19,3,19,440,8,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,1,19,5,19,450, + 8,19,10,19,12,19,453,9,19,1,19,1,19,1,19,3,19,458,8,19,1,20,1,20,1,20, + 1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,1,20,3,20,475,8, + 20,1,21,1,21,3,21,479,8,21,1,22,1,22,1,22,1,22,1,22,3,22,486,8,22,1,23, + 1,23,1,23,3,23,491,8,23,1,23,5,23,494,8,23,10,23,12,23,497,9,23,1,24,1, + 24,1,24,1,24,1,24,1,25,1,25,3,25,506,8,25,1,25,3,25,509,8,25,1,25,3,25, + 512,8,25,1,25,1,25,1,25,1,25,3,25,518,8,25,1,25,1,25,3,25,522,8,25,1,26, + 3,26,525,8,26,1,26,1,26,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3, + 27,538,8,27,1,27,1,27,1,27,1,27,1,27,1,27,1,27,3,27,547,8,27,1,27,1,27, + 1,27,3,27,552,8,27,1,27,1,27,3,27,556,8,27,1,27,1,27,3,27,560,8,27,1,27, + 1,27,3,27,564,8,27,1,27,1,27,1,27,1,27,1,27,1,27,5,27,572,8,27,10,27,12, + 27,575,9,27,1,27,1,27,3,27,579,8,27,1,27,5,27,582,8,27,10,27,12,27,585, + 9,27,1,27,1,27,1,27,1,27,1,27,3,27,592,8,27,1,27,1,27,3,27,596,8,27,3, + 27,598,8,27,1,28,1,28,3,28,602,8,28,1,28,1,28,1,28,3,28,607,8,28,1,28, + 1,28,1,28,1,28,5,28,613,8,28,10,28,12,28,616,9,28,1,28,1,28,1,28,1,28, + 3,28,622,8,28,1,28,1,28,3,28,626,8,28,1,28,1,28,1,28,1,28,5,28,632,8,28, + 10,28,12,28,635,9,28,1,28,1,28,1,28,1,28,3,28,641,8,28,1,28,1,28,1,28, + 1,28,3,28,647,8,28,1,28,1,28,1,28,1,28,5,28,653,8,28,10,28,12,28,656,9, + 28,1,28,1,28,1,28,1,28,1,28,1,28,1,28,5,28,665,8,28,10,28,12,28,668,9, + 28,1,28,1,28,3,28,672,8,28,1,28,5,28,675,8,28,10,28,12,28,678,9,28,1,28, + 1,28,3,28,682,8,28,1,28,1,28,1,28,1,28,1,28,3,28,689,8,28,1,29,1,29,1, + 29,1,29,1,29,5,29,696,8,29,10,29,12,29,699,9,29,1,30,1,30,1,30,1,30,1, + 30,1,30,3,30,707,8,30,1,31,1,31,1,31,1,31,1,31,1,31,1,31,1,31,3,31,717, + 8,31,1,32,1,32,1,32,1,32,1,32,1,32,1,32,5,32,726,8,32,10,32,12,32,729, + 9,32,1,32,1,32,3,32,733,8,32,1,32,1,32,1,32,1,32,5,32,739,8,32,10,32,12, + 32,742,9,32,1,32,3,32,745,8,32,1,32,1,32,3,32,749,8,32,1,33,1,33,1,33, + 1,33,5,33,755,8,33,10,33,12,33,758,9,33,1,33,1,33,1,34,1,34,3,34,764,8, + 34,1,35,1,35,1,35,1,35,5,35,770,8,35,10,35,12,35,773,9,35,1,35,3,35,776, + 8,35,1,35,3,35,779,8,35,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,1,36,5, + 36,790,8,36,10,36,12,36,793,9,36,3,36,795,8,36,1,37,1,37,3,37,799,8,37, + 1,37,1,37,3,37,803,8,37,1,38,1,38,3,38,807,8,38,1,38,3,38,810,8,38,1,38, + 1,38,1,38,3,38,815,8,38,1,38,3,38,818,8,38,1,38,3,38,821,8,38,1,38,3,38, + 824,8,38,1,38,3,38,827,8,38,1,39,1,39,1,40,1,40,1,40,1,40,1,40,5,40,836, + 8,40,10,40,12,40,839,9,40,1,41,1,41,1,41,1,42,1,42,1,42,1,42,5,42,848, + 8,42,10,42,12,42,851,9,42,1,42,3,42,854,8,42,1,43,1,43,1,43,1,43,1,43, + 1,43,3,43,862,8,43,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,873, + 8,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,881,8,44,1,45,1,45,1,46,1,46,1, + 46,1,46,5,46,889,8,46,10,46,12,46,892,9,46,3,46,894,8,46,1,47,1,47,1,47, + 1,47,1,47,1,47,3,47,902,8,47,1,47,3,47,905,8,47,3,47,907,8,47,1,48,1,48, + 1,48,1,48,5,48,913,8,48,10,48,12,48,916,9,48,1,49,1,49,5,49,920,8,49,10, + 49,12,49,923,9,49,1,50,1,50,3,50,927,8,50,1,50,3,50,930,8,50,1,50,1,50, + 1,50,1,50,3,50,936,8,50,1,50,3,50,939,8,50,1,50,1,50,1,50,1,50,3,50,945, + 8,50,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51,1,51, + 1,51,1,51,3,51,962,8,51,1,52,3,52,965,8,52,1,52,1,52,3,52,969,8,52,1,52, + 1,52,3,52,973,8,52,1,52,1,52,3,52,977,8,52,3,52,979,8,52,1,53,1,53,1,53, + 1,54,1,54,1,54,1,54,1,54,5,54,989,8,54,10,54,12,54,992,9,54,1,55,1,55, + 3,55,996,8,55,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1004,8,56,1,56,1,56,1, 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,3,56,1027,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,3,56,1036,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1057,8,56,1,56,1,56,1, - 56,1,56,1,56,1,56,1,56,3,56,1066,8,56,1,56,1,56,1,56,1,56,1,56,5,56,1073, - 8,56,10,56,12,56,1076,9,56,1,56,1,56,1,56,1,56,1,56,3,56,1083,8,56,1,56, - 5,56,1086,8,56,10,56,12,56,1089,9,56,1,57,1,57,1,57,1,57,1,57,1,57,1,57, - 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,3,57,1110, - 8,57,1,58,1,58,3,58,1114,8,58,1,58,4,58,1117,8,58,11,58,12,58,1118,1,58, - 1,58,3,58,1123,8,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59,1,60,1,60,1,60, - 1,60,3,60,1136,8,60,1,60,1,60,1,60,5,60,1141,8,60,10,60,12,60,1144,9,60, - 3,60,1146,8,60,1,60,1,60,3,60,1150,8,60,1,60,3,60,1153,8,60,1,60,3,60, - 1156,8,60,1,60,3,60,1159,8,60,1,60,1,60,3,60,1163,8,60,1,61,1,61,1,61, - 1,61,1,61,1,61,3,61,1171,8,61,1,62,1,62,1,62,3,62,1176,8,62,1,62,1,62, - 1,63,1,63,1,63,1,63,3,63,1184,8,63,1,64,1,64,1,64,1,64,1,64,1,64,1,64, - 1,64,1,64,3,64,1195,8,64,1,65,1,65,3,65,1199,8,65,1,65,1,65,3,65,1203, - 8,65,1,65,1,65,3,65,1207,8,65,3,65,1209,8,65,1,66,1,66,1,66,1,67,1,67, - 1,67,1,67,1,67,1,67,5,67,1220,8,67,10,67,12,67,1223,9,67,3,67,1225,8,67, - 1,67,3,67,1228,8,67,1,67,3,67,1231,8,67,1,67,1,67,1,68,1,68,1,69,1,69, - 1,69,1,69,1,69,1,69,1,69,3,69,1244,8,69,1,69,1,69,3,69,1248,8,69,1,70, - 1,70,1,70,1,70,1,70,1,70,1,70,3,70,1257,8,70,1,71,1,71,1,71,1,71,1,71, - 1,71,3,71,1265,8,71,1,72,1,72,1,73,1,73,1,73,1,73,1,73,1,73,1,74,1,74, - 1,74,1,74,1,74,1,74,1,75,1,75,1,75,1,76,1,76,1,76,1,76,0,1,112,77,0,2, - 4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52, - 54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92,94,96,98,100, - 102,104,106,108,110,112,114,116,118,120,122,124,126,128,130,132,134,136, - 138,140,142,144,146,148,150,152,0,21,1,0,101,102,1,0,103,104,1,0,93,94, - 1,0,135,136,2,0,34,35,54,54,1,0,126,127,1,0,47,48,1,0,49,50,2,0,8,8,15, - 15,1,0,122,123,1,0,129,134,2,0,6,6,12,12,2,0,7,7,13,13,2,0,9,9,14,14,1, - 0,62,63,2,0,87,87,89,89,2,0,49,49,108,109,1,0,111,112,1,0,108,121,2,0, - 48,48,118,118,2,0,96,96,119,119,1466,0,155,1,0,0,0,2,178,1,0,0,0,4,200, - 1,0,0,0,6,202,1,0,0,0,8,214,1,0,0,0,10,235,1,0,0,0,12,239,1,0,0,0,14,259, - 1,0,0,0,16,268,1,0,0,0,18,275,1,0,0,0,20,286,1,0,0,0,22,310,1,0,0,0,24, - 329,1,0,0,0,26,357,1,0,0,0,28,359,1,0,0,0,30,364,1,0,0,0,32,366,1,0,0, - 0,34,428,1,0,0,0,36,433,1,0,0,0,38,435,1,0,0,0,40,472,1,0,0,0,42,474,1, - 0,0,0,44,483,1,0,0,0,46,485,1,0,0,0,48,496,1,0,0,0,50,503,1,0,0,0,52,522, - 1,0,0,0,54,595,1,0,0,0,56,686,1,0,0,0,58,695,1,0,0,0,60,704,1,0,0,0,62, - 714,1,0,0,0,64,716,1,0,0,0,66,748,1,0,0,0,68,761,1,0,0,0,70,763,1,0,0, - 0,72,792,1,0,0,0,74,800,1,0,0,0,76,802,1,0,0,0,78,826,1,0,0,0,80,828,1, - 0,0,0,82,838,1,0,0,0,84,841,1,0,0,0,86,859,1,0,0,0,88,878,1,0,0,0,90,880, - 1,0,0,0,92,891,1,0,0,0,94,904,1,0,0,0,96,906,1,0,0,0,98,915,1,0,0,0,100, - 942,1,0,0,0,102,959,1,0,0,0,104,976,1,0,0,0,106,978,1,0,0,0,108,981,1, - 0,0,0,110,991,1,0,0,0,112,1001,1,0,0,0,114,1109,1,0,0,0,116,1111,1,0,0, - 0,118,1126,1,0,0,0,120,1131,1,0,0,0,122,1170,1,0,0,0,124,1175,1,0,0,0, - 126,1183,1,0,0,0,128,1194,1,0,0,0,130,1208,1,0,0,0,132,1210,1,0,0,0,134, - 1213,1,0,0,0,136,1234,1,0,0,0,138,1236,1,0,0,0,140,1256,1,0,0,0,142,1264, - 1,0,0,0,144,1266,1,0,0,0,146,1268,1,0,0,0,148,1274,1,0,0,0,150,1280,1, - 0,0,0,152,1283,1,0,0,0,154,156,3,18,9,0,155,154,1,0,0,0,155,156,1,0,0, - 0,156,171,1,0,0,0,157,172,3,2,1,0,158,172,3,20,10,0,159,172,3,38,19,0, - 160,172,3,22,11,0,161,172,3,24,12,0,162,172,3,32,16,0,163,172,3,40,20, - 0,164,172,3,64,32,0,165,172,3,8,4,0,166,172,3,12,6,0,167,172,3,130,65, - 0,168,172,3,6,3,0,169,172,3,14,7,0,170,172,3,70,35,0,171,157,1,0,0,0,171, - 158,1,0,0,0,171,159,1,0,0,0,171,160,1,0,0,0,171,161,1,0,0,0,171,162,1, - 0,0,0,171,163,1,0,0,0,171,164,1,0,0,0,171,165,1,0,0,0,171,166,1,0,0,0, - 171,167,1,0,0,0,171,168,1,0,0,0,171,169,1,0,0,0,171,170,1,0,0,0,172,174, - 1,0,0,0,173,175,5,139,0,0,174,173,1,0,0,0,174,175,1,0,0,0,175,176,1,0, - 0,0,176,177,5,0,0,1,177,1,1,0,0,0,178,180,5,32,0,0,179,181,5,8,0,0,180, - 179,1,0,0,0,180,181,1,0,0,0,181,182,1,0,0,0,182,183,5,31,0,0,183,184,5, - 135,0,0,184,185,3,70,35,0,185,186,5,136,0,0,186,187,5,33,0,0,187,188,3, - 4,2,0,188,3,1,0,0,0,189,201,3,20,10,0,190,201,3,38,19,0,191,201,3,22,11, - 0,192,201,3,24,12,0,193,201,3,32,16,0,194,201,3,40,20,0,195,201,3,64,32, - 0,196,201,3,8,4,0,197,201,3,12,6,0,198,201,3,6,3,0,199,201,3,70,35,0,200, - 189,1,0,0,0,200,190,1,0,0,0,200,191,1,0,0,0,200,192,1,0,0,0,200,193,1, - 0,0,0,200,194,1,0,0,0,200,195,1,0,0,0,200,196,1,0,0,0,200,197,1,0,0,0, - 200,198,1,0,0,0,200,199,1,0,0,0,201,5,1,0,0,0,202,203,7,0,0,0,203,212, - 3,126,63,0,204,209,3,112,56,0,205,206,5,137,0,0,206,208,3,112,56,0,207, - 205,1,0,0,0,208,211,1,0,0,0,209,207,1,0,0,0,209,210,1,0,0,0,210,213,1, - 0,0,0,211,209,1,0,0,0,212,204,1,0,0,0,212,213,1,0,0,0,213,7,1,0,0,0,214, - 215,5,79,0,0,215,220,3,98,49,0,216,217,5,137,0,0,217,219,3,98,49,0,218, - 216,1,0,0,0,219,222,1,0,0,0,220,218,1,0,0,0,220,221,1,0,0,0,221,223,1, - 0,0,0,222,220,1,0,0,0,223,224,5,83,0,0,224,229,3,10,5,0,225,226,5,137, - 0,0,226,228,3,10,5,0,227,225,1,0,0,0,228,231,1,0,0,0,229,227,1,0,0,0,229, - 230,1,0,0,0,230,233,1,0,0,0,231,229,1,0,0,0,232,234,3,106,53,0,233,232, - 1,0,0,0,233,234,1,0,0,0,234,9,1,0,0,0,235,236,3,124,62,0,236,237,5,129, - 0,0,237,238,3,112,56,0,238,11,1,0,0,0,239,245,5,78,0,0,240,241,3,126,63, - 0,241,242,5,138,0,0,242,243,5,122,0,0,243,246,1,0,0,0,244,246,5,122,0, - 0,245,240,1,0,0,0,245,244,1,0,0,0,245,246,1,0,0,0,246,247,1,0,0,0,247, - 248,5,2,0,0,248,253,3,98,49,0,249,250,5,137,0,0,250,252,3,98,49,0,251, - 249,1,0,0,0,252,255,1,0,0,0,253,251,1,0,0,0,253,254,1,0,0,0,254,257,1, - 0,0,0,255,253,1,0,0,0,256,258,3,106,53,0,257,256,1,0,0,0,257,258,1,0,0, - 0,258,13,1,0,0,0,259,260,5,1,0,0,260,265,3,16,8,0,261,262,5,137,0,0,262, - 264,3,16,8,0,263,261,1,0,0,0,264,267,1,0,0,0,265,263,1,0,0,0,265,266,1, - 0,0,0,266,15,1,0,0,0,267,265,1,0,0,0,268,273,5,140,0,0,269,271,5,5,0,0, - 270,269,1,0,0,0,270,271,1,0,0,0,271,272,1,0,0,0,272,274,3,126,63,0,273, - 270,1,0,0,0,273,274,1,0,0,0,274,17,1,0,0,0,275,276,5,100,0,0,276,281,3, - 28,14,0,277,278,5,137,0,0,278,280,3,28,14,0,279,277,1,0,0,0,280,283,1, - 0,0,0,281,279,1,0,0,0,281,282,1,0,0,0,282,284,1,0,0,0,283,281,1,0,0,0, - 284,285,5,139,0,0,285,19,1,0,0,0,286,288,5,57,0,0,287,289,5,91,0,0,288, - 287,1,0,0,0,288,289,1,0,0,0,289,290,1,0,0,0,290,291,5,58,0,0,291,292,3, - 126,63,0,292,293,5,135,0,0,293,298,3,46,23,0,294,295,5,137,0,0,295,297, - 3,46,23,0,296,294,1,0,0,0,297,300,1,0,0,0,298,296,1,0,0,0,298,299,1,0, - 0,0,299,305,1,0,0,0,300,298,1,0,0,0,301,302,5,137,0,0,302,304,3,56,28, - 0,303,301,1,0,0,0,304,307,1,0,0,0,305,303,1,0,0,0,305,306,1,0,0,0,306, - 308,1,0,0,0,307,305,1,0,0,0,308,309,5,136,0,0,309,21,1,0,0,0,310,311,5, - 57,0,0,311,312,5,98,0,0,312,324,3,126,63,0,313,314,5,135,0,0,314,319,3, - 126,63,0,315,316,5,137,0,0,316,318,3,126,63,0,317,315,1,0,0,0,318,321, - 1,0,0,0,319,317,1,0,0,0,319,320,1,0,0,0,320,322,1,0,0,0,321,319,1,0,0, - 0,322,323,5,136,0,0,323,325,1,0,0,0,324,313,1,0,0,0,324,325,1,0,0,0,325, - 326,1,0,0,0,326,327,5,5,0,0,327,328,3,70,35,0,328,23,1,0,0,0,329,330,5, - 57,0,0,330,331,5,99,0,0,331,333,3,126,63,0,332,334,3,26,13,0,333,332,1, - 0,0,0,333,334,1,0,0,0,334,335,1,0,0,0,335,336,5,5,0,0,336,337,3,36,18, - 0,337,25,1,0,0,0,338,339,5,135,0,0,339,344,3,28,14,0,340,341,5,137,0,0, - 341,343,3,28,14,0,342,340,1,0,0,0,343,346,1,0,0,0,344,342,1,0,0,0,344, - 345,1,0,0,0,345,347,1,0,0,0,346,344,1,0,0,0,347,348,5,136,0,0,348,358, - 1,0,0,0,349,354,3,28,14,0,350,351,5,137,0,0,351,353,3,28,14,0,352,350, - 1,0,0,0,353,356,1,0,0,0,354,352,1,0,0,0,354,355,1,0,0,0,355,358,1,0,0, - 0,356,354,1,0,0,0,357,338,1,0,0,0,357,349,1,0,0,0,358,27,1,0,0,0,359,360, - 3,30,15,0,360,361,3,50,25,0,361,29,1,0,0,0,362,365,3,126,63,0,363,365, - 5,141,0,0,364,362,1,0,0,0,364,363,1,0,0,0,365,31,1,0,0,0,366,367,5,64, - 0,0,367,368,5,58,0,0,368,369,3,126,63,0,369,370,3,34,17,0,370,33,1,0,0, - 0,371,373,5,67,0,0,372,374,5,69,0,0,373,372,1,0,0,0,373,374,1,0,0,0,374, - 375,1,0,0,0,375,429,3,46,23,0,376,377,5,67,0,0,377,429,3,56,28,0,378,380, - 5,64,0,0,379,381,5,69,0,0,380,379,1,0,0,0,380,381,1,0,0,0,381,382,1,0, - 0,0,382,383,3,126,63,0,383,387,3,50,25,0,384,386,3,54,27,0,385,384,1,0, - 0,0,386,389,1,0,0,0,387,385,1,0,0,0,387,388,1,0,0,0,388,429,1,0,0,0,389, - 387,1,0,0,0,390,392,5,64,0,0,391,393,5,69,0,0,392,391,1,0,0,0,392,393, - 1,0,0,0,393,394,1,0,0,0,394,395,3,126,63,0,395,396,5,83,0,0,396,397,5, - 84,0,0,397,398,3,112,56,0,398,429,1,0,0,0,399,401,5,64,0,0,400,402,5,69, - 0,0,401,400,1,0,0,0,401,402,1,0,0,0,402,403,1,0,0,0,403,404,3,126,63,0, - 404,405,5,68,0,0,405,406,5,84,0,0,406,429,1,0,0,0,407,408,5,68,0,0,408, - 409,5,69,0,0,409,429,3,126,63,0,410,411,5,68,0,0,411,412,5,75,0,0,412, - 429,3,126,63,0,413,414,5,65,0,0,414,415,5,66,0,0,415,429,3,126,63,0,416, - 417,5,65,0,0,417,418,5,69,0,0,418,419,3,126,63,0,419,420,5,66,0,0,420, - 421,3,126,63,0,421,429,1,0,0,0,422,423,5,65,0,0,423,424,5,90,0,0,424,425, - 3,126,63,0,425,426,5,66,0,0,426,427,3,126,63,0,427,429,1,0,0,0,428,371, - 1,0,0,0,428,376,1,0,0,0,428,378,1,0,0,0,428,390,1,0,0,0,428,399,1,0,0, - 0,428,407,1,0,0,0,428,410,1,0,0,0,428,413,1,0,0,0,428,416,1,0,0,0,428, - 422,1,0,0,0,429,35,1,0,0,0,430,434,3,70,35,0,431,434,3,64,32,0,432,434, - 3,20,10,0,433,430,1,0,0,0,433,431,1,0,0,0,433,432,1,0,0,0,434,37,1,0,0, - 0,435,437,5,57,0,0,436,438,5,86,0,0,437,436,1,0,0,0,437,438,1,0,0,0,438, - 439,1,0,0,0,439,440,5,90,0,0,440,441,3,126,63,0,441,442,5,25,0,0,442,443, - 3,126,63,0,443,444,5,135,0,0,444,449,3,42,21,0,445,446,5,137,0,0,446,448, - 3,42,21,0,447,445,1,0,0,0,448,451,1,0,0,0,449,447,1,0,0,0,449,450,1,0, - 0,0,450,452,1,0,0,0,451,449,1,0,0,0,452,455,5,136,0,0,453,454,5,92,0,0, - 454,456,3,44,22,0,455,453,1,0,0,0,455,456,1,0,0,0,456,39,1,0,0,0,457,458, - 5,68,0,0,458,459,5,58,0,0,459,473,3,126,63,0,460,461,5,68,0,0,461,462, - 5,90,0,0,462,463,3,126,63,0,463,464,5,25,0,0,464,465,3,126,63,0,465,473, - 1,0,0,0,466,467,5,68,0,0,467,468,5,99,0,0,468,473,3,126,63,0,469,470,5, - 68,0,0,470,471,5,98,0,0,471,473,3,126,63,0,472,457,1,0,0,0,472,460,1,0, - 0,0,472,466,1,0,0,0,472,469,1,0,0,0,473,41,1,0,0,0,474,476,3,126,63,0, - 475,477,7,1,0,0,476,475,1,0,0,0,476,477,1,0,0,0,477,43,1,0,0,0,478,484, - 5,73,0,0,479,480,5,95,0,0,480,484,5,107,0,0,481,482,5,96,0,0,482,484,5, - 107,0,0,483,478,1,0,0,0,483,479,1,0,0,0,483,481,1,0,0,0,484,45,1,0,0,0, - 485,486,3,126,63,0,486,488,3,50,25,0,487,489,3,48,24,0,488,487,1,0,0,0, - 488,489,1,0,0,0,489,493,1,0,0,0,490,492,3,54,27,0,491,490,1,0,0,0,492, - 495,1,0,0,0,493,491,1,0,0,0,493,494,1,0,0,0,494,47,1,0,0,0,495,493,1,0, - 0,0,496,497,5,5,0,0,497,498,5,135,0,0,498,499,3,112,56,0,499,500,5,136, - 0,0,500,49,1,0,0,0,501,504,3,126,63,0,502,504,5,88,0,0,503,501,1,0,0,0, - 503,502,1,0,0,0,504,506,1,0,0,0,505,507,3,126,63,0,506,505,1,0,0,0,506, - 507,1,0,0,0,507,509,1,0,0,0,508,510,3,126,63,0,509,508,1,0,0,0,509,510, - 1,0,0,0,510,519,1,0,0,0,511,512,5,135,0,0,512,515,3,52,26,0,513,514,5, - 137,0,0,514,516,3,52,26,0,515,513,1,0,0,0,515,516,1,0,0,0,516,517,1,0, - 0,0,517,518,5,136,0,0,518,520,1,0,0,0,519,511,1,0,0,0,519,520,1,0,0,0, - 520,51,1,0,0,0,521,523,5,127,0,0,522,521,1,0,0,0,522,523,1,0,0,0,523,524, - 1,0,0,0,524,525,5,143,0,0,525,53,1,0,0,0,526,527,5,8,0,0,527,596,5,107, - 0,0,528,596,5,107,0,0,529,530,5,84,0,0,530,596,3,112,56,0,531,532,5,92, - 0,0,532,596,7,2,0,0,533,534,5,75,0,0,534,536,3,126,63,0,535,533,1,0,0, - 0,535,536,1,0,0,0,536,537,1,0,0,0,537,538,5,97,0,0,538,539,5,135,0,0,539, - 540,3,58,29,0,540,541,5,136,0,0,541,596,1,0,0,0,542,543,5,75,0,0,543,545, - 3,126,63,0,544,542,1,0,0,0,544,545,1,0,0,0,545,546,1,0,0,0,546,547,5,73, - 0,0,547,549,5,74,0,0,548,550,3,136,68,0,549,548,1,0,0,0,549,550,1,0,0, - 0,550,596,1,0,0,0,551,552,5,75,0,0,552,554,3,126,63,0,553,551,1,0,0,0, - 553,554,1,0,0,0,554,555,1,0,0,0,555,557,5,86,0,0,556,558,3,136,68,0,557, - 556,1,0,0,0,557,558,1,0,0,0,558,596,1,0,0,0,559,560,5,75,0,0,560,562,3, - 126,63,0,561,559,1,0,0,0,561,562,1,0,0,0,562,563,1,0,0,0,563,564,5,77, - 0,0,564,576,3,126,63,0,565,566,5,135,0,0,566,571,3,126,63,0,567,568,5, - 137,0,0,568,570,3,126,63,0,569,567,1,0,0,0,570,573,1,0,0,0,571,569,1,0, - 0,0,571,572,1,0,0,0,572,574,1,0,0,0,573,571,1,0,0,0,574,575,5,136,0,0, - 575,577,1,0,0,0,576,565,1,0,0,0,576,577,1,0,0,0,577,581,1,0,0,0,578,580, - 3,60,30,0,579,578,1,0,0,0,580,583,1,0,0,0,581,579,1,0,0,0,581,582,1,0, - 0,0,582,596,1,0,0,0,583,581,1,0,0,0,584,593,5,88,0,0,585,586,5,135,0,0, - 586,589,3,52,26,0,587,588,5,137,0,0,588,590,3,52,26,0,589,587,1,0,0,0, - 589,590,1,0,0,0,590,591,1,0,0,0,591,592,5,136,0,0,592,594,1,0,0,0,593, - 585,1,0,0,0,593,594,1,0,0,0,594,596,1,0,0,0,595,526,1,0,0,0,595,528,1, - 0,0,0,595,529,1,0,0,0,595,531,1,0,0,0,595,535,1,0,0,0,595,544,1,0,0,0, - 595,553,1,0,0,0,595,561,1,0,0,0,595,584,1,0,0,0,596,55,1,0,0,0,597,598, - 5,75,0,0,598,600,3,126,63,0,599,597,1,0,0,0,599,600,1,0,0,0,600,601,1, - 0,0,0,601,602,5,73,0,0,602,604,5,74,0,0,603,605,3,136,68,0,604,603,1,0, - 0,0,604,605,1,0,0,0,605,606,1,0,0,0,606,607,5,135,0,0,607,612,3,126,63, - 0,608,609,5,137,0,0,609,611,3,126,63,0,610,608,1,0,0,0,611,614,1,0,0,0, - 612,610,1,0,0,0,612,613,1,0,0,0,613,615,1,0,0,0,614,612,1,0,0,0,615,616, - 5,136,0,0,616,687,1,0,0,0,617,618,5,75,0,0,618,620,3,126,63,0,619,617, - 1,0,0,0,619,620,1,0,0,0,620,621,1,0,0,0,621,623,5,86,0,0,622,624,3,136, - 68,0,623,622,1,0,0,0,623,624,1,0,0,0,624,625,1,0,0,0,625,626,5,135,0,0, - 626,631,3,126,63,0,627,628,5,137,0,0,628,630,3,126,63,0,629,627,1,0,0, - 0,630,633,1,0,0,0,631,629,1,0,0,0,631,632,1,0,0,0,632,634,1,0,0,0,633, - 631,1,0,0,0,634,635,5,136,0,0,635,687,1,0,0,0,636,637,5,75,0,0,637,639, - 3,126,63,0,638,636,1,0,0,0,638,639,1,0,0,0,639,640,1,0,0,0,640,641,5,76, - 0,0,641,644,5,74,0,0,642,643,5,85,0,0,643,645,5,90,0,0,644,642,1,0,0,0, - 644,645,1,0,0,0,645,646,1,0,0,0,646,647,5,135,0,0,647,652,3,126,63,0,648, - 649,5,137,0,0,649,651,3,126,63,0,650,648,1,0,0,0,651,654,1,0,0,0,652,650, - 1,0,0,0,652,653,1,0,0,0,653,655,1,0,0,0,654,652,1,0,0,0,655,656,5,136, - 0,0,656,657,5,77,0,0,657,669,3,126,63,0,658,659,5,135,0,0,659,664,3,126, - 63,0,660,661,5,137,0,0,661,663,3,126,63,0,662,660,1,0,0,0,663,666,1,0, - 0,0,664,662,1,0,0,0,664,665,1,0,0,0,665,667,1,0,0,0,666,664,1,0,0,0,667, - 668,5,136,0,0,668,670,1,0,0,0,669,658,1,0,0,0,669,670,1,0,0,0,670,674, - 1,0,0,0,671,673,3,60,30,0,672,671,1,0,0,0,673,676,1,0,0,0,674,672,1,0, - 0,0,674,675,1,0,0,0,675,687,1,0,0,0,676,674,1,0,0,0,677,678,5,75,0,0,678, - 680,3,126,63,0,679,677,1,0,0,0,679,680,1,0,0,0,680,681,1,0,0,0,681,682, - 5,97,0,0,682,683,5,135,0,0,683,684,3,58,29,0,684,685,5,136,0,0,685,687, - 1,0,0,0,686,599,1,0,0,0,686,619,1,0,0,0,686,638,1,0,0,0,686,679,1,0,0, - 0,687,57,1,0,0,0,688,694,8,3,0,0,689,690,5,135,0,0,690,691,3,58,29,0,691, - 692,5,136,0,0,692,694,1,0,0,0,693,688,1,0,0,0,693,689,1,0,0,0,694,697, - 1,0,0,0,695,693,1,0,0,0,695,696,1,0,0,0,696,59,1,0,0,0,697,695,1,0,0,0, - 698,699,5,25,0,0,699,700,5,79,0,0,700,705,3,62,31,0,701,702,5,25,0,0,702, - 703,5,78,0,0,703,705,3,62,31,0,704,698,1,0,0,0,704,701,1,0,0,0,705,61, - 1,0,0,0,706,715,5,80,0,0,707,708,5,85,0,0,708,715,5,82,0,0,709,715,5,81, - 0,0,710,711,5,83,0,0,711,715,5,107,0,0,712,713,5,83,0,0,713,715,5,84,0, - 0,714,706,1,0,0,0,714,707,1,0,0,0,714,709,1,0,0,0,714,710,1,0,0,0,714, - 712,1,0,0,0,715,63,1,0,0,0,716,717,5,70,0,0,717,718,5,71,0,0,718,746,3, - 126,63,0,719,720,5,135,0,0,720,725,3,126,63,0,721,722,5,137,0,0,722,724, - 3,126,63,0,723,721,1,0,0,0,724,727,1,0,0,0,725,723,1,0,0,0,725,726,1,0, - 0,0,726,728,1,0,0,0,727,725,1,0,0,0,728,729,5,136,0,0,729,731,1,0,0,0, - 730,719,1,0,0,0,730,731,1,0,0,0,731,742,1,0,0,0,732,733,5,72,0,0,733,738, - 3,66,33,0,734,735,5,137,0,0,735,737,3,66,33,0,736,734,1,0,0,0,737,740, - 1,0,0,0,738,736,1,0,0,0,738,739,1,0,0,0,739,743,1,0,0,0,740,738,1,0,0, - 0,741,743,3,70,35,0,742,732,1,0,0,0,742,741,1,0,0,0,743,747,1,0,0,0,744, - 745,5,84,0,0,745,747,5,72,0,0,746,730,1,0,0,0,746,744,1,0,0,0,747,65,1, - 0,0,0,748,749,5,135,0,0,749,754,3,68,34,0,750,751,5,137,0,0,751,753,3, - 68,34,0,752,750,1,0,0,0,753,756,1,0,0,0,754,752,1,0,0,0,754,755,1,0,0, - 0,755,757,1,0,0,0,756,754,1,0,0,0,757,758,5,136,0,0,758,67,1,0,0,0,759, - 762,5,84,0,0,760,762,3,112,56,0,761,759,1,0,0,0,761,760,1,0,0,0,762,69, - 1,0,0,0,763,769,3,72,36,0,764,765,3,74,37,0,765,766,3,72,36,0,766,768, - 1,0,0,0,767,764,1,0,0,0,768,771,1,0,0,0,769,767,1,0,0,0,769,770,1,0,0, - 0,770,773,1,0,0,0,771,769,1,0,0,0,772,774,3,108,54,0,773,772,1,0,0,0,773, - 774,1,0,0,0,774,776,1,0,0,0,775,777,3,88,44,0,776,775,1,0,0,0,776,777, - 1,0,0,0,777,71,1,0,0,0,778,793,3,76,38,0,779,780,5,135,0,0,780,781,3,70, - 35,0,781,782,5,136,0,0,782,793,1,0,0,0,783,784,5,72,0,0,784,789,3,66,33, - 0,785,786,5,137,0,0,786,788,3,66,33,0,787,785,1,0,0,0,788,791,1,0,0,0, - 789,787,1,0,0,0,789,790,1,0,0,0,790,793,1,0,0,0,791,789,1,0,0,0,792,778, - 1,0,0,0,792,779,1,0,0,0,792,783,1,0,0,0,793,73,1,0,0,0,794,796,5,53,0, - 0,795,797,5,54,0,0,796,795,1,0,0,0,796,797,1,0,0,0,797,801,1,0,0,0,798, - 801,5,55,0,0,799,801,5,56,0,0,800,794,1,0,0,0,800,798,1,0,0,0,800,799, - 1,0,0,0,801,75,1,0,0,0,802,804,5,1,0,0,803,805,3,78,39,0,804,803,1,0,0, - 0,804,805,1,0,0,0,805,807,1,0,0,0,806,808,3,84,42,0,807,806,1,0,0,0,807, - 808,1,0,0,0,808,809,1,0,0,0,809,812,3,92,46,0,810,811,5,71,0,0,811,813, - 3,126,63,0,812,810,1,0,0,0,812,813,1,0,0,0,813,815,1,0,0,0,814,816,3,96, - 48,0,815,814,1,0,0,0,815,816,1,0,0,0,816,818,1,0,0,0,817,819,3,106,53, - 0,818,817,1,0,0,0,818,819,1,0,0,0,819,821,1,0,0,0,820,822,3,80,40,0,821, - 820,1,0,0,0,821,822,1,0,0,0,822,824,1,0,0,0,823,825,3,82,41,0,824,823, - 1,0,0,0,824,825,1,0,0,0,825,77,1,0,0,0,826,827,7,4,0,0,827,79,1,0,0,0, - 828,829,5,27,0,0,829,830,5,29,0,0,830,835,3,112,56,0,831,832,5,137,0,0, - 832,834,3,112,56,0,833,831,1,0,0,0,834,837,1,0,0,0,835,833,1,0,0,0,835, - 836,1,0,0,0,836,81,1,0,0,0,837,835,1,0,0,0,838,839,5,30,0,0,839,840,3, - 112,56,0,840,83,1,0,0,0,841,842,5,4,0,0,842,847,3,86,43,0,843,844,7,5, - 0,0,844,846,3,86,43,0,845,843,1,0,0,0,846,849,1,0,0,0,847,845,1,0,0,0, - 847,848,1,0,0,0,848,851,1,0,0,0,849,847,1,0,0,0,850,852,5,36,0,0,851,850, - 1,0,0,0,851,852,1,0,0,0,852,85,1,0,0,0,853,860,5,143,0,0,854,860,5,141, - 0,0,855,856,5,135,0,0,856,857,3,112,56,0,857,858,5,136,0,0,858,860,1,0, - 0,0,859,853,1,0,0,0,859,854,1,0,0,0,859,855,1,0,0,0,860,87,1,0,0,0,861, - 862,5,45,0,0,862,863,3,112,56,0,863,870,3,90,45,0,864,865,5,46,0,0,865, - 866,7,6,0,0,866,867,3,112,56,0,867,868,3,90,45,0,868,869,5,51,0,0,869, - 871,1,0,0,0,870,864,1,0,0,0,870,871,1,0,0,0,871,879,1,0,0,0,872,873,5, - 46,0,0,873,874,7,6,0,0,874,875,3,112,56,0,875,876,3,90,45,0,876,877,5, - 51,0,0,877,879,1,0,0,0,878,861,1,0,0,0,878,872,1,0,0,0,879,89,1,0,0,0, - 880,881,7,7,0,0,881,91,1,0,0,0,882,892,5,122,0,0,883,888,3,94,47,0,884, - 885,5,137,0,0,885,887,3,94,47,0,886,884,1,0,0,0,887,890,1,0,0,0,888,886, - 1,0,0,0,888,889,1,0,0,0,889,892,1,0,0,0,890,888,1,0,0,0,891,882,1,0,0, - 0,891,883,1,0,0,0,892,93,1,0,0,0,893,894,3,126,63,0,894,895,5,138,0,0, - 895,896,5,122,0,0,896,905,1,0,0,0,897,902,3,112,56,0,898,900,5,5,0,0,899, - 898,1,0,0,0,899,900,1,0,0,0,900,901,1,0,0,0,901,903,3,126,63,0,902,899, - 1,0,0,0,902,903,1,0,0,0,903,905,1,0,0,0,904,893,1,0,0,0,904,897,1,0,0, - 0,905,95,1,0,0,0,906,907,5,2,0,0,907,912,3,98,49,0,908,909,5,137,0,0,909, - 911,3,98,49,0,910,908,1,0,0,0,911,914,1,0,0,0,912,910,1,0,0,0,912,913, - 1,0,0,0,913,97,1,0,0,0,914,912,1,0,0,0,915,919,3,100,50,0,916,918,3,102, - 51,0,917,916,1,0,0,0,918,921,1,0,0,0,919,917,1,0,0,0,919,920,1,0,0,0,920, - 99,1,0,0,0,921,919,1,0,0,0,922,927,3,126,63,0,923,925,5,5,0,0,924,923, - 1,0,0,0,924,925,1,0,0,0,925,926,1,0,0,0,926,928,3,126,63,0,927,924,1,0, - 0,0,927,928,1,0,0,0,928,943,1,0,0,0,929,930,5,135,0,0,930,931,3,70,35, - 0,931,936,5,136,0,0,932,934,5,5,0,0,933,932,1,0,0,0,933,934,1,0,0,0,934, - 935,1,0,0,0,935,937,3,126,63,0,936,933,1,0,0,0,936,937,1,0,0,0,937,943, - 1,0,0,0,938,939,5,135,0,0,939,940,3,98,49,0,940,941,5,136,0,0,941,943, - 1,0,0,0,942,922,1,0,0,0,942,929,1,0,0,0,942,938,1,0,0,0,943,101,1,0,0, - 0,944,945,3,104,52,0,945,946,5,23,0,0,946,947,3,100,50,0,947,948,5,25, - 0,0,948,949,3,112,56,0,949,960,1,0,0,0,950,951,5,37,0,0,951,952,5,23,0, - 0,952,960,3,100,50,0,953,954,5,37,0,0,954,955,5,38,0,0,955,960,3,100,50, - 0,956,957,5,22,0,0,957,958,5,38,0,0,958,960,3,100,50,0,959,944,1,0,0,0, - 959,950,1,0,0,0,959,953,1,0,0,0,959,956,1,0,0,0,960,103,1,0,0,0,961,963, - 5,18,0,0,962,961,1,0,0,0,962,963,1,0,0,0,963,977,1,0,0,0,964,966,5,19, - 0,0,965,967,5,22,0,0,966,965,1,0,0,0,966,967,1,0,0,0,967,977,1,0,0,0,968, - 970,5,20,0,0,969,971,5,22,0,0,970,969,1,0,0,0,970,971,1,0,0,0,971,977, - 1,0,0,0,972,974,5,21,0,0,973,975,5,22,0,0,974,973,1,0,0,0,974,975,1,0, - 0,0,975,977,1,0,0,0,976,962,1,0,0,0,976,964,1,0,0,0,976,968,1,0,0,0,976, - 972,1,0,0,0,977,105,1,0,0,0,978,979,5,3,0,0,979,980,3,112,56,0,980,107, - 1,0,0,0,981,982,5,26,0,0,982,983,5,29,0,0,983,988,3,110,55,0,984,985,5, - 137,0,0,985,987,3,110,55,0,986,984,1,0,0,0,987,990,1,0,0,0,988,986,1,0, - 0,0,988,989,1,0,0,0,989,109,1,0,0,0,990,988,1,0,0,0,991,993,3,112,56,0, - 992,994,7,1,0,0,993,992,1,0,0,0,993,994,1,0,0,0,994,111,1,0,0,0,995,996, - 6,56,-1,0,996,997,7,5,0,0,997,1002,3,112,56,19,998,999,7,8,0,0,999,1002, - 3,112,56,7,1000,1002,3,114,57,0,1001,995,1,0,0,0,1001,998,1,0,0,0,1001, - 1000,1,0,0,0,1002,1087,1,0,0,0,1003,1004,10,20,0,0,1004,1005,5,125,0,0, - 1005,1086,3,112,56,21,1006,1007,10,18,0,0,1007,1008,7,9,0,0,1008,1086, - 3,112,56,19,1009,1010,10,17,0,0,1010,1011,5,124,0,0,1011,1086,3,112,56, - 18,1012,1013,10,16,0,0,1013,1014,5,17,0,0,1014,1086,3,112,56,17,1015,1016, - 10,15,0,0,1016,1017,7,5,0,0,1017,1086,3,112,56,16,1018,1019,10,14,0,0, - 1019,1020,5,128,0,0,1020,1086,3,112,56,15,1021,1022,10,13,0,0,1022,1023, - 7,10,0,0,1023,1086,3,112,56,14,1024,1026,10,12,0,0,1025,1027,5,8,0,0,1026, - 1025,1,0,0,0,1026,1027,1,0,0,0,1027,1028,1,0,0,0,1028,1029,5,52,0,0,1029, - 1030,3,112,56,0,1030,1031,5,6,0,0,1031,1032,3,112,56,13,1032,1086,1,0, - 0,0,1033,1035,10,11,0,0,1034,1036,5,8,0,0,1035,1034,1,0,0,0,1035,1036, - 1,0,0,0,1036,1037,1,0,0,0,1037,1038,5,16,0,0,1038,1086,3,112,56,12,1039, - 1040,10,6,0,0,1040,1041,7,11,0,0,1041,1086,3,112,56,7,1042,1043,10,5,0, - 0,1043,1044,7,12,0,0,1044,1086,3,112,56,6,1045,1046,10,4,0,0,1046,1047, - 7,13,0,0,1047,1086,3,112,56,5,1048,1049,10,3,0,0,1049,1050,5,10,0,0,1050, - 1086,3,112,56,4,1051,1052,10,2,0,0,1052,1053,5,11,0,0,1053,1086,3,112, - 56,3,1054,1056,10,10,0,0,1055,1057,5,8,0,0,1056,1055,1,0,0,0,1056,1057, - 1,0,0,0,1057,1058,1,0,0,0,1058,1059,5,24,0,0,1059,1060,5,135,0,0,1060, - 1061,3,70,35,0,1061,1062,5,136,0,0,1062,1086,1,0,0,0,1063,1065,10,9,0, - 0,1064,1066,5,8,0,0,1065,1064,1,0,0,0,1065,1066,1,0,0,0,1066,1067,1,0, - 0,0,1067,1068,5,24,0,0,1068,1069,5,135,0,0,1069,1074,3,112,56,0,1070,1071, - 5,137,0,0,1071,1073,3,112,56,0,1072,1070,1,0,0,0,1073,1076,1,0,0,0,1074, - 1072,1,0,0,0,1074,1075,1,0,0,0,1075,1077,1,0,0,0,1076,1074,1,0,0,0,1077, - 1078,5,136,0,0,1078,1086,1,0,0,0,1079,1080,10,8,0,0,1080,1082,5,28,0,0, - 1081,1083,5,8,0,0,1082,1081,1,0,0,0,1082,1083,1,0,0,0,1083,1084,1,0,0, - 0,1084,1086,5,107,0,0,1085,1003,1,0,0,0,1085,1006,1,0,0,0,1085,1009,1, - 0,0,0,1085,1012,1,0,0,0,1085,1015,1,0,0,0,1085,1018,1,0,0,0,1085,1021, - 1,0,0,0,1085,1024,1,0,0,0,1085,1033,1,0,0,0,1085,1039,1,0,0,0,1085,1042, - 1,0,0,0,1085,1045,1,0,0,0,1085,1048,1,0,0,0,1085,1051,1,0,0,0,1085,1054, - 1,0,0,0,1085,1063,1,0,0,0,1085,1079,1,0,0,0,1086,1089,1,0,0,0,1087,1085, - 1,0,0,0,1087,1088,1,0,0,0,1088,113,1,0,0,0,1089,1087,1,0,0,0,1090,1110, - 3,128,64,0,1091,1110,3,116,58,0,1092,1110,3,120,60,0,1093,1110,3,124,62, - 0,1094,1110,5,141,0,0,1095,1110,5,140,0,0,1096,1097,5,31,0,0,1097,1098, - 5,135,0,0,1098,1099,3,70,35,0,1099,1100,5,136,0,0,1100,1110,1,0,0,0,1101, - 1102,5,135,0,0,1102,1103,3,70,35,0,1103,1104,5,136,0,0,1104,1110,1,0,0, - 0,1105,1106,5,135,0,0,1106,1107,3,112,56,0,1107,1108,5,136,0,0,1108,1110, - 1,0,0,0,1109,1090,1,0,0,0,1109,1091,1,0,0,0,1109,1092,1,0,0,0,1109,1093, - 1,0,0,0,1109,1094,1,0,0,0,1109,1095,1,0,0,0,1109,1096,1,0,0,0,1109,1101, - 1,0,0,0,1109,1105,1,0,0,0,1110,115,1,0,0,0,1111,1113,5,41,0,0,1112,1114, - 3,112,56,0,1113,1112,1,0,0,0,1113,1114,1,0,0,0,1114,1116,1,0,0,0,1115, - 1117,3,118,59,0,1116,1115,1,0,0,0,1117,1118,1,0,0,0,1118,1116,1,0,0,0, - 1118,1119,1,0,0,0,1119,1122,1,0,0,0,1120,1121,5,43,0,0,1121,1123,3,112, - 56,0,1122,1120,1,0,0,0,1122,1123,1,0,0,0,1123,1124,1,0,0,0,1124,1125,5, - 44,0,0,1125,117,1,0,0,0,1126,1127,5,42,0,0,1127,1128,3,112,56,0,1128,1129, - 5,33,0,0,1129,1130,3,112,56,0,1130,119,1,0,0,0,1131,1132,3,122,61,0,1132, - 1145,5,135,0,0,1133,1146,5,122,0,0,1134,1136,5,35,0,0,1135,1134,1,0,0, - 0,1135,1136,1,0,0,0,1136,1137,1,0,0,0,1137,1142,3,112,56,0,1138,1139,5, - 137,0,0,1139,1141,3,112,56,0,1140,1138,1,0,0,0,1141,1144,1,0,0,0,1142, - 1140,1,0,0,0,1142,1143,1,0,0,0,1143,1146,1,0,0,0,1144,1142,1,0,0,0,1145, - 1133,1,0,0,0,1145,1135,1,0,0,0,1145,1146,1,0,0,0,1146,1147,1,0,0,0,1147, - 1149,5,136,0,0,1148,1150,3,148,74,0,1149,1148,1,0,0,0,1149,1150,1,0,0, - 0,1150,1152,1,0,0,0,1151,1153,3,146,73,0,1152,1151,1,0,0,0,1152,1153,1, - 0,0,0,1153,1162,1,0,0,0,1154,1156,3,150,75,0,1155,1154,1,0,0,0,1155,1156, - 1,0,0,0,1156,1158,1,0,0,0,1157,1159,3,152,76,0,1158,1157,1,0,0,0,1158, - 1159,1,0,0,0,1159,1160,1,0,0,0,1160,1161,5,39,0,0,1161,1163,3,134,67,0, - 1162,1155,1,0,0,0,1162,1163,1,0,0,0,1163,121,1,0,0,0,1164,1171,3,126,63, - 0,1165,1171,5,19,0,0,1166,1171,5,20,0,0,1167,1171,5,103,0,0,1168,1171, - 5,48,0,0,1169,1171,5,40,0,0,1170,1164,1,0,0,0,1170,1165,1,0,0,0,1170,1166, - 1,0,0,0,1170,1167,1,0,0,0,1170,1168,1,0,0,0,1170,1169,1,0,0,0,1171,123, - 1,0,0,0,1172,1173,3,126,63,0,1173,1174,5,138,0,0,1174,1176,1,0,0,0,1175, - 1172,1,0,0,0,1175,1176,1,0,0,0,1176,1177,1,0,0,0,1177,1178,3,126,63,0, - 1178,125,1,0,0,0,1179,1184,5,150,0,0,1180,1184,5,148,0,0,1181,1184,5,149, - 0,0,1182,1184,3,144,72,0,1183,1179,1,0,0,0,1183,1180,1,0,0,0,1183,1181, - 1,0,0,0,1183,1182,1,0,0,0,1184,127,1,0,0,0,1185,1195,5,143,0,0,1186,1195, - 5,144,0,0,1187,1195,5,142,0,0,1188,1195,5,145,0,0,1189,1195,5,146,0,0, - 1190,1195,5,147,0,0,1191,1195,5,105,0,0,1192,1195,5,106,0,0,1193,1195, - 5,107,0,0,1194,1185,1,0,0,0,1194,1186,1,0,0,0,1194,1187,1,0,0,0,1194,1188, - 1,0,0,0,1194,1189,1,0,0,0,1194,1190,1,0,0,0,1194,1191,1,0,0,0,1194,1192, - 1,0,0,0,1194,1193,1,0,0,0,1195,129,1,0,0,0,1196,1198,5,59,0,0,1197,1199, - 7,14,0,0,1198,1197,1,0,0,0,1198,1199,1,0,0,0,1199,1209,1,0,0,0,1200,1202, - 5,60,0,0,1201,1203,7,14,0,0,1202,1201,1,0,0,0,1202,1203,1,0,0,0,1203,1209, - 1,0,0,0,1204,1206,5,61,0,0,1205,1207,7,14,0,0,1206,1205,1,0,0,0,1206,1207, - 1,0,0,0,1207,1209,1,0,0,0,1208,1196,1,0,0,0,1208,1200,1,0,0,0,1208,1204, - 1,0,0,0,1209,131,1,0,0,0,1210,1211,3,112,56,0,1211,1212,5,0,0,1,1212,133, - 1,0,0,0,1213,1224,5,135,0,0,1214,1215,5,40,0,0,1215,1216,5,29,0,0,1216, - 1221,3,112,56,0,1217,1218,5,137,0,0,1218,1220,3,112,56,0,1219,1217,1,0, - 0,0,1220,1223,1,0,0,0,1221,1219,1,0,0,0,1221,1222,1,0,0,0,1222,1225,1, - 0,0,0,1223,1221,1,0,0,0,1224,1214,1,0,0,0,1224,1225,1,0,0,0,1225,1227, - 1,0,0,0,1226,1228,3,108,54,0,1227,1226,1,0,0,0,1227,1228,1,0,0,0,1228, - 1230,1,0,0,0,1229,1231,3,138,69,0,1230,1229,1,0,0,0,1230,1231,1,0,0,0, - 1231,1232,1,0,0,0,1232,1233,5,136,0,0,1233,135,1,0,0,0,1234,1235,7,15, - 0,0,1235,137,1,0,0,0,1236,1243,7,16,0,0,1237,1238,5,52,0,0,1238,1239,3, - 140,70,0,1239,1240,5,6,0,0,1240,1241,3,140,70,0,1241,1244,1,0,0,0,1242, - 1244,3,140,70,0,1243,1237,1,0,0,0,1243,1242,1,0,0,0,1244,1247,1,0,0,0, - 1245,1246,5,114,0,0,1246,1248,3,142,71,0,1247,1245,1,0,0,0,1247,1248,1, - 0,0,0,1248,139,1,0,0,0,1249,1250,5,110,0,0,1250,1257,7,17,0,0,1251,1252, - 5,113,0,0,1252,1257,5,50,0,0,1253,1254,3,112,56,0,1254,1255,7,17,0,0,1255, - 1257,1,0,0,0,1256,1249,1,0,0,0,1256,1251,1,0,0,0,1256,1253,1,0,0,0,1257, - 141,1,0,0,0,1258,1259,5,113,0,0,1259,1265,5,50,0,0,1260,1265,5,27,0,0, - 1261,1265,5,115,0,0,1262,1263,5,85,0,0,1263,1265,5,116,0,0,1264,1258,1, - 0,0,0,1264,1260,1,0,0,0,1264,1261,1,0,0,0,1264,1262,1,0,0,0,1265,143,1, - 0,0,0,1266,1267,7,18,0,0,1267,145,1,0,0,0,1268,1269,5,121,0,0,1269,1270, - 5,135,0,0,1270,1271,5,3,0,0,1271,1272,3,112,56,0,1272,1273,5,136,0,0,1273, - 147,1,0,0,0,1274,1275,5,117,0,0,1275,1276,5,27,0,0,1276,1277,5,135,0,0, - 1277,1278,3,108,54,0,1278,1279,5,136,0,0,1279,149,1,0,0,0,1280,1281,5, - 2,0,0,1281,1282,7,19,0,0,1282,151,1,0,0,0,1283,1284,7,20,0,0,1284,1285, - 5,120,0,0,1285,153,1,0,0,0,162,155,171,174,180,200,209,212,220,229,233, - 245,253,257,265,270,273,281,288,298,305,319,324,333,344,354,357,364,373, - 380,387,392,401,428,433,437,449,455,472,476,483,488,493,503,506,509,515, - 519,522,535,544,549,553,557,561,571,576,581,589,593,595,599,604,612,619, - 623,631,638,644,652,664,669,674,679,686,693,695,704,714,725,730,738,742, - 746,754,761,769,773,776,789,792,796,800,804,807,812,815,818,821,824,835, - 847,851,859,870,878,888,891,899,902,904,912,919,924,927,933,936,942,959, - 962,966,970,974,976,988,993,1001,1026,1035,1056,1065,1074,1082,1085,1087, - 1109,1113,1118,1122,1135,1142,1145,1149,1152,1155,1158,1162,1170,1175, - 1183,1194,1198,1202,1206,1208,1221,1224,1227,1230,1243,1247,1256,1264 + 56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1029,8,56,1,56,1,56,1,56,1,56,1, + 56,1,56,1,56,3,56,1038,8,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1, + 56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1059,8,56,1, + 56,1,56,1,56,1,56,1,56,1,56,1,56,3,56,1068,8,56,1,56,1,56,1,56,1,56,1, + 56,5,56,1075,8,56,10,56,12,56,1078,9,56,1,56,1,56,1,56,1,56,1,56,3,56, + 1085,8,56,1,56,5,56,1088,8,56,10,56,12,56,1091,9,56,1,57,1,57,1,57,1,57, + 1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57,1,57, + 1,57,3,57,1112,8,57,1,58,1,58,3,58,1116,8,58,1,58,4,58,1119,8,58,11,58, + 12,58,1120,1,58,1,58,3,58,1125,8,58,1,58,1,58,1,59,1,59,1,59,1,59,1,59, + 1,60,1,60,1,60,1,60,3,60,1138,8,60,1,60,1,60,1,60,5,60,1143,8,60,10,60, + 12,60,1146,9,60,3,60,1148,8,60,1,60,1,60,3,60,1152,8,60,1,60,3,60,1155, + 8,60,1,60,3,60,1158,8,60,1,60,3,60,1161,8,60,1,60,1,60,3,60,1165,8,60, + 1,61,1,61,1,61,1,61,1,61,1,61,3,61,1173,8,61,1,62,1,62,1,62,3,62,1178, + 8,62,1,62,1,62,1,63,1,63,1,63,1,63,3,63,1186,8,63,1,64,1,64,1,64,1,64, + 1,64,1,64,1,64,1,64,1,64,3,64,1197,8,64,1,65,1,65,3,65,1201,8,65,1,65, + 1,65,3,65,1205,8,65,1,65,1,65,3,65,1209,8,65,3,65,1211,8,65,1,66,1,66, + 1,66,1,67,1,67,1,67,1,67,1,67,1,67,5,67,1222,8,67,10,67,12,67,1225,9,67, + 3,67,1227,8,67,1,67,3,67,1230,8,67,1,67,3,67,1233,8,67,1,67,1,67,1,68, + 1,68,1,69,1,69,1,69,1,69,1,69,1,69,1,69,3,69,1246,8,69,1,69,1,69,3,69, + 1250,8,69,1,70,1,70,1,70,1,70,1,70,1,70,1,70,3,70,1259,8,70,1,71,1,71, + 1,71,1,71,1,71,1,71,3,71,1267,8,71,1,72,1,72,1,73,1,73,1,73,1,73,1,73, + 1,73,1,74,1,74,1,74,1,74,1,74,1,74,1,75,1,75,1,75,1,76,1,76,1,76,1,76, + 0,1,112,77,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42, + 44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90, + 92,94,96,98,100,102,104,106,108,110,112,114,116,118,120,122,124,126,128, + 130,132,134,136,138,140,142,144,146,148,150,152,0,21,1,0,101,102,1,0,103, + 104,1,0,93,94,1,0,135,136,2,0,34,35,54,54,1,0,126,127,1,0,47,48,1,0,49, + 50,2,0,8,8,15,15,1,0,122,123,1,0,129,134,2,0,6,6,12,12,2,0,7,7,13,13,2, + 0,9,9,14,14,1,0,62,63,2,0,87,87,89,89,2,0,49,49,108,109,1,0,111,112,1, + 0,108,121,2,0,48,48,118,118,2,0,96,96,119,119,1470,0,155,1,0,0,0,2,178, + 1,0,0,0,4,200,1,0,0,0,6,202,1,0,0,0,8,214,1,0,0,0,10,235,1,0,0,0,12,239, + 1,0,0,0,14,259,1,0,0,0,16,268,1,0,0,0,18,275,1,0,0,0,20,286,1,0,0,0,22, + 310,1,0,0,0,24,329,1,0,0,0,26,357,1,0,0,0,28,359,1,0,0,0,30,364,1,0,0, + 0,32,366,1,0,0,0,34,428,1,0,0,0,36,435,1,0,0,0,38,437,1,0,0,0,40,474,1, + 0,0,0,42,476,1,0,0,0,44,485,1,0,0,0,46,487,1,0,0,0,48,498,1,0,0,0,50,505, + 1,0,0,0,52,524,1,0,0,0,54,597,1,0,0,0,56,688,1,0,0,0,58,697,1,0,0,0,60, + 706,1,0,0,0,62,716,1,0,0,0,64,718,1,0,0,0,66,750,1,0,0,0,68,763,1,0,0, + 0,70,765,1,0,0,0,72,794,1,0,0,0,74,802,1,0,0,0,76,804,1,0,0,0,78,828,1, + 0,0,0,80,830,1,0,0,0,82,840,1,0,0,0,84,843,1,0,0,0,86,861,1,0,0,0,88,880, + 1,0,0,0,90,882,1,0,0,0,92,893,1,0,0,0,94,906,1,0,0,0,96,908,1,0,0,0,98, + 917,1,0,0,0,100,944,1,0,0,0,102,961,1,0,0,0,104,978,1,0,0,0,106,980,1, + 0,0,0,108,983,1,0,0,0,110,993,1,0,0,0,112,1003,1,0,0,0,114,1111,1,0,0, + 0,116,1113,1,0,0,0,118,1128,1,0,0,0,120,1133,1,0,0,0,122,1172,1,0,0,0, + 124,1177,1,0,0,0,126,1185,1,0,0,0,128,1196,1,0,0,0,130,1210,1,0,0,0,132, + 1212,1,0,0,0,134,1215,1,0,0,0,136,1236,1,0,0,0,138,1238,1,0,0,0,140,1258, + 1,0,0,0,142,1266,1,0,0,0,144,1268,1,0,0,0,146,1270,1,0,0,0,148,1276,1, + 0,0,0,150,1282,1,0,0,0,152,1285,1,0,0,0,154,156,3,18,9,0,155,154,1,0,0, + 0,155,156,1,0,0,0,156,171,1,0,0,0,157,172,3,2,1,0,158,172,3,20,10,0,159, + 172,3,38,19,0,160,172,3,22,11,0,161,172,3,24,12,0,162,172,3,32,16,0,163, + 172,3,40,20,0,164,172,3,64,32,0,165,172,3,8,4,0,166,172,3,12,6,0,167,172, + 3,130,65,0,168,172,3,6,3,0,169,172,3,14,7,0,170,172,3,70,35,0,171,157, + 1,0,0,0,171,158,1,0,0,0,171,159,1,0,0,0,171,160,1,0,0,0,171,161,1,0,0, + 0,171,162,1,0,0,0,171,163,1,0,0,0,171,164,1,0,0,0,171,165,1,0,0,0,171, + 166,1,0,0,0,171,167,1,0,0,0,171,168,1,0,0,0,171,169,1,0,0,0,171,170,1, + 0,0,0,172,174,1,0,0,0,173,175,5,139,0,0,174,173,1,0,0,0,174,175,1,0,0, + 0,175,176,1,0,0,0,176,177,5,0,0,1,177,1,1,0,0,0,178,180,5,32,0,0,179,181, + 5,8,0,0,180,179,1,0,0,0,180,181,1,0,0,0,181,182,1,0,0,0,182,183,5,31,0, + 0,183,184,5,135,0,0,184,185,3,70,35,0,185,186,5,136,0,0,186,187,5,33,0, + 0,187,188,3,4,2,0,188,3,1,0,0,0,189,201,3,20,10,0,190,201,3,38,19,0,191, + 201,3,22,11,0,192,201,3,24,12,0,193,201,3,32,16,0,194,201,3,40,20,0,195, + 201,3,64,32,0,196,201,3,8,4,0,197,201,3,12,6,0,198,201,3,6,3,0,199,201, + 3,70,35,0,200,189,1,0,0,0,200,190,1,0,0,0,200,191,1,0,0,0,200,192,1,0, + 0,0,200,193,1,0,0,0,200,194,1,0,0,0,200,195,1,0,0,0,200,196,1,0,0,0,200, + 197,1,0,0,0,200,198,1,0,0,0,200,199,1,0,0,0,201,5,1,0,0,0,202,203,7,0, + 0,0,203,212,3,126,63,0,204,209,3,112,56,0,205,206,5,137,0,0,206,208,3, + 112,56,0,207,205,1,0,0,0,208,211,1,0,0,0,209,207,1,0,0,0,209,210,1,0,0, + 0,210,213,1,0,0,0,211,209,1,0,0,0,212,204,1,0,0,0,212,213,1,0,0,0,213, + 7,1,0,0,0,214,215,5,79,0,0,215,220,3,98,49,0,216,217,5,137,0,0,217,219, + 3,98,49,0,218,216,1,0,0,0,219,222,1,0,0,0,220,218,1,0,0,0,220,221,1,0, + 0,0,221,223,1,0,0,0,222,220,1,0,0,0,223,224,5,83,0,0,224,229,3,10,5,0, + 225,226,5,137,0,0,226,228,3,10,5,0,227,225,1,0,0,0,228,231,1,0,0,0,229, + 227,1,0,0,0,229,230,1,0,0,0,230,233,1,0,0,0,231,229,1,0,0,0,232,234,3, + 106,53,0,233,232,1,0,0,0,233,234,1,0,0,0,234,9,1,0,0,0,235,236,3,124,62, + 0,236,237,5,129,0,0,237,238,3,112,56,0,238,11,1,0,0,0,239,245,5,78,0,0, + 240,241,3,126,63,0,241,242,5,138,0,0,242,243,5,122,0,0,243,246,1,0,0,0, + 244,246,5,122,0,0,245,240,1,0,0,0,245,244,1,0,0,0,245,246,1,0,0,0,246, + 247,1,0,0,0,247,248,5,2,0,0,248,253,3,98,49,0,249,250,5,137,0,0,250,252, + 3,98,49,0,251,249,1,0,0,0,252,255,1,0,0,0,253,251,1,0,0,0,253,254,1,0, + 0,0,254,257,1,0,0,0,255,253,1,0,0,0,256,258,3,106,53,0,257,256,1,0,0,0, + 257,258,1,0,0,0,258,13,1,0,0,0,259,260,5,1,0,0,260,265,3,16,8,0,261,262, + 5,137,0,0,262,264,3,16,8,0,263,261,1,0,0,0,264,267,1,0,0,0,265,263,1,0, + 0,0,265,266,1,0,0,0,266,15,1,0,0,0,267,265,1,0,0,0,268,273,5,140,0,0,269, + 271,5,5,0,0,270,269,1,0,0,0,270,271,1,0,0,0,271,272,1,0,0,0,272,274,3, + 126,63,0,273,270,1,0,0,0,273,274,1,0,0,0,274,17,1,0,0,0,275,276,5,100, + 0,0,276,281,3,28,14,0,277,278,5,137,0,0,278,280,3,28,14,0,279,277,1,0, + 0,0,280,283,1,0,0,0,281,279,1,0,0,0,281,282,1,0,0,0,282,284,1,0,0,0,283, + 281,1,0,0,0,284,285,5,139,0,0,285,19,1,0,0,0,286,288,5,57,0,0,287,289, + 5,91,0,0,288,287,1,0,0,0,288,289,1,0,0,0,289,290,1,0,0,0,290,291,5,58, + 0,0,291,292,3,126,63,0,292,293,5,135,0,0,293,298,3,46,23,0,294,295,5,137, + 0,0,295,297,3,46,23,0,296,294,1,0,0,0,297,300,1,0,0,0,298,296,1,0,0,0, + 298,299,1,0,0,0,299,305,1,0,0,0,300,298,1,0,0,0,301,302,5,137,0,0,302, + 304,3,56,28,0,303,301,1,0,0,0,304,307,1,0,0,0,305,303,1,0,0,0,305,306, + 1,0,0,0,306,308,1,0,0,0,307,305,1,0,0,0,308,309,5,136,0,0,309,21,1,0,0, + 0,310,311,5,57,0,0,311,312,5,98,0,0,312,324,3,126,63,0,313,314,5,135,0, + 0,314,319,3,126,63,0,315,316,5,137,0,0,316,318,3,126,63,0,317,315,1,0, + 0,0,318,321,1,0,0,0,319,317,1,0,0,0,319,320,1,0,0,0,320,322,1,0,0,0,321, + 319,1,0,0,0,322,323,5,136,0,0,323,325,1,0,0,0,324,313,1,0,0,0,324,325, + 1,0,0,0,325,326,1,0,0,0,326,327,5,5,0,0,327,328,3,70,35,0,328,23,1,0,0, + 0,329,330,5,57,0,0,330,331,5,99,0,0,331,333,3,126,63,0,332,334,3,26,13, + 0,333,332,1,0,0,0,333,334,1,0,0,0,334,335,1,0,0,0,335,336,5,5,0,0,336, + 337,3,36,18,0,337,25,1,0,0,0,338,339,5,135,0,0,339,344,3,28,14,0,340,341, + 5,137,0,0,341,343,3,28,14,0,342,340,1,0,0,0,343,346,1,0,0,0,344,342,1, + 0,0,0,344,345,1,0,0,0,345,347,1,0,0,0,346,344,1,0,0,0,347,348,5,136,0, + 0,348,358,1,0,0,0,349,354,3,28,14,0,350,351,5,137,0,0,351,353,3,28,14, + 0,352,350,1,0,0,0,353,356,1,0,0,0,354,352,1,0,0,0,354,355,1,0,0,0,355, + 358,1,0,0,0,356,354,1,0,0,0,357,338,1,0,0,0,357,349,1,0,0,0,358,27,1,0, + 0,0,359,360,3,30,15,0,360,361,3,50,25,0,361,29,1,0,0,0,362,365,3,126,63, + 0,363,365,5,141,0,0,364,362,1,0,0,0,364,363,1,0,0,0,365,31,1,0,0,0,366, + 367,5,64,0,0,367,368,5,58,0,0,368,369,3,126,63,0,369,370,3,34,17,0,370, + 33,1,0,0,0,371,373,5,67,0,0,372,374,5,69,0,0,373,372,1,0,0,0,373,374,1, + 0,0,0,374,375,1,0,0,0,375,429,3,46,23,0,376,377,5,67,0,0,377,429,3,56, + 28,0,378,380,5,64,0,0,379,381,5,69,0,0,380,379,1,0,0,0,380,381,1,0,0,0, + 381,382,1,0,0,0,382,383,3,126,63,0,383,387,3,50,25,0,384,386,3,54,27,0, + 385,384,1,0,0,0,386,389,1,0,0,0,387,385,1,0,0,0,387,388,1,0,0,0,388,429, + 1,0,0,0,389,387,1,0,0,0,390,392,5,64,0,0,391,393,5,69,0,0,392,391,1,0, + 0,0,392,393,1,0,0,0,393,394,1,0,0,0,394,395,3,126,63,0,395,396,5,83,0, + 0,396,397,5,84,0,0,397,398,3,112,56,0,398,429,1,0,0,0,399,401,5,64,0,0, + 400,402,5,69,0,0,401,400,1,0,0,0,401,402,1,0,0,0,402,403,1,0,0,0,403,404, + 3,126,63,0,404,405,5,68,0,0,405,406,5,84,0,0,406,429,1,0,0,0,407,408,5, + 68,0,0,408,409,5,69,0,0,409,429,3,126,63,0,410,411,5,68,0,0,411,412,5, + 75,0,0,412,429,3,126,63,0,413,414,5,65,0,0,414,415,5,66,0,0,415,429,3, + 126,63,0,416,417,5,65,0,0,417,418,5,69,0,0,418,419,3,126,63,0,419,420, + 5,66,0,0,420,421,3,126,63,0,421,429,1,0,0,0,422,423,5,65,0,0,423,424,5, + 90,0,0,424,425,3,126,63,0,425,426,5,66,0,0,426,427,3,126,63,0,427,429, + 1,0,0,0,428,371,1,0,0,0,428,376,1,0,0,0,428,378,1,0,0,0,428,390,1,0,0, + 0,428,399,1,0,0,0,428,407,1,0,0,0,428,410,1,0,0,0,428,413,1,0,0,0,428, + 416,1,0,0,0,428,422,1,0,0,0,429,35,1,0,0,0,430,436,3,70,35,0,431,436,3, + 64,32,0,432,436,3,8,4,0,433,436,3,12,6,0,434,436,3,20,10,0,435,430,1,0, + 0,0,435,431,1,0,0,0,435,432,1,0,0,0,435,433,1,0,0,0,435,434,1,0,0,0,436, + 37,1,0,0,0,437,439,5,57,0,0,438,440,5,86,0,0,439,438,1,0,0,0,439,440,1, + 0,0,0,440,441,1,0,0,0,441,442,5,90,0,0,442,443,3,126,63,0,443,444,5,25, + 0,0,444,445,3,126,63,0,445,446,5,135,0,0,446,451,3,42,21,0,447,448,5,137, + 0,0,448,450,3,42,21,0,449,447,1,0,0,0,450,453,1,0,0,0,451,449,1,0,0,0, + 451,452,1,0,0,0,452,454,1,0,0,0,453,451,1,0,0,0,454,457,5,136,0,0,455, + 456,5,92,0,0,456,458,3,44,22,0,457,455,1,0,0,0,457,458,1,0,0,0,458,39, + 1,0,0,0,459,460,5,68,0,0,460,461,5,58,0,0,461,475,3,126,63,0,462,463,5, + 68,0,0,463,464,5,90,0,0,464,465,3,126,63,0,465,466,5,25,0,0,466,467,3, + 126,63,0,467,475,1,0,0,0,468,469,5,68,0,0,469,470,5,99,0,0,470,475,3,126, + 63,0,471,472,5,68,0,0,472,473,5,98,0,0,473,475,3,126,63,0,474,459,1,0, + 0,0,474,462,1,0,0,0,474,468,1,0,0,0,474,471,1,0,0,0,475,41,1,0,0,0,476, + 478,3,126,63,0,477,479,7,1,0,0,478,477,1,0,0,0,478,479,1,0,0,0,479,43, + 1,0,0,0,480,486,5,73,0,0,481,482,5,95,0,0,482,486,5,107,0,0,483,484,5, + 96,0,0,484,486,5,107,0,0,485,480,1,0,0,0,485,481,1,0,0,0,485,483,1,0,0, + 0,486,45,1,0,0,0,487,488,3,126,63,0,488,490,3,50,25,0,489,491,3,48,24, + 0,490,489,1,0,0,0,490,491,1,0,0,0,491,495,1,0,0,0,492,494,3,54,27,0,493, + 492,1,0,0,0,494,497,1,0,0,0,495,493,1,0,0,0,495,496,1,0,0,0,496,47,1,0, + 0,0,497,495,1,0,0,0,498,499,5,5,0,0,499,500,5,135,0,0,500,501,3,112,56, + 0,501,502,5,136,0,0,502,49,1,0,0,0,503,506,3,126,63,0,504,506,5,88,0,0, + 505,503,1,0,0,0,505,504,1,0,0,0,506,508,1,0,0,0,507,509,3,126,63,0,508, + 507,1,0,0,0,508,509,1,0,0,0,509,511,1,0,0,0,510,512,3,126,63,0,511,510, + 1,0,0,0,511,512,1,0,0,0,512,521,1,0,0,0,513,514,5,135,0,0,514,517,3,52, + 26,0,515,516,5,137,0,0,516,518,3,52,26,0,517,515,1,0,0,0,517,518,1,0,0, + 0,518,519,1,0,0,0,519,520,5,136,0,0,520,522,1,0,0,0,521,513,1,0,0,0,521, + 522,1,0,0,0,522,51,1,0,0,0,523,525,5,127,0,0,524,523,1,0,0,0,524,525,1, + 0,0,0,525,526,1,0,0,0,526,527,5,143,0,0,527,53,1,0,0,0,528,529,5,8,0,0, + 529,598,5,107,0,0,530,598,5,107,0,0,531,532,5,84,0,0,532,598,3,112,56, + 0,533,534,5,92,0,0,534,598,7,2,0,0,535,536,5,75,0,0,536,538,3,126,63,0, + 537,535,1,0,0,0,537,538,1,0,0,0,538,539,1,0,0,0,539,540,5,97,0,0,540,541, + 5,135,0,0,541,542,3,58,29,0,542,543,5,136,0,0,543,598,1,0,0,0,544,545, + 5,75,0,0,545,547,3,126,63,0,546,544,1,0,0,0,546,547,1,0,0,0,547,548,1, + 0,0,0,548,549,5,73,0,0,549,551,5,74,0,0,550,552,3,136,68,0,551,550,1,0, + 0,0,551,552,1,0,0,0,552,598,1,0,0,0,553,554,5,75,0,0,554,556,3,126,63, + 0,555,553,1,0,0,0,555,556,1,0,0,0,556,557,1,0,0,0,557,559,5,86,0,0,558, + 560,3,136,68,0,559,558,1,0,0,0,559,560,1,0,0,0,560,598,1,0,0,0,561,562, + 5,75,0,0,562,564,3,126,63,0,563,561,1,0,0,0,563,564,1,0,0,0,564,565,1, + 0,0,0,565,566,5,77,0,0,566,578,3,126,63,0,567,568,5,135,0,0,568,573,3, + 126,63,0,569,570,5,137,0,0,570,572,3,126,63,0,571,569,1,0,0,0,572,575, + 1,0,0,0,573,571,1,0,0,0,573,574,1,0,0,0,574,576,1,0,0,0,575,573,1,0,0, + 0,576,577,5,136,0,0,577,579,1,0,0,0,578,567,1,0,0,0,578,579,1,0,0,0,579, + 583,1,0,0,0,580,582,3,60,30,0,581,580,1,0,0,0,582,585,1,0,0,0,583,581, + 1,0,0,0,583,584,1,0,0,0,584,598,1,0,0,0,585,583,1,0,0,0,586,595,5,88,0, + 0,587,588,5,135,0,0,588,591,3,52,26,0,589,590,5,137,0,0,590,592,3,52,26, + 0,591,589,1,0,0,0,591,592,1,0,0,0,592,593,1,0,0,0,593,594,5,136,0,0,594, + 596,1,0,0,0,595,587,1,0,0,0,595,596,1,0,0,0,596,598,1,0,0,0,597,528,1, + 0,0,0,597,530,1,0,0,0,597,531,1,0,0,0,597,533,1,0,0,0,597,537,1,0,0,0, + 597,546,1,0,0,0,597,555,1,0,0,0,597,563,1,0,0,0,597,586,1,0,0,0,598,55, + 1,0,0,0,599,600,5,75,0,0,600,602,3,126,63,0,601,599,1,0,0,0,601,602,1, + 0,0,0,602,603,1,0,0,0,603,604,5,73,0,0,604,606,5,74,0,0,605,607,3,136, + 68,0,606,605,1,0,0,0,606,607,1,0,0,0,607,608,1,0,0,0,608,609,5,135,0,0, + 609,614,3,126,63,0,610,611,5,137,0,0,611,613,3,126,63,0,612,610,1,0,0, + 0,613,616,1,0,0,0,614,612,1,0,0,0,614,615,1,0,0,0,615,617,1,0,0,0,616, + 614,1,0,0,0,617,618,5,136,0,0,618,689,1,0,0,0,619,620,5,75,0,0,620,622, + 3,126,63,0,621,619,1,0,0,0,621,622,1,0,0,0,622,623,1,0,0,0,623,625,5,86, + 0,0,624,626,3,136,68,0,625,624,1,0,0,0,625,626,1,0,0,0,626,627,1,0,0,0, + 627,628,5,135,0,0,628,633,3,126,63,0,629,630,5,137,0,0,630,632,3,126,63, + 0,631,629,1,0,0,0,632,635,1,0,0,0,633,631,1,0,0,0,633,634,1,0,0,0,634, + 636,1,0,0,0,635,633,1,0,0,0,636,637,5,136,0,0,637,689,1,0,0,0,638,639, + 5,75,0,0,639,641,3,126,63,0,640,638,1,0,0,0,640,641,1,0,0,0,641,642,1, + 0,0,0,642,643,5,76,0,0,643,646,5,74,0,0,644,645,5,85,0,0,645,647,5,90, + 0,0,646,644,1,0,0,0,646,647,1,0,0,0,647,648,1,0,0,0,648,649,5,135,0,0, + 649,654,3,126,63,0,650,651,5,137,0,0,651,653,3,126,63,0,652,650,1,0,0, + 0,653,656,1,0,0,0,654,652,1,0,0,0,654,655,1,0,0,0,655,657,1,0,0,0,656, + 654,1,0,0,0,657,658,5,136,0,0,658,659,5,77,0,0,659,671,3,126,63,0,660, + 661,5,135,0,0,661,666,3,126,63,0,662,663,5,137,0,0,663,665,3,126,63,0, + 664,662,1,0,0,0,665,668,1,0,0,0,666,664,1,0,0,0,666,667,1,0,0,0,667,669, + 1,0,0,0,668,666,1,0,0,0,669,670,5,136,0,0,670,672,1,0,0,0,671,660,1,0, + 0,0,671,672,1,0,0,0,672,676,1,0,0,0,673,675,3,60,30,0,674,673,1,0,0,0, + 675,678,1,0,0,0,676,674,1,0,0,0,676,677,1,0,0,0,677,689,1,0,0,0,678,676, + 1,0,0,0,679,680,5,75,0,0,680,682,3,126,63,0,681,679,1,0,0,0,681,682,1, + 0,0,0,682,683,1,0,0,0,683,684,5,97,0,0,684,685,5,135,0,0,685,686,3,58, + 29,0,686,687,5,136,0,0,687,689,1,0,0,0,688,601,1,0,0,0,688,621,1,0,0,0, + 688,640,1,0,0,0,688,681,1,0,0,0,689,57,1,0,0,0,690,696,8,3,0,0,691,692, + 5,135,0,0,692,693,3,58,29,0,693,694,5,136,0,0,694,696,1,0,0,0,695,690, + 1,0,0,0,695,691,1,0,0,0,696,699,1,0,0,0,697,695,1,0,0,0,697,698,1,0,0, + 0,698,59,1,0,0,0,699,697,1,0,0,0,700,701,5,25,0,0,701,702,5,79,0,0,702, + 707,3,62,31,0,703,704,5,25,0,0,704,705,5,78,0,0,705,707,3,62,31,0,706, + 700,1,0,0,0,706,703,1,0,0,0,707,61,1,0,0,0,708,717,5,80,0,0,709,710,5, + 85,0,0,710,717,5,82,0,0,711,717,5,81,0,0,712,713,5,83,0,0,713,717,5,107, + 0,0,714,715,5,83,0,0,715,717,5,84,0,0,716,708,1,0,0,0,716,709,1,0,0,0, + 716,711,1,0,0,0,716,712,1,0,0,0,716,714,1,0,0,0,717,63,1,0,0,0,718,719, + 5,70,0,0,719,720,5,71,0,0,720,748,3,126,63,0,721,722,5,135,0,0,722,727, + 3,126,63,0,723,724,5,137,0,0,724,726,3,126,63,0,725,723,1,0,0,0,726,729, + 1,0,0,0,727,725,1,0,0,0,727,728,1,0,0,0,728,730,1,0,0,0,729,727,1,0,0, + 0,730,731,5,136,0,0,731,733,1,0,0,0,732,721,1,0,0,0,732,733,1,0,0,0,733, + 744,1,0,0,0,734,735,5,72,0,0,735,740,3,66,33,0,736,737,5,137,0,0,737,739, + 3,66,33,0,738,736,1,0,0,0,739,742,1,0,0,0,740,738,1,0,0,0,740,741,1,0, + 0,0,741,745,1,0,0,0,742,740,1,0,0,0,743,745,3,70,35,0,744,734,1,0,0,0, + 744,743,1,0,0,0,745,749,1,0,0,0,746,747,5,84,0,0,747,749,5,72,0,0,748, + 732,1,0,0,0,748,746,1,0,0,0,749,65,1,0,0,0,750,751,5,135,0,0,751,756,3, + 68,34,0,752,753,5,137,0,0,753,755,3,68,34,0,754,752,1,0,0,0,755,758,1, + 0,0,0,756,754,1,0,0,0,756,757,1,0,0,0,757,759,1,0,0,0,758,756,1,0,0,0, + 759,760,5,136,0,0,760,67,1,0,0,0,761,764,5,84,0,0,762,764,3,112,56,0,763, + 761,1,0,0,0,763,762,1,0,0,0,764,69,1,0,0,0,765,771,3,72,36,0,766,767,3, + 74,37,0,767,768,3,72,36,0,768,770,1,0,0,0,769,766,1,0,0,0,770,773,1,0, + 0,0,771,769,1,0,0,0,771,772,1,0,0,0,772,775,1,0,0,0,773,771,1,0,0,0,774, + 776,3,108,54,0,775,774,1,0,0,0,775,776,1,0,0,0,776,778,1,0,0,0,777,779, + 3,88,44,0,778,777,1,0,0,0,778,779,1,0,0,0,779,71,1,0,0,0,780,795,3,76, + 38,0,781,782,5,135,0,0,782,783,3,70,35,0,783,784,5,136,0,0,784,795,1,0, + 0,0,785,786,5,72,0,0,786,791,3,66,33,0,787,788,5,137,0,0,788,790,3,66, + 33,0,789,787,1,0,0,0,790,793,1,0,0,0,791,789,1,0,0,0,791,792,1,0,0,0,792, + 795,1,0,0,0,793,791,1,0,0,0,794,780,1,0,0,0,794,781,1,0,0,0,794,785,1, + 0,0,0,795,73,1,0,0,0,796,798,5,53,0,0,797,799,5,54,0,0,798,797,1,0,0,0, + 798,799,1,0,0,0,799,803,1,0,0,0,800,803,5,55,0,0,801,803,5,56,0,0,802, + 796,1,0,0,0,802,800,1,0,0,0,802,801,1,0,0,0,803,75,1,0,0,0,804,806,5,1, + 0,0,805,807,3,78,39,0,806,805,1,0,0,0,806,807,1,0,0,0,807,809,1,0,0,0, + 808,810,3,84,42,0,809,808,1,0,0,0,809,810,1,0,0,0,810,811,1,0,0,0,811, + 814,3,92,46,0,812,813,5,71,0,0,813,815,3,126,63,0,814,812,1,0,0,0,814, + 815,1,0,0,0,815,817,1,0,0,0,816,818,3,96,48,0,817,816,1,0,0,0,817,818, + 1,0,0,0,818,820,1,0,0,0,819,821,3,106,53,0,820,819,1,0,0,0,820,821,1,0, + 0,0,821,823,1,0,0,0,822,824,3,80,40,0,823,822,1,0,0,0,823,824,1,0,0,0, + 824,826,1,0,0,0,825,827,3,82,41,0,826,825,1,0,0,0,826,827,1,0,0,0,827, + 77,1,0,0,0,828,829,7,4,0,0,829,79,1,0,0,0,830,831,5,27,0,0,831,832,5,29, + 0,0,832,837,3,112,56,0,833,834,5,137,0,0,834,836,3,112,56,0,835,833,1, + 0,0,0,836,839,1,0,0,0,837,835,1,0,0,0,837,838,1,0,0,0,838,81,1,0,0,0,839, + 837,1,0,0,0,840,841,5,30,0,0,841,842,3,112,56,0,842,83,1,0,0,0,843,844, + 5,4,0,0,844,849,3,86,43,0,845,846,7,5,0,0,846,848,3,86,43,0,847,845,1, + 0,0,0,848,851,1,0,0,0,849,847,1,0,0,0,849,850,1,0,0,0,850,853,1,0,0,0, + 851,849,1,0,0,0,852,854,5,36,0,0,853,852,1,0,0,0,853,854,1,0,0,0,854,85, + 1,0,0,0,855,862,5,143,0,0,856,862,5,141,0,0,857,858,5,135,0,0,858,859, + 3,112,56,0,859,860,5,136,0,0,860,862,1,0,0,0,861,855,1,0,0,0,861,856,1, + 0,0,0,861,857,1,0,0,0,862,87,1,0,0,0,863,864,5,45,0,0,864,865,3,112,56, + 0,865,872,3,90,45,0,866,867,5,46,0,0,867,868,7,6,0,0,868,869,3,112,56, + 0,869,870,3,90,45,0,870,871,5,51,0,0,871,873,1,0,0,0,872,866,1,0,0,0,872, + 873,1,0,0,0,873,881,1,0,0,0,874,875,5,46,0,0,875,876,7,6,0,0,876,877,3, + 112,56,0,877,878,3,90,45,0,878,879,5,51,0,0,879,881,1,0,0,0,880,863,1, + 0,0,0,880,874,1,0,0,0,881,89,1,0,0,0,882,883,7,7,0,0,883,91,1,0,0,0,884, + 894,5,122,0,0,885,890,3,94,47,0,886,887,5,137,0,0,887,889,3,94,47,0,888, + 886,1,0,0,0,889,892,1,0,0,0,890,888,1,0,0,0,890,891,1,0,0,0,891,894,1, + 0,0,0,892,890,1,0,0,0,893,884,1,0,0,0,893,885,1,0,0,0,894,93,1,0,0,0,895, + 896,3,126,63,0,896,897,5,138,0,0,897,898,5,122,0,0,898,907,1,0,0,0,899, + 904,3,112,56,0,900,902,5,5,0,0,901,900,1,0,0,0,901,902,1,0,0,0,902,903, + 1,0,0,0,903,905,3,126,63,0,904,901,1,0,0,0,904,905,1,0,0,0,905,907,1,0, + 0,0,906,895,1,0,0,0,906,899,1,0,0,0,907,95,1,0,0,0,908,909,5,2,0,0,909, + 914,3,98,49,0,910,911,5,137,0,0,911,913,3,98,49,0,912,910,1,0,0,0,913, + 916,1,0,0,0,914,912,1,0,0,0,914,915,1,0,0,0,915,97,1,0,0,0,916,914,1,0, + 0,0,917,921,3,100,50,0,918,920,3,102,51,0,919,918,1,0,0,0,920,923,1,0, + 0,0,921,919,1,0,0,0,921,922,1,0,0,0,922,99,1,0,0,0,923,921,1,0,0,0,924, + 929,3,126,63,0,925,927,5,5,0,0,926,925,1,0,0,0,926,927,1,0,0,0,927,928, + 1,0,0,0,928,930,3,126,63,0,929,926,1,0,0,0,929,930,1,0,0,0,930,945,1,0, + 0,0,931,932,5,135,0,0,932,933,3,70,35,0,933,938,5,136,0,0,934,936,5,5, + 0,0,935,934,1,0,0,0,935,936,1,0,0,0,936,937,1,0,0,0,937,939,3,126,63,0, + 938,935,1,0,0,0,938,939,1,0,0,0,939,945,1,0,0,0,940,941,5,135,0,0,941, + 942,3,98,49,0,942,943,5,136,0,0,943,945,1,0,0,0,944,924,1,0,0,0,944,931, + 1,0,0,0,944,940,1,0,0,0,945,101,1,0,0,0,946,947,3,104,52,0,947,948,5,23, + 0,0,948,949,3,100,50,0,949,950,5,25,0,0,950,951,3,112,56,0,951,962,1,0, + 0,0,952,953,5,37,0,0,953,954,5,23,0,0,954,962,3,100,50,0,955,956,5,37, + 0,0,956,957,5,38,0,0,957,962,3,100,50,0,958,959,5,22,0,0,959,960,5,38, + 0,0,960,962,3,100,50,0,961,946,1,0,0,0,961,952,1,0,0,0,961,955,1,0,0,0, + 961,958,1,0,0,0,962,103,1,0,0,0,963,965,5,18,0,0,964,963,1,0,0,0,964,965, + 1,0,0,0,965,979,1,0,0,0,966,968,5,19,0,0,967,969,5,22,0,0,968,967,1,0, + 0,0,968,969,1,0,0,0,969,979,1,0,0,0,970,972,5,20,0,0,971,973,5,22,0,0, + 972,971,1,0,0,0,972,973,1,0,0,0,973,979,1,0,0,0,974,976,5,21,0,0,975,977, + 5,22,0,0,976,975,1,0,0,0,976,977,1,0,0,0,977,979,1,0,0,0,978,964,1,0,0, + 0,978,966,1,0,0,0,978,970,1,0,0,0,978,974,1,0,0,0,979,105,1,0,0,0,980, + 981,5,3,0,0,981,982,3,112,56,0,982,107,1,0,0,0,983,984,5,26,0,0,984,985, + 5,29,0,0,985,990,3,110,55,0,986,987,5,137,0,0,987,989,3,110,55,0,988,986, + 1,0,0,0,989,992,1,0,0,0,990,988,1,0,0,0,990,991,1,0,0,0,991,109,1,0,0, + 0,992,990,1,0,0,0,993,995,3,112,56,0,994,996,7,1,0,0,995,994,1,0,0,0,995, + 996,1,0,0,0,996,111,1,0,0,0,997,998,6,56,-1,0,998,999,7,5,0,0,999,1004, + 3,112,56,19,1000,1001,7,8,0,0,1001,1004,3,112,56,7,1002,1004,3,114,57, + 0,1003,997,1,0,0,0,1003,1000,1,0,0,0,1003,1002,1,0,0,0,1004,1089,1,0,0, + 0,1005,1006,10,20,0,0,1006,1007,5,125,0,0,1007,1088,3,112,56,21,1008,1009, + 10,18,0,0,1009,1010,7,9,0,0,1010,1088,3,112,56,19,1011,1012,10,17,0,0, + 1012,1013,5,124,0,0,1013,1088,3,112,56,18,1014,1015,10,16,0,0,1015,1016, + 5,17,0,0,1016,1088,3,112,56,17,1017,1018,10,15,0,0,1018,1019,7,5,0,0,1019, + 1088,3,112,56,16,1020,1021,10,14,0,0,1021,1022,5,128,0,0,1022,1088,3,112, + 56,15,1023,1024,10,13,0,0,1024,1025,7,10,0,0,1025,1088,3,112,56,14,1026, + 1028,10,12,0,0,1027,1029,5,8,0,0,1028,1027,1,0,0,0,1028,1029,1,0,0,0,1029, + 1030,1,0,0,0,1030,1031,5,52,0,0,1031,1032,3,112,56,0,1032,1033,5,6,0,0, + 1033,1034,3,112,56,13,1034,1088,1,0,0,0,1035,1037,10,11,0,0,1036,1038, + 5,8,0,0,1037,1036,1,0,0,0,1037,1038,1,0,0,0,1038,1039,1,0,0,0,1039,1040, + 5,16,0,0,1040,1088,3,112,56,12,1041,1042,10,6,0,0,1042,1043,7,11,0,0,1043, + 1088,3,112,56,7,1044,1045,10,5,0,0,1045,1046,7,12,0,0,1046,1088,3,112, + 56,6,1047,1048,10,4,0,0,1048,1049,7,13,0,0,1049,1088,3,112,56,5,1050,1051, + 10,3,0,0,1051,1052,5,10,0,0,1052,1088,3,112,56,4,1053,1054,10,2,0,0,1054, + 1055,5,11,0,0,1055,1088,3,112,56,3,1056,1058,10,10,0,0,1057,1059,5,8,0, + 0,1058,1057,1,0,0,0,1058,1059,1,0,0,0,1059,1060,1,0,0,0,1060,1061,5,24, + 0,0,1061,1062,5,135,0,0,1062,1063,3,70,35,0,1063,1064,5,136,0,0,1064,1088, + 1,0,0,0,1065,1067,10,9,0,0,1066,1068,5,8,0,0,1067,1066,1,0,0,0,1067,1068, + 1,0,0,0,1068,1069,1,0,0,0,1069,1070,5,24,0,0,1070,1071,5,135,0,0,1071, + 1076,3,112,56,0,1072,1073,5,137,0,0,1073,1075,3,112,56,0,1074,1072,1,0, + 0,0,1075,1078,1,0,0,0,1076,1074,1,0,0,0,1076,1077,1,0,0,0,1077,1079,1, + 0,0,0,1078,1076,1,0,0,0,1079,1080,5,136,0,0,1080,1088,1,0,0,0,1081,1082, + 10,8,0,0,1082,1084,5,28,0,0,1083,1085,5,8,0,0,1084,1083,1,0,0,0,1084,1085, + 1,0,0,0,1085,1086,1,0,0,0,1086,1088,5,107,0,0,1087,1005,1,0,0,0,1087,1008, + 1,0,0,0,1087,1011,1,0,0,0,1087,1014,1,0,0,0,1087,1017,1,0,0,0,1087,1020, + 1,0,0,0,1087,1023,1,0,0,0,1087,1026,1,0,0,0,1087,1035,1,0,0,0,1087,1041, + 1,0,0,0,1087,1044,1,0,0,0,1087,1047,1,0,0,0,1087,1050,1,0,0,0,1087,1053, + 1,0,0,0,1087,1056,1,0,0,0,1087,1065,1,0,0,0,1087,1081,1,0,0,0,1088,1091, + 1,0,0,0,1089,1087,1,0,0,0,1089,1090,1,0,0,0,1090,113,1,0,0,0,1091,1089, + 1,0,0,0,1092,1112,3,128,64,0,1093,1112,3,116,58,0,1094,1112,3,120,60,0, + 1095,1112,3,124,62,0,1096,1112,5,141,0,0,1097,1112,5,140,0,0,1098,1099, + 5,31,0,0,1099,1100,5,135,0,0,1100,1101,3,70,35,0,1101,1102,5,136,0,0,1102, + 1112,1,0,0,0,1103,1104,5,135,0,0,1104,1105,3,70,35,0,1105,1106,5,136,0, + 0,1106,1112,1,0,0,0,1107,1108,5,135,0,0,1108,1109,3,112,56,0,1109,1110, + 5,136,0,0,1110,1112,1,0,0,0,1111,1092,1,0,0,0,1111,1093,1,0,0,0,1111,1094, + 1,0,0,0,1111,1095,1,0,0,0,1111,1096,1,0,0,0,1111,1097,1,0,0,0,1111,1098, + 1,0,0,0,1111,1103,1,0,0,0,1111,1107,1,0,0,0,1112,115,1,0,0,0,1113,1115, + 5,41,0,0,1114,1116,3,112,56,0,1115,1114,1,0,0,0,1115,1116,1,0,0,0,1116, + 1118,1,0,0,0,1117,1119,3,118,59,0,1118,1117,1,0,0,0,1119,1120,1,0,0,0, + 1120,1118,1,0,0,0,1120,1121,1,0,0,0,1121,1124,1,0,0,0,1122,1123,5,43,0, + 0,1123,1125,3,112,56,0,1124,1122,1,0,0,0,1124,1125,1,0,0,0,1125,1126,1, + 0,0,0,1126,1127,5,44,0,0,1127,117,1,0,0,0,1128,1129,5,42,0,0,1129,1130, + 3,112,56,0,1130,1131,5,33,0,0,1131,1132,3,112,56,0,1132,119,1,0,0,0,1133, + 1134,3,122,61,0,1134,1147,5,135,0,0,1135,1148,5,122,0,0,1136,1138,5,35, + 0,0,1137,1136,1,0,0,0,1137,1138,1,0,0,0,1138,1139,1,0,0,0,1139,1144,3, + 112,56,0,1140,1141,5,137,0,0,1141,1143,3,112,56,0,1142,1140,1,0,0,0,1143, + 1146,1,0,0,0,1144,1142,1,0,0,0,1144,1145,1,0,0,0,1145,1148,1,0,0,0,1146, + 1144,1,0,0,0,1147,1135,1,0,0,0,1147,1137,1,0,0,0,1147,1148,1,0,0,0,1148, + 1149,1,0,0,0,1149,1151,5,136,0,0,1150,1152,3,148,74,0,1151,1150,1,0,0, + 0,1151,1152,1,0,0,0,1152,1154,1,0,0,0,1153,1155,3,146,73,0,1154,1153,1, + 0,0,0,1154,1155,1,0,0,0,1155,1164,1,0,0,0,1156,1158,3,150,75,0,1157,1156, + 1,0,0,0,1157,1158,1,0,0,0,1158,1160,1,0,0,0,1159,1161,3,152,76,0,1160, + 1159,1,0,0,0,1160,1161,1,0,0,0,1161,1162,1,0,0,0,1162,1163,5,39,0,0,1163, + 1165,3,134,67,0,1164,1157,1,0,0,0,1164,1165,1,0,0,0,1165,121,1,0,0,0,1166, + 1173,3,126,63,0,1167,1173,5,19,0,0,1168,1173,5,20,0,0,1169,1173,5,103, + 0,0,1170,1173,5,48,0,0,1171,1173,5,40,0,0,1172,1166,1,0,0,0,1172,1167, + 1,0,0,0,1172,1168,1,0,0,0,1172,1169,1,0,0,0,1172,1170,1,0,0,0,1172,1171, + 1,0,0,0,1173,123,1,0,0,0,1174,1175,3,126,63,0,1175,1176,5,138,0,0,1176, + 1178,1,0,0,0,1177,1174,1,0,0,0,1177,1178,1,0,0,0,1178,1179,1,0,0,0,1179, + 1180,3,126,63,0,1180,125,1,0,0,0,1181,1186,5,150,0,0,1182,1186,5,148,0, + 0,1183,1186,5,149,0,0,1184,1186,3,144,72,0,1185,1181,1,0,0,0,1185,1182, + 1,0,0,0,1185,1183,1,0,0,0,1185,1184,1,0,0,0,1186,127,1,0,0,0,1187,1197, + 5,143,0,0,1188,1197,5,144,0,0,1189,1197,5,142,0,0,1190,1197,5,145,0,0, + 1191,1197,5,146,0,0,1192,1197,5,147,0,0,1193,1197,5,105,0,0,1194,1197, + 5,106,0,0,1195,1197,5,107,0,0,1196,1187,1,0,0,0,1196,1188,1,0,0,0,1196, + 1189,1,0,0,0,1196,1190,1,0,0,0,1196,1191,1,0,0,0,1196,1192,1,0,0,0,1196, + 1193,1,0,0,0,1196,1194,1,0,0,0,1196,1195,1,0,0,0,1197,129,1,0,0,0,1198, + 1200,5,59,0,0,1199,1201,7,14,0,0,1200,1199,1,0,0,0,1200,1201,1,0,0,0,1201, + 1211,1,0,0,0,1202,1204,5,60,0,0,1203,1205,7,14,0,0,1204,1203,1,0,0,0,1204, + 1205,1,0,0,0,1205,1211,1,0,0,0,1206,1208,5,61,0,0,1207,1209,7,14,0,0,1208, + 1207,1,0,0,0,1208,1209,1,0,0,0,1209,1211,1,0,0,0,1210,1198,1,0,0,0,1210, + 1202,1,0,0,0,1210,1206,1,0,0,0,1211,131,1,0,0,0,1212,1213,3,112,56,0,1213, + 1214,5,0,0,1,1214,133,1,0,0,0,1215,1226,5,135,0,0,1216,1217,5,40,0,0,1217, + 1218,5,29,0,0,1218,1223,3,112,56,0,1219,1220,5,137,0,0,1220,1222,3,112, + 56,0,1221,1219,1,0,0,0,1222,1225,1,0,0,0,1223,1221,1,0,0,0,1223,1224,1, + 0,0,0,1224,1227,1,0,0,0,1225,1223,1,0,0,0,1226,1216,1,0,0,0,1226,1227, + 1,0,0,0,1227,1229,1,0,0,0,1228,1230,3,108,54,0,1229,1228,1,0,0,0,1229, + 1230,1,0,0,0,1230,1232,1,0,0,0,1231,1233,3,138,69,0,1232,1231,1,0,0,0, + 1232,1233,1,0,0,0,1233,1234,1,0,0,0,1234,1235,5,136,0,0,1235,135,1,0,0, + 0,1236,1237,7,15,0,0,1237,137,1,0,0,0,1238,1245,7,16,0,0,1239,1240,5,52, + 0,0,1240,1241,3,140,70,0,1241,1242,5,6,0,0,1242,1243,3,140,70,0,1243,1246, + 1,0,0,0,1244,1246,3,140,70,0,1245,1239,1,0,0,0,1245,1244,1,0,0,0,1246, + 1249,1,0,0,0,1247,1248,5,114,0,0,1248,1250,3,142,71,0,1249,1247,1,0,0, + 0,1249,1250,1,0,0,0,1250,139,1,0,0,0,1251,1252,5,110,0,0,1252,1259,7,17, + 0,0,1253,1254,5,113,0,0,1254,1259,5,50,0,0,1255,1256,3,112,56,0,1256,1257, + 7,17,0,0,1257,1259,1,0,0,0,1258,1251,1,0,0,0,1258,1253,1,0,0,0,1258,1255, + 1,0,0,0,1259,141,1,0,0,0,1260,1261,5,113,0,0,1261,1267,5,50,0,0,1262,1267, + 5,27,0,0,1263,1267,5,115,0,0,1264,1265,5,85,0,0,1265,1267,5,116,0,0,1266, + 1260,1,0,0,0,1266,1262,1,0,0,0,1266,1263,1,0,0,0,1266,1264,1,0,0,0,1267, + 143,1,0,0,0,1268,1269,7,18,0,0,1269,145,1,0,0,0,1270,1271,5,121,0,0,1271, + 1272,5,135,0,0,1272,1273,5,3,0,0,1273,1274,3,112,56,0,1274,1275,5,136, + 0,0,1275,147,1,0,0,0,1276,1277,5,117,0,0,1277,1278,5,27,0,0,1278,1279, + 5,135,0,0,1279,1280,3,108,54,0,1280,1281,5,136,0,0,1281,149,1,0,0,0,1282, + 1283,5,2,0,0,1283,1284,7,19,0,0,1284,151,1,0,0,0,1285,1286,7,20,0,0,1286, + 1287,5,120,0,0,1287,153,1,0,0,0,162,155,171,174,180,200,209,212,220,229, + 233,245,253,257,265,270,273,281,288,298,305,319,324,333,344,354,357,364, + 373,380,387,392,401,428,435,439,451,457,474,478,485,490,495,505,508,511, + 517,521,524,537,546,551,555,559,563,573,578,583,591,595,597,601,606,614, + 621,625,633,640,646,654,666,671,676,681,688,695,697,706,716,727,732,740, + 744,748,756,763,771,775,778,791,794,798,802,806,809,814,817,820,823,826, + 837,849,853,861,872,880,890,893,901,904,906,914,921,926,929,935,938,944, + 961,964,968,972,976,978,990,995,1003,1028,1037,1058,1067,1076,1084,1087, + 1089,1111,1115,1120,1124,1137,1144,1147,1151,1154,1157,1160,1164,1172, + 1177,1185,1196,1200,1204,1208,1210,1223,1226,1229,1232,1245,1249,1258, + 1266 }; public static readonly ATN _ATN = diff --git a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs index 366aaa7d5..558a71e7d 100644 --- a/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs +++ b/src/LibRed/LibRed.Sql/Grammar/Generated/AccessSqlVisitor.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -// Generated from D:/toolkits/efcorejetlibred/src/LibRed/LibRed.Sql/Grammar/AccessSql.g4 by ANTLR 4.13.1 +// Generated from AccessSql.g4 by ANTLR 4.13.1 // Unreachable code detected #pragma warning disable 0162 diff --git a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs index 4891482c0..498c3ebe4 100644 --- a/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs +++ b/src/LibRed/LibRed.Sql/Parsing/AstBuilder.cs @@ -401,7 +401,8 @@ private static SqlStatement BuildCreateView(CreateViewStatementContext ctx) private static SqlStatement BuildCreateProcedure(CreateProcedureStatementContext ctx) { var parameters = (ctx.procParamList()?.procParam() ?? []) - .Select(p => new ProcedureParameter(ParamName(p), TypeName(p.dataType()))) + .Select(p => new ProcedureParameter( + ParamName(p), TypeName(p.dataType()), Size(p.dataType()), Scale(p.dataType()))) .ToList(); // A procedure body is a SELECT (stored as a parameterized query, like a view) or an action query @@ -411,29 +412,59 @@ private static SqlStatement BuildCreateProcedure(CreateProcedureStatementContext if (body.createTableStatement() is { } ddl) { - RejectParametersOnAction(parameters); + // A data-definition query is stored as its verbatim text, with nothing decomposed — there is no + // row for a parameter to reach, so one declared here could never bind. + if (parameters.Count > 0) + throw new NotSupportedException( + "Parameters on a data-definition procedure are not supported: its SQL is stored verbatim."); return new CreateActionProcedureStatement( name, ProcedureActionKind.DataDefinition, OriginalText(ddl), null, null); } - if (body.insertStatement() is { } insert) - { - RejectParametersOnAction(parameters); - return BuildAppendProcedure(name, insert); - } + if (body.insertStatement() is { } insert) return BuildAppendProcedure(name, insert, parameters); + if (body.updateStatement() is { } update) return BuildUpdateProcedure(name, update, parameters); + if (body.deleteStatement() is { } delete) return BuildDeleteProcedure(name, delete, parameters); + + QueryExpressionContext query = body.queryExpression(); + // A SELECT with an INTO is a make-table query — an action query that stores its target on the action + // row — not a view of the SELECT. + if (MakeTableTarget(query) is { } target) + return new CreateActionProcedureStatement( + name, ProcedureActionKind.MakeTable, null, target, null, + BuildViewDefinition(query), null, parameters); - ViewDefinition definition = BuildViewDefinition(body.queryExpression()); - return new CreateProcedureStatement(name, parameters, definition, OriginalText(body.queryExpression())); + ViewDefinition definition = BuildViewDefinition(query); + return new CreateProcedureStatement(name, parameters, definition, OriginalText(query)); } - private static void RejectParametersOnAction(IReadOnlyList parameters) - { - if (parameters.Count > 0) - throw new NotSupportedException("Parameters on an action-query procedure are not stored yet."); - } + /// The table a SELECT … INTO t body writes into, or null for an ordinary SELECT. + private static string? MakeTableTarget(QueryExpressionContext ctx) => + ctx.setOperator().Length == 0 && ctx.queryTerm(0) is SelectTermContext term + && term.querySpecification().into is { } into + ? Identifier(into) + : null; - private static SqlStatement BuildAppendProcedure(string name, InsertStatementContext insert) + private static SqlStatement BuildAppendProcedure( + string name, InsertStatementContext insert, IReadOnlyList parameters) { var columns = insert._columns; + if (columns.Count == 0) + throw new NotSupportedException("An INSERT procedure body must list its target columns."); + + // The multiple-record form: the values come from a SELECT, which is stored as the query's own source + // — the same table / join / where rows a view stores — with each column row naming what it reads. + if (insert.source is { } source) + { + ViewDefinition body = BuildViewDefinition(source); + if (body.Columns.Count != columns.Count) + throw new SqlParseException( + $"INSERT lists {columns.Count} columns but its SELECT returns {body.Columns.Count}."); + + var sourced = columns + .Select((col, i) => new AppendColumn(Identifier(col), body.Columns[i].Expression)) + .ToList(); + return new CreateActionProcedureStatement( + name, ProcedureActionKind.Append, null, Identifier(insert.table), sourced, body, null, parameters); + } // A stored append query keeps its columns and values as text pairs, which has room for exactly one // row — so a multi-row table value constructor cannot be stored as a procedure even though it is @@ -451,8 +482,6 @@ private static SqlStatement BuildAppendProcedure(string name, InsertStatementCon "An INSERT procedure body cannot use DEFAULT as a value."); var values = rowValues.Select(v => v.expression()).ToArray(); - if (columns.Count == 0) - throw new NotSupportedException("An INSERT procedure body must list its target columns."); if (columns.Count != values.Length) throw new SqlParseException( $"INSERT lists {columns.Count} columns but {values.Length} values."); @@ -461,7 +490,47 @@ private static SqlStatement BuildAppendProcedure(string name, InsertStatementCon .Select((col, i) => new AppendColumn(Identifier(col), OriginalText(values[i]))) .ToList(); return new CreateActionProcedureStatement( - name, ProcedureActionKind.Append, null, Identifier(insert.table), appendColumns); + name, ProcedureActionKind.Append, null, Identifier(insert.table), appendColumns, + null, null, parameters); + } + + /// An UPDATE body: its sources and WHERE are stored exactly as a view's are, and each SET + /// assignment becomes a column row naming its target (qualified, over a join) and holding the new + /// value's verbatim text. + private static SqlStatement BuildUpdateProcedure( + string name, UpdateStatementContext update, IReadOnlyList parameters) + { + var assignments = update.assignment() + .Select(a => new AppendColumn(OriginalText(a.target), OriginalText(a.expression()))) + .ToList(); + return new CreateActionProcedureStatement( + name, ProcedureActionKind.Update, null, null, assignments, + ActionBody(update.tableSource(), update.whereClause()), null, parameters); + } + + /// A DELETE body: its sources and WHERE, plus the table.* target when the statement names + /// one — which Access stores verbatim, and omits entirely for a bare DELETE FROM. + private static SqlStatement BuildDeleteProcedure( + string name, DeleteStatementContext delete, IReadOnlyList parameters) + { + string? target = delete.target is { } t ? $"{Identifier(t)}.*" : null; + return new CreateActionProcedureStatement( + name, ProcedureActionKind.Delete, null, null, null, + ActionBody(delete.tableSource(), delete.whereClause()), target, parameters); + } + + /// The sources, joins and WHERE of an UPDATE or DELETE body, in the shape a view stores them — + /// they carry no output columns of their own. + private static ViewDefinition ActionBody(TableSourceContext[] sources, WhereClauseContext? where) + { + var tables = new List(); + var joins = new List(); + foreach (TableSourceContext ts in sources) CollectSources(ts, tables, joins); + + return new ViewDefinition( + Distinct: false, Columns: [], tables, joins, + where is null ? null : OriginalText(where.expression()), + GroupBy: [], OrderBy: [], Top: null); } /// A declared parameter's name, with any leading @ stripped — Access stores the bare @@ -496,6 +565,19 @@ private static string TypeName(DataTypeContext type) => string.Join(' ', // appear in its WHERE just as it can in a VALUES list. Source = ins.Source is null ? null : LowerParameters(ins.Source, names), }, + // An action query declares parameters exactly as a SELECT does, and Access stores UPDATE and DELETE + // ones — a stored `UPDATE t SET c = pValue WHERE k = pKey` reads back through here. + UpdateStatement upd => upd with + { + From = LowerFrom(upd.From, names)!, + Assignments = upd.Assignments.Select(a => a with { Value = LowerExpr(a.Value, names) }).ToList(), + Where = upd.Where is null ? null : LowerExpr(upd.Where, names), + }, + DeleteStatement del => del with + { + From = LowerFrom(del.From, names)!, + Where = del.Where is null ? null : LowerExpr(del.Where, names), + }, _ => s, }; @@ -582,10 +664,12 @@ private static ViewDefinition BuildViewDefinition(QueryExpressionContext ctx) : select.selectList().selectItem().Select(BuildViewColumn).ToList(); // Flatten the FROM into a flat list of source tables and joins, descending through any parenthesised - // join groups (Access stores them flat — one Attribute=5 per table, one Attribute=7 per join). + // join groups (Access stores them flat — one Attribute=5 per table, one Attribute=7 per join). A + // body with no FROM at all (`SELECT 1 AS n`) is a query Access stores and runs, and it stores it the + // same way minus the table rows — so it decomposes to no sources rather than being rejected. var tables = new List(); var joins = new List(); - foreach (TableSourceContext ts in select.fromClause().tableSource()) + foreach (TableSourceContext ts in select.fromClause()?.tableSource() ?? []) CollectSources(ts, tables, joins); string? where = select.whereClause() is { } w ? OriginalText(w.expression()) : null; @@ -1036,20 +1120,29 @@ private static Expression BuildFunctionCall(FunctionCallContext ctx) /// private static IReadOnlyList? BuildWithinGroup(FunctionCallContext ctx, string name, List args) { - bool orderedSet = FunctionCall.IsOrderedSetAggregate(name); + bool stringAgg = name.Equals("STRING_AGG", StringComparison.OrdinalIgnoreCase); if (ctx.withinGroup() is not { } within) { - return orderedSet + // STRING_AGG's order is optional — without it the values list as the rows arrive — but its + // separator is not, which is the other way round from LISTAGG. + if (stringAgg) + { + ValidateStringAgg(ctx, args); + return null; + } + return FunctionCall.IsOrderedSetAggregate(name) ? throw new SqlParseException($"{name} needs WITHIN GROUP (ORDER BY …).") : null; } - if (!orderedSet) + if (!FunctionCall.AcceptsWithinGroup(name)) throw new SqlParseException($"{name} takes no WITHIN GROUP."); if (ctx.star is not null) throw new SqlParseException($"{name} takes no *."); var keys = within.orderByClause().orderByItem().Select(BuildOrderByItem).ToList(); - if (name.Equals("LISTAGG", StringComparison.OrdinalIgnoreCase)) + if (stringAgg) + ValidateStringAgg(ctx, args); + else if (name.Equals("LISTAGG", StringComparison.OrdinalIgnoreCase)) { if (args.Count is not (1 or 2) || args is [_, not LiteralExpression { Value: string }]) throw new SqlParseException("LISTAGG takes a value and, optionally, a separator written as a string."); @@ -1061,6 +1154,17 @@ private static Expression BuildFunctionCall(FunctionCallContext ctx) return keys.Select(k => k.Direction).ToList(); } + /// SQL Server's STRING_AGG(expression, separator): both arguments, no *, and the + /// separator written as a string, which is what makes it the one value the whole group shares. + private static void ValidateStringAgg(FunctionCallContext ctx, List args) + { + if (ctx.star is not null) + throw new SqlParseException("STRING_AGG takes no *."); + if (args.Count != 2 || args[1] is not LiteralExpression { Value: string }) + throw new SqlParseException( + "STRING_AGG takes a value and a separator written as a string."); + } + private static WindowSpec BuildWindowSpec(WindowSpecificationContext ctx) => new(ctx._partition.Select(BuildExpression).ToList(), ctx.orderByClause() is { } o ? o.orderByItem().Select(BuildOrderByItem).ToList() : [], diff --git a/src/LibRed/LibRed.Sql/SqlKeywords.cs b/src/LibRed/LibRed.Sql/SqlKeywords.cs new file mode 100644 index 000000000..dcbc9ca9f --- /dev/null +++ b/src/LibRed/LibRed.Sql/SqlKeywords.cs @@ -0,0 +1,42 @@ +using Antlr4.Runtime; +using LibRed.Sql.Grammar; + +namespace LibRed.Sql; + +/// +/// The words the SQL front end treats as keywords, taken from the grammar itself rather than a list kept +/// beside it: every lexer token whose name lexes back to that same token is a keyword rule (SELECT, +/// FROM, …), while a token named for a category (IDENT, a literal, whitespace) lexes as an +/// identifier and drops out. Served through ADO.NET's ReservedWords metadata collection. +/// +public static class SqlKeywords +{ + private static readonly Lazy> All = new(Collect); + + /// Every keyword, upper-case and sorted. + public static IReadOnlyList Reserved => All.Value; + + private static IReadOnlyList Collect() + { + var vocabulary = AccessSqlLexer.DefaultVocabulary; + var words = new List(); + + // Token types run 1..n in the lexer's own rule order; the vocabulary names them. + for (int type = 1; type <= AccessSqlLexer.ruleNames.Length; type++) + { + string? name = vocabulary.GetSymbolicName(type); + if (name is null || !name.All(char.IsAsciiLetterUpper)) continue; + + // The name is a keyword only if lexing it produces exactly that token. An identifier-shaped token + // name lexes to IDENT instead, and so is not one. + var lexer = new AccessSqlLexer(CharStreams.fromString(name)) { TokenFactory = CommonTokenFactory.Default }; + lexer.RemoveErrorListeners(); + IToken first = lexer.NextToken(); + if (first.Type == type && lexer.NextToken().Type == TokenConstants.EOF) + words.Add(name); + } + + words.Sort(StringComparer.Ordinal); + return words; + } +} diff --git a/src/LibRed/README.md b/src/LibRed/README.md index e90e2642a..a8452ca4f 100644 --- a/src/LibRed/README.md +++ b/src/LibRed/README.md @@ -203,7 +203,8 @@ Treat the number as of its date — an EF Core version bump moves it. - **Ordered-set aggregates** — `PERCENTILE_CONT(p)` and `PERCENTILE_DISC(p) WITHIN GROUP (ORDER BY x [DESC])`, grouped or over a window. `PERCENTILE_CONT` interpolates between numbers or dates; `PERCENTILE_DISC` returns one of the values, so it also takes text. `LISTAGG([DISTINCT] x [, 'separator']) WITHIN GROUP (ORDER BY …)` - joins the values as text. + joins the values as text, and SQL Server's `STRING_AGG(x, 'separator')` is the same aggregate with its + `WITHIN GROUP` optional and its separator required. - **`FILTER (WHERE …)`** on any aggregate, grouped or windowed: `COUNT(*) FILTER (WHERE Amount > 100)`. - `FULL [OUTER] JOIN` — ACE offers only inner/left/right, and its query designer cannot express a full one. - **`OFFSET … ROWS FETCH NEXT … ROWS ONLY`** paging, where the count may be any expression, not just a @@ -211,6 +212,9 @@ Treat the number as of its date — an EF Core version bump moves it. - **Standard scalar syntax** ACE lacks: `CASE`, `COALESCE`, `NULLIF`, `GREATEST`/`LEAST` (NULL arguments ignored, as SQL Server and PostgreSQL treat them — extended mode translates `Math.Max`/`Math.Min` to them), and the `VALUES` table value constructor standing in for a query. + - **Arguments ACE's own functions do not take**: `LOG(x, base)`, and SQL Server 2022's + `LTRIM(x, characters)` / `RTRIM(x, characters)`, where the second argument is a set of characters to + strip rather than a substring. - **Set operations in a subquery predicate** — `IN (… UNION …)`, `EXISTS (… EXCEPT …)`, and a scalar subquery over a set operation. - **`ORDER BY` bound to the query expression**, so it applies to a whole set operation rather than to its diff --git a/src/LibRed/docs/design/transactions.md b/src/LibRed/docs/design/transactions.md index e6694f011..3603c2dde 100644 --- a/src/LibRed/docs/design/transactions.md +++ b/src/LibRed/docs/design/transactions.md @@ -193,17 +193,45 @@ interface ILockManager : IDisposable — which is our compatibility target, *not* SQL Server's "unqualified ROLLBACK unwinds all levels". A named `SAVE`/`ROLLBACK TRANSACTION ` addresses a specific frame. No new mechanism is needed: nesting is the Phase-1 savepoint stack, driven by the controller. -- **Durability:** commit flushes dirty pages then clears the commit-byte; a crash before +- **Durability:** commit writes dirty pages out (to the OS, as ACE does — §5) then clears + the commit-byte; a crash before the clear leaves the Jet "suspect" signal (later, with `JetLockManager`) → repair path. With the self-consistent manager, recovery is process-local (no cross-process crash interop claimed yet). ## 5. Flush / commit ordering (matching Jet, staying ACE-safe) -On commit, in order: (1) write all dirty data/index/LVAL/usage-map pages; (2) fsync; -(3) write the page-0 header/commit-state update; (4) fsync; (5) clear commit-byte / -release locks. Never leave a header pointing at pages that aren't durable. No structure -is written that Access cannot parse — the commit-byte table and lock offsets are the only +**What ACE does (measured: ACE 16 over OLE DB, Process Monitor).** ACE never forces the OS +cache to disk — no `FlushFileBuffers` on a statement, on an explicit `COMMIT`, or at close — +and it opens the file without write-through. Its durability is the OS file cache plus the +commit-byte protocol, nothing more. What its settings change is only *when* the write +reaches the OS: + +| Setting (registry default) | Effect | +| --- | --- | +| `ImplicitCommitSync` = **no** | A lone statement's writes are issued *after* the statement returns — deferred, and flushed to the OS when the next statement starts or the timeout fires | +| `UserCommitSync` = **yes** | An explicit `BEGIN…COMMIT` writes its pages inside `COMMIT`, before it returns | +| `FlushTransactionTimeout` = **500 ms** | How long a deferred write waits; overrides `SharedAsyncDelay` (50) and `ExclusiveAsyncDelay` (2000) | +| `PageTimeout` = **5000 ms** | How long another user's cached pages may stay stale | + +Two orderings hold in every mode, and they are the ones that matter for concurrency: + +- **Pages reach the OS before the lock is released.** A page's `.laccdb` lock is released + only after that page's `WriteFile`, never before. Cross-connection visibility therefore + rests on the write reaching the OS — which every handle on the machine reads through — + not on it reaching the disk. +- **The commit-slot write brackets the batch**: the connection's own slot at `0xE02` is + written immediately before the first page and again after the last + ([page-00 §2.2](../format/page-00-database.md)). An explicit transaction writes nothing + at all until its commit. +- File growth is a 1-byte write at the last byte of the new page, then the page itself. + +**What LibRed does.** The same, deliberately: a commit publishes every overlay page to the +OS under the publish lock, and neither a commit nor a close forces them to disk. An +autocommit statement writes its pages immediately rather than deferring them — stricter +than ACE's default, and the deferral is what the engine-settings work would add. Never +leave a header pointing at pages the OS has not been given. No structure is written that +Access cannot parse — the commit-byte table and lock offsets are the only concurrency-visible state, exactly as Jet uses them. ## 6. Phased implementation plan diff --git a/src/LibRed/docs/format/data-types.md b/src/LibRed/docs/format/data-types.md index 6caac5b52..6595deed4 100644 --- a/src/LibRed/docs/format/data-types.md +++ b/src/LibRed/docs/format/data-types.md @@ -227,6 +227,11 @@ Points verified against ACE that aren't obvious from that page: `CHARACTER_MAXIMUM_LENGTH`), **not** 1. - **Bare `TEXT` → Memo** (long text); `TEXT(n)` → `varchar(n)` (a Jet quirk, ACE-verified). - Sized Text/Binary dimensions must be positive: Text is `1..255` characters and Binary is `1..510` bytes. +- **A declaration wider than that is refused at DDL time — it is not promoted to Memo and not clamped.** + Verified: `VARCHAR(255)` creates a 255-character column, `VARCHAR(256)` and everything above it + (`VARCHAR(1000)`, `TEXT(1000)`, `CHAR(1000)`, `VARCHAR(65535)`) fail the whole `CREATE TABLE` with + ACE's *"Size of field 'c' is too long."* LibRed refuses the same declarations at the same threshold and + opens its message with ACE's wording, so a caller matching on it behaves the same against either engine. - **`CHAR(n)` / `BINARY(n)` are FIXED-length columns; `TEXT(n)` / `VARBINARY(n)` are variable** — ACE's own DDL produces both forms, so the fixed form is not a LibRed-only construct. - **An over-long value is refused on both forms, with one message**: *"The field is too small to accept the @@ -348,3 +353,37 @@ goes through the broken conversion. It is live in the ordinary `System.Data.OleD COM consumer: a materialised value from any month but January shifts back a month to a valid date, and only a January value throws the `ArgumentOutOfRangeException` above. So: predicates are right, corruption is silent outside January, and Access itself never reads through OLE DB. + +## Footnote — what the drivers know about `BIGINT` and `DATETIME2` + +*Driver behaviour, not file format. Both types postdate the drivers, and each layer was left at a different +moment, so what a caller is told depends entirely on which one it asks.* Measured against ACE 16 on a file the +engine raised to `0x06` when the columns were created: + +| | schema metadata | a value in a query | +| --- | --- | --- | +| **`BIGINT`**, OLE DB | `DATA_TYPE` **20** (`DBTYPE_I8`), but `COLUMN_FLAGS` **106** — variable length, no precision | correct: `Int64`, `DBTYPE_I8` | +| **`BIGINT`**, ODBC | `DATA_TYPE` **-8** (`SQL_WCHAR`), `TYPE_NAME` **CHAR**, size **4** | the 8 stored bytes as 4 UTF-16 characters — `9223372036854775807` reads back as `"￿￿￿翿"` | +| **`DATETIME2`**, OLE DB | `DATA_TYPE` **135**, `COLUMN_FLAGS` **106**, no precision | corrupt (above) | +| **`DATETIME2`**, ODBC | `DATA_TYPE` **-3** (`SQL_VARBINARY`), size **42** | the raw 42 bytes, uncorrupted | +| **both**, DAO | — | correct: `BIGINT` as `Int64` (field type **16**), `DATETIME2` as a full-precision string (field type **26**) | + +Two rules fall out. **ODBC describes what it does not know by its stored width**, through whichever generic +type fits — 8 bytes as 4 wide characters, 42 bytes as binary — and hands back exactly those bytes; it never +converts, so nothing is corrupted but nothing is decoded either. + +**Only materialisation is broken, and only in the driver.** Through ODBC, an `INSERT` or `UPDATE` of either +type stores the right value (read back from the file directly to confirm), `WHERE Big = 4294967297` and +`WHERE Stamp = #…#` both match, and anything the engine computes comes back correctly — `CStr(Big)` returns +`"4294967297"`, `Year(Stamp)` returns `2021`. It is only selecting the column itself, `MAX(Big)` included, that +yields the undecoded bytes. So a caller stuck on ODBC can use either type by never selecting it bare; a tool +that issues `SELECT *` gets nonsense with no error raised. **OLE DB knows both type codes** and reads a +`BIGINT` correctly, yet describes both columns with the generic "variable length, no precision" it gives a type +it has no entry for. Its `DataTypes` list never learned either type: it still reports the same 15 as it did for +Jet 4, so neither `BigInt` nor `DateTime2` appears in the types the provider claims to support. + +**DAO is the exception on both counts**: it has type codes for the two types and returns their values +correctly, and it is the only surface that knows a column is calculated — `Field.Properties("Expression")` +hands back the expression, which OLE DB, ODBC and ADOX all withhold while reporting the expression's result +type as if it were a stored one. The engine and DAO moved together; the interop layers did not, which is +consistent with Access never reading its own files through them. diff --git a/src/LibRed/docs/format/long-values.md b/src/LibRed/docs/format/long-values.md index e17e1cce2..3b486891e 100644 --- a/src/LibRed/docs/format/long-values.md +++ b/src/LibRed/docs/format/long-values.md @@ -129,6 +129,12 @@ LVAL pages are data pages (type `0x01`) whose owner field (`0x04`) is the ASCII > engine opens one per statement, so a failed reclamation rolls back with the statement. A direct > `LibRed.Core` caller that opens none gets the same non-atomic behaviour as any other multi-page write. +> **How aggressively the engine reclaims is a setting, and its default differs by engine.** Jet's +> `RecycleLVs` (the OLE DB property `Jet OLEDB:Recycle Long-Valued Pages`) controls whether freed LVAL pages +> are reclaimed aggressively; it ships as **1 on ACE** (both 14 and 16) and **0 on Jet 4.0**. So the same +> delete can leave different free/owned-map state behind depending on which engine wrote the file — worth +> pinning before treating an ACE-vs-Jet4 reclamation difference as a format one. + ### 3.3.2 Column usage-map list (trailing the index names) After the index names (in the TDEF body, §3.3) comes a list of per-**long-value-column** (memo/OLE) diff --git a/src/LibRed/docs/format/page-00-database.md b/src/LibRed/docs/format/page-00-database.md index 07d66f87f..bae6ee9e2 100644 --- a/src/LibRed/docs/format/page-00-database.md +++ b/src/LibRed/docs/format/page-00-database.md @@ -291,6 +291,13 @@ Four properties follow: seven-statement sequence is 722 → 729, exactly +7. The same five inserts with and without a `CREATE INDEX` differ by one. +**The slot write brackets every batch of page writes (verified).** A connection's writes reach the file as: +a 2-byte write to *its own* slot, then the data/index/usage-map pages, then a second 2-byte write to that same +slot. This holds for a lone statement, for an explicit transaction's commit, and for the final writes at close, +and it is the on-disk half of the mid-write signal above: the file carries the "this user is writing" state for +exactly as long as the pages are in flight. (The two values written are not themselves captured — only the +order and the 2-byte extent at the slot's offset.) + **Reopening does not reset it; compacting does.** The counter carries straight across a close and reopen (`…DA` before, `…DA` after). A DAO `CompactDatabase` writes a whole new file and its slot 1 starts at **256**, the idle value, then counts normally from there (744 → 256 → 260 after five inserts). diff --git a/src/LibRed/docs/format/system-catalog.md b/src/LibRed/docs/format/system-catalog.md index 46c7f430a..2810c9442 100644 --- a/src/LibRed/docs/format/system-catalog.md +++ b/src/LibRed/docs/format/system-catalog.md @@ -18,6 +18,15 @@ > creating the model's tables. Real user tables carry `Flags = 0x00000000`, so excluding the > system/hidden bits never drops a genuine table. + > **The same bits name the object kind in a schema rowset** (verified against ACE's `Tables`, which + > classifies every object by `Flags` rather than by name): the system bit (`0x80000000`) makes it a + > **`SYSTEM TABLE`** — the engine's own catalog, `MSysObjects` / `MSysQueries` / `MSysRelationships` / + > `MSysACEs` / `MSysComplexColumns`; the hidden bit (`0x08`) without it makes an **`ACCESS TABLE`** — + > Access's application tables, the nav-pane group and `MSysResources`; anything carrying `0x00030000` + > is **not listed at all** (the `MSysComplexType_*` tables, `Flags 0x80030000`); everything else is a + > **`TABLE`**, which is why `MSysAccessStorage` (`Flags = 0`) appears among the user tables despite its + > name. Stored queries are listed in the same rowset as **`VIEW`**. + **Writing a table object** (verified against Access-written rows). A complete user-table row sets: `Id` = TDEF page; `ParentId` = `0x0F000001` (the database's "Tables" container, constant); `Type` = `1`; `Name`; `Flags` = `0`; `Owner` = a 2-byte binary SID (`0x69 0x0C` for a @@ -292,6 +301,14 @@ > (`0x08|0x01`) is what Access writes for its auto-generated form/report record-source queries**, the > `~sq_f…` / `~sq_r…` / `~sq_c…` objects, which it renders as `SELECT DISTINCTROW * FROM
`. + > **A query with no `0x05` rows at all has no FROM clause.** `SELECT 1 AS n` is a query Access stores (as + > a view or a procedure) and stores exactly as any other, minus the table rows: the type row, one `0x06` + > column row (`Name1` = the alias, `Expression` = the value) and the end row, with the ordinary view flags + > `0x10000000`. ACE will **open** such a query and return its row, but refuses to use it as a *source* — + > `SELECT n FROM [Q]` fails with "Query input must contain at least one table or query" — on its own files + > as much as on LibRed's. LibRed stores the same rows and is the more permissive of the two: its engine + > resolves one as a derived table like any other view. + > > **A query with no `0x06` rows at all is `SELECT *`.** The absence of output columns is the encoding, not a > sign of an unreadable query — every auto-generated record-source query takes this shape. **Nested / parenthesised joins are stored flat** — one `0x05` per base table and one `0x07` per join condition, no grouping — so Access re-derives @@ -320,6 +337,15 @@ > query with a leading `PARAMETERS name Type, …;` clause (the `0x02` rows) and lowers body references to a > declared name into engine parameters, so LibRed's own engine executes the stored procedure when values are supplied. > + > **A declared name wins over a column of the same name — and a `@` prefix does not distinguish them.** + > Measured against ACE 12 on Northwind: *every* unqualified occurrence of a declared parameter name is the + > parameter, whether written bare or as `@name`, even where the query's own table has a column by that name. + > Only a table-qualified reference is read as the column. Northwind's own `CustOrdersOrders` — declared + > `CustomerID Text(5)`, body `WHERE CustomerID = @CustomerID` — is therefore a tautology in ACE and returns + > all 830 orders for any supplied value; a body written `Orders.CustomerID = [CustomerID]` returns the 6 that + > match. LibRed reproduces all four combinations exactly, so the "obvious" fix of resolving the left-hand + > name to the column would be a divergence, not a repair. + > > **Complex-column system tables (ACE 12+ only).** Access 2007 introduced multi-value and attachment > columns, and with them `MSysComplexColumns` (the registry) plus nine `MSysComplexType_*` flat storage > tables. **Jet 4 has none of them.** As the engine creates them (verified, DAO-created ACE 12 database): @@ -352,21 +378,66 @@ > LibRed-created database. > **Action-query procedure bodies** (a CREATE PROCEDURE body that is not a SELECT) are stored with a - > different MSysObjects `Flags` and an `Attribute=0x01` row (verified vs ACE): - > - **Delete**: the `0x01` action row has `Flag 5`. - > - **Update**: the `0x01` action row has `Flag 4`. + > different MSysObjects `Flags` and an `Attribute=0x01` row (verified vs ACE). **Every kind keeps its + > sources, predicate and declared parameters exactly where a SELECT keeps them** — one `0x05` row per table, + > one `0x07` per join condition, `0x08` for the WHERE, `0x02` per declared parameter — and they differ only + > in the action row and in what the `0x06` column rows mean: > - **Data-definition** (CREATE TABLE / DROP TABLE): MSysObjects `Flags=0x10000060`; one `0x01` row with - > `Flag 7` and `Expression` = the **whole DDL statement** verbatim (ACE prepends a single space). + > `Flag 7` and `Expression` = the **whole DDL statement** verbatim (ACE prepends a single space). No other + > rows: the statement is not decomposed at all. > - **Append** (INSERT): MSysObjects `Flags=0x10000040`; a `0x01` row with `Flag 3` and `Name1` = the > target table, then one `0x06` column row per appended column — `Name2` = target column, `Expression` > = the value; `Flag 0x8000` marks an INSERT … **VALUES** append (an INSERT … **SELECT** instead uses > `Flag 0` on the `0x06` rows plus the usual `0x05` table / `0x08` where rows). + > - **Update**: the `0x01` action row has `Flag 4` and nothing else on it — the target is the FROM source. + > One `0x06` row per SET assignment: **`Name2` = the assigned column**, `Expression` = the new value. + > Over a join, `Name2` is **table-qualified** (`Orders.ShipCountry`) and the join is an ordinary `0x07` + > row, so an UPDATE over a join stores exactly what the same join in a SELECT stores. + > - **Delete**: the `0x01` action row has `Flag 5`. A `DELETE
.* FROM …` keeps that target as a + > single `0x06` row whose `Expression` is the verbatim `
.*` and which has no `Name2`; a + > `DELETE FROM …` (no named target) stores **no** `0x06` row at all, and ACE renders it back as + > `DELETE * FROM …`. + > - **Make-table** (`SELECT … INTO`): `Flag 2`, with the target table in `Name1` and, when the target is in + > another database file, its path in `Name2`. Everything else is stored as the SELECT it is. + > + > **The MSysObjects `Flags` low byte is DAO's own `QueryDef.Type`** — not the `0x01` row's `Flag`, which + > numbers the kinds differently. Measured across six kinds: `0x10000000` plus crosstab `0x10`, delete + > `0x20`, update `0x30`, append `0x40`, make-table `0x50`, data-definition `0x60` — exactly the DAO values + > in the table above (16/32/48/64/80/96). + > + > **A declared parameter's facets live in the `0x02` row's `LvExtra`**, and Access renders the PARAMETERS + > clause from them — a row without them reads back as `Text(255)`. Verified against ACE, declaration by + > declaration: + > + > | declared | `Flag` | `LvExtra` | + > |---|---|---| + > | `Text(50)` | 10 | `50` — the length | + > | `Decimal(18,4)` / `Numeric(18,4)` | 16 | `262162` = `(scale << 16) \| precision` | + > | `Numeric(10,2)` | 16 | `131082` = `(2 << 16) \| 10` | + > | `Binary(10)` | 9 | *nothing* — a sized binary records no facet | + > | `Long`, and every type with no declared size | its code | *nothing* | + > + > On every *other* row `LvExtra` holds nothing: for one statement ACE left it null with no parameter + > declared and wrote `0` and `226` on those same rows once one was, and Northwind's designer-authored query + > carries `936840680` throughout. Don't model it outside a parameter row. + > + > **The `0x02` `Flag` is the Jet on-disk type code**, the same code a column of that type carries — not + > DAO's type constants, which agree with it only up to `15` (GUID) and then diverge. Measured across every + > declarable type: `Bit 1`, `Byte 2`, `Short 3`, `Long 4`, `Currency 5`, `Single 6`, `Double 7`, + > `DateTime 8`, `Binary 9`, `Text 10`, `LongBinary 11`, `Memo 12`, `GUID 15`, `Decimal 16`, **`BigInt 19`**, + > **`DateTime2 20`**, and `0` for Access's untyped `Value` parameter. The last two are worth noting twice + > over: DAO numbers `dbBigInt` 16, which is Decimal's code here, and ACE accepts a `BigInt` or `DateTime2` + > **parameter** on an ACE 12 file, where a *column* of either type is refused — a parameter declares no + > storage, so nothing forces the format's hand. > - > (A plain view/SELECT query uses `Flags=0x10000000` and no `0x01` row.) LibRed writes CREATE TABLE and - > INSERT … VALUES bodies; INSERT … SELECT and UPDATE/DELETE are not written yet. **Read-back:** LibRed - > reconstructs a stored action query from these rows (DDL → the verbatim SQL; INSERT … VALUES → a rebuilt - > `INSERT INTO t (cols) VALUES (…)`) and executes it by name; kinds it can't run (INSERT … SELECT, etc.) - > read back with an "unsupported" reason and throw when executed. + > (A plain view/SELECT query uses `Flags=0x10000000` and no `0x01` row.) LibRed **writes** every kind it + > reads: CREATE TABLE verbatim, INSERT from VALUES or from a SELECT, UPDATE (joins included), DELETE (with + > or without a `table.*` target) and make-table, each with its declared parameters and their facets — row + > for row what ACE writes for the same statement, including the object flags, and ACE runs the result. **Read-back:** LibRed reconstructs and runs + > every kind whose statement its engine can execute — DDL (verbatim), INSERT from VALUES or from a SELECT, + > UPDATE (joins included), DELETE and make-table — rebuilt with a leading `PARAMETERS` clause when the query + > declares parameters, so `EXECUTE name arg, …` binds them. Crosstab, pass-through and UNION read back with + > an "unsupported" reason naming the kind, and throw when executed. - **MSysRelationships** defines foreign keys (one row per relationship column): `szRelationship` (name), `szObject` (child/referencing table), `szColumn` (child column), `szReferencedObject` diff --git a/src/LibRed/docs/functions.md b/src/LibRed/docs/functions.md index 1c5fc64a0..f2e038675 100644 --- a/src/LibRed/docs/functions.md +++ b/src/LibRed/docs/functions.md @@ -86,6 +86,10 @@ procedure call, and a result past a Double an overflow. `StrReverse` `StrComp` `StrConv` `Str` `Val` `Chr` `Asc` `Hex` `Oct` - `Trim`/`LTrim`/`RTrim` strip the space and the ideographic space U+3000 — nothing else. +- **`LTrim(x, characters)` / `RTrim(x, characters)`** (a LibRed extension, SQL Server 2022's): the second + argument is a **set** of characters, not a substring — every leading (or trailing) character that appears + anywhere in it is removed, stopping at the first that does not. An empty set strips nothing; either argument + Null gives Null. ACE takes only the one argument, and `Trim` takes only the one here too. - `Asc` and `Chr` work in the system ANSI code page (`Chr` takes 0–255). - A value that is not text is read as `CStr` writes it. @@ -257,6 +261,10 @@ Server counts a `bit`. A Long. - **`LISTAGG([DISTINCT] x [, 'separator']) WITHIN GROUP (ORDER BY k [DESC], …)`** — the non-Null values as text (each written as `&` writes it) in that order, joined by the separator: a string literal, as the standard has it, and none when left out. Null when there are no values. +- **`STRING_AGG([DISTINCT] x, 'separator') [WITHIN GROUP (ORDER BY k [DESC], …)]`** — SQL Server's spelling of + the same aggregate, and it computes the same thing. The two differ only in what each insists on: `LISTAGG` + needs the `WITHIN GROUP` and lets the separator go, `STRING_AGG` needs the separator and lets the order go. + Without a `WITHIN GROUP` the values list in the order the rows arrive — over a window, in window order. - **`FILTER (WHERE condition)`** — every aggregate, Access's included, takes it after the call (and after `WITHIN GROUP`): only the rows the condition is true for go in, so `COUNT(*) FILTER (WHERE x > 1)` counts those rows, and a group none of whose rows pass has what an empty group has. diff --git a/test/LibRed.Ado.Tests/LibRedCommandTests.cs b/test/LibRed.Ado.Tests/LibRedCommandTests.cs index 1f2cad20e..a15b31ba8 100644 --- a/test/LibRed.Ado.Tests/LibRedCommandTests.cs +++ b/test/LibRed.Ado.Tests/LibRedCommandTests.cs @@ -1,4 +1,5 @@ using System.Data; +using System.Data.Common; using LibRed.Data; using Xunit; @@ -573,9 +574,137 @@ public void Bare_system_variable_select_needs_no_from_clause() [InlineData("SELECT ';'", 1)] // semicolon inside a string literal [InlineData("SELECT `a;b` FROM T; SELECT 2", 2)] // semicolon inside a backtick identifier [InlineData("SELECT [a;b] FROM T", 1)] // semicolon inside a bracket identifier + [InlineData("PARAMETERS [p] TEXT; SELECT * FROM T WHERE C = [p]", 1)] // the clause belongs to its query + [InlineData("PARAMETERS [p] TEXT; SELECT 1; SELECT 2", 2)] public void SplitStatements_splits_only_on_top_level_semicolons(string sql, int expected) => Assert.Equal(expected, LibRedCommand.SplitStatements(sql).Count()); + [Fact] + public void SchemaOnly_returns_the_columns_and_no_rows() + { + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT OrderID, CustomerID FROM Orders ORDER BY OrderID"; + + using var reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly); + Assert.Equal(2, reader.FieldCount); + Assert.False(reader.HasRows); + Assert.False(reader.Read()); + Assert.Equal(-1, reader.RecordsAffected); + + // The schema is the one the query would have returned, stored column and all. + var columns = reader.GetColumnSchema(); + Assert.Equal(["OrderID", "CustomerID"], columns.Select(c => c.ColumnName)); + Assert.Equal("Orders", columns[0].BaseTableName); + Assert.True(columns[0].IsKey); + } + + [Fact] + public void SchemaOnly_describes_a_stored_procedure_without_running_it() + { + // No parameter value is supplied: a shape never depended on one, so describing must not ask for it. + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandType = CommandType.StoredProcedure; + cmd.CommandText = "CustOrdersOrders"; + + using var reader = cmd.ExecuteReader(CommandBehavior.SchemaOnly); + Assert.Equal( + ["OrderID", "OrderDate", "RequiredDate", "ShippedDate"], + reader.GetColumnSchema().Select(c => c.ColumnName)); + Assert.False(reader.Read()); + } + + [Fact] + public void SingleRow_stops_after_the_first_row() + { + using var conn = OpenConnection(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT OrderID FROM Orders"; + + using var reader = cmd.ExecuteReader(CommandBehavior.SingleRow); + Assert.True(reader.HasRows); + Assert.True(reader.Read()); + Assert.False(reader.Read()); // the rest of the 830 are never read + } + + [Fact] + public void CloseConnection_closes_the_connection_with_the_reader() + { + var conn = OpenConnection(); + try + { + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT OrderID FROM Orders"; + using var reader = cmd.ExecuteReader(CommandBehavior.CloseConnection); + Assert.True(reader.Read()); + Assert.Equal(ConnectionState.Open, conn.State); + } + + Assert.Equal(ConnectionState.Closed, conn.State); + } + finally { conn.Dispose(); } + } + + [Fact] + public void A_reader_without_CloseConnection_leaves_the_connection_open() + { + using var conn = OpenConnection(); + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = "SELECT OrderID FROM Orders"; + using var reader = cmd.ExecuteReader(); + Assert.True(reader.Read()); + } + + Assert.Equal(ConnectionState.Open, conn.State); + } + + [Fact] + public void SchemaOnly_does_not_run_a_statement_that_writes() + { + // Measured against ACE: an INSERT executed under SchemaOnly leaves its table untouched. + string path = Path.Combine(Path.GetTempPath(), $"libred-schemaonly-{Guid.NewGuid():N}.accdb"); + File.Copy(Northwind, path); + try + { + using var conn = new LibRedConnection($"Data Source={path}"); + conn.Open(); + + int before = Shippers(conn); + using (var write = conn.CreateCommand()) + { + write.CommandText = + "INSERT INTO Shippers (CompanyName) VALUES ('SchemaOnly'); SELECT @@IDENTITY AS `Id`"; + using var reader = write.ExecuteReader(CommandBehavior.SchemaOnly); + // The batch describes as its last statement, which is the SELECT. + Assert.Equal("Id", Assert.Single(reader.GetColumnSchema()).ColumnName); + Assert.False(reader.Read()); + } + + Assert.Equal(before, Shippers(conn)); + } + finally { try { File.Delete(path); } catch (IOException) { } } + + static int Shippers(LibRedConnection conn) + { + using var count = conn.CreateCommand(); + count.CommandText = "SELECT COUNT(*) FROM Shippers"; + return Convert.ToInt32(count.ExecuteScalar()); + } + } + + [Fact] + public void SplitStatements_keeps_a_PARAMETERS_clause_with_its_query() + { + // Access ends the clause with a semicolon, so splitting there hands the parser a fragment that is + // not a statement — which is how every stored parameterized query reads back. + Assert.Equal( + "PARAMETERS [p] TEXT; SELECT * FROM T WHERE C = [p]", + Assert.Single(LibRedCommand.SplitStatements("PARAMETERS [p] TEXT; SELECT * FROM T WHERE C = [p]"))); + } + [Fact] public void Rolled_back_transaction_undoes_its_writes() { diff --git a/test/LibRed.Ado.Tests/LibRedDataReaderMetadataTests.cs b/test/LibRed.Ado.Tests/LibRedDataReaderMetadataTests.cs index a5d2aa756..77eab1faa 100644 --- a/test/LibRed.Ado.Tests/LibRedDataReaderMetadataTests.cs +++ b/test/LibRed.Ado.Tests/LibRedDataReaderMetadataTests.cs @@ -1,3 +1,5 @@ +using System.Data; +using System.Data.Common; using LibRed.Data; using Xunit; @@ -20,8 +22,45 @@ public void Empty_result_preserves_declared_column_types() Assert.False(reader.HasRows); Assert.Equal(typeof(int), reader.GetFieldType(0)); Assert.Equal(typeof(string), reader.GetFieldType(1)); + // The provider's name for the type, not the CLR type's: one type, one name across GetDataTypeName, + // the column schema and the DataTypes collection. ProductName is Text(40), a variable-length column. + Assert.Equal("Long", reader.GetDataTypeName(0)); + Assert.Equal("VarChar", reader.GetDataTypeName(1)); + } + + [Fact] + public void GetDataTypeName_is_the_name_the_column_schema_gives() + { + using var connection = new LibRedConnection($"Data Source={Northwind}"); + connection.Open(); + using var command = connection.CreateCommand(); + // A fixed-width text column, a variable one, a number, a date, a currency and a computed column — + // the fixed and variable text are the pair that used to read alike. + command.CommandText = + "SELECT Orders.CustomerID, Customers.CompanyName, Orders.OrderID, Orders.OrderDate, " + + "Orders.Freight, Orders.Freight * 2 AS Doubled FROM Orders INNER JOIN Customers " + + "ON Orders.CustomerID = Customers.CustomerID"; + + using DbDataReader reader = command.ExecuteReader(); + Assert.Equal( + ["Char", "VarChar", "Long", "DateTime", "Currency", "Currency"], + Enumerable.Range(0, reader.FieldCount).Select(reader.GetDataTypeName)); + Assert.Equal( + reader.GetColumnSchema().Select(c => c.DataTypeName), + Enumerable.Range(0, reader.FieldCount).Select(reader.GetDataTypeName)); + } + + [Fact] + public void A_result_with_nothing_described_behind_it_falls_back_to_the_clr_name() + { + // @@IDENTITY is session state rather than a column, so there is no stored type to name. + using var connection = new LibRedConnection($"Data Source={Northwind}"); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = "SELECT @@ROWCOUNT AS `Rows`"; + + using var reader = command.ExecuteReader(); Assert.Equal(nameof(Int32), reader.GetDataTypeName(0)); - Assert.Equal(nameof(String), reader.GetDataTypeName(1)); } [Fact] @@ -47,7 +86,7 @@ public void First_null_result_preserves_the_declared_column_type() using var reader = command.ExecuteReader(); Assert.True(reader.HasRows); Assert.Equal(typeof(string), reader.GetFieldType(0)); - Assert.Equal(nameof(String), reader.GetDataTypeName(0)); + Assert.Equal("VarChar", reader.GetDataTypeName(0)); Assert.True(reader.Read()); Assert.True(reader.IsDBNull(0)); Assert.True(reader.Read()); @@ -77,6 +116,95 @@ public void Empty_computed_projection_reports_known_expression_types() Assert.Equal(typeof(bool), reader.GetFieldType(2)); } + /// A query whose four columns are each a different kind: a stored key column, an aliased stored + /// column, one reached through a join, and one the query computes. + private const string MixedProvenance = + "SELECT o.OrderID, o.CustomerID AS Cust, c.CompanyName, o.Freight * 2 AS Doubled " + + "FROM Orders AS o INNER JOIN Customers AS c ON o.CustomerID = c.CustomerID"; + + [Fact] + public void GetColumnSchema_traces_each_column_back_to_its_stored_column() + { + using var connection = new LibRedConnection($"Data Source={Northwind}"); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = MixedProvenance; + + using DbDataReader reader = command.ExecuteReader(); + Assert.True(reader.CanGetColumnSchema()); // the reader implements IDbColumnSchemaGenerator + var columns = reader.GetColumnSchema(); + + DbColumn key = columns[0]; + Assert.Equal("Orders", key.BaseTableName); + Assert.Equal("OrderID", key.BaseColumnName); + Assert.Equal(typeof(int), key.DataType); + Assert.Equal("Long", key.DataTypeName); + Assert.True(key.IsKey); + Assert.True(key.IsAutoIncrement); + Assert.True(key.IsIdentity); + Assert.False(key.AllowDBNull); + Assert.False(key.IsAliased); + + DbColumn aliased = columns[1]; + Assert.Equal("Cust", aliased.ColumnName); + Assert.Equal("CustomerID", aliased.BaseColumnName); // the alias does not change the stored name + Assert.True(aliased.IsAliased); + Assert.Equal(5, aliased.ColumnSize); + Assert.False(aliased.IsExpression); + + DbColumn joined = columns[2]; + Assert.Equal("Customers", joined.BaseTableName); // the other side of the join + Assert.Equal(40, joined.ColumnSize); + + DbColumn computed = columns[3]; + Assert.Equal("Doubled", computed.ColumnName); + Assert.True(computed.IsExpression); + Assert.True(computed.IsReadOnly); + Assert.Null(computed.BaseTableName); // nothing stored stands behind it + Assert.Null(computed.BaseColumnName); + + // Not applicable to a file rather than unknown. + Assert.All(columns, c => Assert.Null(c.BaseServerName)); + Assert.All(columns, c => Assert.Null(c.BaseSchemaName)); + Assert.All(columns, c => Assert.False(c.IsHidden)); + } + + [Fact] + public void GetSchemaTable_reports_the_same_facts_in_the_DataTable_form() + { + using var connection = new LibRedConnection($"Data Source={Northwind}"); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = MixedProvenance; + + using DbDataReader reader = command.ExecuteReader(); + DataTable schema = reader.GetSchemaTable()!; + + Assert.Equal(4, schema.Rows.Count); + Assert.Contains(SchemaTableColumn.ProviderType, schema.Columns.Cast().Select(c => c.ColumnName)); + + DataRow key = schema.Rows[0]; + Assert.Equal("OrderID", key[SchemaTableColumn.ColumnName]); + Assert.Equal(0, key[SchemaTableColumn.ColumnOrdinal]); + Assert.Equal(3, key[SchemaTableColumn.ProviderType]); // OLE DB DBTYPE_I4, as the Columns collection reports it + Assert.Equal(typeof(int), key[SchemaTableColumn.DataType]); + Assert.Equal("Long", key["DataTypeName"]); + Assert.True((bool)key[SchemaTableColumn.IsKey]); + Assert.True((bool)key[SchemaTableOptionalColumn.IsAutoIncrement]); + Assert.False((bool)key[SchemaTableColumn.AllowDBNull]); + + // A Jet file has no server, catalog or schema, no row versions, and hides no column. + Assert.Equal(DBNull.Value, key[SchemaTableOptionalColumn.BaseServerName]); + Assert.Equal(DBNull.Value, key[SchemaTableOptionalColumn.BaseCatalogName]); + Assert.Equal(DBNull.Value, key[SchemaTableColumn.BaseSchemaName]); + Assert.False((bool)key[SchemaTableOptionalColumn.IsRowVersion]); + Assert.False((bool)key[SchemaTableOptionalColumn.IsHidden]); + + DataRow computed = schema.Rows[3]; + Assert.True((bool)computed[SchemaTableColumn.IsExpression]); + Assert.Equal(DBNull.Value, computed[SchemaTableColumn.BaseTableName]); + } + [Fact] public void Null_aggregate_result_preserves_its_argument_type() { diff --git a/test/LibRed.Ado.Tests/LibRedSchemaTests.cs b/test/LibRed.Ado.Tests/LibRedSchemaTests.cs new file mode 100644 index 000000000..1928bc0d5 --- /dev/null +++ b/test/LibRed.Ado.Tests/LibRedSchemaTests.cs @@ -0,0 +1,270 @@ +using System.Data; +using LibRed.Data; +using Xunit; + +namespace LibRed.Ado.Tests; + +/// +/// The metadata collections GetSchema serves. The expected values are Northwind's, and the shapes are +/// ACE's: where ACE serves the same collection, these rows match it column for column (verified against the +/// ACE 12 OLE DB provider over this same file). +/// +public class LibRedSchemaTests +{ + private static readonly string Northwind = Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"); + + private static LibRedConnection OpenConnection() + { + var connection = new LibRedConnection($"Data Source={Northwind}"); + connection.Open(); + return connection; + } + + private static DataTable Schema(string collection, params string?[] restrictions) + { + using LibRedConnection connection = OpenConnection(); + return restrictions.Length == 0 + ? connection.GetSchema(collection) + : connection.GetSchema(collection, restrictions); + } + + [Fact] + public void MetaDataCollections_lists_the_framework_collections_first() + { + DataTable collections = Schema("MetaDataCollections"); + + var names = collections.Rows.Cast().Select(r => (string)r["CollectionName"]).ToList(); + Assert.Equal( + ["MetaDataCollections", "DataSourceInformation", "DataTypes", "Restrictions", "ReservedWords"], + names.Take(5)); + Assert.Equal(20, names.Count); + Assert.Contains("ViewColumns", names); + } + + [Theory] + [InlineData("MetaDataCollections", 20)] + [InlineData("DataSourceInformation", 1)] + [InlineData("DataTypes", 19)] + [InlineData("Restrictions", 67)] + [InlineData("ReservedWords", 122)] + [InlineData("Tables", 41)] + [InlineData("Columns", 228)] // every table's columns and every view's output columns + [InlineData("Indexes", 69)] + [InlineData("Views", 17)] + [InlineData("Procedures", 6)] // the stored queries that declare parameters + [InlineData("ForeignKeys", 15)] + [InlineData("PrimaryKeys", 21)] + [InlineData("TableConstraints", 36)] + [InlineData("KeyColumnUsage", 41)] + [InlineData("ConstraintColumnUsage", 41)] + [InlineData("ReferentialConstraints", 15)] + [InlineData("CheckConstraints", 0)] // Jet has no CHECK constraints + [InlineData("Statistics", 24)] + [InlineData("ProcedureParameters", 9)] + [InlineData("ViewColumns", 90)] + public void Collection_has_the_rows_Northwind_holds(string collection, int expected) => + Assert.Equal(expected, Schema(collection).Rows.Count); + + [Fact] + public void Tables_separates_the_four_kinds_of_object() + { + // TABLE_TYPE comes from MSysObjects.Flags: Access's own nav-pane tables are ACCESS TABLE, the MSys* + // catalog is SYSTEM TABLE, and a stored SELECT is a VIEW rather than a table of its own. + var byType = Schema("Tables").Rows.Cast() + .ToLookup(r => (string)r["TABLE_TYPE"], r => (string)r["TABLE_NAME"]); + + Assert.Equal(14, byType["TABLE"].Count()); + Assert.Equal(17, byType["VIEW"].Count()); + Assert.Equal(5, byType["SYSTEM TABLE"].Count()); + Assert.Equal(5, byType["ACCESS TABLE"].Count()); + + Assert.Contains("Orders", byType["TABLE"]); + Assert.Contains("Invoices", byType["VIEW"]); + Assert.Contains("MSysObjects", byType["SYSTEM TABLE"]); + } + + [Fact] + public void Columns_describes_an_AutoNumber_primary_key() + { + DataRow column = Assert.Single(Schema("Columns", null, null, "Orders", "OrderID").Rows.Cast()); + + Assert.Equal(1L, column["ORDINAL_POSITION"]); + Assert.Equal(3, column["DATA_TYPE"]); // OLE DB DBTYPE_I4 + Assert.Equal("Long", column["TYPE_NAME"]); + Assert.Equal(10, column["NUMERIC_PRECISION"]); + Assert.False((bool)column["IS_NULLABLE"]); + Assert.False((bool)column["IS_COMPUTED"]); + Assert.True((bool)column["IS_AUTOINCREMENT"]); + Assert.Equal(1, Convert.ToInt32(column["INCREMENT"])); + // MAYDEFER | WRITEUNKNOWN | ISFIXEDLENGTH | MAYBENULL — the flags ACE reports for a stored column. + Assert.Equal(0x02L | 0x08L | 0x10L | 0x40L, column["COLUMN_FLAGS"]); + } + + [Fact] + public void Columns_reports_a_views_output_columns() + { + // A view's columns are the shape its query produces, which only planning the query can say. + var invoices = Schema("Columns", null, null, "Invoices", null).Rows.Cast() + .Select(r => (string)r["COLUMN_NAME"]).ToList(); + + Assert.Equal(26, invoices.Count); + Assert.Contains("ExtendedPrice", invoices); // computed by the view + Assert.Contains("Salesperson", invoices); // an expression over two stored columns + } + + [Fact] + public void ViewColumns_names_the_stored_column_behind_each_one() + { + // This is OLE DB's view-column *usage*: which stored column a view's column reads, so an alias reports + // the column's own name and a computed column has no row at all. + var usage = Schema("ViewColumns", null, null, "Invoices", null).Rows.Cast() + .Select(r => ((string)r["TABLE_NAME"], (string)r["COLUMN_NAME"])).ToList(); + + Assert.Equal(24, usage.Count); + Assert.Contains(("Orders", "OrderID"), usage); + // CustomerName and ShipperName are both aliases of a CompanyName column, from different tables. + Assert.Contains(("Customers", "CompanyName"), usage); + Assert.Contains(("Shippers", "CompanyName"), usage); + Assert.DoesNotContain(usage, u => u.Item2 is "ExtendedPrice" or "Salesperson"); + } + + [Fact] + public void Indexes_describes_a_primary_key_index() + { + DataRow index = Assert.Single(Schema("Indexes", null, null, null, null, "Shippers").Rows.Cast()); + + Assert.Equal("PK_Shippers", index["INDEX_NAME"]); + Assert.True((bool)index["PRIMARY_KEY"]); + Assert.True((bool)index["UNIQUE"]); + Assert.False((bool)index["CLUSTERED"]); + Assert.Equal("ShipperID", index["COLUMN_NAME"]); + Assert.Equal(1L, index["ORDINAL_POSITION"]); + Assert.Equal(3L, Convert.ToInt64(index["CARDINALITY"])); + } + + [Fact] + public void PrimaryKeys_and_ForeignKeys_report_a_relationship_from_both_ends() + { + DataRow key = Assert.Single(Schema("PrimaryKeys", null, null, "Orders").Rows.Cast()); + Assert.Equal("OrderID", key["COLUMN_NAME"]); + Assert.Equal("PK_Orders", key["PK_NAME"]); + + DataRow relation = Assert.Single( + Schema("ForeignKeys", null, null, "Customers", null, null, "Orders").Rows.Cast()); + Assert.Equal("CustomerID", relation["PK_COLUMN_NAME"]); + Assert.Equal("CustomerID", relation["FK_COLUMN_NAME"]); + Assert.Equal("FK_Orders_Customers", relation["FK_NAME"]); + // Jet enforces the rules itself rather than declaring them, so an unenforced rule reads as NO ACTION. + Assert.Equal("NO ACTION", relation["UPDATE_RULE"]); + Assert.Equal("NO ACTION", relation["DELETE_RULE"]); + } + + [Fact] + public void TableConstraints_names_the_primary_key_constraint() + { + DataRow constraint = Assert.Single( + Schema("TableConstraints", null, null, null, null, null, "Shippers", null).Rows.Cast()); + + Assert.Equal("PK_Shippers", constraint["CONSTRAINT_NAME"]); + Assert.Equal("PRIMARY KEY", constraint["CONSTRAINT_TYPE"]); + Assert.False((bool)constraint["IS_DEFERRABLE"]); + } + + [Fact] + public void Statistics_reports_a_tables_row_count() + { + DataRow statistic = Assert.Single(Schema("Statistics", null, null, "Orders").Rows.Cast()); + Assert.Equal(830m, statistic["CARDINALITY"]); + } + + [Fact] + public void ProcedureParameters_describes_a_stored_querys_declared_parameter() + { + DataRow parameter = Assert.Single( + Schema("ProcedureParameters", null, null, "CustOrdersOrders", null).Rows.Cast()); + + Assert.Equal("CustomerID", parameter["PARAMETER_NAME"]); + Assert.Equal(1, parameter["ORDINAL_POSITION"]); + Assert.Equal(1, parameter["PARAMETER_TYPE"]); // input + Assert.Equal(130, parameter["DATA_TYPE"]); // DBTYPE_WSTR + Assert.Equal("VarChar", parameter["TYPE_NAME"]); + // The declared length, read back from the parameter row's LvExtra; octets are two per character. + Assert.Equal(5L, parameter["CHARACTER_MAXIMUM_LENGTH"]); + Assert.Equal(10L, parameter["CHARACTER_OCTET_LENGTH"]); + } + + [Fact] + public void Procedures_carry_the_parameters_clause_ahead_of_their_statement() + { + DataRow procedure = Assert.Single( + Schema("Procedures", null, null, "CustOrdersOrders", null).Rows.Cast()); + + string definition = (string)procedure["PROCEDURE_DEFINITION"]; + // Including the length it was declared with, which the parameter row keeps in its LvExtra. + Assert.StartsWith("PARAMETERS [CustomerID] TEXT(5);", definition); + Assert.Contains("FROM [Orders]", definition); + } + + [Fact] + public void DataTypes_names_every_type_the_file_can_hold() + { + var types = Schema("DataTypes").Rows.Cast() + .ToDictionary(r => (string)r["TypeName"], r => (int)r["ProviderDbType"]); + + Assert.Equal(19, types.Count); + Assert.Equal(3, types["Long"]); + Assert.Equal(6, types["Currency"]); + // Fixed-length text and binary are types of their own, told apart from the variable-length forms. + Assert.Equal(130, types["Char"]); + Assert.Equal(128, types["Binary"]); + // ACE 12 and up only: the engine has these two even though its own OLE DB and ODBC drivers cannot + // materialise them. + Assert.Equal(20, types["BigInt"]); + Assert.Equal(135, types["DateTime2"]); + } + + [Fact] + public void Restrictions_are_declared_for_the_collections_that_take_them() + { + var restrictions = Schema("Restrictions").Rows.Cast() + .ToLookup(r => (string)r["CollectionName"], r => (string)r["RestrictionName"]); + + Assert.Equal(["TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "COLUMN_NAME"], restrictions["Columns"]); + // Indexes takes the table name LAST, after the index name and type, as OLE DB orders it. + Assert.Equal("TABLE_NAME", restrictions["Indexes"].Last()); + } + + [Fact] + public void A_restriction_filters_the_rows() + { + var columns = Schema("Columns", null, null, "Shippers", null).Rows.Cast().ToList(); + + Assert.Equal(3, columns.Count); + Assert.All(columns, c => Assert.Equal("Shippers", c["TABLE_NAME"])); + } + + [Fact] + public void A_catalog_or_schema_restriction_matches_everything() + { + // A Jet file holds one nameless catalog and no schemas, so filtering on the null every row carries + // would return nothing at all. + Assert.Equal(3, Schema("Columns", "anything", "anything", "Shippers", null).Rows.Count); + } + + [Fact] + public void An_unknown_collection_or_too_many_restrictions_is_rejected() + { + using LibRedConnection connection = OpenConnection(); + + Assert.Throws(() => connection.GetSchema("NoSuchCollection")); + // Tables takes four restrictions. + Assert.Throws(() => connection.GetSchema("Tables", [null, null, null, null, null])); + } + + [Fact] + public void Schema_needs_an_open_connection() + { + using var connection = new LibRedConnection($"Data Source={Northwind}"); + Assert.Throws(() => connection.GetSchema("Tables")); + } +} diff --git a/test/LibRed.Core.AccessTests/ActionQueryProcedureAccessTests.cs b/test/LibRed.Core.AccessTests/ActionQueryProcedureAccessTests.cs index 91aabb2bb..8dc91827e 100644 --- a/test/LibRed.Core.AccessTests/ActionQueryProcedureAccessTests.cs +++ b/test/LibRed.Core.AccessTests/ActionQueryProcedureAccessTests.cs @@ -76,35 +76,54 @@ public void Access_runs_a_libred_written_make_table_and_append_procedure() finally { TemporaryDatabase.Delete(path); } } - // An INSERT ... SELECT stored query (written by ACE) is read back but classified unsupported: LibRed - // reconstructs no executable SQL for it, only a reason ("throw on the rest"). - [Fact] - public void Insert_select_stored_query_is_read_back_as_unsupported() + /// + /// An ACE-written query of each action kind, read back as the statement LibRed runs. Every kind keeps its + /// sources, predicate and declared parameters where a SELECT keeps them — one 0x05 row per table, + /// 0x07 per join, 0x08 for the WHERE — and differs only in what the 0x06 column rows + /// mean: a SET assignment names its target in Name2 (qualified, over a join), an append names the + /// target column there and holds the value in Expression, and a DELETE's single row holds the + /// verbatim table.* that Access writes when the query names one. + /// + [Theory] + // UPDATE (kind 4): one column row per assignment. + [InlineData(4, "UPDATE Customers SET ContactTitle = 'Changed' WHERE Country = 'UK'", + "UPDATE [Customers] SET [ContactTitle] = 'Changed' WHERE Country = 'UK'")] + [InlineData(4, "UPDATE Products SET UnitPrice = UnitPrice * 1.1, Discontinued = True WHERE CategoryID = 1", + "UPDATE [Products] SET [UnitPrice] = UnitPrice * 1.1, [Discontinued] = True WHERE CategoryID = 1")] + [InlineData(4, "UPDATE Orders INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID " + + "SET Orders.ShipCountry = Customers.Country WHERE Customers.Country = 'UK'", + "UPDATE [Orders] INNER JOIN [Customers] ON Orders.CustomerID = Customers.CustomerID " + + "SET [Orders].[ShipCountry] = Customers.Country WHERE Customers.Country = 'UK'")] + // DELETE (kind 5): Access writes `DELETE * FROM` when the query names no columns, `DELETE t.* FROM` when + // it does, and stores that `t.*` verbatim. + [InlineData(5, "DELETE FROM Shippers WHERE CompanyName = 'Does not exist'", + "DELETE * FROM [Shippers] WHERE CompanyName = 'Does not exist'")] + [InlineData(5, "DELETE Shippers.* FROM Shippers WHERE Phone IS NULL", + "DELETE Shippers.* FROM [Shippers] WHERE Phone IS NULL")] + // Append (kind 3), from a SELECT rather than from VALUES. + [InlineData(3, "INSERT INTO Shippers (CompanyName) SELECT ContactName FROM Customers WHERE Country = 'UK'", + "INSERT INTO [Shippers] ([CompanyName]) SELECT ContactName FROM [Customers] WHERE Country = 'UK'")] + // Make-table (kind 2): the target is on the action row rather than in the SQL. + [InlineData(2, "SELECT ShipperID, CompanyName INTO [ShipperCopy] FROM Shippers WHERE ShipperID > 1", + "SELECT ShipperID, CompanyName INTO [ShipperCopy] FROM [Shippers] WHERE ShipperID > 1")] + public void Ace_written_action_query_is_read_back_as_runnable_sql(short kind, string body, string expected) { - string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "action-sel-"); + string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "action-kinds-"); try { - using (var conn = OpenOleDb(path)) - { - using var cmd = conn.CreateCommand(); - cmd.CommandText = - "CREATE PROCEDURE CopyUkShippers AS " + - "INSERT INTO Shippers (CompanyName) SELECT ContactName FROM Customers WHERE Country = 'UK'"; - cmd.ExecuteNonQuery(); - } + using (var conn = OpenOleDb(path)) CreateProcedure(conn, "P", body); using var db = JetDatabase.Open(path); - StoredActionQuery q = db.Catalog.ActionQueries["CopyUkShippers"]; - Assert.Null(q.Sql); - Assert.Contains("INSERT", q.UnsupportedReason!, StringComparison.OrdinalIgnoreCase); - Assert.Contains("SELECT", q.UnsupportedReason!, StringComparison.OrdinalIgnoreCase); - Assert.Equal((short)3, ActionFlag(db, "CopyUkShippers")); + StoredActionQuery query = db.Catalog.ActionQueries["P"]; + Assert.Equal(kind, ActionFlag(db, "P")); + Assert.Null(query.UnsupportedReason); + Assert.Equal(expected, query.Sql); } finally { TemporaryDatabase.Delete(path); } } [Fact] - public void Ace_parameterized_update_retains_parameter_order_while_remaining_explicitly_unsupported() + public void Ace_parameterized_update_is_read_back_with_its_parameters_clause() { string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "action-params-"); try @@ -119,38 +138,38 @@ public void Ace_parameterized_update_retains_parameter_order_while_remaining_exp } using var db = JetDatabase.Open(path); - Assert.Equal(["pTitle", "pCountry"], db.Catalog.QueryParameters["UpdateByCountry"]); + Assert.Equal(["pTitle", "pCountry"], db.Catalog.QueryParameters["UpdateByCountry"].Select(p => p.Name)); + StoredActionQuery query = db.Catalog.ActionQueries["UpdateByCountry"]; - Assert.Null(query.Sql); - Assert.Contains("UPDATE", query.UnsupportedReason!, StringComparison.OrdinalIgnoreCase); Assert.Equal((short)4, ActionFlag(db, "UpdateByCountry")); + // An action query declares its parameters exactly as a SELECT does, and is rebuilt with the same + // leading clause — which is what makes the body's references to them parameters and not columns. + // The declared lengths come back too: they are stored in the parameter row's LvExtra. + Assert.Equal( + "PARAMETERS [pTitle] TEXT(50), [pCountry] TEXT(20); " + + "UPDATE [Customers] SET [ContactTitle] = pTitle WHERE Country = pCountry", + query.Sql); + Assert.Equal([50, 20], db.Catalog.QueryParameters["UpdateByCountry"].Select(p => p.Size)); } finally { TemporaryDatabase.Delete(path); } } [Fact] - public void Ace_update_and_delete_procedures_are_retained_with_their_exact_action_kinds() + public void A_kind_libred_cannot_run_still_reports_which_kind_it_is() { - string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "action-kinds-"); + string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "action-crosstab-"); try { using (var conn = OpenOleDb(path)) - { - CreateProcedure(conn, "UpdateUkTitles", - "UPDATE Customers SET ContactTitle = 'Changed' WHERE Country = 'UK'"); - CreateProcedure(conn, "DeleteNoShipper", - "DELETE FROM Shippers WHERE CompanyName = 'Does not exist'"); - } + CreateProcedure(conn, "ByCountry", + "TRANSFORM Count(*) SELECT Country FROM Customers GROUP BY Country PIVOT City"); using var db = JetDatabase.Open(path); - StoredActionQuery update = db.Catalog.ActionQueries["UpdateUkTitles"]; - StoredActionQuery delete = db.Catalog.ActionQueries["DeleteNoShipper"]; - Assert.Equal((short)4, ActionFlag(db, "UpdateUkTitles")); - Assert.Equal((short)5, ActionFlag(db, "DeleteNoShipper")); - Assert.Null(update.Sql); - Assert.Contains("UPDATE", update.UnsupportedReason!, StringComparison.OrdinalIgnoreCase); - Assert.Null(delete.Sql); - Assert.Contains("DELETE", delete.UnsupportedReason!, StringComparison.OrdinalIgnoreCase); + StoredActionQuery query = db.Catalog.ActionQueries["ByCountry"]; + Assert.Null(query.Sql); + // "Not supported" that doesn't say what it is leaves a caller no way to tell an unimplemented + // feature from an unreadable file. + Assert.Contains("Crosstab", query.UnsupportedReason!, StringComparison.OrdinalIgnoreCase); } finally { TemporaryDatabase.Delete(path); } } diff --git a/test/LibRed.Core.AccessTests/StoredQueryKindAccessTests.cs b/test/LibRed.Core.AccessTests/StoredQueryKindAccessTests.cs index c31580b30..81484ca90 100644 --- a/test/LibRed.Core.AccessTests/StoredQueryKindAccessTests.cs +++ b/test/LibRed.Core.AccessTests/StoredQueryKindAccessTests.cs @@ -104,11 +104,10 @@ public void Top_percent_keeps_its_percent() } [Theory] - [InlineData("UpdQ", "UPDATE Shippers SET Shippers.Phone = '555-0100'", 4, "UPDATE")] - [InlineData("DelQ", "DELETE FROM Shippers WHERE Shippers.CompanyName = 'nope'", 5, "DELETE")] - [InlineData("UniQ", "SELECT CompanyName FROM Shippers UNION SELECT CompanyName FROM Customers", 9, "UNION")] - [InlineData("MakeQ", "SELECT Shippers.* INTO ShipCopy FROM Shippers", 2, "Make-table")] - public void Action_query_kinds_are_named_in_the_refusal(string name, string sql, int flag, string expected) + [InlineData("UpdQ", "UPDATE Shippers SET Shippers.Phone = '555-0100'", 4, "SET")] + [InlineData("DelQ", "DELETE FROM Shippers WHERE Shippers.CompanyName = 'nope'", 5, "FROM [Shippers]")] + [InlineData("MakeQ", "SELECT Shippers.* INTO ShipCopy FROM Shippers", 2, "INTO [ShipCopy]")] + public void Action_query_kinds_authored_through_dao_are_rebuilt(string name, string sql, int flag, string expected) { string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "qkind-action-"); try @@ -118,11 +117,31 @@ public void Action_query_kinds_are_named_in_the_refusal(string name, string sql, using var db = JetDatabase.Open(path); Assert.Equal((short)flag, OperationFlag(db, name)); + // Authored the way the Access UI authors them, rather than through ACE's CREATE PROCEDURE — the + // rows have to be read the same either way. + StoredActionQuery q = db.Catalog.ActionQueries[name]; + Assert.Null(q.UnsupportedReason); + Assert.Contains(expected, q.Sql!, StringComparison.OrdinalIgnoreCase); + } + finally { TemporaryDatabase.Delete(path); } + } + + [Fact] + public void A_kind_libred_cannot_run_is_named_in_the_refusal() + { + string path = TemporaryDatabase.CopyPath(TestDatabases.NorthwindAccdb, "qkind-union-"); + try + { + if (!Author(path, ("UniQ", "SELECT CompanyName FROM Shippers UNION SELECT CompanyName FROM Customers"))) return; + + using var db = JetDatabase.Open(path); + Assert.Equal((short)9, OperationFlag(db, "UniQ")); + // Not executed — but the reason has to say WHICH kind. "Not supported" on its own gives a caller // no way to tell an unimplemented feature from a file LibRed failed to read. - StoredActionQuery q = db.Catalog.ActionQueries[name]; + StoredActionQuery q = db.Catalog.ActionQueries["UniQ"]; Assert.Null(q.Sql); - Assert.Contains(expected, q.UnsupportedReason!, StringComparison.OrdinalIgnoreCase); + Assert.Contains("UNION", q.UnsupportedReason!, StringComparison.OrdinalIgnoreCase); } finally { TemporaryDatabase.Delete(path); } } diff --git a/test/LibRed.Engine.AccessTests/FunctionArityAccessTests.cs b/test/LibRed.Engine.AccessTests/FunctionArityAccessTests.cs index d8818fcf5..75ffb9659 100644 --- a/test/LibRed.Engine.AccessTests/FunctionArityAccessTests.cs +++ b/test/LibRed.Engine.AccessTests/FunctionArityAccessTests.cs @@ -18,11 +18,14 @@ private sealed record Arity(string Name, int Min, int? Max, params string[] Argu [ .. Unary("CBool", "CByte", "CInt", "CLng", "CSng", "CDbl", "CCur", "CStr", "CDate", "CVar", "Abs", "Sgn", "Int", "Fix", "Sqr", "Exp", "Sin", "Cos", "Tan", "Atn", - "Len", "LCase", "UCase", "Trim", "LTrim", "RTrim", "Space", "StrReverse", "Str", "Val", + "Len", "LCase", "UCase", "Trim", "Space", "StrReverse", "Str", "Val", "Chr", "Asc", "Hex", "Oct"), new("Round", 1, 2, "1", "0", "0"), new("Rnd", 0, 1, "1", "1"), new("Timer", 0, 0, "1"), // ACE's Log takes one argument; LibRed's also takes the standard's base as a second (docs/functions.md). new("Log", 1, 2, "1", "1", "1"), + // Likewise LTrim/RTrim: one argument in ACE, and a second — SQL Server 2022's set of characters to + // strip — in LibRed. + new("LTrim", 1, 2, "'abc'", "'a'", "0"), new("RTrim", 1, 2, "'abc'", "'c'", "0"), new("Left", 2, 2, "'abc'", "1", "0"), new("Right", 2, 2, "'abc'", "1", "0"), new("Mid", 2, 3, "'abc'", "1", "1", "0"), new("InStr", 2, 4, "1", "'abc'", "'b'", "0", "0"), diff --git a/test/LibRed.Engine.AccessTests/StoredActionQueryExecutionAccessTests.cs b/test/LibRed.Engine.AccessTests/StoredActionQueryExecutionAccessTests.cs new file mode 100644 index 000000000..9f9af099b --- /dev/null +++ b/test/LibRed.Engine.AccessTests/StoredActionQueryExecutionAccessTests.cs @@ -0,0 +1,165 @@ +using System.Data; +using System.Data.OleDb; +using LibRed; +using LibRed.Engine; +using Xunit; + +namespace LibRed.Engine.Tests; + +// Running a stored action query BY NAME, cross-checked against ACE running the same one. That LibRed rebuilds +// the statement is one thing; what decides whether the reconstruction is right is whether executing the query +// leaves the database in the state Access leaves it in — a SET assignment read from the wrong row column, or +// a join dropped from the FROM, changes which rows are touched rather than failing outright. +[Collection(AceCollection.Name)] +public class StoredActionQueryExecutionAccessTests : TempDatabaseTest +{ + private static string Copy() => TemporaryDatabase.CopyPath( + Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "action-exec-"); + + /// Has ACE store as a procedure, then runs it in ACE on one copy and in + /// LibRed on another, comparing what reports afterwards. + private static void AssertSameAsAce(string body, string verify) + { + string acePath = Copy(), libRedPath = Copy(); + try + { + object? aceResult; + foreach (string path in new[] { acePath, libRedPath }) + using (var connection = AceTestDatabase.Open(path)) + { + using var create = connection.CreateCommand(); + create.CommandText = $"CREATE PROCEDURE [P] AS {body}"; + create.ExecuteNonQuery(); + } + + using (var connection = AceTestDatabase.Open(acePath)) + { + using (var run = connection.CreateCommand()) + { + run.CommandText = "P"; + run.CommandType = CommandType.StoredProcedure; + run.ExecuteNonQuery(); + } + using var check = connection.CreateCommand(); + check.CommandText = verify; + aceResult = check.ExecuteScalar(); + } + + using var db = TemporaryDatabase.OpenTracked(libRedPath, readOnly: false); + var engine = new QueryEngine(db); + engine.ExecuteNonQuery("EXECUTE [P]"); + object? ourResult = engine.ExecuteQuery(verify).Rows.Single()[0]; + + Assert.Equal(Convert.ToString(aceResult), Convert.ToString(ourResult)); + } + finally + { + TemporaryDatabase.Delete(acePath); + TemporaryDatabase.Delete(libRedPath); + } + } + + [Fact] + public void Update_query_touches_the_rows_ace_touches() => + AssertSameAsAce( + "UPDATE Customers SET ContactTitle = 'Changed' WHERE Country = 'UK'", + "SELECT COUNT(*) FROM Customers WHERE ContactTitle = 'Changed'"); + + [Fact] + public void Update_query_over_a_join_reads_the_other_table() + { + // The join lives in rows of its own, so losing it would silently update every order instead of the + // seven UK ones. + AssertSameAsAce( + "UPDATE Orders INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID " + + "SET Orders.ShipCountry = 'Checked' WHERE Customers.Country = 'UK'", + "SELECT COUNT(*) FROM Orders WHERE ShipCountry = 'Checked'"); + } + + // Deleted from the child table: a customer or an order has dependent rows, and both engines refuse to + // orphan them — which would be a test of referential integrity rather than of the stored query. + [Fact] + public void Delete_query_removes_the_rows_ace_removes() => + AssertSameAsAce( + "DELETE FROM [Order Details] WHERE OrderID = 10248", + "SELECT COUNT(*) FROM [Order Details]"); + + [Fact] + public void Delete_query_written_with_a_table_star_target_removes_the_same_rows() => + AssertSameAsAce( + "DELETE [Order Details].* FROM [Order Details] WHERE OrderID = 10249", + "SELECT COUNT(*) FROM [Order Details]"); + + [Fact] + public void Append_query_from_a_select_inserts_the_rows_ace_inserts() => + AssertSameAsAce( + "INSERT INTO Shippers (CompanyName) SELECT ContactName FROM Customers WHERE Country = 'UK'", + "SELECT COUNT(*) FROM Shippers"); + + [Fact] + public void Make_table_query_writes_the_table_ace_writes() => + AssertSameAsAce( + "SELECT ShipperID, CompanyName INTO [ShipperCopy] FROM Shippers WHERE ShipperID > 1", + "SELECT COUNT(*) FROM ShipperCopy"); + + [Fact] + public void A_parameterized_action_query_runs_with_the_values_supplied_to_it() + { + string path = Copy(); + try + { + using (var connection = AceTestDatabase.Open(path)) + { + using var create = connection.CreateCommand(); + create.CommandText = + "CREATE PROCEDURE [UpdateByCountry] (pTitle Text(50), pCountry Text(20)) AS " + + "UPDATE Customers SET ContactTitle = pTitle WHERE Country = pCountry"; + create.ExecuteNonQuery(); + } + + using var db = TemporaryDatabase.OpenTracked(path, readOnly: false); + var engine = new QueryEngine(db); + + // Positional arguments bind to the parameters in declaration order, as EXECUTE does everywhere. + // The title is one no Northwind customer already holds, so every row counted below was set here. + Assert.Equal(7, engine.ExecuteNonQuery("EXECUTE [UpdateByCountry] 'Set by parameter', 'UK'")); + Assert.Equal( + 7, + Convert.ToInt32(engine.ExecuteQuery( + "SELECT COUNT(*) FROM Customers WHERE ContactTitle = 'Set by parameter' AND Country = 'UK'") + .Rows.Single()[0])); + + // Nothing else was touched: the second parameter is the predicate, not a constant folded into it. + Assert.Equal( + 0, + Convert.ToInt32(engine.ExecuteQuery( + "SELECT COUNT(*) FROM Customers WHERE ContactTitle = 'Set by parameter' AND Country <> 'UK'") + .Rows.Single()[0])); + } + finally { TemporaryDatabase.Delete(path); } + } + + [Fact] + public void A_kind_libred_cannot_run_is_refused_by_name() + { + string path = Copy(); + try + { + using (var connection = AceTestDatabase.Open(path)) + { + using var create = connection.CreateCommand(); + create.CommandText = + "CREATE PROCEDURE [ByCountry] AS " + + "TRANSFORM Count(*) SELECT Country FROM Customers GROUP BY Country PIVOT City"; + create.ExecuteNonQuery(); + } + + using var db = TemporaryDatabase.OpenTracked(path, readOnly: false); + var engine = new QueryEngine(db); + + var error = Assert.Throws(() => engine.ExecuteNonQuery("EXECUTE [ByCountry]")); + Assert.Contains("Crosstab", error.Message, StringComparison.OrdinalIgnoreCase); + } + finally { TemporaryDatabase.Delete(path); } + } +} diff --git a/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs b/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs new file mode 100644 index 000000000..ce27cca51 --- /dev/null +++ b/test/LibRed.Engine.AccessTests/StoredActionQueryWriteAccessTests.cs @@ -0,0 +1,252 @@ +using System.Data; +using System.Data.OleDb; +using LibRed; +using LibRed.Catalog; +using LibRed.Engine; +using Xunit; + +namespace LibRed.Engine.Tests; + +// Writing a stored action query of each kind. The test is not that LibRed can write rows — it is that the +// rows are the ones ACE writes for the same statement, and that ACE then reads and runs the query. Storing a +// query the engine will not run is worse than refusing to store it: the file opens, the query is listed, and +// it fails only when someone tries to use it. +[Collection(AceCollection.Name)] +public class StoredActionQueryWriteAccessTests : TempDatabaseTest +{ + private static string Copy() => TemporaryDatabase.CopyPath( + Path.Combine(AppContext.BaseDirectory, "Data", "Northwind.accdb"), "action-write-"); + + [Theory] + [InlineData("UPDATE Customers SET ContactTitle = 'Owner' WHERE Country = 'UK'")] + [InlineData("UPDATE Products SET UnitPrice = UnitPrice * 1.1, Discontinued = True WHERE CategoryID = 1")] + [InlineData("UPDATE Orders INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID " + + "SET Orders.ShipCountry = Customers.Country WHERE Customers.Country = 'UK'")] + [InlineData("DELETE FROM Shippers WHERE ShipperID > 900")] + [InlineData("DELETE Shippers.* FROM Shippers WHERE ShipperID > 900")] + [InlineData("SELECT ShipperID, CompanyName INTO ShipperCopy FROM Shippers WHERE ShipperID > 1")] + [InlineData("INSERT INTO Shippers (CompanyName, Phone) SELECT CompanyName, Phone FROM Customers WHERE Country = 'UK'")] + public void A_libred_written_action_query_stores_the_rows_ace_stores(string body) + { + string ourPath = Copy(), acePath = Copy(); + try + { + using (var db = TemporaryDatabase.OpenTracked(ourPath, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery($"CREATE PROCEDURE [P] AS {body}"); + + using (var connection = AceTestDatabase.Open(acePath)) + { + using var create = connection.CreateCommand(); + create.CommandText = $"CREATE PROCEDURE [P] AS {body}"; + create.ExecuteNonQuery(); + } + + Assert.Equal(QueryRows(acePath, "P"), QueryRows(ourPath, "P")); + Assert.Equal(ObjectFlags(acePath, "P"), ObjectFlags(ourPath, "P")); + + // And the row set is one ACE acts on, not merely one it tolerates: it runs the query. + using (var connection = AceTestDatabase.Open(ourPath)) + { + using var run = connection.CreateCommand(); + run.CommandText = "P"; + run.CommandType = CommandType.StoredProcedure; + run.ExecuteNonQuery(); + } + } + finally + { + TemporaryDatabase.Delete(ourPath); + TemporaryDatabase.Delete(acePath); + } + } + + [Fact] + public void A_libred_written_action_query_declares_its_parameters_as_ace_does() + { + const string Body = + "CREATE PROCEDURE [ByCountry] (pTitle Text(50), pCountry Text(20)) AS " + + "UPDATE Customers SET ContactTitle = pTitle WHERE Country = pCountry"; + + string ourPath = Copy(), acePath = Copy(); + try + { + using (var db = TemporaryDatabase.OpenTracked(ourPath, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery(Body); + + using (var connection = AceTestDatabase.Open(acePath)) + { + using var create = connection.CreateCommand(); + create.CommandText = Body; + create.ExecuteNonQuery(); + } + + // The parameter rows carry the type code AND the declared length (in LvExtra, which is where + // Access reads it back from) — a bare Text would be a memo, and a missing length reads as 255. + Assert.Equal(QueryRows(acePath, "ByCountry"), QueryRows(ourPath, "ByCountry")); + + using var ace = AceTestDatabase.Open(ourPath); + using var run = ace.CreateCommand(); + run.CommandText = "ByCountry"; + run.CommandType = CommandType.StoredProcedure; + run.Parameters.Add(new OleDbParameter { OleDbType = OleDbType.VarWChar, Size = 50, Value = "Set by ACE" }); + run.Parameters.Add(new OleDbParameter { OleDbType = OleDbType.VarWChar, Size = 20, Value = "UK" }); + Assert.Equal(7, run.ExecuteNonQuery()); // Northwind has seven UK customers + } + finally + { + TemporaryDatabase.Delete(ourPath); + TemporaryDatabase.Delete(acePath); + } + } + + /// A declared parameter's facets ride in its row's LvExtra: a length for text, precision + /// and scale packed into one value for a decimal, and nothing for the types that record none — a sized + /// binary included, which is why this cannot just write whatever size was declared. + [Theory] + [InlineData("TEXT(50)")] + [InlineData("DECIMAL(18,4)")] + [InlineData("NUMERIC(10,2)")] + [InlineData("BINARY(10)")] + [InlineData("LONG")] + public void A_declared_parameters_facets_are_stored_as_ace_stores_them(string declared) + { + const string Body = "AS SELECT CompanyName FROM Shippers WHERE CompanyName = p"; + string ourPath = Copy(), acePath = Copy(); + try + { + using (var db = TemporaryDatabase.OpenTracked(ourPath, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery($"CREATE PROCEDURE [P] (p {declared}) {Body}"); + + using (var connection = AceTestDatabase.Open(acePath)) + { + using var create = connection.CreateCommand(); + create.CommandText = $"CREATE PROCEDURE [P] (p {declared}) {Body}"; + create.ExecuteNonQuery(); + } + + Assert.Equal(QueryRows(acePath, "P"), QueryRows(ourPath, "P")); + } + finally + { + TemporaryDatabase.Delete(ourPath); + TemporaryDatabase.Delete(acePath); + } + } + + /// A stored query whose body has no FROM at all. ACE stores and runs one, and stores it as any + /// other query minus the table rows — so LibRed's has to be the same rows, and ACE has to run it. + [Theory] + [InlineData("CREATE VIEW [Q] AS SELECT 1 AS n")] + [InlineData("CREATE PROCEDURE [Q] AS SELECT 1 AS n")] + public void A_from_less_body_is_stored_as_ace_stores_it(string sql) + { + string ourPath = Copy(), acePath = Copy(); + try + { + using (var db = TemporaryDatabase.OpenTracked(ourPath, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery(sql); + + using (var connection = AceTestDatabase.Open(acePath)) + { + using var create = connection.CreateCommand(); + create.CommandText = sql; + create.ExecuteNonQuery(); + } + + Assert.Equal(QueryRows(acePath, "Q"), QueryRows(ourPath, "Q")); + Assert.Equal(ObjectFlags(acePath, "Q"), ObjectFlags(ourPath, "Q")); + + // ACE opens the query LibRed wrote and returns its row. It will not use such a query as a + // SOURCE — `SELECT n FROM [Q]` fails with "Query input must contain at least one table or + // query" — but it fails that way on its own file too, so the two files behave identically; + // LibRed's engine is the more permissive of the two, not the odd one out. + using var ace = AceTestDatabase.Open(ourPath); + using var run = ace.CreateCommand(); + run.CommandText = "Q"; + run.CommandType = CommandType.StoredProcedure; + using var reader = run.ExecuteReader(); + Assert.True(reader.Read()); + Assert.Equal(1, Convert.ToInt32(reader.GetValue(0))); + } + finally + { + TemporaryDatabase.Delete(ourPath); + TemporaryDatabase.Delete(acePath); + } + } + + [Fact] + public void A_written_action_query_reads_back_as_the_statement_it_was_written_from() + { + string path = Copy(); + try + { + using var db = TemporaryDatabase.OpenTracked(path, readOnly: false); + var engine = new QueryEngine(db); + engine.ExecuteNonQuery( + "CREATE PROCEDURE [P] AS UPDATE Customers SET ContactTitle = 'Owner' WHERE Country = 'UK'"); + + // Round trip: what was written is read back as runnable SQL, and running it by name works. + StoredActionQuery stored = db.Catalog.ActionQueries["P"]; + Assert.Null(stored.UnsupportedReason); + Assert.Equal("UPDATE [Customers] SET [ContactTitle] = 'Owner' WHERE Country = 'UK'", stored.Sql); + Assert.Equal(7, engine.ExecuteNonQuery("EXECUTE [P]")); + } + finally { TemporaryDatabase.Delete(path); } + } + + /// + /// Every MSysQueries field of a stored query's rows, ordered so two files compare — a field + /// neither side sets cannot hide behind a narrower comparison. Two are left out: ObjectId, which + /// is a per-file identity, and LvExtra on any row that is not a declared parameter, where it holds + /// nothing. It carries the parameter's declared length on an 0x02 row and is uninitialised + /// elsewhere — measured: for the same statement ACE left it null with no parameters declared, and wrote 0 + /// and 226 on the very same rows once one was, while Northwind's designer-authored query has 936840680 + /// throughout. + /// + private static List QueryRows(string path, string queryName) + { + using var db = JetDatabase.Open(path); + TableDef queries = db.Catalog.FindTable("MSysQueries")!; + int objectIdIndex = Index(queries, "ObjectId"); + int attributeIndex = Index(queries, "Attribute"); + int id = QueryObjectId(db, queryName); + + var rows = db.OpenTable("MSysQueries").Rows() + .Where(row => Equals(row[objectIdIndex], id)) + .Select(row => string.Join(" | ", queries.Columns + .Where(c => c.Name != "ObjectId" + && (c.Name != "LvExtra" || Equals(row[attributeIndex], (byte)0x02))) + .Select(c => $"{c.Name}={Text(row[c.Index])}"))) + .ToList(); + rows.Sort(StringComparer.Ordinal); + return rows; + + static string Text(object? value) => value switch + { + null => "-", + byte[] bytes => Convert.ToHexString(bytes), + _ => value.ToString()!, + }; + } + + private static int ObjectFlags(string path, string queryName) + { + using var db = JetDatabase.Open(path); + TableDef objects = db.Catalog.FindTable("MSysObjects")!; + int flagsIndex = Index(objects, "Flags"); + int idIndex = Index(objects, "Id"); + int id = QueryObjectId(db, queryName); + return (int)db.OpenTable("MSysObjects").Rows().Single(row => Equals(row[idIndex], id))[flagsIndex]!; + } + + private static int QueryObjectId(JetDatabase db, string queryName) + { + TableDef objects = db.Catalog.FindTable("MSysObjects")!; + int idIndex = Index(objects, "Id"), nameIndex = Index(objects, "Name"); + return (int)db.OpenTable("MSysObjects").Rows() + .Single(row => string.Equals(row[nameIndex] as string, queryName, StringComparison.OrdinalIgnoreCase))[idIndex]!; + } + + private static int Index(TableDef table, string column) => table.FindColumn(column)!.Index; +} diff --git a/test/LibRed.Engine.Tests/CreateProcedureTests.cs b/test/LibRed.Engine.Tests/CreateProcedureTests.cs index 58ecc6705..cf85949c5 100644 --- a/test/LibRed.Engine.Tests/CreateProcedureTests.cs +++ b/test/LibRed.Engine.Tests/CreateProcedureTests.cs @@ -198,23 +198,83 @@ public void Action_queries_read_back_and_execute_through_libred() finally { TemporaryDatabase.Delete(path); } } - // Bodies we don't support: UPDATE/DELETE/DROP have no grammar; an INSERT without a column list can't be - // stored as an append query. Each is rejected. - [Theory] - [InlineData("CREATE PROCEDURE `DelCust` AS DELETE FROM Customers")] - [InlineData("CREATE PROCEDURE `UpdCust` AS UPDATE Customers SET City = 'X'")] - [InlineData("CREATE PROCEDURE `AddNoCols` AS INSERT INTO Shippers VALUES ('ZZ Co')")] - public void Unsupported_procedure_body_is_rejected(string sql) + // An INSERT with no column list can't be stored as an append query: the rows pair each value with the + // column it goes in, so there is nowhere to put a positional value list. + [Fact] + public void Unsupported_procedure_body_is_rejected() { string path = Fresh(); try { using var db = JetDatabase.Open(path, readOnly: false); - if (sql.Contains("AddNoCols", StringComparison.Ordinal)) - Assert.Throws(() => new QueryEngine(db).ExecuteNonQuery(sql)); - else - Assert.Throws(() => - new QueryEngine(db).ExecuteNonQuery(sql)); + Assert.Throws(() => new QueryEngine(db).ExecuteNonQuery( + "CREATE PROCEDURE `AddNoCols` AS INSERT INTO Shippers VALUES ('ZZ Co')")); + } + finally { TemporaryDatabase.Delete(path); } + } + + // Each remaining action kind, written and then read back from the file as the statement it was written + // from, and run by name. (That the stored rows are the ones ACE writes, and that ACE runs them, is + // measured in StoredActionQueryWriteAccessTests — this is the half that needs no engine but LibRed's.) + [Theory] + [InlineData("UPDATE Customers SET City = 'X' WHERE Country = 'UK'", + "UPDATE [Customers] SET [City] = 'X' WHERE Country = 'UK'", 7)] + [InlineData("UPDATE Orders INNER JOIN Customers ON Orders.CustomerID = Customers.CustomerID " + + "SET Orders.ShipCity = 'X' WHERE Customers.Country = 'UK'", + "UPDATE [Orders] INNER JOIN [Customers] ON Orders.CustomerID = Customers.CustomerID " + + "SET [Orders].[ShipCity] = 'X' WHERE Customers.Country = 'UK'", 56)] + [InlineData("DELETE FROM Shippers WHERE ShipperID > 900", + "DELETE * FROM [Shippers] WHERE ShipperID > 900", 0)] + [InlineData("DELETE Shippers.* FROM Shippers WHERE ShipperID > 900", + "DELETE Shippers.* FROM [Shippers] WHERE ShipperID > 900", 0)] + [InlineData("SELECT ShipperID, CompanyName INTO ShipperCopy FROM Shippers WHERE ShipperID > 1", + "SELECT ShipperID, CompanyName INTO [ShipperCopy] FROM [Shippers] WHERE ShipperID > 1", 2)] + [InlineData("INSERT INTO Shippers (CompanyName) SELECT ContactName FROM Customers WHERE Country = 'UK'", + "INSERT INTO [Shippers] ([CompanyName]) SELECT ContactName FROM [Customers] WHERE Country = 'UK'", 7)] + public void Action_query_body_round_trips_through_the_file(string body, string expected, int affected) + { + string path = Fresh(); + try + { + using (var db = JetDatabase.Open(path, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery($"CREATE PROCEDURE [P] AS {body}"); + + using (var db = JetDatabase.Open(path, readOnly: false)) // fresh open: read from the file + { + Assert.Equal(expected, db.Catalog.ActionQueries["P"].Sql); + Assert.Equal(affected, new QueryEngine(db).ExecuteNonQuery("EXECUTE [P]")); + } + } + finally { TemporaryDatabase.Delete(path); } + } + + [Fact] + public void A_written_action_querys_parameters_bind_when_it_is_executed() + { + string path = Fresh(); + try + { + using (var db = JetDatabase.Open(path, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery( + "CREATE PROCEDURE [ByCountry] (pCity Text(50), pCountry Text(20)) AS " + + "UPDATE Customers SET City = pCity WHERE Country = pCountry"); + + using (var db = JetDatabase.Open(path, readOnly: false)) + { + var engine = new QueryEngine(db); + // Read back with the PARAMETERS clause that makes the body's references parameters, declared + // lengths and all — they ride in the parameter row's LvExtra. + Assert.Equal( + "PARAMETERS [pCity] TEXT(50), [pCountry] TEXT(20); " + + "UPDATE [Customers] SET [City] = pCity WHERE Country = pCountry", + db.Catalog.ActionQueries["ByCountry"].Sql); + + Assert.Equal(7, engine.ExecuteNonQuery("EXECUTE [ByCountry] 'Ankh-Morpork', 'UK'")); + Assert.Equal( + 7, + engine.ExecuteQuery("SELECT COUNT(*) FROM Customers WHERE City = 'Ankh-Morpork'") + .Rows.Single()[0]); + } } finally { TemporaryDatabase.Delete(path); } } diff --git a/test/LibRed.Engine.Tests/CreateViewTests.cs b/test/LibRed.Engine.Tests/CreateViewTests.cs index 44201808a..5726adb58 100644 --- a/test/LibRed.Engine.Tests/CreateViewTests.cs +++ b/test/LibRed.Engine.Tests/CreateViewTests.cs @@ -26,6 +26,29 @@ public void Create_view_executes() finally { TemporaryDatabase.Delete(path); } } + // A body with no FROM at all. Access stores and runs one — the rows are an ordinary query's minus the + // table rows — so this must store, read back and run rather than fail. It used to throw a bare + // NullReferenceException out of the decomposer. + [Theory] + [InlineData("CREATE VIEW `Const` AS SELECT 1 AS `n`")] + [InlineData("CREATE PROCEDURE `Const` AS SELECT 1 AS `n`")] + public void A_from_less_body_is_stored_and_queryable(string sql) + { + string path = Fresh(); + try + { + using (var db = JetDatabase.Open(path, readOnly: false)) + new QueryEngine(db).ExecuteNonQuery(sql); + + using (var db = JetDatabase.Open(path)) // fresh open: read from the file + { + Assert.Equal("SELECT 1 AS [n]", db.Catalog.Views["Const"]); + Assert.Equal(1, new QueryEngine(db).ExecuteQuery("SELECT `n` FROM `Const`").Rows.Single()[0]); + } + } + finally { TemporaryDatabase.Delete(path); } + } + // A view is read back from the file (its MSysQueries rows), reconstructed to SQL, and resolved as a // derived table when queried through LibRed's own engine. [Fact] diff --git a/test/LibRed.Engine.Tests/ListAggTests.cs b/test/LibRed.Engine.Tests/ListAggTests.cs index 81d241950..f13f3369b 100644 --- a/test/LibRed.Engine.Tests/ListAggTests.cs +++ b/test/LibRed.Engine.Tests/ListAggTests.cs @@ -61,4 +61,40 @@ public void Over_a_window_each_frame_is_listed(string expression, string expecte [InlineData("LISTAGG(*) WITHIN GROUP (ORDER BY Id)")] public void The_syntax_is_the_standards(string aggregate) => Assert.Throws(() => Rows($"SELECT {aggregate} FROM S")); + + // SQL Server's STRING_AGG(expression, separator) computes what LISTAGG computes, and the two differ only + // in what each insists on: LISTAGG needs its WITHIN GROUP and lets the separator go, STRING_AGG needs the + // separator and lets the order go. + [Theory] + [InlineData("STRING_AGG(Rep, ', ') WITHIN GROUP (ORDER BY Rep)", "E:eve N:ann, ann, bob S:cat, dan, dan")] + [InlineData("STRING_AGG(DISTINCT Rep, ',') WITHIN GROUP (ORDER BY Rep)", "E:eve N:ann,bob S:cat,dan")] + [InlineData("STRING_AGG(Rep, ',') FILTER (WHERE Amount > 60)", "E:eve N:ann,bob S:cat")] + public void String_agg_lists_each_group(string aggregate, string expected) => + Assert.Equal(expected, Rows($"SELECT Region, {aggregate} FROM S GROUP BY Region ORDER BY Region")); + + [Fact] + public void String_agg_without_within_group_lists_in_the_order_the_rows_arrive() => + Assert.Equal("ann,ann,bob,cat,dan,dan,eve", Rows("SELECT STRING_AGG(Rep, ',') FROM S")); + + [Fact] + public void String_agg_leaves_nulls_out_and_lists_nothing_as_null() + { + Assert.Equal("ann,bob", Rows( + "SELECT STRING_AGG(IIF(Amount > 60 AND Region = 'N', Rep, NULL), ',') FROM S")); + Assert.Equal("", Rows("SELECT STRING_AGG(Rep, ',') FROM S WHERE Id > 100")); + } + + [Fact] + public void String_agg_over_a_window_lists_each_frame() => + Assert.Equal( + "1:ann 2:ann,ann 3:ann,ann,bob 4:cat 5:cat,dan 6:cat,dan,dan 7:eve", + Rows("SELECT Id, STRING_AGG(Rep, ',') OVER (PARTITION BY Region ORDER BY Id) FROM S ORDER BY Id")); + + [Theory] + [InlineData("STRING_AGG(Rep)")] // the separator is not optional + [InlineData("STRING_AGG(Rep, Region)")] // and must be written as a string + [InlineData("STRING_AGG(Rep, ',', 'x') WITHIN GROUP (ORDER BY Id)")] + [InlineData("STRING_AGG(*)")] + public void String_agg_takes_a_value_and_a_written_separator(string aggregate) => + Assert.Throws(() => Rows($"SELECT {aggregate} FROM S")); } diff --git a/test/LibRed.Engine.Tests/TrimFunctionsTests.cs b/test/LibRed.Engine.Tests/TrimFunctionsTests.cs index da210b6ac..4edf451be 100644 --- a/test/LibRed.Engine.Tests/TrimFunctionsTests.cs +++ b/test/LibRed.Engine.Tests/TrimFunctionsTests.cs @@ -4,8 +4,10 @@ namespace LibRed.Engine.Tests; -// Trim/LTrim/RTrim are single-argument and remove ONLY spaces — the space and the ideographic space U+3000, in any -// mixture; not tabs or other whitespace, and no trim-char parameter — verified vs ACE. NULL-propagating. +// With one argument Trim/LTrim/RTrim remove ONLY spaces — the space and the ideographic space U+3000, in any +// mixture; not tabs or other whitespace — verified vs ACE. NULL-propagating. LTrim and RTrim also take SQL +// Server 2022's second argument, a set of characters to strip; that one is a LibRed extension, since ACE takes +// no such parameter. public class TrimFunctionsTests : TempDatabaseTest { private static QueryEngine Fresh() @@ -54,4 +56,36 @@ public void Other_unicode_spaces_are_kept(int codePoint) [InlineData("RTrim(Null)")] public void Trim_propagates_null(string expr) => Assert.Null(Eval(expr)); + + // The second argument is a SET of characters, not a substring: every leading (or trailing) character that + // appears anywhere in it is removed, and the stripping stops at the first character that does not. + [Theory] + [InlineData("LTrim('xxhixx', 'x')", "hixx")] + [InlineData("RTrim('xxhixx', 'x')", "xxhi")] + [InlineData("LTrim('xyxhiyx', 'xy')", "hiyx")] + [InlineData("RTrim('xyhixyx', 'xy')", "xyhi")] + [InlineData("LTrim('.,.hi', '.,')", "hi")] + [InlineData("LTrim('xyz', 'zyx')", "")] // every character stripped + [InlineData("LTrim('hi', 'x')", "hi")] // nothing to strip + [InlineData("LTrim('hi', '')", "hi")] // an empty set strips nothing + [InlineData("LTrim(' hi ', ' ')", "hi ")] // spaces, said explicitly + [InlineData("RTrim(Chr(9) & 'hi' & Chr(9), Chr(9))", "\thi")] // a tab, which the one-argument form keeps + public void Two_argument_trim_strips_the_characters_given(string expr, string expected) + => Assert.Equal(expected, Convert.ToString(Eval(expr))); + + [Theory] + [InlineData("LTrim(Null, 'x')")] + [InlineData("LTrim('xhi', Null)")] + [InlineData("RTrim(Null, 'x')")] + [InlineData("RTrim('hix', Null)")] + public void Two_argument_trim_propagates_null(string expr) + => Assert.Null(Eval(expr)); + + // Access has no third argument, and Trim itself takes only the one. + [Theory] + [InlineData("LTrim('hi', 'x', 'y')")] + [InlineData("RTrim('hi', 'x', 'y')")] + [InlineData("Trim('hi', 'x')")] + public void A_third_argument_is_rejected(string expr) + => Assert.Throws(() => Eval(expr)); }