Skip to content

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.

  1. Install the package.

    Terminal window
    pnpm add @lanexio/parser-grammar-sql
  2. Parse a SQL query.

    import { parseSql } from '@lanexio/parser-grammar-sql';
    const encoder = new TextEncoder();
    const tree = parseSql(encoder.encode(`
    SELECT name, email
    FROM users
    WHERE active = true
    ORDER BY name
    `));

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
DialectQuotingCommentNotable
'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.

ModeBatch-boundary GOsqlcmd :directive line
"accept" (default)Plain Keyword nodeLineComment leaf
"split"SqlKind.BatchBoundary leafSqlKind.ClientDirective leaf
"reject"Error node, sets hasErrorError 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); // true

The 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.

Prepared-statement bind markers are emitted as SqlKind.Parameter nodes. Which marker a dialect accepts is a fixed lexer-level matrix:

Markeransipostgresmysqlsqlitetsql
?acceptreject (JSON operator)acceptacceptaccept (ODBC)
$1acceptacceptrejectrejectmoney literal
:nameacceptacceptacceptacceptaccept
@p1rejectrejectacceptrejectaccept

$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); // false

The 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); // false
const cursor = tree.cursor();
cursor.gotoFirstChild(); // SELECT statement
while (cursor.gotoNextSibling()) {
// Walk SELECT, FROM, WHERE, ORDER BY clauses
console.log(cursor.current.kind);
}