Parsing SQL
Lanexio™ Parser implements SQL:2016 with dialect-specific lexing for PostgreSQL, MySQL, SQLite, and T-SQL. Dialect support is a lexer-level recognition surface plus a curated statement container set, not a per-vendor parser (ADR 0035). The parser produces structured trees with keyword, identifier, literal, and clause nodes.
Quick start
Section titled “Quick start”-
Install the package.
Terminal window pnpm add @lanexio/parser-grammar-sqlTerminal window npm install @lanexio/parser-grammar-sqlTerminal window yarn add @lanexio/parser-grammar-sql -
Parse a SQL query.
import { parseSql } from '@lanexio/parser-grammar-sql';const encoder = new TextEncoder();const tree = parseSql(encoder.encode(`SELECT name, emailFROM usersWHERE active = trueORDER BY name`));
Dialect selection
Section titled “Dialect selection”Pass a dialect option to control quoting, comments, and operators:
import { parseSql } from '@lanexio/parser-grammar-sql';
const tree = parseSql(encoder.encode(` SELECT * FROM "users" WHERE "name" = 'Alice'`), { dialect: 'postgres' }); // PostgreSQL-style double-quoted identifiers| Dialect | Quoting | Comment | Notable |
|---|---|---|---|
'ansi' | 'string', "identifier" | --, /* */ | Default |
'postgres' | 'string', "identifier" | --, /* */ | :: casts, $$ strings |
'mysql' | 'string', `identifier` | #, --, /* */ | Backtick identifiers |
'sqlite' | 'string', "identifier" | --, /* */ | No lexer-specific rules |
'tsql' | 'string', [identifier] | --, /* */ | #-temp ids, @var, GO batches, money literals |
Dialect selection is also reachable through the unified parse() entry point. The grammarOptions.dialect option forwards to the SQL grammar, so parse(src, { language: "sql", grammarOptions: { dialect: "postgres" } }) selects PostgreSQL the same way parseSql(bytes, { dialect: 'postgres' }) does, and a top-level parse(src, { language: "sql", dialect: "postgres" }) is honored too. The selected dialect is stamped on tree.metadata.dialect.
The tsql, mssql, and sqlserver spellings are accepted as a language hint
and route to the SQL grammar with the tsql dialect forced (ADR 0045):
const tree = parse('DECLARE @x int; SET @x = 1;', { language: 'tsql' });console.log(tree.metadata.language); // "sql"console.log(tree.metadata.dialect); // "tsql"Client batches and sqlcmd directives (T-SQL)
Section titled “Client batches and sqlcmd directives (T-SQL)”T-SQL deployment scripts written for SQL Server Management Studio or sqlcmd
separate batches with a GO line and configure the client with :directive
lines such as :setvar, :r, and :connect. The grammar treats both as
client-boundary markers, not T-SQL statements. The clientBatch option picks
how they are surfaced. The mode is tsql-specific: on the other four dialects
GO stays a plain keyword and : stays a bind-marker context.
| Mode | Batch-boundary GO | sqlcmd :directive line |
|---|---|---|
"accept" (default) | Plain Keyword node | LineComment leaf |
"split" | SqlKind.BatchBoundary leaf | SqlKind.ClientDirective leaf |
"reject" | Error node, sets hasError | Error node, sets hasError |
All three modes stay lossless (the flat AST leaf partition holds) and never
throw. reject keeps parsing, so later statements still materialize.
strict: true with no clientBatch behaves as clientBatch: "reject"; an
explicit clientBatch always wins.
import { parseSql, SqlKind } from '@lanexio/parser-grammar-sql';
const bytes = encoder.encode("SELECT 1;\nGO\nSELECT 2;\nGO\n");
// Default: GO is a plain keyword, no error.const lenient = parseSql(bytes, { dialect: 'tsql' });console.log(lenient.root.hasError); // false
// split: each GO becomes a BatchBoundary leaf over its exact span, so a// loader can slice the file into batches at those ranges.const split = parseSql(bytes, { dialect: 'tsql', clientBatch: 'split' });const boundaries: Array<[number, number]> = [];const cur = split.cursor();if (cur.gotoFirstChild()) { do { if (cur.current.kind === SqlKind.BatchBoundary) { boundaries.push(cur.current.range); } } while (cur.gotoNextSibling());}console.log(boundaries.length); // 2
// reject: each batch-boundary GO becomes an Error node; later statements parse.const strict = parseSql(bytes, { dialect: 'tsql', clientBatch: 'reject' });console.log(strict.root.hasError); // trueThe clientBatch option also reaches the grammar through the unified entry
point via grammarOptions:
const tree = parse('SELECT 1;\nGO\nSELECT 2;\nGO\n', { language: 'tsql', grammarOptions: { clientBatch: 'split' },});The tsql manifest corpus ledger classifies files that are valid only under
sqlcmd or SSMS preprocessing as accept-client. Under
clientBatch: "reject" those GO separators and directives carry hasError,
the two reject-core cases are no longer tolerated, and the four recover
cases keep hasError while still producing a later statement node.
Placeholders
Section titled “Placeholders”Prepared-statement bind markers are emitted as SqlKind.Parameter nodes. Which
marker a dialect accepts is a fixed lexer-level matrix:
| Marker | ansi | postgres | mysql | sqlite | tsql |
|---|---|---|---|---|---|
? | accept | reject (JSON operator) | accept | accept | accept (ODBC) |
$1 | accept | accept | reject | reject | money literal |
:name | accept | accept | accept | accept | accept |
@p1 | reject | reject | accept | reject | accept |
$1 is not SQL:2016 (ANSI uses ?); it is accepted under ansi as a
deliberate leniency extension so tooling that emits $1 and parses without an
explicit dialect gets a clean parse. ? under mysql is MySQL’s native
prepared-statement marker. A marker that the active dialect does not accept
produces an Error node and a human-readable hint on tree.diagnostics, for
example SQL parameter marker `$1` requires the postgres or ansi dialect (active: mysql).
const ansi = parseSql(encoder.encode('SELECT * FROM t WHERE id = ?'));console.log(ansi.root.hasError); // false
const mysql = parseSql(encoder.encode('SELECT * FROM t WHERE id = ?'), { dialect: 'mysql' });console.log(mysql.root.hasError); // false
const ansiPositional = parseSql(encoder.encode('SELECT * FROM t WHERE id = $1'));console.log(ansiPositional.root.hasError); // falseThe marker matrix also applies through the unified entry point, which forwards
grammarOptions.dialect to the SQL grammar:
import { parse } from '@lanexio/parser';
const tree = parse('SELECT * FROM t WHERE id = ?', { language: 'sql', grammarOptions: { dialect: 'mysql' } });console.log(tree.root.hasError); // falseInspecting the tree
Section titled “Inspecting the tree”const cursor = tree.cursor();cursor.gotoFirstChild(); // SELECT statement
while (cursor.gotoNextSibling()) { // Walk SELECT, FROM, WHERE, ORDER BY clauses console.log(cursor.current.kind);}