# hasql-th-format
Format the SQL inside your
[hasql-th](https://hackage.haskell.org/package/hasql-th) quasiquotes, in place.
It parses each statement with
[postgresql-syntax](https://hackage.haskell.org/package/postgresql-syntax) and
renders it again, so the layout comes from the statement's structure rather
than from whoever typed it:
```haskell
recent =
[vectorStatement|
select o.id::int8, c.email::text, o.total::numeric from orders o join customer c on c.id = o.customer_id where o.placed_at > $1::timestamptz and o.status = $2::text order by o.placed_at desc limit 50
|]
```
```
$ hasql-th-format src/Shop/Orders.hs
Reformatted: src/Shop/Orders.hs
```
```haskell
recent =
[vectorStatement|
select
o.id::int8
, c.email::text
, o.total::numeric
from orders o
join customer c
on c.id = o.customer_id
where o.placed_at > $1::timestamptz
and o.status = $2::text
order by o.placed_at desc
limit 50
|]
```
## Contents
- [Installing](#installing)
- [Formatting](#formatting)
- [It will not change what a statement means](#it-will-not-change-what-a-statement-means)
- [Checking a refactor did not either: `hasql-th-audit`](#checking-a-refactor-did-not-either-hasql-th-audit)
- [The `kronor-syntax` flag](#the-kronor-syntax-flag)
- [Reference](#reference)
- [Limitations](#limitations)
- [Development](#development)
## Installing
```
git clone https://github.com/kronor-io/hasql-check
cd hasql-check/format
cabal install exe:hasql-th-format exe:hasql-th-audit
```
It reads quotes through `hasql-check`'s lexer, which links against libpq, so
install your platform's PostgreSQL client library first (`libpq-dev` on Debian
and Ubuntu).
## Formatting
Give it files. It rewrites every supported quasiquote in place and says which
files it touched:
```
$ hasql-th-format src/Shop/Orders.hs src/Shop/Customers.hs
Reformatted: src/Shop/Orders.hs
```
`--check` changes nothing and exits 1 if any file would be rewritten, which is
what you want in CI or a pre-commit hook:
```
$ hasql-th-format --check src/Shop/Orders.hs
Would reformat: src/Shop/Orders.hs
```
`--stdin` reads one file's worth of Haskell on standard input and writes the
formatted result to stdout, which is the quickest way to see what the formatter
does with a quote. It is Haskell it reads, not bare SQL: a statement on its own
has no quasiquote around it, and comes back untouched.
```
$ printf 'q =\n [vectorStatement|\n select c.id::int8, c.email::text from customer c where c.active\n |]\n' \
| hasql-th-format --stdin
q =
[vectorStatement|
select
c.id::int8
, c.email::text
from customer c
where c.active
|]
```
These quoters are recognised: `singletonStatement`, `maybeStatement`,
`vectorStatement`, `foldStatement`, `resultlessStatement`,
`rowsAffectedStatement`.
## It will not change what a statement means
After rendering, the formatter **re-parses its own output and compares the two
ASTs**. If they differ it writes nothing, leaves the file byte for byte as it
was, and fails:
```
$ hasql-th-format src/Report/Ledger.hs
Error processing src/Report/Ledger.hs: line 164: AST mismatch: formatter rendered semantically-different SQL (renderer bug)
```
That is a bug in this tool, not in your SQL — please report it. An earlier
version fell back silently to the unformatted text, and a renderer bug that
dropped a `DISTINCT ON` clause reached production that way.
There is no false-positive risk: `postgresql-syntax` parses
semantically-equivalent text to identical ASTs — whitespace, keyword case and
`::` spacing all normalise — so a mismatch is always real.
The other failure is `AST mismatch: formatter output did not re-parse`, meaning
the renderer emitted SQL that is not valid. It should be unreachable, and
reports the underlying parse error if it happens.
## Checking a refactor did not either: `hasql-th-audit`
The check above runs per quote, at format time, against the input the formatter
was just handed. It cannot see a hand-edit, a tree-wide rename, or any change
`hasql-th-format` was never run on — and if the formatter itself crashes, it
does not run at all.
`hasql-th-audit` takes two versions of a file and compares their quotes by AST,
using only `postgresql-syntax`:
```
$ hasql-th-audit old/Orders.hs new/Orders.hs
new/Orders.hs:6 (qq #1): AST DIFFERS
--- old (line 6) ---
... order by o.placed_at desc
limit 50
--- new (line 6) ---
... order by o.placed_at desc
limit 20
```
Exit codes: `0` every quote is equivalent, `1` one or more differ (or the
counts differ), `2` usage error or lexer failure.
Across a branch:
```bash
BIN=$(cabal list-bin hasql-th-audit)
base=$(git merge-base origin/main HEAD)
while read -r f; do
old=$(mktemp); new=$(mktemp)
git show "$base:$f" > "$old" 2>/dev/null || { rm -f "$old"; continue; }
git show "HEAD:$f" > "$new" 2>/dev/null || { rm -f "$old" "$new"; continue; }
"$BIN" "$old" "$new" || echo "REGRESSION: $f"
rm -f "$old" "$new"
done < <(git diff --name-only "$base" HEAD -- '*.hs')
```
## The `kronor-syntax` flag
Off by default, the formatter handles what upstream `postgresql-syntax` accepts
plus hasql-th's `::type?` nullability markers.
Kronor's fork of hasql-th adds two more forms, and `+kronor-syntax` parses
*and* renders both:
```sql
select $Just (name :: text), $Row { id = o.id :: int8 }
from orders o
where o.customer_id = $1.$customerId :: int8
```
The flag needs Kronor's fork of `postgresql-syntax`, where those constructors
live, so a build against Hackage must leave it off. It is the same flag,
meaning the same thing, as `hasql-check`'s.
## Reference
### `hasql-th-format`
| | |
|---|---|
| `hasql-th-format FILES...` | Rewrite every quote in each file, in place |
| `--check` | Change nothing; exit 1 if any file would change |
| `--stdin` | Format one statement from stdin onto stdout |
Exit codes: `0` success, `1` a file would change (`--check`) or a quote failed
to parse or round-trip, `2` usage error.
### `hasql-th-audit`
| | |
|---|---|
| `hasql-th-audit OLD NEW` | Compare the quotes of two versions of one file |
## Limitations
- **Long lines it cannot break.** The renderer wraps at a set of known
constructs. An expression built from others — a deeply nested `case` inside a
function argument, say — can still exceed the line budget.
- **`(select ...)` in a `join`** renders with both the subquery's parens and
the join's, so you get `((select ...))`. It parses the same; it just looks
odd.
- **Some `AExpr` / `CExpr` constructors have no multi-line form** and fall
through to the inline renderer whatever their length.
## Development
```
cabal build
cabal test # 64 golden cases, each also asserting AST equivalence
```
The test suite in `test/Test.hs` is the right home for a minimised reproducer
once you have narrowed a layout bug down. Each case is an input and the exact
expected output, so a change in layout shows up as a diff rather than as a
judgement call.
Two render paths run over the same AST: an inline one used inside subqueries
and short targets, and a multi-line one. Most layout bugs are a construct
handled by one and not the other. Wrapping is driven by three thresholds in
`SqlFormat.hs` — `inlineLengthThreshold` (60), `subqueryInlineThreshold` (100)
and `selectTargetInlineThreshold` (150) — and by the `isWrapEligible*`
predicates, which decide whether a construct has a multi-line form at all.
`cabal test` rebuilds the test suite but **not** the executables. Run
`cabal build` before running the binary over a codebase, or you will be running
the previous one.