Format (pretty-print) SQL¶
This page is a recipe book for the SQL formatter that ships inside General SQL Parser. If you just want to take messy SQL and pretty-print it, copy the minimum example below and you are done. Read on only when you need to change how the output looks.
Every output on this page is real program output
The AST-formatter recipes were run in their own processes against Java 4.1.6
and .NET 4.1.0.7. The fault-tolerant Java example was run against Java 4.1.9.
The results are pasted from the console. The separate-process part matters — see
the newInstance() warning.
The minimum that works¶
Three steps: parse, make an options object, format.
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 28 | |
Output:
1 2 3 4 5 6 7 8 | |
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 28 29 30 | |
Output:
1 2 3 4 5 6 7 8 | |
Project setup, if you are starting from scratch:
1 2 3 4 5 | |
Note the default style: keywords are upper-cased and right-aligned in their own
column, and values line up in a second column. Note too that name came out as
NAME — the formatter treats some unquoted words as keywords. If that surprises
you, set the casing options explicitly rather than relying on defaults.
The same input formats slightly differently in each edition
Compare the two tabs above: the schema is scott.employees on Java and
SCOTT.employees on .NET, from identical input and identical default options. The
editions classify some unquoted words differently (identifier vs non-reserved
keyword), so default casing is not portable.
If you need byte-identical output across both editions, set caseIdentifier and
caseKeywords explicitly instead of accepting the defaults.
The three moving parts of the AST formatter¶
| Type | What it is | Why you need it |
|---|---|---|
TGSqlParser |
The parser. Reads SQL text, builds an AST. | The formatter walks the AST, so the SQL must parse first. |
GFmtOpt |
Style options. 70 public fields on Java, 69 on .NET. | Everything you tune lives here. |
FormatterFactory.pp(parser, option) |
The formatter. | Returns the formatted SQL as a string. |
This is the traditional, parse-dependent pp(...) path. Java also provides a
fault-tolerant pp2(...) path for SQL that cannot produce a complete AST.
Why is the namespace pp?
The formatter began as a pretty printer, so the package/namespace and the
method pp(...) both keep the abbreviation. Read it as "pretty-print".
pp(parser, null) throws — it is not a shortcut
Passing null for the options does not return the original SQL untouched. It
throws: NullPointerException on Java, NullReferenceException on .NET. Always
pass a real GFmtOpt.
Format invalid or unparseable SQL with Java¶
GSP Java has a second formatter entry point, FormatterFactory.pp2(...). It
does not require you to call parse() first and does not require the whole
script to be syntactically valid. It divides the input into regions:
- parseable regions use the guarded AST formatter, preserving normal
pp(...)output; - malformed regions use the token-preserving lexical recovery formatter;
- valid and invalid statements can therefore be formatted in the same script.
This feature is currently Java-only. GSP .NET's FormatterFactory.pp(...)
still requires a successful parse.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
Output:
1 2 3 4 5 6 7 | |
OK_WITH_RECOVERY means output was produced without losing the malformed
region, but at least one region could not use the AST formatter. Formatting does
not validate or repair that SQL: SELECT FROM WHERE remains invalid, and its
tokens stay in their original order.
| Status | Meaning | Application action |
|---|---|---|
OK |
Every region used the guarded AST formatter. | Treat it like normal pp(...) output. |
OK_WITH_RECOVERY |
Complete output was produced, but one or more regions used fallback formatting. | Keep the output and surface getDiagnostics() when correctness matters. |
FAILED |
An internal invariant prevented usable output for at least one region. Ordinary malformed SQL should not reach this status. | Do not overwrite the input; log the diagnostics and preserve the original SQL. |
result.getRegions() also reports the source offsets, regional status, and the
actual renderer (GUARDED_AST, LEXICAL_ISLAND, or CONSERVATIVE) used for
each statement region.
Choose the overload for your application¶
Use the structured result when an editor, CI job, or service needs to distinguish normal formatting from recovered formatting:
1 2 3 4 5 6 7 8 9 | |
If you only need the text, the convenience overload accepts the familiar
GFmtOpt and returns a String:
1 2 | |
To combine the structured result with the normal 70 Java style controls, copy
them into Pp2FormatOptions:
1 2 3 4 5 6 | |
Pp2FormatOptions.from(...) copies the fields; it does not retain or mutate the
caller's GFmtOpt.
Recovery-specific controls¶
| Field | Default | Effect |
|---|---|---|
tolerantMode |
true |
Reserved for a future strict mode. The current engine always recovers ordinary parse failures and does not read this field. |
errorRegionStrategy |
PRESERVE |
Reserved strategy selector. The current engine does not yet read this field, so PRESERVE, LIGHT_FORMAT, and BEST_EFFORT currently produce the same dispatch behavior. |
maxLineWidth |
120 |
Reserved soft-wrap target. The current pp2 renderers do not read this field. |
maxErrorRegionSize |
10000 |
Caps the span tagged as an ERROR_REGION by lexical island recognition. The current layout pipeline records the boundary but does not use it to select a different renderer. |
maxRegionParseChars |
200000 |
Sends oversized regions directly to lexical recovery instead of attempting a regional parse. |
commentPolicy |
PRESERVE |
Preserves comment placement. REANCHOR and REFLOW are experimental and are not fully implemented by the region assembler. |
breakStatementsOnNewLine |
true |
Puts consecutive top-level statements that shared one physical line onto separate lines. |
showIndentMarkers |
false |
Reserved diagnostic control. The current pp2 renderers do not read this field. |
astOverlayEnabled |
false |
Enables the experimental AST-overlay annotator; it currently does not change rendered output. |
For production controls today, expose maxRegionParseChars and
breakStatementsOnNewLine. Keep the reserved and experimental fields at their
defaults until their render paths are completed.
pp2(...) reads sqltext, not sqlfilename
For a file, read its contents and assign them to parser.sqltext. Setting only
parser.sqlfilename produces an empty pp2 input. The
formatsql demo
shows file loading, dialect selection, recovery diagnostics, and the
/tolerant command-line switch.
A .NET-only trap: newInstance() is a shared singleton¶
This one will bite you in a real application, and it is the reason every recipe on this page was measured in a separate process.
| Java | .NET | |
|---|---|---|
GFmtOptFactory.newInstance() returns a fresh object |
yes | no — the same instance every time |
| Mutating the result affects later callers | no | yes |
On .NET, this sequence leaves the second format lower-cased too:
1 2 3 4 5 6 | |
So on .NET, treat the options object as global mutable state. Either build it once at startup and never change it again, or reset every field you touched before handing control back. A library that formats SQL on behalf of callers should not mutate the shared instance at all.
Reading from a file, writing the result back¶
For the traditional pp(...) formatter, set sqlfilename instead of sqltext;
the parser handles UTF-8 and UTF-16 with or without BOM, so do not pre-load the
file yourself. Java's fault-tolerant pp2(...) is the exception: it reads
parser.sqltext, as described above.
1 2 3 4 5 6 7 8 9 10 11 12 | |
1 2 3 4 5 6 7 8 9 10 11 | |
Write to a new path rather than over the input while you are still developing — a formatter bug that eats a script is much less painful when the original survives.
Picking the dialect¶
The traditional pp(...) formatter is only as good as the parse, and the parse
depends on the vendor. If your script does not parse, that is often the wrong
EDbVendor rather than a formatter problem: the error looks like
state:NNNN(10102) near: SOMETOKEN(line,col).
The dialect still matters to Java pp2(...): it drives token classification,
statement boundaries, and the regional parse attempts. Recovery lets formatting
continue after a syntax error; it does not make vendor selection irrelevant.
Java exposes 45 vendor constants and .NET 23 — see
Database Compatibility for the full
picture, including the three .NET constants that throw. Note that dbvaccess is a
distinct constant that routes to the T-SQL grammar, not an alias of
dbvmssql.
Customisation recipes¶
Every knob is a field on GFmtOpt. The style enums live in their own
namespace/package — gudusoft.gsqlparser.pp.para.styleenums — which is a fourth
import beyond the three in the minimum example. Miss it and the recipes below do
not compile.
1 2 3 | |
1 | |
Recipe 1 — lower-case keywords¶
1 2 3 | |
For select e.last_name as name, e.commission_pct comm from scott.employees as e where e.salary>1000;:
1 2 3 4 | |
1 2 3 | |
Same input:
1 2 3 4 | |
The four casing values, identical in both editions:
TCaseOption value |
Effect |
|---|---|
CoUppercase |
SELECT — the default for keywords |
CoLowercase |
select |
CoInitCap |
Select |
CoNoChange |
leave as written |
Apply them independently to five different token classes:
Field on GFmtOpt |
Affects |
|---|---|
caseKeywords |
SELECT, FROM, WHERE, JOIN, … |
caseIdentifier |
unquoted table and column names |
caseQuotedIdentifier |
identifiers in "..." or [...] |
caseFuncname |
function names such as count, sum |
caseDatatype |
VARCHAR, INT, NUMBER, … |
Recipe 2 — indentation¶
The option-setting code below is character-for-character the same in Java and C#:
1 2 3 4 | |
indentLen does not do what its name suggests
Setting indentLen = 4 produced output identical to the default in both
editions for select a, b from t where a=1;:
1 2 3 4 | |
The default layout aligns continuation lines under a keyword column rather than
indenting by a fixed number of spaces, so indentLen has no visible effect on
ordinary SELECT statements. It applies to constructs that genuinely nest —
BEGIN/END blocks, CASE expressions, sub-selects. Do not reach for it
expecting to change SELECT list alignment; use the *Style fields for that.
Recipe 3 — comma at the start of the line¶
1 2 | |
1 2 3 4 | |
1 2 | |
1 2 3 4 | |
TLinefeedsCommaOption has three values, not two:
| Value | Effect |
|---|---|
LfAfterComma |
a, at end of line (default) |
LfBeforeComma |
,a at start of line, no space after the comma |
LfbeforeCommaWithSpace |
, a at start of line, with a space |
If you want the classic comma-first style with a space, LfbeforeCommaWithSpace
is the one you want — LfBeforeComma packs the comma against the column name.
The same field exists as selectFromclauseComma, parametersComma, and
defaultCommaOption (which catches everything else).
Recipe 4 — wrap the column list instead of stacking it¶
1 2 3 | |
1 2 3 | |
1 2 3 | |
1 2 3 | |
TAlignStyle has exactly two values: AsStacked (one item per line, the default)
and AsWrapped (fill the line, then break).
Recipe 5 — line up AND / OR under WHERE¶
Same code in both languages:
1 2 | |
Identical in both editions:
1 2 3 4 5 | |
Recipe 6 — tighter spacing inside expressions¶
Same code in both languages:
1 2 3 | |
Identical in both editions:
1 2 3 | |
Recipe 7 — multi-statement scripts¶
Nothing special is required: every statement in sqltext is formatted, in order.
For select id from t1; select name from t2 where id = 1; update t1 set name = 'x' where id = 2;:
1 2 3 4 5 6 7 8 | |
Same input:
1 2 3 4 5 6 7 8 | |
Two things to notice. There is no blank line between statements — if you want
one, insert it yourself. And this is the clearest example of the casing divergence:
.NET upper-cased id and name while Java left them alone, from the same input.
With the traditional formatter, one syntax error makes parse() return non-zero.
Check getSyntaxErrors() / SyntaxErrors to find which statement failed. Java
applications that still need best-effort output can run the complete script
through pp2(...) and inspect Pp2FormatResult.getDiagnostics().
Recipe 8 — format a folder of .sql files¶
Construct a new parser per file. The parser holds the AST of the last script it parsed, and reusing one across files is more error-prone than the few microseconds a fresh instance costs.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
On .NET, hoist the GFmtOpt out of the loop rather than calling newInstance()
each time — it returns the same object anyway, so the call inside the loop is
misleading.
Comments¶
Block comments (/* ... */) and line comments (-- ...) are preserved, in both
editions. They do affect layout: a leading block comment pushes the whole statement
across, because the formatter aligns to the comment.
For /* header */ select a, -- trailing / b from t where a=1;:
1 2 3 4 | |
If comments vanish, you almost certainly modified the AST between parse() and
pp(...), which can disconnect comment tokens from the nodes that owned them.
Format first, then modify.
The full option surface¶
GFmtOpt carries 70 public fields in Java and 69 in .NET. Use the dedicated
edition reference for every field, its exact default, allowed enum values,
interactions, limitations, and edition-specific behavior:
Those pages are generated from the Javadoc and .NET XML documentation beside the fields in source. That matters for controls built on top of the library: reserved compatibility fields and unsupported output selectors are listed, but clearly distinguished from effective presentation settings.
Browse the API reference for the complete list; the field names are descriptive, so searching for the relevant word usually finds the knob.
Troubleshooting¶
parse() returned non-zero, so pp(...) did not format the SQL
The AST formatter cannot run if the parse fails. Check the vendor first —
TOP 10, CONNECT BY and LIMIT 10,20 are all dialect-specific. Then read the
error message and getSyntaxErrors() / SyntaxErrors for line and column. On
Java, use FormatterFactory.pp2(...)
when preserving and formatting malformed input is the intended behavior.
Output is empty
You called pp(...) before parse(). The AST formatter walks the tree that
parse() builds; without it there is nothing to walk. Java pp2(...) does not
need a prior parse, but it does require the input in parser.sqltext.
A NullPointerException / NullReferenceException from pp(...)
You passed null as the options argument. That is not a no-op shortcut — build a
GFmtOpt with GFmtOptFactory.newInstance().
My style change affected other parts of the application (.NET)
GFmtOptFactory.newInstance() returns a shared singleton on .NET. See
the warning above.
Output differs between the Java and .NET builds
Expected for default options — the editions upper-case unquoted identifiers
differently. Set caseIdentifier and caseKeywords explicitly for portable output.
See also¶
- Quick Start — parsing, formatting and error handling end to end
- Basic SQL Parsing — the parser side
- Advanced Features — modifying the AST and re-emitting SQL, which is a different job from formatting
- Database Compatibility — picking the vendor