SQL Formatter

Slow-query logs and ORM output arrive as a single unbroken line, which is exactly the form in which nobody can find the missing join condition. This splits it at clause boundaries and indents subqueries by paren depth.

SQL Formatter — Break a One-Line Query into Readable ClausesBuildFigure

Where the line breaks go

The input is tokenised first, then reassembled. A new line starts before each top-level clause keyword — SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY, LIMIT, OFFSET, INSERT INTO, VALUES, UPDATE, SET, DELETE, the CREATE and ALTER forms, WITH, UNION and friends, plus the dialect-specific RETURNING, ON CONFLICT and ON DUPLICATE KEY UPDATE. JOIN variants sit at the same depth as FROM. AND and OR inside a WHERE start their own indented line, which is what makes a long predicate readable.

Parentheses are treated two ways depending on what follows them. An open paren followed by SELECT or WITH is a subquery, so the depth increases and the contents indent. Any other open paren is a function call or a grouping, and stays inline — COUNT(*) does not become three lines. CASE increases depth and each WHEN and ELSE gets its own line, with END closing back out. One targeted exception: the AND in BETWEEN a AND b joins two operands rather than two predicates, so it is left inline; the formatter looks backwards a short distance for a BETWEEN before deciding.

What is protected from rewriting

Single-quoted strings, double-quoted and backtick-quoted identifiers, bracketed identifiers in the SQL Server style, and PostgreSQL $$ blocks are each captured as one token and emitted byte-for-byte. Keyword casing therefore never reaches inside a literal: a row containing the text 'select' comes out exactly as it went in. Doubled quotes inside a string ('it''s') are handled as an escape rather than as a terminator.

Comments survive the formatting direction. A -- or # line comment ends its line, and a /* */ block comment stays inline where it was. The collapse direction drops both, which is worth knowing if your query carries optimiser hints — Oracle and MySQL hints ride inside /*+ ... */ comments, so collapsing a hinted query silently changes its plan.

Keyword casing and the column-name problem

SQL keywords are case-insensitive, so this is purely a legibility preference. Uppercase keywords make the skeleton of a query visible at a glance against lowercase identifiers, which is why most style guides land there; lowercase is easier to type and increasingly common in codebases where the SQL is generated anyway.

The conversion only touches words in a fixed keyword list, so ordinary identifiers like user_id and created_at pass through unchanged. The exception you will actually hit is a column that shares a name with a keyword — date, key, index, count, text, range. Those get cased along with everything else, because a tokeniser without a schema cannot tell a column named date from the type DATE. Quote such columns in the source and they will be protected, or set the casing option to leave alone.

Limits worth knowing before you trust the output

There is no parser here and no validation, so invalid SQL comes back neatly indented and still invalid. Procedural code is where the heuristics run out: BEGIN … END blocks in stored procedures, triggers, PL/pgSQL and T-SQL control flow get the same generic clause treatment as a plain SELECT, which usually produces something readable but not something idiomatic. Deeply nested window function clauses similarly come out formatted but not artfully so.

Collapsing to one line is safe for meaning — it drops comments and squeezes whitespace, neither of which changes results — with the hint caveat above. If you are pasting into a JSON or YAML field, collapse first: an embedded newline in a config value causes more trouble than an unreadable line does.

Questions people ask

My column names got uppercased.

They match keywords. date, key, index, count, text, range and a few dozen others are in the keyword list, and without a schema there is no way to tell your column from the reserved word. Wrap them in double quotes or backticks in the source, which also protects them from the tokeniser, or switch the casing option to leave alone.

Can I paste several statements at once?

Yes. Semicolons split statements, and each one starts fresh at depth zero with a blank line between. The statement count in the results is just a semicolon count, so a semicolon inside a string literal will not inflate it but a trailing one on the last statement is not counted twice either.

Does collapsing change what the query does?

Not in terms of results — whitespace and comments are not semantically meaningful in SQL. The one real risk is optimiser hints, which live inside comments: MySQL and Oracle read /*+ ... */ as instructions, and dropping them can change the execution plan without changing the result set. If your query has hints, format it, do not collapse it.

Will it handle a CTE with several WITH clauses?

WITH is treated as a top-level clause and the parenthesised body of each CTE is recognised as a subquery, so it indents. Chained CTEs separated by commas are formatted, though the comma before each subsequent CTE name follows the general comma rule rather than a CTE-specific one. It reads fine; it is not going to match a hand-tuned house style exactly.

Related