Parsing and Formatting SQL in JavaScript โ Without a Grammar File
Building a SQL formatter sounds like it requires a full parser. For most practical cases, it doesn't. Here's a keyword-based approach that handles 90% of real-world SQL.
The Core Idea
SQL formatting is mostly about:
- Uppercasing reserved words
- Adding newlines before clauses
- Indenting sub-clauses
A full AST parser is overkill for formatting. Instead, tokenize by keywords and apply rules.
Tokenization
const KEYWORDS = [
'SELECT', 'FROM', 'WHERE', 'JOIN', 'LEFT JOIN', 'RIGHT JOIN',
'INNER JOIN', 'ON', 'GROUP BY', 'ORDER BY', 'HAVING', 'LIMIT',
'INSERT INTO', 'VALUES', 'UPDATE', 'SET', 'DELETE FROM',
'CREATE TABLE', 'DROP TABLE', 'ALTER TABLE', 'WITH', 'UNION'
];
function tokenize(sql) {
// Sort by length descending to match longer keywords first
const sorted = [...KEYWORDS].sort((a, b) => b.length - a.length);
const pattern = sorted.map(k => k.replace(/ /g, '\s+')).join('|');
const regex = new RegExp(`(${pattern})`, 'gi');
return sql.split(regex).filter(t => t.trim());
}
Formatting Rules
const NEWLINE_BEFORE = new Set([
'SELECT', 'FROM', 'WHERE', 'GROUP BY', 'ORDER BY',
'HAVING', 'LIMIT', 'UNION'
]);
const INDENT_BEFORE = new Set(['AND', 'OR', 'ON']);
function format(sql) {
const tokens = tokenize(sql);
let result = '';
let indent = 0;
for (const token of tokens) {
const upper = token.trim().toUpperCase();
if (NEWLINE_BEFORE.has(upper)) {
result += `
${upper}`;
} else if (INDENT_BEFORE.has(upper)) {
result += `
${upper}`;
} else if (upper === 'JOIN' || upper.endsWith(' JOIN')) {
result += `
${upper}`;
} else {
result += ` ${token.trim()}`;
}
}
return result.trim();
}
Handling Strings and Comments
The trickiest part: don't uppercase keywords inside string literals or comments.
function stripStrings(sql) {
const placeholders = [];
const stripped = sql.replace(/'([^']*)'/g, (match) => {
const idx = placeholders.length;
placeholders.push(match);
return `__STR${idx}__`;
});
return { stripped, placeholders };
}
function restoreStrings(sql, placeholders) {
return sql.replace(/__STR(\d+)__/g, (_, i) => placeholders[i]);
}
Minification
The reverse operation โ remove all whitespace and produce a single line โ is simple:
function minify(sql) {
return sql.replace(/\s+/g, ' ').trim();
}
Try the formatter at toolzip.app/tools/sql-formatter.