Modify a SQL AST and regenerate SQL safely¶
Use General SQL Parser (GSP) when an application must change SQL without brittle string replacement. GSP parses vendor SQL into a structured abstract syntax tree (AST), lets your code inspect and modify specific nodes, and regenerates a valid SQL statement from the changed tree.
The runnable demo on this page builds a small SQL policy and rewrite layer
between user-submitted SQL and the database. It accepts one SELECT, removes a
sensitive output column, adds a server-controlled tenant filter, regenerates the
statement, and parses the result again before it can be executed.
View the Java source View the .NET source
What you will get
The Java and .NET projects are complete console applications with automated tests. They run offline, require no database connection, and use a bind placeholder for the tenant value. Open either project to run the complete implementation, inspect its tests, and adapt the policy to your application.
The problem this demo solves¶
Suppose a report builder, analytics endpoint, AI assistant, or customer-facing application accepts a query before sending it to a database. Passing that SQL straight to the database gives the application no structured place to enforce its own rules.
This demo applies three rules before database execution:
- The input must parse as exactly one
SELECTstatement. - The result must not expose the
internal_notecolumn. - Every accepted query must be restricted by
o.tenant_id = ?, where trusted application code supplies the bind value.
The sample starts with this query:
1 2 3 4 5 6 7 | |
The regenerated statement is equivalent to:
1 2 3 4 5 6 7 | |
Two changes are visible: o.internal_note is gone, and the tenant predicate is
present. The parentheses are just as important. Appending text carelessly could
produce A OR B AND tenant, which SQL evaluates as A OR (B AND tenant) and
could expose rows from another tenant. The demo creates a parenthesis AST node
around the original OR expression before attaching the new AND node.
Why modify the AST instead of the SQL string?¶
SQL text is not a flat sequence of interchangeable words. The same identifier can appear in a projection, predicate, alias, comment, string literal, nested subquery, or vendor-specific construct. A regular expression cannot reliably tell those roles apart.
| Requirement | String or regular-expression rewrite | GSP AST rewrite |
|---|---|---|
| Remove only an output column | Can also alter literals, aliases, or nested text | Removes a node from the result-column list |
| Add a tenant predicate | Must reproduce SQL operator precedence manually | Combines expression nodes and preserves grouping |
| Reject a second statement | Delimiter splitting breaks on procedural SQL and literals | Checks the parsed statement list |
| Support vendor dialects | Requires custom text rules for each grammar | Parses with the selected EDbVendor grammar |
| Produce auditable decisions | Usually records text before and after | Can record statement types, nodes, and policy outcomes |
AST modification makes the transformation explicit: your policy targets a
TResultColumnList, TWhereClause, or TExpression, not an accidental text
match. GSP then serializes the modified tree with toScript() in Java or
ToScript() in .NET.
How the pre-execution workflow works¶
flowchart LR
A[User or generated SQL] --> B[Parse with the selected GSP dialect]
B --> C{One allowed SELECT?}
C -- No --> R[Reject and record the reason]
C -- Yes --> D[Inspect statement, columns, and expressions]
D --> E[Apply trusted policy changes to AST nodes]
E --> F[Regenerate SQL from the AST]
F --> G{Reparse and validate output?}
G -- No --> R
G -- Yes --> H[Bind trusted values and send to database driver]
GSP performs the parse, AST access, modification, and regeneration in the application process. No database metadata or connection is required for this demo. The application owns the policy decision and only hands the regenerated statement to its database layer after every check succeeds.
1. Parse and classify before changing anything¶
The demo creates a TGSqlParser for the intended database dialect and parses
the complete input. It fails closed when parsing fails, when more than one
statement is present, or when the statement is not a SELECT.
This first gate prevents a caller from hiding a second DELETE, UPDATE, DDL,
or procedural statement after an apparently acceptable query. Production rules
can narrow the accepted SELECT shape further by checking tables, functions,
subqueries, set operators, joins, and clauses.
2. Inspect and modify the result-column list¶
The demo walks the TResultColumnList, identifies the restricted projection,
and removes that result-column node. Other occurrences of the same text are not
globally replaced. A production policy can use schema metadata and identifier
resolution to distinguish identically named columns from different tables.
3. Build the new boolean expression as nodes¶
The existing WHERE condition is a logical_or_t expression. The demo makes
that expression the child of a parenthesis_t node, parses the trusted tenant
predicate into another expression, and joins the two with a logical_and_t
node. If no WHERE clause exists, the same policy can add one through the AST.
The predicate text is controlled by the application. The tenant value remains
? so the database driver can bind it separately; user data is never
concatenated into generated SQL.
4. Regenerate and validate again¶
Calling the script generator serializes the changed AST back to vendor SQL. The
demo immediately feeds that output to a new parser instance and requires it to
parse as one SELECT again. This second parse is a useful invariant at the
boundary between transformation and execution: if regeneration or a custom
rule produces an invalid statement, nothing reaches the database.
Clone and run the complete demo¶
Choose the edition used by your application. Both commands build against the published trial package and run the same policy scenario.
1 2 3 4 | |
The implementation is in
ModifySqlAst.java,
with its behavior tests in
ModifySqlAstTest.java.
1 2 3 4 | |
The implementation is in
modifySqlAst.cs,
with its behavior tests in
ModifySqlAstTests.cs.
The important console messages are the policy decisions, regenerated SQL, and:
1 | |
Run the focused tests after changing a policy rule:
1 | |
1 | |
Where this pattern adds value¶
Check and rewrite customer SQL before execution¶
Put the policy gate in the request path before the repository, ORM raw-SQL method, JDBC command, or ADO.NET command. It can reject unsupported statement types, inspect the parsed structure, apply approved rewrites, and emit an audit record before the database sees the query.
This is useful for embedded query consoles, self-service analytics, database administration products, AI-generated SQL, and any application that accepts SQL from a less-trusted boundary. The benefit is a consistent application-level decision point rather than scattered string checks immediately before execute.
Enforce tenant or account scope¶
A multi-tenant service can add a tenant, organization, account, region, or
workspace predicate to every eligible query. Because GSP exposes the boolean
expression tree, the service can preserve AND/OR precedence and handle an
existing WHERE clause deliberately.
For production use, validate table identity and aliases before adding a predicate, decide how subqueries and CTEs inherit scope, and keep the actual tenant value in a bound parameter supplied by trusted server state.
Remove or mask sensitive output columns¶
Report builders and data products can inspect the result-column AST before a
query runs. A policy may remove restricted columns, replace an expression with
a masking function, or reject SELECT * when the visible schema cannot be
proven. This reduces accidental exposure and lets the application explain
which projection violated policy.
Apply schema migrations without hand-editing SQL¶
Migration tools can rename tables and columns, replace deprecated functions, add schema qualifiers, or update clauses across stored query libraries. Node- level changes avoid modifying comments and string literals that merely contain the old name, and regenerated SQL can be reparsed and regression-tested before deployment.
Build query fixers, optimizers, and governance rules¶
An editor or CI rule can detect an undesirable AST shape and offer a safe fix: add a required predicate, remove a forbidden projection, normalize a function, or replace a vendor-specific construct. The original node location and policy decision can power diagnostics, previews, approval workflows, and change logs.
Integrate the pattern into your application¶
Keep the rewrite boundary small and explicit:
- Select
EDbVendorfrom trusted configuration, not from the SQL text. - Parse the complete request and reject syntax errors or disallowed statement counts and types.
- Evaluate allowlists for schemas, tables, functions, clauses, and query shape.
- Apply only server-owned rewrite rules to known AST nodes.
- Regenerate, reparse, and rerun the policy checks against the output.
- Bind tenant and user values through the database driver.
- Record the rule version, decisions, and a protected form of the original and rewritten SQL for diagnostics or audit.
- Execute with a least-privilege database identity and enforce time and resource limits.
The demo exposes rewrite(...) in Java and Rewrite(...) in .NET so the flow
can be called from a web handler, query service, background job, IDE feature, or
data product. Replace the sample rule constants with immutable policy
configuration and return a domain result that distinguishes rejection from
parser or infrastructure failure.
Security boundary and production hardening¶
An AST policy layer is valuable because it creates a structured, testable, and auditable decision point before execution. It is not, by itself, a complete SQL security system.
| The AST gate can help with | Keep these controls as well |
|---|---|
| Statement-type and statement-count restrictions | Prepared statements and parameter binding |
| Table, column, function, and clause allowlists | Least-privilege database users and roles |
| Tenant-filter and projection rewrites | Database authorization and row-level security |
| Rejecting syntax or unsupported query shapes | Query timeout, row, memory, and cost limits |
| Recording policy decisions and rewritten structure | Authentication, audit storage, monitoring, and incident response |
Before using a custom rewrite in production, add tests for nested subqueries,
CTEs, set operations such as UNION, quoted and case-sensitive identifiers,
aliases, wildcard projections, comments, functions, vendor-specific syntax,
and statements with no WHERE clause. Fail closed whenever your code cannot
prove that the required rule was applied to the intended node.
Next steps¶
- Review the basic expression AST
and
SELECTstatement nodes. - See Error Handling for parse failures and validation strategies.
- Use Format SQL when the goal is presentation rather than a semantic rewrite.
- Browse all runnable examples in gsp_demo_java and gsp_demo_dotnet.