A SQL formatter for Postgres and SQLite.
Get familiar with its code style in the playground. The playground works in any modern browser, and your input stays on your machine.
You can use mise to download a precompiled binary from the latest Github release.
mise use -g github:aslilac/squill
squill fmt --check .A Nix flake is available if you're a Nix fan.
nix shell github:aslilac/squillCargo can clone the source, checkout the latest release tag, and install the binary in a single command.
cargo install --git https://github.com/aslilac/squill.git --tag v0.6.0 cli --bin squillEvery built-in grammar for embedded SQL is its own cargo feature (rust, go, python, javascript, typescript, gleam, cxx, csharp, java, kotlin, swift), as is external-grammars, which loads grammars from .wasm files at runtime (and needs cmake to build). All are on by default; for a smaller binary, pick just the ones you need:
cargo install --git https://github.com/aslilac/squill.git --tag v0.6.0 cli --bin squill --no-default-features --features rust,gosquill init # write a starter squill.toml: pick languages to format embedded SQL in, and dialects
squill fmt --check .
squill . # fmt is the default command: `squill .`, `squill -` (stdin), `squill --check .`Set in the nearest squill.toml or .config/squill.toml (or squill.yaml / squill.yml, with the same keys; squill init --yaml writes one), overridable with flags. The search upward stops at a git repository root, a mount point, or a symlinked directory, so a config outside a checkout never reaches inside it. The defaults are the house style; everything is optional.
| key | flag | values | default |
|---|---|---|---|
dialect |
--dialect |
postgres | sqlite |
postgres |
indent |
--indent |
tabs | spaces |
tabs (embedded: the host file's) |
indent-width |
--indent-width |
width of one level (and tab measure) | 2 (embedded: the host file's) |
max-width |
--max-width |
target line width, 20 to 500 | 80 |
keyword-case |
--keyword-case |
lower | upper |
lower |
quote-idents |
--quote-idents |
as-needed | always |
as-needed |
trailing-semicolons |
--trailing-semicolons |
always | none: whether the last statement ends in ; |
always (none for embedded SQL) |
line-ending |
--line-ending |
lf | crlf, never detected from the input; lines inside a string, or a statement squill can't parse, keep theirs; in embedded SQL, a host may keep them in the string's value |
lf |
at-params |
--at-params |
lex sqlc-style and ADO.NET-style @name parameters |
false |
question-params |
--question-params |
lex JDBC-style ? and JPA-style ?1 parameters in Postgres |
false |
colon-params |
--colon-params |
lex :name parameters (SQLAlchemy, Spring, JPA, sqlx) in Postgres; : inside […] is still a slice |
false |
pyformat-params |
--pyformat-params |
lex Python DB-API %s / %(name)s parameters |
false |
ignore |
--ignore |
glob patterns to skip when recursing | [] |
frozen |
--frozen |
glob patterns that are immutable once on the baseline ref | [] |
frozen-ref |
--frozen-ref |
the baseline ref frozen compares against |
discovered from the remote |
frozen-fetch |
--frozen-fetch / --no-frozen-fetch |
let frozen ask the remote for its HEAD when it isn't recorded locally |
true |
Flags with no config key: --check (print diffs and exit 1 if any file would change), --stdin (or - as the path) / --stdout, --stdin-filepath <path> (read stdin as the file at that path, for editors: its config, rules, and ignores apply, and it need not exist yet), --strict (fail when anything was left unformatted), --locked (fail rather than record a grammar URL squill.lock doesn't have, for CI), --no-config, --version / -V, and --help / -h.
When recursing directories, squill honors .gitignore and skips hidden files; the ignore key and repeated --ignore flags skip more, with *, **, ?, [abc], and {a,b} glob syntax. Explicitly listed files always format.
Some files can't be rewritten after they ship, even into an identical-meaning form. sqlx records a checksum of every migration and refuses to run when one changes — but a new migration should still be formatted, and ignoring the whole directory gives that up.
frozen marks paths that squill formats only while they are new:
frozen = ["migrations/**"]A matching file is skipped once it exists in the baseline ref's tree, so the shape it shipped in is the shape it keeps — including when squill's own style changes later, and including when you adopt squill on a codebase whose existing migrations were never formatted. New files under the same globs format normally.
This is not ignore: it applies to files named explicitly on the command line too, since the point is that they are never rewritten. If a frozen path can't be checked — no git repository, or a baseline ref that doesn't resolve — squill stops with an error rather than guess, because guessing wrong is the failure the setting exists to prevent.
The baseline is read with one git ls-tree per repository, and only the ref's tip is needed — so a CI checkout at fetch-depth: 1 is enough.
Every baseline comes from the remote, never from a guess about which branch is which. In order: frozen-ref if you set one; then refs/remotes/<remote>/HEAD, which git clone records; then a --depth=1 fetch of the remote's HEAD. Whatever your default branch is called, it just works.
That last step is why CI works unchanged. A pull_request checkout has no base-branch ref at all, and a push build of a topic branch has exactly one remote-tracking ref which is the topic branch — taking it as the baseline would freeze migrations that never shipped. Only the remote can tell those apart.
--no-frozen-fetch (or frozen-fetch = false) keeps squill off the network, for an offline or air-gapped build. A normal clone still works, because git clone already recorded the remote's HEAD. When nothing authoritative is available, squill stops and asks for frozen-ref rather than inferring one.
Editors pipe the buffer through squill fmt --stdin-filepath <path>, which formats it as that file would be formatted on disk (config, rules, embedded SQL and all). Setups for Helix, Zed, and VS Code are in the editor docs.
Rules scope settings to paths. Each needs an include list of globs (relative to the config, like ignore); every rule whose include matches a file applies, in file order, later rules winning key by key.
[[files]] rules cover plain SQL. *.sql files always format; a rule sets options for the files it matches and brings in files by other names, so a mixed-dialect tree needs one config:
dialect = "postgres"
[[files]]
include = ["**/*.sql.sqlite", "storage/sqlite/**"]
dialect = "sqlite"[[embedded]] rules format SQL embedded in host code. A rule names the tree-sitter grammar that parses the file and, optionally, a query that finds the SQL strings in it:
[[embedded]]
include = ["**/*.rs"]
grammar = "rust"
dialect = "sqlite"
[[embedded]]
include = ["web/**/*.ts"]
grammar = "typescript"
indent = "spaces"
indent-width = 2Built-in grammars, each with a default query: rust (sqlx's query!-family macros and query/query_as/query_scalar functions), go (database/sql calls), python (.execute-family and text(...), with %s / %(name)s params preserved under pyformat-params), javascript/typescript/tsx (.query/.execute/.prepare and sql-tagged templates), gleam (strings passed or piped to sqlight.query as SQLite, and to pog.query etc. as the configured dialect), c++ (raw strings passed to sqlite3, libpq, and libpqxx), c# (raw strings in EF Core migrations, raw-SQL and Dapper calls, and CommandText), java/kotlin (text blocks and raw strings passed to JDBC, JPA, Spring, and Exposed calls, with ? placeholders preserved under question-params), and swift (""" strings passed to GRDB as SQLite, and to SQLite.swift, PostgresNIO, and SQLKit calls).
Any other language works with a grammar compiled to wasm (tree-sitter build --wasm, or the .wasm many grammars publish with each release) and a query of your own:
[[embedded]]
include = ["**/*.lua"]
grammar = ".config/squill/tree-sitter-lua.wasm"
query = ".config/squill/lua.scm"squill knows nothing about a grammar's strings but what its query says, so unless the query says otherwise, it formats only a string that already spans lines, and never one holding a backslash. A pattern promises more with #set!: (#set! squill.raw) for a string that takes no escapes; (#set! squill.multiline) for one whose syntax takes raw line breaks, so it can be formatted from one line onto several; (#set! squill.escape "whitespace"), once per kind ("punctuation", "\\xHH", "\\u{XXXX}", …), for the escapes a string takes, which squill reads and writes back exactly as spelled; and (#set! squill.promote-to-raw-syntax "r#\"{}\"#") for raw syntaxes a string spanning lines can be rewritten in. A capture named @squill.skip leaves out any SQL capture it overlaps, like a template's ${…} hole; one named @squill.parameter marks a hole the library sends as a parameter, which squill reads as a parameter as wide as the hole and writes back as it was. The built-in queries, in crates/embed/src/queries/, say what their strings allow the same way.
grammar can also be an https URL to the .wasm. The first download records its SHA-256 in a squill.lock beside the config (commit it), and every later download must match; downloads are cached by hash, and squill fmt --locked (for CI) refuses to record a URL the lockfile doesn't have.
Only multiline string syntaxes are reformatted — raw strings, backticks, triple quotes, templates, text blocks, Gleam strings — and the query inside is formatted just as in a .sql file. A string written on one line stays on it while the query fits; one that already spans lines, or a query that doesn't fit, gets its quotes on lines of their own with the query between them. A plain Rust or Go string that already spans lines, or spells a line break with \n, joins them, and becomes a raw string when its SQL spans lines and no escape is part of the SQL itself; any other single-line one stays byte-identical, and so do Python f-strings and other interpolated strings, whose holes paste text into the SQL. Where the library sends each hole as a parameter — postgres.js's and Bun's sql templates, Prisma's $queryRaw, EF Core's FromSql, psycopg's t-strings, PostgresNIO, SQLKit's \(bind:), GRDB's literal: — squill formats around the holes and writes them back as they were. Every rewrite is checked by re-parsing the host file, and a string squill declines to touch is reported as a diagnostic. Embedded SQL indents the way its host file does — the same character and the same step, unless indent or indent-width is configured — and max-width counts from the file's left edge, not from where the SQL starts.
crates/parser— hand-written dual-dialect lexer (lossless: every byte is a token, including comments), recursive-descent parser with Pratt expressions, error recovery into verbatimErrorStatementnodes, and a codegen'd CST layer oncstree(seesyntax.def).crates/formatter— Wadler/Prettier doc IR and renderer, the CST-to-doc rules, vendored Postgres/SQLite keyword tables, and the semantics-preserving identifier-quoting transform.crates/embed— formats SQL embedded in host files, located via tree-sitter queries over built-in or wasm-loaded grammars.crates/cli— thesquillbinary.crates/corpus-report— the corpus coverage harness (not installed with the cli).