How the SQL is laid out
The formatter reads the script with the same tokenizer rules as ScriptDom, then only changes the whitespace between tokens and the case of keywords:
- Each clause starts a line:
SELECT,FROM, eachJOIN,WHERE,GROUP BY,HAVING,ORDER BY,UNION,SET,VALUES,OUTPUTandOPTION. - A select list with more than one column puts each column on its own line.
ANDandORinWHERE,HAVINGandONstart indented lines; theANDof aBETWEENstays put. - A
CASEwith severalWHENbranches gets one line per branch. Short ones stay on one line. - Subqueries longer than about 60 characters, and every CTE body, get their own indented block.
IN (1, 2, 3), function calls andOVER (...)stay on the line. BEGIN...END,BEGIN TRYandBEGIN CATCHindent their statements; the statement afterIF,ELSEorWHILEis indented when it is not a block.CREATE TABLEputs each column on its own line;CREATE PROCEDUREputs each parameter on its own line.- Comments stay where they were: a comment at the end of a line stays at the end of that line, and one on its own line keeps its own line. Blank lines you had are kept (one at most).
GOgets its own line: it is a batch separator only when it is alone on its line.
What the options change
| Option | What it does |
|---|---|
| Output: Minify | Writes the script on as few lines as possible: one space only where two tokens would otherwise run together (a- -1 keeps its space, or it would become a comment). -- comments become /* comments */ so they cannot swallow the rest of the line; GO keeps its own line. |
| Keywords | Upper or lower case for the reserved words (SELECT, FROM, JOIN...), built-in functions followed by ( (COUNT(, GETDATE(...) and keywords that are only keywords in their place (ROWS in OFFSET, MATCHED, NOCOUNT...). Table, column, alias and variable names are never changed. |
| Indent | 2 or 4 spaces, or a tab, per level. |
| Commas | Trailing commas end the line (a,); leading commas start the next one (, b), which makes it easy to comment out the last column. |
| Comments | Keep or remove -- and /* */ comments, including nested block comments. |
Why names keep their case
Keywords and built-in function names mean the same thing in any case. Table, column and variable names do not always: in a database with a case-sensitive collation such as Latin1_General_CS_AS, dbo.Orders and dbo.ORDERS are two different tables. So the formatter only changes the case of words that cannot be names, and a column called Year or a table called Log stays as you wrote it, even though YEAR( and LOG( are functions.
Errors
The page reports the errors that stop the tokenizer, with the error number, message, line and column that ScriptDom's TSql160Parser gives: an unclosed string (46030), an unclosed [name] or "name" (46031), an unclosed /* comment (46032) and characters T-SQL does not have, such as a backtick (46010). It also checks the parentheses; for an unclosed ( it shows where that parenthesis is. A misspelled keyword or a missing FROM is a syntax error only a full parser finds: the page formats the script anyway, and the C# below reports it.
Format SQL in C# with ScriptDom
ScriptDom (Microsoft.SqlServer.TransactSql.ScriptDom on NuGet) is Microsoft's T-SQL parser, the one SSMS, SqlPackage and SQL Server Data Tools use. TSql160Parser reads SQL Server 2022 syntax and returns every syntax error with its line and column; Sql160ScriptGenerator writes the parsed tree back as formatted T-SQL. The code follows the options above:
// dotnet add package Microsoft.SqlServer.TransactSql.ScriptDom
using Microsoft.SqlServer.TransactSql.ScriptDom;
// ScriptDom rebuilds the script from the syntax tree, so comments are not kept and the layout
// is its own. It rejects anything SQL Server would not parse.
static string FormatSql(string sql)
{
var parser = new TSql160Parser(initialQuotedIdentifiers: true);
TSqlFragment tree = parser.Parse(new StringReader(sql), out IList<ParseError> errors);
if (errors.Count > 0)
{
ParseError e = errors[0];
throw new FormatException($"Line {e.Line}, column {e.Column}: {e.Message}");
}
var generator = new Sql160ScriptGenerator(new SqlScriptGeneratorOptions
{
KeywordCasing = KeywordCasing.Uppercase,
IndentationSize = 4,
IncludeSemicolons = true,
});
generator.GenerateScript(tree, out string script);
return script;
}
Tested with ScriptDom 180.117.0 on .NET 10. The generator's layout differs from this page's (it aligns clause bodies, for one), it adds semicolons, and it drops comments, because comments are not part of the tree. To minify, pick Minify above: the code then joins the parsed tokens with single spaces.
FAQ
How do I format SQL in C#?
Parse it with ScriptDom's TSql160Parser, check the ParseError list, and write the tree with Sql160ScriptGenerator, as in the code above. SqlScriptGeneratorOptions sets the keyword casing, the indentation size and a few layout switches.
Can formatting change what my SQL does?
No. Only the whitespace between tokens and the case of keywords and built-in function names change. Strings, names and comments stay exactly as they were, and a -- comment always ends its line, so it can never comment out code that follows.
Why are my table and column names not upper-cased?
In a database with a case-sensitive collation, Orders and ORDERS are different tables. Keywords and built-in function names are never affected by collation, so only those change case.
Does it work for MySQL or PostgreSQL?
The tokenizer follows T-SQL, so plain SELECT, INSERT, UPDATE and DELETE statements format fine, but MySQL backticks and PostgreSQL dollar-quoted strings are reported as errors.
Why does SQL Server say "Incorrect syntax near" when this page formats my script?
The page checks tokens and parentheses, not the whole grammar. Run the C# above: ScriptDom parses the full T-SQL grammar and lists each syntax error with its line and column.
Is my SQL uploaded or saved?
No. The formatter runs in your browser. Only the settings are remembered, never the SQL.