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
without review bikeshedding.
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, and go tool <name> builds and runs
the pinned version from the module cache. Compared to
go install ...@latest, the version a person runs and the version CI
runs are the same version, because both come from go.mod. Compared to
a repo-local cmd/pgfmt invoked with go run ./cmd/pgfmt, the
formatter is a dependency rather than a copy, so several repos share one
implementation and upgrade by a go get -u commit.
There is no separate install step and nothing to put on $PATH. A fresh
clone can run the formatter after go mod download, and a formatter
upgrade shows up in a diff instead of appearing one morning after
somebody re-ran go install.
The binary is cached after the first build, so go tool pgfmt costs
about what running the binary directly costs.
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 non-zero if any file would
change, and writes nothing. It prints the exact command to fix each
file:
FAIL: <path> needs formatting. Run: pgfmt -w <path>
-c reports only real formatting violations, so it does not need 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 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 re-lexes its own 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. Standalone comments on their own line survive; trailing comments and comments inside parentheses are dropped.
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.