Advanced Features¶
Complete the learning path by turning parsed SQL into reusable analysis and transformation workflows. The examples cover both SDK editions where their APIs or traversal behavior differ.
What you will learn¶
- Modify a parsed AST and re-emit the SQL two different ways
- Walk the AST with a custom visitor
- Split multi-statement scripts without corrupting procedure bodies
- Apply performance techniques that match how the parser actually behaves
Before you begin¶
- Completed all previous tutorials in the series
- Working knowledge of Java or C#, and of SQL
1. Modify the AST, then re-emit the SQL¶
There are two ways to get SQL back out after mutating the tree, and they give noticeably different results. Rename a table by rewriting the token its name was lexed from, then compare both.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | |
Output, identical in both editions:
1 2 3 4 5 6 | |
Both applied the rename. The difference is everything else:
| Approach | Result |
|---|---|
TScriptGenerator.generateScript(node) |
Rebuilds SQL from the tree. Normalises keywords to lower case and puts clauses on their own lines. Your original formatting and comments are gone. |
Re-emitting sourcetokenlist |
Byte-for-byte the original text with only your edit applied. Comments, casing and whitespace survive. |
Pick the token list for rewrites, the generator for canonical output
If your job is "change one thing in the customer's SQL and hand it back",
re-emit the token list. generateScript is for when you want normalised output
and do not care about the input's shape.
2. Custom visitors¶
Subclass TParseTreeVisitor and override the preVisit overload for the node type
you care about. There are hundreds of overloads (949 in Java, 528 in .NET), one per
node type, and the framework dispatches on runtime type.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Output:
1 2 3 4 | |
Note join_expr: a join node also presents as a TTable, so a naive collector
picks up a pseudo-table. Filter it out, or check the table type before adding.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Output:
1 2 | |
Visitor traversal is NOT the same in both editions
The same visitor over the same SQL gives different answers, measured on Java 4.1.6 and .NET 4.1.0.7:
| Java | .NET | |
|---|---|---|
acceptChildren reaches the subquery's banned_users |
yes | no |
surfaces a join_expr pseudo-table |
yes | no |
accept (instead of acceptChildren) |
visits nodes | visits nothing at all |
Two consequences worth internalising before you build on this:
- Do not assume a visitor sees subqueries on .NET. It does not. Recurse
explicitly through
stmt.Statements, or readstmt.tablesper nested statement. - Use
acceptChildren, notaccept. On .NET,acceptreturned zero visits in every case tested, so a walk built on it silently does nothing.
stmt.tables has the same gap on .NET: for the SQL above it contains only users
and posts. If you need every table including subqueries, walk the nested
statements yourself — see
Extract Table Names in the quick start.
3. Split a multi-statement script safely¶
A common instinct is to split a script on ;. Do not — it cuts procedure
bodies in half, because the statements inside BEGIN ... END; contain their own
semicolons. Use the parser's own splitter, getrawsqlstatements(), which
understands the grammar.
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
Output:
1 2 3 4 | |
1 2 3 4 5 6 7 8 9 10 11 | |
Output:
1 2 3 4 | |
Two statements, correctly. The naive split produces four or five fragments
depending on trailing-semicolon handling, and neither the procedure nor its body
survives intact. This is not a theoretical concern — splitting on ; has produced
measurably wrong dialect-coverage numbers in our own tooling.
4. Performance¶
The full picture, with measured numbers, is in Performance Considerations. The two things that matter most here:
The expensive step is the first parse, not constructing parsers
Constructing a second parser costs about 3 µs on Java and 218 µs on .NET.
The first parse() in the process costs 1.8 s on Java and 0.6 s on .NET,
because it initialises the grammar tables once.
So warm up at startup, off the request path, and do not build a pooling layer expecting it to recover the cold-start cost — it cannot.
Reuse a parser across a batch because it keeps the code simple:
1 2 3 4 5 6 | |
1 2 3 4 5 6 7 | |
Tokenise when you only need lexical information — measured 7.5× faster on Java and 9.4× on .NET than a full parse:
1 2 3 4 5 6 | |
1 2 3 4 | |
TGSqlParser is not thread-safe — give each thread its own instance via
ThreadLocal. See
Performance Considerations for the threading and
memory guidance.
What you can do now¶
You learned how to:
- Rename a table by rewriting its source token, and choose between
generateScript(normalised) and token-list re-emission (source-preserving) - Write a
TParseTreeVisitorsubclass, and filter thejoin_exprpseudo-table - Recognise that visitor traversal differs between the editions, and that
acceptvisits nothing on .NET - Split multi-statement scripts with
getrawsqlstatements()instead of on; - Warm up rather than pool, because the cost is the first parse
Put these patterns into production¶
- The How-to Guides for specific scenarios
- Software Architecture for the bigger picture
- Performance Considerations for the measured numbers
- API Reference for the full surface