cmd / pgfmt
pgfmt is a small SQL formatter for Postgres query files.
For package-local SQL query layout and embedding, see
go / postgres. For a static check
over the same query files, see cmd / nullscan.
I run it on */queries/*.sql so SQL style stays consistent
across code reviews.
Why
When SQL lives in many package-local directories, style drifts fast. Diffs get noisy. Reviews spend time on formatting.
A formatter keeps diffs focused on behavior.
Install
pgfmt lives in its own module. Add it to a project as a module tool:
go get -tool github.com/croaky/pgfmt/cmd/pgfmt
That writes a tool line to go.mod next to the require line that
pins the version:
tool github.com/croaky/pgfmt/cmd/pgfmt
Why go tool
Go 1.24 added the tool directive. go tool <name> builds and runs the
pinned version from the module cache. Local commands and CI use the same
version from go.mod, unlike go install ...@latest. Managing pgfmt as a
module dependency also lets several repos share one implementation, rather
than copying code into a local cmd/pgfmt.
There is no separate install step and nothing to put on $PATH. A fresh clone
can run the formatter after go mod download. A tool upgrade appears in git
history instead of changing silently after someone re-runs go install.
Go caches the binary after the first build, so go tool pgfmt runs as fast as
a direct binary.
Command
Format in place:
go tool pgfmt -w <pkg>/queries/*.sql
Format a named file to stdout:
go tool pgfmt query.sql
Format stdin to stdout:
cat query.sql | go tool pgfmt
CI behavior
The -c flag checks formatting, exits with an error if a file needs
changes, and writes nothing. It prints the exact command to fix each
file:
FAIL: <path> needs formatting. Run: pgfmt -w <path>
The -c flag reports only real formatting violations, so it does not require
a clean working tree. -c and -w are mutually exclusive.
In a cibot Checkfile, that is one line:
sqlfmt: git ls-files -z '*queries/*.sql' | xargs -0 go tool pgfmt -c
Style it enforces
- Uppercase SQL keywords. Type names (
bigint,text,interval) stay lowercase. - Lowercase function names.
- Two-space indentation.
- An 80-column limit decides when a line stays inline or wraps.
- Vertical clause lists (
SELECT,FROM,WHERE,ORDER BY). - Stable wrapping for common constructs (
CASE,NOT EXISTS, joins). - Paired-argument functions (
jsonb_build_object) wrap two arguments per line.
DDL keywords like index, key, and add become uppercase only inside
CREATE/ALTER/DROP. In DML, where they are often column names,
they stay lowercase.
pgfmt is idempotent. Running it twice produces the same output.
Safety
After formatting, pgfmt parses its output and compares the token
stream to the input. If they differ, it fails and writes nothing. The
formatter cannot silently corrupt a query.
Comments are the one exception. pgfmt keeps standalone comments on their own
line, but drops trailing comments and comments inside parentheses.
Workflow
For package-local queries:
- Write or edit SQL in
<pkg>/queries/*.sql. - Run
go tool pgfmt -w <pkg>/queries/*.sql. - Commit the formatted SQL with the Go call-site changes.