STRUMENTA LANGUAGE ENGINES · MICROSOFT TRANSACT-SQL

Transact-SQL, with a semantic layer on top of the parse.

Two implementations, one dialect. The .NET engine reads analytic SELECT, INSERT and UPDATE and then resolves every column against your schema and types the result — 66 automated tests, PIVOT, FOR SYSTEM_TIME, OPTION() hints and bracketed identifiers included. The JVM engine carries the broader statement tree and a command-line tool.

You are not choosing which one is better. You are choosing by scope and runtime: what your corpus contains, and whether your tooling is .NET or the JVM.

The engines at a glance
DIALECTSQL Server · Azure SQL · Synapse · Fabric
RUNTIMES.NET Standard 2.0 · JVM (Kotlin)
TESTS66 on the .NET engine
SEMANTICSsymbol resolution · type inference
OUTPUTAST · LionWeb · JSON · XML
BINDINGSJava · Kotlin · Python · TypeScript · C#

One dialect, two engines, and a keyword that is not a statement

MicrosoftParser.g4 · 762 lines · ExtendedSqlParser.g4 · the shared statement tree

Transact-SQL is two problems wearing one name. There is the analytic query surface — window functions, temporal tables, query hints, bracketed identifiers — where the hard part is not the grammar but knowing what the columns mean. And there is the script surface, where a file is not one program at all.

Engine A · .NET

The analytic DML engine, with symbols and types

A .NET Standard 2.0 component. Its statement rule is exactly selectStatement | insertStatement | updateStatement — deliberately narrow, so that everything above the parse tree could be built properly. That is where the value is: a DatabaseModel you supply, symbol resolution returning positioned referents, and expression typing through TabularType. It shares one AST and one SQLSymbolSolver with our Firebird engine.

RUNTIME.NET Standard 2.0
SCOPESELECT · INSERT · UPDATE
TESTS66
Engine B · JVM

The broader statement tree, and a command-line tool

A Kotlin engine in which Transact-SQL enters the same statement tree that carries our Oracle DDL and procedural surface, with the Oracle-only alternatives gated behind grammar predicates. Its documented dialect target is Analytics Platform System and Parallel Data Warehouse — warehouse Transact-SQL rather than mainstream OLTP. It is the one that serializes: run the jar over a directory and collect XML or JSON.

RUNTIMEJVM · Java 11 or later
SCOPEthe shared statement tree
CLI–language tsql

Why GO decides whether a script is one program or twelve

Microsoft states it plainly: GO is not a Transact-SQL statement. It is a command that the client tools recognize and the server never sees. A batch is everything typed since the last GO, a statement cannot share a line with it, and GO 5 re-executes the preceding batch five times.

That makes it the only construct in a .sql file whose meaning depends on it being alone on its line. Treat it as an ordinary keyword and you will happily accept SELECT 1 GO, which no Microsoft tool accepts.

It also draws a scope boundary. DECLARE @x INT in one batch and PRINT @x in the next is an error — and if your parser drops GO into the whitespace channel, you resolve @x across the boundary and hand the customer a symbol table that is quietly wrong.

Our JVM grammar models it as a script-level command, so it survives into the tree instead of disappearing. What it does not do today is enforce the column-0 rule, read the repeat count, or scope variables per batch. If batch-scoped analysis is what you are buying, ask us before you sign — it is a piece of work, not a switch.

The rule, as shipped

ExtendedSqlParser.g4

sql_server_command
    : GO
    | USE database_name=element_name
    ;
Two script-level commands, in the grammar rather than in a pre-processing step. The .NET engine does not model batches at all — its entry rule is a flat (statement SEMICOLON?)+, which is the right shape for statements pulled out of application code and the wrong one for a deployment script.

Input — analytic query, SQL Server

headcount_by_title.sql

WITH recent AS (
    SELECT TOP (100) e.BusinessEntityID, e.JobTitle
    FROM HumanResources.Employee
         FOR SYSTEM_TIME CONTAINED IN ('2024-01-01', '2025-01-01') AS e
    WHERE e.JobTitle LIKE N'%Engineer%'
)
SELECT p.JobTitle, p.[2024], p.[2025]
FROM ( SELECT JobTitle, YEAR(ModifiedDate) AS yr,
                BusinessEntityID FROM recent ) AS src
PIVOT ( COUNT(BusinessEntityID) FOR yr IN ([2024],[2025]) ) AS p
OPTION (FORCE ORDER, HASH JOIN);
Four marked constructs, all Microsoft-only spellings. TOP (n) takes parentheses here and does not in Teradata; [2024] is a bracketed identifier that Oracle, Teradata and Firebird all reject; N'…' is a national string literal.

Output — parse tree, by grammar rule

intermediate rules elided

statements
└── statement
    └── selectStatement
        ├── topClause TOP (100), line 2
        ├── ⋯
        │   ├── forTimeClause
        │   │   └── systemTime CONTAINED IN, line 4
        │   └── pivotClause FOR yr IN […], line 11
        └── optionClause
            ├── queryHint FORCE ORDER
            └── queryHint HASH JOIN
These are the real rule names in the shipped .NET grammar. The AST you program against is derived from that tree, every node carries a line and column, and difficult input comes back as a partial tree plus a positioned issues list rather than an exception.

What it handles

Four places where Transact-SQL asks the lexer, not the parser, to make the decision — which is why a generic SQL grammar dropped onto a SQL Server estate fails on the first file.

Four sigils, four namespaces

A local variable, a system function, a temporary table and a global temporary table are distinguished by nothing but a prefix character. Nothing in the syntax marks them; the difference is lexical and it changes where the name resolves.

DECLARE @rows INT = @@ROWCOUNT;
SELECT * INTO #stage FROM dbo.Orders;
SELECT * FROM ##shared_stage;

Double quotes that change meaning mid-file

SET QUOTED_IDENTIFIER decides whether "abc" is a delimited identifier or a string literal, and a script may flip it halfway through. We expose it as a construction flag, QuotedIdentifiersAreStrings — one setting per parse, not a mid-file switch. Say which convention your corpus uses.

SET QUOTED_IDENTIFIER OFF;
SELECT "a string, not a column";
-- and elsewhere: [Order Details], srv.db..tbl

Temporal tables and query hints

FOR SYSTEM_TIME attaches a time predicate to a table reference, so the shape of a FROM clause changes. OPTION() hangs a hint list off the end of the statement. Both are Microsoft-only and both are in the grammar with rules of their own.

FROM dbo.Employee
     FOR SYSTEM_TIME AS OF '2025-06-01' e
OPTION (DISABLE EXTERNALPUSHDOWN);

OUTPUT and the pseudo-tables

inserted and deleted exist only inside an OUTPUT clause or a trigger, and their column list is the shape of the target table. You cannot resolve them without the DDL — which is exactly why the engine takes a DatabaseModel from you rather than guessing.

UPDATE TOP (10) dbo.[Order Details]
SET    Discount = Discount * 1.1
OUTPUT inserted.OrderID, deleted.Discount
WHERE  Quantity > 100;

Coverage, by construct and by engine

Rule names in the second column are rules in the .NET grammar we ship. Read the two engine columns as scope, not as quality: the .NET engine is the one with the semantic layer, the JVM engine is the one with the wider statement tree and the serializer.
Construct Grammar rules (.NET) .NET engine JVM engine
Analytic SELECT — CTEs, joins, GROUP BY, named windows, frames, UNION selectStatement Supported Supported
TOP (n), DISTINCT, ORDER BY topClause Supported Supported
INSERT and UPDATE with OUTPUT inserted. / deleted. insertStatement, updateStatement Supported Supported
PIVOT and UNPIVOT pivotClause Supported Ask us for your corpus
Temporal tables, all five FOR SYSTEM_TIME forms forTimeClause, systemTime Supported Ask us for your corpus
Query hints — HASH/LOOP/MERGE JOIN, FORCE ORDER, external pushdown optionClause, queryHint Supported Ask us for your corpus
Bracketed and four-part names, N'…' literals, the quoted-identifier switch QuotedIdentifiersAreStrings Supported Ask us for your corpus
Symbol resolution against a supplied schema, expression typing DatabaseModel, TabularType Supported Not on this engine
Script commands — GO, USE Not modeled Script-level command in the tree
Warehouse DDL — WITH (DISTRIBUTION = …, CLUSTERED INDEX (…)) Not offered Supported — the documented target
Serialized AST on the command line Library only XML or JSON, --language tsql
Control-of-flow — BEGIN…END, IF, WHILE, TRY…CATCH, cursors, procedures Not on this engine Bring us a sample and we will report exactly what parses
Dynamic SQL — EXEC, sp_executesql Not supported Not supported

We do not print a certified SQL Server version range, because the number would not answer your question. Microsoft services 2016 through 2025 plus the cloud editions, and the constructs above are what the grammars demonstrably accept. Send a representative set of your own files and we will report what parses and what does not.

One file that only this engine takes

We license five SQL-family engines. This script parses with exactly one of them, and it is what a Microsoft warehouse looks like from the inside rather than what a syntax tutorial looks like.

Input — Analytics Platform System deployment script

create_clustered_table.sql

USE AdventureWorksPDW
GO
CREATE TABLE myTable
  (  id int NOT NULL,
     lastName varchar(20),
     zipCode varchar(6) )
WITH ( DISTRIBUTION = REPLICATE,
        CLUSTERED INDEX (lastName) );
GO
The CREATE TABLE body is the committed Transact-SQL fixture from the JVM engine’s own test resources, not an example written for this page; USE and GO are the two forms the script-command rule admits. Nothing else on the list reads it: Oracle PL/SQL has no GO and no distribution clause, Teradata spells physical design MULTISET … PRIMARY INDEX, Firebird and our generic SQL engine have no warehouse DDL at all.
WHAT THIS ENGINE IS FOR

If I drop this column, what breaks?

A parse tree tells you a query mentions a name. It does not tell you which table that name came from. Closing that gap, across a reporting estate nobody has read in five years, is the work this engine was built for.

Give the engine your schema as a DatabaseModel — tables, columns, types — and every column reference in every view and report query resolves to a source table. Resolved references carry referents with positions, so the answer is not “this query uses Discount” but “line 14, column 22, of this file, resolving to dbo.OrderDetails.Discount”.

On top of that sits the type system: getType on any expression, and TabularType for the row shape a SELECT produces. That is what turns a folder of .sql files into a lineage graph, an impact matrix, or a rule in your build that fails a pull request.

The same SQLSymbolSolver resolves Firebird trees behind the same interface, because both engines ship in one package over one shared AST. An estate with SQL Server on one side and an embedded Firebird product on the other is one component and one API, not two integrations.

var result = parser.Parse(sqlText);
var ctx    = new SymbolResolutionContext(databaseModel);

foreach (var col in result.Root.GetReferencedColumns(ctx))
{
    Console.WriteLine(
        $"{col.Relation} . {col.Name}  " +
        $"<- {col.Referents[0].Position}");
}

var shape = result.Root.GetType(ctx) as TabularType;

Symbol resolution and typing on the .NET engine. Without a database model the parse still succeeds — unqualified columns simply stay unattributed, and the type comes back unknown.

762
LINES OF ANTLR GRAMMAR, .NET ENGINE
66
AUTOMATED TESTS ON THE .NET ENGINE
5
TEMPORAL-TABLE FORMS COVERED
2
DIALECTS BEHIND ONE SYMBOL SOLVER

What you receive

A component, the documents that describe the tree it produces, and a person to write to. It runs inside your own network, on your own machines, with no connection back to us and none to the database the code came from.

01 The engine

A .NET library, a JVM library, or both

The analytic engine is a .NET Standard 2.0 class library, so it loads into .NET Framework 4.6.1 and later, .NET Core 2.0 and later, and modern .NET. The JVM engine is a Kotlin jar that is also its own command-line tool. Which you take is a scoping conversation, not a price list.

.NET Standard 2.0 · JVM jar and CLI
02 The model

An AST with positions, symbols and types

A typed AST rooted at a compilation unit, positions on every node, a positioned issue list instead of an exception on difficult input, and the semantic layer above it. The engine supports LionWeb, so the model travels to tooling we did not write, and the JVM engine serializes to JSON or XML.

AST · symbols · types · LionWeb
03 The documents

A generated API reference, per node type

The .NET engine ships an HTML API reference generated from the source, which enumerates the AST node types individually rather than describing them in prose. With the JVM engine you get the manual, the generated AST document, worked examples and a sample serialized tree to check your reader against on day one.

HTML API docs · examples
04 The license

Standard, Distribution or Service — with support included

Standard for use inside your own organization, Distribution if the engine ships inside a product you sell, Service if it runs behind a service you operate. Support is part of the license, not an upsell. The guarantee runs a year and is extendable, or can be taken pay-as-you-go.

three tiers · support included
Onboarding

The first useful week is spent populating a database model from your own DDL — not waiting for an environment.

Parsing works on day one without it. Symbol resolution and typing are what the model unlocks, and getting your schema in is the only setup step that takes real thought. If the integration needs help, that is what the support in the license is for. Write to products@strumenta.com.

THE REST OF THE ESTATE

The SQL Server estate is rarely only SQL Server.

In the places where Transact-SQL matters most — insurance, banking, logistics — the queries were not written in a query tool. They are string constants inside a COBOL batch, an RPG program on IBM i, or a C# service, and the interesting question always crosses that boundary.

“Which program writes this column?” cannot be answered inside the database. The write happens in the host language; the SQL is embedded in it. Parsing the host language with one tool and the SQL with another leaves you joining two unrelated models by hand, on names, and hoping.

Every Strumenta engine is built on Starlasu, so a Transact-SQL tree, a COBOL tree and an RPG tree have the same shape, the same traversal model and the same API. One tool walks all of them, and the embedded statement is a node in the host program’s own tree rather than a string somebody has to re-parse. That is the thing you cannot assemble by gluing unrelated open-source parsers together.

Every engine also supports LionWeb, so the models interchange with LionWeb-compliant tooling instead of being locked inside one process — and every engine is available through bindings from Java, Kotlin, Python, TypeScript and C#. The host application picks the language; the parser does not.

Engines in the same estate

one model, one traversal

COBOL engine →
The batch that holds the embedded SQL.

RPG & DDS engine →
The IBM i application on the other side of the link.

Generic SQL engine →
The editor-side sibling, in TypeScript.

The whole catalog →
15+ engines, one AST framework underneath.

Why license this instead of starting from the public grammar

A technical buyer already knows the antlr/grammars-v4 Transact-SQL grammar exists, is free, and is maintained by people who deserve the credit. It is also, honestly, the cheap part of this problem.

What that grammar gives you is a parse tree: raw ANTLR contexts, in whatever shape the rules happened to take. What it does not give you is a designed AST, a database model, symbol resolution, a type system, positioned issues instead of console errors, a switch for SET QUOTED_IDENTIFIER, or anybody to call when a customer file fails at four in the afternoon.

That list is the project. Writing it is where the quarters go, and it is roughly what the license buys: Strumenta.SQL.AST, the symbol solver, the database model, the type system, and the tests that keep them honest as the grammar changes underneath.

There is a second thing a grammar cannot give you, and it is the one that decides most migrations: interoperability. Because this engine is built on Starlasu and speaks LionWeb, its output is the same kind of object as our COBOL, RPG and Oracle output. A free grammar per language leaves you with a translation layer per pair.

None of that makes the free grammar a bad choice for a weekend tool. It makes it a bad choice for a system you have to support for five years with somebody else’s deadline attached.

The method behind this work has a name. Read the Chisel Method for how we make parser work estimable, or Parsers and transpilers for the three ways into it.

What to discuss before you license

Five things we would rather tell you now than have you find in week three.

  • The .NET engine is DML only. SELECT, INSERT, UPDATE. No DELETE, no MERGE, no DDL and no control-of-flow. That narrowness is deliberate — it is what paid for the symbol solver and the type system — but if your corpus is stored procedures, you are talking about the JVM engine and we should look at your files together.
  • There is no Transact-SQL code generator. Neither engine writes Transact-SQL back out from a modified tree. Our Oracle PL/SQL engine has a printer; this one does not, and we would rather say so than let you find out during a proof of concept. If regeneration is the requirement, tell us early and we will scope it as work rather than sell it as a feature.
  • Batch semantics are partial. GO and USE are in the JVM tree as script-level commands. The column-0 rule, GO [count] and per-batch variable scoping are not modeled today. If your analysis depends on knowing that @x died at the batch boundary, that is a scoped piece of work on top of the engine.
  • Dynamic SQL is not parsed at all. EXEC (@sql) and sp_executesql assemble a statement at run time out of string concatenation, and neither grammar accepts them today. In a real SQL Server estate the proportion of dynamic SQL is the single number that decides whether static analysis answers your question — count it before you buy anything.
  • Symbol resolution needs your DDL. The engine does not connect to a SQL Server instance and read the catalog. You supply the tables and columns; SELECT *, unqualified columns and the inserted/deleted pseudo-tables resolve only once you have. Bring the schema and we will tell you what it takes to load it.

What we will ask you

  1. What is in your corpus — analytic SELECT statements, or stored procedures and triggers? This decides which engine you are buying.
  2. .NET or JVM? The two implementations are not interchangeable, and the semantic layer is on the .NET side.
  3. SQL Server, Azure SQL, Synapse or Analytics Platform System, Fabric — and which version?
  4. Do you have the DDL, or only the statements?
  5. Do your scripts use GO batches, and does your analysis depend on batch scope?
  6. How much dynamic SQL, as a proportion, and can we see a sample?
  7. Is the SQL standing on its own, or embedded in COBOL, RPG or C# that we should be parsing too?
  8. Do you need LionWeb output for interoperability with other tooling?

Straight answers

Which of the two engines will we get?

Whichever matches your corpus and your runtime. If you are analyzing analytic queries from a .NET application and you want columns resolved and expressions typed, that is the .NET engine. If you are running a batch over a warehouse of scripts on the JVM and you want serialized trees out of a command line, that is the Kotlin one. We decide it by looking at a sample of your files, not by price.

Does it generate Transact-SQL back from the AST?

No. Neither implementation ships a Transact-SQL printer. Among the five SQL-family engines we license, only the Oracle PL/SQL one writes its language back out. This engine reads, resolves and types; if your project needs regeneration, say so at the first call and we will scope it honestly instead of promising a module that does not exist.

Can it handle GO-separated deployment scripts?

The JVM engine models GO and USE as script-level commands, so a batched script parses and the commands appear in the tree rather than vanishing into whitespace. What is not there yet is the column-0 rule, the repeat count and per-batch variable scoping. The .NET engine does not model batches at all — it expects statements, which is the right shape for SQL pulled out of application code.

How does it know what SELECT * means?

Because you tell it. You supply a database model — tables, columns and types — and the engine resolves references against it, returning referents with positions, and computes an expression type or a TabularType for the row shape. Without a model the parse still succeeds and the references simply stay unattributed. The engine never connects to your server.

Can we use it from Python, TypeScript or Java?

Yes. The engine is built on Starlasu and is available through bindings from Java, Kotlin, Python, TypeScript and C#, and it supports LionWeb, so the model interchanges with LionWeb-compliant tooling. If you would rather not embed anything, the JVM engine’s command-line tool serializes the tree to JSON or XML and any language can read that.

Send us a folder of .sql files the ones nobody wants to open

Bring us your queries, and we will tell you what resolves.

A representative sample — report queries, a deployment script, whatever your worst file is — plus the DDL if you have it. We will come back with what parses, what resolves against your schema, how much of it is dynamic SQL, and which of the two engines you actually want. If Transact-SQL is not the hard part of your estate, we will say that too.

Scroll to Top