Skip to content

Parser Design Principles

General SQL Parser is built around a handful of principles that have stayed stable across the library's lifetime. Knowing them helps you reason about why the API looks the way it does, and what to expect from a release.

1. Per-vendor accuracy over universal coverage

Every supported dialect has its own lexer and parser, generated from a vendor-specific .l/.y grammar source. There is no "ANSI core plus dialect overlay" — Oracle SQL, T-SQL and Snowflake SQL are entirely separate grammars.

The trade-off:

  • Pro: vendor-specific syntax (CONNECT BY, TOP, QUALIFY) parses with the same accuracy as ANSI SQL. There is no second-class citizen.
  • Pro: a parse error always tells you whether the SQL is valid for the target dialect, not "valid SQL in the abstract".
  • Con: far more grammar to maintain than a single-grammar parser. Mitigated by shared tooling and a regression suite that runs every grammar against thousands of fixture files.

2. Strongly-typed AST

Every SQL element gets its own class. There is no generic Node { Type, Children } — there is TSelectSqlStatement, TFunctionCall, TJoinExpr, and hundreds more, organised under the nodes and stmt packages plus vendor-specific subpackages.

Consumer code walks the AST by type test rather than by string-matching node-type IDs:

1
2
3
4
5
6
7
if (statement instanceof TSelectSqlStatement) {
    TSelectSqlStatement select = (TSelectSqlStatement) statement;
    for (int i = 0; i < select.tables.size(); i++) {
        TTable table = select.tables.getTable(i);
        // ...
    }
}
1
2
3
4
5
6
7
8
if (statement is TSelectSqlStatement select)
{
    for (int i = 0; i < select.tables.size(); i++)
    {
        TTable table = select.tables.getTable(i);
        // ...
    }
}

The cost is a large type surface. The API reference is the canonical map.

3. Backwards compatibility

Public APIs — classes, methods, property names, enum members — are stable across minor and patch releases. TGSqlParser.sqltext does not get renamed, and classes do not move between packages without a deprecation cycle.

This costs some long-term cleanliness but is essential for consumers: most use cases involve traversing a deeply nested AST, so any name change ripples through their code. It is also why the .NET edition keeps Java-style lowercase member names where they were fields, instead of renaming everything to .NET conventions.

4. Grammar tables ship inside the library

All grammar tables for every supported dialect are embedded in the parser artifact itself. There is no data directory to deploy, no grammar file to distribute alongside, and no download at first use. That is what makes the library viable in air-gapped and regulated environments.

"Self-contained" is about grammars, not about having zero dependencies

The grammar tables are embedded, and the parser never connects to a database. But the published packages do declare dependencies, so plan your dependency closure accordingly:

  • Java: the published com.gudusoft:gsqlparser:4.1.6 POM declares jakarta.xml.bind-api and jaxb-runtime, which resolve to six jars in total. JAXB is needed because the data-lineage APIs emit XML, and it was removed from the JDK in Java 11.
  • .NET: the net10.0 build has no dependencies. The netstandard2.0 build depends on System.Text.Json 8.0.5.

See offline / air-gapped installs for how to transfer the full closure.

5. Round-tripping over re-implementing

The parser is paired with a script writer that emits SQL from a possibly modified AST. The common case is consumers wanting to modify SQL — rename columns, swap dialects, redact constants — rather than only analyse it.

This is why visitors are a first-class citizen, why every node has a string representation, and why the AST is mutable in place.

6. Per-vendor isolation in the codebase

Per-vendor logic lives in per-vendor delegate classes and command tables rather than in a central dispatcher. Changing how Oracle parses a WITH clause should never affect SQL Server, and adding a vendor should not require editing shared code.

The .NET build takes the same idea to the build level: each vendor is behind its own conditional-compilation flag, so a build can omit dialects it does not need.

7. Errors are recoverable

A syntax error in one statement of a multi-statement script does not abort the others. The parser collects errors and continues, which is what lets a linter or IDE plugin report every problem in a file rather than only the first.

Errors land in getSyntaxErrors(), one entry per problem, each with line, column and token.

Errors land in the SyntaxErrors property, one entry per problem. Setting EnablePartialParsing = true additionally builds a best-effort AST around a broken statement.

8. Both editions track current runtimes

The parser JAR is Java 8 bytecode, so it runs on Java 8 through the latest LTS. Pin your compiler level to 1.8 so a modern JDK does not fall back to an unsupported default.

The library multi-targets net10.0 (recommended) and netstandard2.0 (the compatibility facade for .NET Framework 4.6.2+, Mono and Unity). It does not target netstandard2.1, netcoreapp*, or the EOL net5.0net7.0 runtimes.

The .NET Framework floor is 4.6.2 rather than 4.6.1 because the netstandard2.0 build depends on System.Text.Json 8.0.5, whose lowest .NET Framework asset is net462.

See also