DotGram.Sql
0.2.0
dotnet add package DotGram.Sql --version 0.2.0
NuGet\Install-Package DotGram.Sql -Version 0.2.0
<PackageReference Include="DotGram.Sql" Version="0.2.0" />
<PackageVersion Include="DotGram.Sql" Version="0.2.0" />
<PackageReference Include="DotGram.Sql" />
paket add DotGram.Sql --version 0.2.0
#r "nuget: DotGram.Sql, 0.2.0"
#:package DotGram.Sql@0.2.0
#addin nuget:?package=DotGram.Sql&version=0.2.0
#tool nuget:?package=DotGram.Sql&version=0.2.0
DotGram.Sql
SQL parsers written in .gram, and the trees of records they build. Where an
example shows one feature, a parser here is written against a whole specification.
They are ordinary C# libraries. .Gram generates the parsers into this assembly at compile time, so nothing here carries a parser runtime, and neither does anything that references it.
The dialects
Each dialect is a directory, a namespace and a grammar of its own. SQL-92 and T-SQL meet in the tree
in DotGram.Sql — the records, SqlWriter, which prints them back, and
SqlWalker, which visits them. SqlStandardParser builds the standard's own tree,
and which parser answers with which is under the table.
| Parser | Namespace | What it reads |
|---|---|---|
SqlStandardParser |
DotGram.Sql.Standard |
ISO SQL:2023, in part: its lexical elements, names, scalar expressions, aggregates and window functions, the JSON functions, query expressions with row pattern recognition, predicates, the data change statements, the whole schema — tables, views, domains, sequences, privileges, routines, triggers, user-defined types, casts, orderings, transforms, character sets and collations — and the transaction, session, connection, diagnostics, dynamic and direct statements |
Sql92Parser |
DotGram.Sql.Standard |
SQL-92 as the standard writes it |
TransactSqlParser |
DotGram.Sql.TransactSql |
SQL Server's T-SQL, as the engine reads it |
The third names the second — [GramInclude(typeof(Sql92Parser), As = "Sql92")] — and
rebinds the rules where T-SQL differs, so what the two languages share is written once and the
dialect is the size of the difference.
TransactSqlParser builds the tree, and Sql92Parser the expressions in it that the two share.
SqlStandardParser builds a tree of its own, the standard's, laid out in
Sql2023Ast.cs in DotGram.Sql.Ast and printed back by
Sql2023Writer. Its records are named for the standard's
constructs, so DotGram.Sql.Ast.Statement and DotGram.Sql.Statement are two types.
SqlStandardParser publishes the standard's productions under their own names —
ParseValueExpression, ParseSearchCondition, ParseQueryExpression, ParseSQLSchemaStatement
and the rest listed at the end of its grammar. TransactSqlParser publishes ParseSelect,
ParseQuery, ParseSearchCondition, ParseValueExpression, ParseStatement, ParseSql and
ParseScript, each with two TryParse…: one giving a Match<T> with the refusal's message and
position, and a bool TryParse…(string input, out T value) for where only the answer is wanted.
All three read through a lexical split (Lexical = true): a lexical half makes tokens, and the
syntactic half above it decides each choice by the token in front of it, which is what a parser
written by hand does. SqlStandardParser reads two things less than the standard does because of
it: a comment holds others six deep, and a /* inside a comment always opens one.
using DotGram.Sql;
using DotGram.Sql.TransactSql;
var match = TransactSqlParser.TryParseSelect("select name from Users where id > @id");
var select = (Statement.Select)match.Value;
var query = (Query.Specification)select.Of;
var from = (TableReference.Named)query.From[0]; // from.Table is "Users"
A database's compatibility level gates a small part of what SQL Server reads — the WINDOW
clause from 160, OPENJSON's schema from 130. TransactSqlParser.ParseStatement130 reads what
the engine reads at level 130, and so on from 100 to 170; ParseStatement names no level and
reads them all. The levels are one grammar and one machine, told apart by a number, and what each
gates is held against SQL Server.
TransactSqlParser.ParseStatement reads one statement. TransactSqlParser.ParseSql reads a text
of them — what a client sends the server in one call — and gives back a Statement[]: each ended
by a ; or by nothing, except before a WITH, which needs the statement before it ended, as the
server does. ParseSql100 to ParseSql170 are its levels.
GO is not T-SQL: it is the line a client cuts a script at, and the server never sees it.
TransactSqlParser.ParseScript reads a script — batches cut apart at the lines that say GO, the
way ScriptDom and the management tools cut them — and gives back a Batch[], each with its
statements and the GO line that ended it. GO 5, a batch sent five times, is read too.
ParseScript100 to ParseScript170 are its levels. ParseSql is one batch, and a GO in it is
refused.
Where each node was written is there for whoever asks for it. TransactSqlParser.Located is the
same grammar compiled with LocationType = typeof(ISqlSpan) —
TransactSqlParser.Located.TryParseStatement — and every node it builds carries in Span the
range of text it was read from. TransactSqlParser itself pays nothing for them.
The tree they build is described in docs/ast.md.
What they read is held against SQL Server itself, against a corpus of somebody else's SQL and
against a round trip — parse, print, and compare the two readings — which catches a parser that
answers yes and builds the wrong thing.
All three of these parsers may be called from any number of threads at once; that is the
generated parser's own contract, not something this package adds
(DotGram's README).
Taking one
<PackageReference Include="DotGram.Sql" Version="0.2.0" />
There is no companion runtime package, and no generator to install alongside it: the parsers were generated when this assembly was compiled.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Memory (>= 4.6.3)
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Every method that was here before still reads what it read: what moved is what the grammars read, the tree of the standard, which grows toward T-SQL, and there is one more way to ask. One public type built by SqlStandardParser's own GRANT and REVOKE (the standard's, not T-SQL's, which build their own Statement.Grant/Deny/Revoke unchanged) changes shape: PrivilegeObject was a single record with a nullable Class beside the standard's Kind; it is now a family of two (below), so code that constructed or pattern-matched the old PrivilegeObject needs PrivilegeObject.Named or PrivilegeObject.Classed instead.
The standard's tree (DotGram.Sql.Ast) gains values it will need to hold T-SQL as well: operators (Modulo, the bitwise and shift operators, BitwiseNot, NotLess and NotGreater for !< and !>), Bracketed identifiers, a Money literal, CROSS and OUTER APPLY and a join hint, the Snapshot isolation level, three flags on a parameter definition, a table type, and two spellings kept as flags so that the text writes back: Exclamation for != and Max for a length of MAX. A new optional property shows up in a record's printed form. Sql2023Writer prints every one of them, and a value it has no text for is now an ArgumentOutOfRangeException naming the enum and the value, where before it printed some other operator. It also carries the first of T-SQL’s own nodes, which no parser builds yet: the parts a statement is made of — TOP, INTO, OUTPUT, FOR XML and FOR JSON, table and query hints, the generic WITH option, an execution context — the SET options as one family instead of five, and ALTER DATABASE’s thirteen actions with the files and filegroups they take. The DROP statements are one record and a kind for the forty-six whose syntax is only the words and a name, with eleven pages that publish more keeping a record of their own. Sql2023Writer now refuses an unknown enum value in all three of its files rather than in one, and it keeps DROP PROC apart from DROP PROCEDURE. T-SQL's cursors and transactions follow: the words the extended DECLARE CURSOR writes after CURSOR are properties on CursorProperties — its scope, its kind, its concurrency, TYPE_WARNING, and FORWARD_ONLY kept as a spelling of NO SCROLL rather than a meaning of its own — DEALLOCATE, SET @c = CURSOR, BEGIN TRANSACTION and SAVE TRANSACTION are statements, and COMMIT and ROLLBACK gain the word that was written where the page makes it optional, as one nullable enum rather than a pair of flags that could say TRAN with no keyword, together with the transaction they may name and delayed durability. A ROLLBACK holding both a transaction name and TO SAVEPOINT has no text that keeps them both, so the writer refuses it rather than dropping one. The permission statements follow: DENY is a statement of its own beside GRANT and REVOKE, whose ON clause may now be left out; PrivilegeKind gains every permission SQL Server publishes, 160 members named for their words (ViewDefinition, AlterAnyDatabase) after the standard's ten, generated from the parser's own permission catalogue; a new SecurableClass is the class of ON SCHEMA::s, and PrivilegeObject is now a family of two, Named for the standard's forms and ON t, Classed for ON SCHEMA::s; ALL and EXEC are kept as Privilege.Abbreviated, spellings of ALL PRIVILEGES and EXECUTE; and NULL among the principals is Grantee.Null.
Every publication now has a third method, bool TryParseX(string input, out T value), beside ParseX and the Match<T> form. It answers only whether the text reads, says nothing about what was expected where it does not, and a refusal costs it about half, since nothing is read a second time to record the message.
SqlStandardParser reads the standard through a lexical split, which bounds two things the ISO grammar leaves unbounded: a comment holds others six deep, and a /* inside a comment always opens one. It now reads what it used to refuse — a datetime less a datetime in brackets before an interval qualifier, a JSON behaviour and its ON clause, and the JSON and period forms of a WHEN operand — and refuses what it used to take, where an operand would swallow a reading that fits the text better. Sql92Parser follows it where the two share a production.
The package ships its XML documentation now, though not for every type yet: the tree that is being replaced carries none, and each node of the one replacing it says what it is and which page of the specification it comes from. What each node is named after is also in docs/ast.md, and how to use the parsers is in the README and the SKILL beside it.
TransactSqlParser leaves a statement's label to that statement: HIDDEN, SORTED_DATA and a login's password words no longer take the label on the line after them. A native module's atomic block is now followed by the end of the batch, said as such, rather than by a refusal naming no statement.
It also refuses two things in GRANT, DENY and REVOKE that it used to read, both because SQL Server refuses them as syntax. A securable's class is one of the twenty-eight the engine takes, so `ON FOO::x` no longer reads, and neither do `ON TABLE::x`, `ON VIEW::x` or `ON PROCEDURE::x`, all of which are `ON OBJECT::x`. And a permission has to be one that class allows: `GRANT SELECT ON ENDPOINT::x` is refused, as the server refuses it, while `GRANT SELECT ON t` is read. Where no ON clause names a class nothing is checked, because the server does not check it while parsing either. The refusal is reported at the token after the securable rather than at the permission the server names. A principal is one part now wherever GRANT, DENY, REVOKE, a role's ADD/DROP MEMBER or WITH NAME, an ALTER AUTHORIZATION or a login's ADD/DROP CREDENTIAL name one, and every inline AUTHORIZATION clause besides — `TO a.b` and `TO dbo.guest` are refused, matching the engine, which reads only a bracketed or quoted one-part name there; a securable's own name, such as `OBJECT::dbo.t`, is unaffected and still reads dotted. Fixed, and present since 0.1.0: TransactSqlParser could throw IndexOutOfRangeException on a statement it refuses, such as a condition that is a value (SELECT * FROM t WHERE c = 1 AND dbo.f(1)), mostly on the first parse on a thread and occasionally on one that had read only smaller statements; and TransactSqlParser.Located threw NullReferenceException on such a statement (SELECT 1 WHERE 1 = 1 AND N) on any thread. Where nothing was thrown the statement was refused, as it should be. No accepted statement was found to reach it.
Fixed: SqlStandardParser and TransactSqlParser.Located could throw IndexOutOfRangeException or NullReferenceException, instead of refusing, on some input ending in a bracketed operand followed by a dangling operator or an incomplete clause — `SELECT (a) +` and an unfinished `CREATE ENDPOINT e AS TCP (LISTENER_PORT = 1,` among them; no accepted statement was found to reach it. A bare CAST, TRY_CAST, PARSE or TRY_PARSE — one SQL Server itself refuses as an ordinary function call — is now read only as its cast and no longer tried again as a call, which also made an unclosed nest of `CAST(` cost linear time instead of exponential; a qualified name (`dbo.PARSE (1)`) still reads as a call. A predicate's row — which a CASE expression is, at every level it nests — is now read once instead of being read again for IS [NOT] DISTINCT FROM, which made a nest of CASE expressions linear instead of exponential too, whether refused unclosed or accepted with IS DISTINCT FROM at every level.