SQL Formatting in CI/CD: Checks and Auto-fix
The useful CI/CD question for a SQL formatter is which files and configuration are authoritative, when to write changes, and what makes a job fail. Developers can use write locally; CI commonly checks and fails on a diff. Both must use the same dialect and configuration for reproducible results.
1. Problem: formatting drift needs a repeatable gate
When the same repository uses different formatter settings, formatting diffs return on every change. CI should decide whether a file already matches formatter output, not silently overwrite a developer's checkout. Write and read-only check are separate modes.
2. Minimal reproduction: check and write have different effects
This function injects a formatter rather than assuming a particular CLI or flag. Check reports a difference; write returns new content. A CI script can turn that result into its exit status.
type Format = (sql: string) => string;
type Mode = "check" | "write";
function processSql(source: string, mode: Mode, format: Format) {
const formatted = format(source);
const changed = formatted !== source;
return { changed, output: mode === "write" ? formatted : source };
}
const format = (sql: string) => sql.trim() + "\n";
console.log(processSql("SELECT 1", "check", format)); // changed: true, original output
console.log(processSql("SELECT 1", "write", format)); // formatted output3. Root cause: CI must fix its file scope and formatter config
Without fixed globs, dialect, line endings, and configuration version, local and CI results drift. Decide whether migrations, generated files, fixtures, and embedded SQL are in scope; files outside the scope should not be rewritten implicitly.
4. Recommendation: local write, CI check, diff means failure
Let developers run write before review. CI should run check, fail when formatter output differs, and print a diff or file list. Pin the formatter, dialect, and config. CI should not commit or overwrite the checkout as an unannounced repair.
5. Complete code: summarize failures and writes by mode
This example processes in-memory files to show CI logic. Repository reads, diff rendering, and exit codes belong to the project script; the function does not depend on one formatter CLI.
type Format = (sql: string) => string;
type Mode = "check" | "write";
type File = { path: string; content: string };
function formatFiles(files: File[], mode: Mode, format: Format) {
const changed: File[] = [];
const output = files.map((file) => {
const next = format(file.content);
if (next !== file.content) changed.push(file);
return mode === "write" ? { ...file, content: next } : file;
});
return { changed, output, ok: mode === "write" || changed.length === 0 };
}
const format = (sql: string) => sql.trim() + "\n";
console.log(formatFiles([{ path: "query.sql", content: "SELECT 1" }], "check", format).ok); // false6. Incorrect approaches
Writing files unconditionally in CI makes the workspace differ from the commit. Checking only some SQL creates false green jobs. Replacing a file with empty output after a formatter error hides parser or dialect failures. Mixing formatter checks with unrelated deployment or monitoring templates also obscures the reason for failure.
7. Boundaries: generated files, dialects, and exceptions
Decide whether migrations, snapshots, fixtures, SQL in comments, and multi-dialect directories are in scope. Exceptions need reviewable configuration rather than scattered path checks. An unknown vendor extension should fail with a location or be placed on an explicit retain list.
8. Verification
Test already formatted files, an extra space, line-ending changes, a wrong dialect, and a formatter exception. Assert that check fails on differences or errors, write changes only expected files, failure output includes paths, and CI does not commit generated results.
9. FAQ
Should CI auto-fix and commit? Reporting a diff is often easier to review; auto-commit requires an explicit repository policy.
Should formatting mismatch and formatter failure differ? Yes; the latter may be a parser or dialect problem, while the former is a style diff.
Why do local and CI results differ? Compare formatter version, dialect, config, line endings, and file scope.
10. Summary
A SQL formatter CI/CD gate fixes configuration and scope, uses write locally and read-only check in CI, and fails predictably on differences or formatter errors. Print paths and diffs, never silently rewrite or commit, and record dialect exceptions separately.
Sources and further reading
Specifications and references for checking the article’s technical details.
- PostgreSQL Documentation — SQL Syntax
PostgreSQL Global Development Group
- RFC 8259 — The JavaScript Object Notation (JSON) Data Interchange Format
RFC Editor
Related articles
SQL Formatting: Dialects and Syntax Compatibility
Explain why a SQL formatter needs a declared dialect and how PostgreSQL, MySQL, and SQLite keywords, functions, quoting, and syntax affect fidelity.
How it worksSQL Formatter: Tokens, Highlighting and Indentation
Explain how a SQL lexer produces tokens for highlighting and indentation, and why a highlighter is not a complete SQL parser.
Best practicesSQL Dump to JSON: Parsing, Redaction and Batching
Transform SQL dump INSERTs into test JSON fixtures by isolating parser, redaction, batching, and SQL type constraints.
Keep exploring
Use the related browser tool to test the workflow with your own sample data.
Open related tool