Why formatted SQL is a code review problem, not a taste problem
SQL is the last part of most codebases where formatting is still argued about instead of automated. Application code goes through Prettier, gofmt, Black or rustfmt before it ever reaches a pull request. The query embedded three levels down in a repository file, or the migration someone pasted out of a database client, arrives however it happened to be typed: one 900-character line, or forty lines indented by whatever the editor felt like doing.
The cost shows up in review. A reviewer reading an unformatted query cannot see the shape of
it — which conditions belong to the ON clause and which to the
WHERE, whether that OR is inside the parentheses or outside them,
how many tables are actually joined. Those are precisely the mistakes that produce a query
that runs, returns plausible-looking rows, and is wrong. An OR that escaped its
parentheses does not throw an error; it quietly returns the whole table.
Consistent formatting also makes version control useful again. When every query in the
repository breaks lines at the same places, changing one condition produces a one-line diff.
When they do not, adding a column to a SELECT list rewrites the entire
statement in the diff and the reviewer has to read all of it again to find the change. That
is the single biggest practical argument for running SQL through a formatter: not beauty,
but small diffs.
The style conventions that fight each other
There is no standard SQL style guide, so teams inherit conventions from whoever wrote the first migration. Four disagreements come up again and again, and it is worth knowing what each side is actually optimising for.
| Choice | Argument for | Argument against |
|---|---|---|
| Uppercase keywords | Keywords stand out from identifiers without syntax highlighting — in a diff, a log, a terminal | Shouting; irrelevant when the editor colours keywords anyway |
| Leading commas | Commenting out or deleting a line never breaks the line above it | Reads oddly to anyone who learned it the other way |
| River / right alignment | Keyword column and value column separate cleanly | Every rename re-aligns the whole block, producing huge diffs |
| One condition per line | Each predicate is an independent, reviewable line | Short queries become tall for no benefit |
The leading-comma argument is the one people underestimate, so it is worth spelling out. With trailing commas, the last item in a list has no comma. Comment that last line out and the line above it now ends in a dangling comma, which is a syntax error in most engines. You have to edit two lines to remove one. With leading commas the comma belongs to the item that follows it, so every line is self-contained: comment out any line, including the last, and the remaining list is still valid. When you are bisecting a broken report by removing columns one at a time, that difference is the whole ballgame.
The counter-argument is real too — leading commas look wrong until they do not, and mixing both styles in one repository is worse than either. That is why the comma position here is a switch and not an opinion baked into the output: pick the one your team already uses and the formatter stops being a source of diff noise.
Dialect differences that actually change the formatting
Most SQL keywords are shared across engines. What is not shared is how each engine delimits things, and delimiters are exactly what a formatter has to get right. A single wrong assumption turns a column name into a string, or a string into code.
- Double quotes. In standard SQL, PostgreSQL, Oracle and SQL Server,
"total"is a delimited identifier — a column named exactly that, case included. In MySQL with its default settings,"total"is a string literal. Same three characters, opposite meanings. A formatter that assumes one of them will happily uppercase the inside of the other. - Backticks and brackets. MySQL delimits identifiers with
`name`; SQL Server uses[name], where the escape for a literal closing bracket is]]. Neither exists in the other engine, and treating[as an identifier delimiter in PostgreSQL would break array subscripts. - Comments.
--and/* */are universal.#as a line comment is MySQL only — and in SQL Server the same character starts a temporary table name,#tmp. PostgreSQL is the odd one out for block comments: it allows them to nest, so/* a /* b */ c */is one comment there and a syntax error almost everywhere else. - String escapes. Doubling the quote,
'it''s', is the standard and works everywhere. MySQL additionally honours backslash escapes; PostgreSQL only does inside anE'...'literal, and adds dollar quoting —$$ ... $$— where the delimiter is chosen by the author and the body can contain anything at all, quotes and comment markers included. - Set operators and paging. Oracle spells the difference operator
MINUS; everyone else spells itEXCEPT. Paging isLIMITin PostgreSQL and MySQL,TOP nin SQL Server, andFETCH FIRST n ROWS ONLYin the standard — three different places for a line break.
How this formatter avoids corrupting your query
Every formatter bug that matters has the same shape: something inside a literal got treated
as code. The classic report is a WHERE note = 'SELECT FROM' that comes back
reformatted, re-cased, or split across lines — a query that still runs and now matches
nothing. So the first thing that happens here is a full tokenizer pass, before any decision
about layout is made.
The tokenizer walks the text character by character with explicit state. Inside a string it knows that two consecutive quotes are an escaped quote and not the end of the literal, which is the exact point where naive implementations terminate early and corrupt everything after. Inside a line comment it knows that an apostrophe is just an apostrophe. Inside a delimited identifier it knows which closing character to look for, which depends on the dialect you selected. Only tokens classified as unquoted words are ever eligible for a case change, and only if they are in the reserved-word list.
Function names get one extra condition: they are re-cased only when a ( follows
immediately. Plenty of real schemas have a column called count, replace
or position, and there is no reason to shout at them.
Two behaviours follow from treating this as a safety problem rather than a rendering problem. First, if the tokenizer cannot finish — an unterminated string, an unclosed block comment, an identifier that never closes — nothing is formatted at all. You get your input back untouched, plus the reason. A partially formatted query that looks finished is worse than no output, because it invites you to copy it. Second, once a result is produced, it is tokenized again and compared token for token against the input. If anything was lost, duplicated or reinterpreted, the result is discarded and the original is returned. Formatting a query is a convenience; corrupting one that then runs against production is real damage, and the two are not worth trading.
Reading the structure: CTEs, subqueries, JOIN … ON and CASE
Indentation earns its keep at exactly four places, and those are the places this formatter spends its complexity on.
Subqueries and CTEs get a nested level. An opening parenthesis whose first
meaningful token is SELECT or WITH becomes a block: it opens at the
end of the current line, its contents are indented one level, and the closing parenthesis
sits alone at the indentation of the line that opened it. Parentheses that are function calls
or IN (…) lists stay inline, because breaking them adds height without adding
information. That single distinction is what makes a three-CTE query readable at a glance —
you can see where each block starts and ends without counting brackets.
JOIN … ON puts the join on its own line at clause level and the condition
one level in. When a join has several conditions, each AND lands on its own
line below the ON, so it is immediately visible whether a predicate belongs to
the join or to the WHERE — a distinction that changes results on outer joins
and is invisible in a single-line query.
CASE expressions put every WHEN and the ELSE on
their own line, with END back at the level of the CASE. A five-branch
CASE written inline is unreadable and a five-branch CASE written
this way is a small table.
Long column lists break one item per line, but only when there is more than
one item. ORDER BY created_at DESC stays on one line; a fifteen-column
SELECT becomes fifteen lines. Breaking a single-item clause is the kind of rule
that makes formatted SQL twice as tall as it needs to be.
The AND inside BETWEEN … AND … is deliberately not treated as a
boolean connector, because it is not one — it is part of the operator. Formatters that break
on it produce a dangling line that reads like a separate condition.
Why pasting production SQL into an upload-based tool is a problem
Most online formatters post your text to a server, format it there, and send the result back. For a colour picker that would be a curiosity. For SQL it is a disclosure.
A production query is a compact description of your system. It names your tables and columns,
which is a schema leak. It encodes business logic — how a customer is classified, what
counts as churn, which flag suppresses a charge. And the literals in the
WHERE clause are frequently live data: an account id, an email address, a
document number someone was investigating when the query stopped working. That combination
is what a security review calls a data disclosure, and it happens through a text box, without
a file, so nothing in a DLP pipeline notices.
The practical consequence is not that formatting is dangerous, but that the processing location decides whether it is. Here the formatter is a JavaScript module delivered with the page. It has no network code in it, and you can verify that: open the network tab, paste a query, format it, and watch nothing leave. You can also load the page, disconnect, and keep formatting. If a tool cannot survive that test, it is uploading your query, whatever its privacy page says.
What this tool deliberately does not do
Being explicit about the limits is more useful than implying they do not exist:
- It does not validate your SQL. There is no grammar and no schema, so a query that references a table that does not exist, or groups by the wrong column, is formatted without complaint. The only thing that stops it is text it cannot tokenize.
- It does not rewrite or optimise. No condition is reordered, no join is converted, no subquery is flattened. Rewriting changes the plan the engine picks, and a formatter has no business doing that.
- It does not align columns into a river. Alignment looks excellent until the first rename, at which point every line in the block changes and the diff becomes unreadable. Fixed indentation is the choice that keeps diffs small.
- It does not reindent the inside of block comments. Whatever you wrote in
/* … */is reproduced exactly, including its own line breaks. Reformatting a comment body would also make formatting non-idempotent, and formatting the same query twice must always give the same result. - It does not handle stored-procedure bodies with real structure. Long
BEGIN … ENDblocks, cursors and control flow are tokenized safely and laid out reasonably, but a statement-level formatter is not a procedural-language formatter and will not pretend to be one.
Frequently asked questions
Is my SQL sent to a server?
No. The tokenizer and the formatter are plain JavaScript that ship with the page and run inside your tab. There is no upload, no API call and no logging. That matters more for SQL than for most formats: a query usually reveals your table names, your column names and a good part of your business rules, and often carries real customer values in the WHERE clause.
Will it ever change what my query does?
It is designed not to. Formatting only rewrites whitespace and the letter case of reserved words; the content of string literals, comments and delimited identifiers is copied byte for byte. Before showing you anything, the tool re-tokenizes its own output and compares it token by token with your input. If a single token differs, the result is thrown away and your original SQL comes back with an explanation instead.
What happens if my SQL has a syntax error?
It depends on the kind of error. This is a formatter, not a parser with a grammar, so an unbalanced GROUP BY or a missing table alias will still be formatted — the tool has no way to know the query is wrong. What it does refuse is anything it cannot tokenize safely: an unterminated string, an unclosed block comment, an identifier whose closing delimiter never arrives. In those cases nothing is changed and you get a message saying why.
Which dialect should I pick?
Pick the database you actually run the query against. The dialect setting is not cosmetic: it decides what a double quote means (an identifier everywhere except MySQL, where it is a string), whether backticks and square brackets delimit identifiers, whether # starts a comment, and whether dollar-quoted strings exist. Choosing the wrong one is the fastest way to make a formatter mangle a literal.
Why does Minify keep a line break after a comment?
Because a -- comment runs to the end of the line. Collapsing a query with a line comment onto a single line would comment out everything after it, turning a working statement into a broken one. So the minifier keeps exactly one newline after each line comment and removes every other break. Block comments have no such problem and stay inline.