Files
Hein bac966c2ac
CI / Test (push) Successful in 31s
CI / Build (push) Successful in 23s
feat(format): add runtime semantic-equality safety gate
2026-07-17 14:29:24 +02:00

134 lines
7.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## Project Overview
**PgTidy** is a PostgreSQL-focused linter and formatter written in Go. It enforces a
consistent SQL style, detects PostgreSQL-specific issues, and formats migrations, schemas,
functions, procedures, triggers, and related database code. The primary day-one use case is
formatting PL/pgSQL stored procedures to a defined house style.
It ships as a CLI, an LSP server, a VSCode extension, and (later) a DataGrip integration.
# Agent Rules
Keep your answers short. Question everything, never assume or guess. Ask the user if you are
unsure about anything.
# Architecture
PgTidy uses a **hybrid** strategy:
- **Formatting** is driven by a custom **lossless lexer + CST** (concrete syntax tree) that
preserves every comment and whitespace span, so formatting can round-trip safely.
- **Linting** (later milestones) is driven by the real PostgreSQL grammar via
`go-pgquery` (libpg_query compiled to WASM with wazero — no cgo), which yields an accurate
AST for deep semantic rules.
A single Go core backs every frontend (CLI, LSP, editors) and shares one `diagnostics` type.
```
cmd/pgtidy/ — CLI entry; subcommands: fmt, lint (v2), lsp (v3), version
pkg/lexer/ — lossless lexer: tokens + trivia (comments/whitespace) attached
pkg/cst/ — concrete syntax tree (round-trippable node model)
pkg/parser/ — recursive-descent parser → CST (DML + DDL + PL/pgSQL)
pkg/format/ — Doc-IR printer (Wadler/Prettier-style) + style application
pkg/config/ — .pgtidy.yaml discovery/merge: style + rule config
pkg/diagnostics/ — shared diagnostic type (CLI + LSP)
pkg/pgast/ — go-pgquery wrapper: SQL → real PG AST (lint, v2)
pkg/lint/ — rule engine + rule packs (v2)
pkg/lsp/ — LSP server (v3)
editors/vscode/ — VSCode extension (v3)
editors/datagrip/ — native JetBrains plugin, shells out to the CLI (v4)
testdata/corpus/ — real-world .pgsql procedures used as the safety/idempotence harness
```
## Why this architecture
- **No cgo** (WASM-embedded libpg_query) keeps cross-compilation trivial and lets us bundle a
single static binary per platform inside the VSCode extension.
- **Formatting needs a lossless CST** — libpg_query drops comments and whitespace, so it cannot
be the sole basis for a formatter. We build our own lexer/CST so the formatter never loses a
comment.
- **Deep lint needs an accurate AST** — go-pgquery gives the real PostgreSQL parse tree.
- **One core, many frontends** — CLI, LSP, VSCode, and DataGrip all reuse the same engine and
the same `diagnostics` type.
## Core invariants (the formatter must never violate these)
1. **Lossless lex**: `emit(lex(src)) == src` byte-for-byte. The lexer keeps all trivia.
2. **Semantic equivalence**: formatting only changes trivia/layout. Verify by re-lexing the
output and comparing the non-trivia token stream against the input.
3. **Idempotence**: `fmt(fmt(x)) == fmt(x)`.
4. **Graceful degradation**: any span the parser cannot handle is passed through verbatim
rather than corrupted.
5. **Runtime safety gate**: every frontend (CLI `fmt`, LSP `textDocument/formatting` and
`rangeFormatting`) calls `format.SemanticallyEqual(src, out)` before writing or returning
formatted output. It re-lexes both sides and compares non-trivia token streams
(case-insensitive for identifiers/keywords, exact otherwise, recursing into dollar-quoted
bodies). If it ever returns false, the formatter has a bug — the caller must refuse to
write/emit the result and keep the original source, never guess or best-effort it. This is
not just a test assertion (`pkg/format/format_test.go`); it is enforced at runtime so a
formatter bug can never silently drop or alter code.
## Commands
```bash
go build ./cmd/pgtidy # Build the binary
go test ./... # Run all tests (incl. corpus safety harness)
go vet ./... # Static analysis
go fmt ./... # Format Go code
```
## House style (formatter defaults; all configurable via .pgtidy.yaml)
- Keywords UPPERCASE; data types lowercase; identifiers lowercase snake_case.
- Indent: 2 spaces per level.
- Leading-comma style, one item per line, for SELECT column lists and function params.
- Function headers: params one-per-line in parens; LANGUAGE / SECURITY / volatility each on
their own line; `AS` then `$$` on its own line; body; closing `$$;` on its own line.
- PL/pgSQL: `DECLARE` alone, vars 2-space indented; `--Block--` comment markers preserved;
`BEGIN`/`END` at body level; IF/ELSIF/ELSE/END IF, loops, CASE indent their bodies.
- Spacing: spaces around binary operators (`=`, `<>`, `||`, …) and `:=`; no space around
`::`, `->`, `->>`, array `[...]`, or before a call's `(`.
- Dollar-quote tags preserved verbatim (`$$`, `$S$`, `$Z$`, …).
## Milestones
### V1 — Formatter + CLI (current priority)
1. **Lexer** (`pkg/lexer`): full PG token coverage incl. dollar-quoted strings, `--` and
`/* */` comments, operators. Comments + whitespace as leading/trailing trivia on tokens.
Acceptance: `emit(lex(src)) == src` byte-for-byte across the corpus.
2. **CST + parser** (`pkg/cst`, `pkg/parser`): recursive descent for DML (SELECT/INSERT/
UPDATE/DELETE/CTE), DDL (CREATE FUNCTION/PROCEDURE/TABLE/INDEX/TRIGGER, ALTER, DO).
3. **PL/pgSQL body parser**: DECLARE/BEGIN/END, IF/CASE/LOOP, assignments, nested SQL.
4. **Printer** (`pkg/format`): Doc-IR (group/indent/line/softline) driven by `style` config.
Graceful degradation — unparsed spans pass through verbatim.
5. **CLI** (`cmd/pgtidy fmt`): `--check`, `--write`/`-w`, stdin→stdout, `--diff`; config
discovery walking up to `.pgtidy.yaml`; CI-friendly exit codes.
6. **Config** (`pkg/config`): load/merge style config; defaults = house style above.
### V2 — Linter
- `pkg/pgast` go-pgquery (WASM) wrapper; `pkg/lint` rule engine (rule ID, severity, config).
- Rule packs: style/consistency, migration safety (ACCESS EXCLUSIVE locks, unsafe ALTER/ADD
COLUMN, non-CONCURRENTLY index builds, blocking constraints), naming conventions,
correctness/anti-patterns (SELECT *, missing WHERE on UPDATE/DELETE). Emit `diagnostics`.
- `pgtidy lint` subcommand; `--fix` for autofixable rules.
### V3 — LSP + VSCode
- `pkg/lsp`: `textDocument/formatting` + range formatting, `publishDiagnostics`, `codeAction`
quick-fixes — all reusing the core.
- `editors/vscode`: TS extension using `vscode-languageclient`, launches bundled `pgtidy lsp`.
Per-platform VSIX (`win32/linux/darwin × x64/arm64`) built in CI matrix.
### V4 — DataGrip
- `editors/datagrip`: native JetBrains plugin (no LSP4IJ dependency) that shells out to the
`pgtidy` binary directly. Formatting and version info run `pgtidy fmt`/`version`; lint
diagnostics and quick-fixes run via a native `ExternalAnnotator` calling `pgtidy lint --json`.
## Verification
- **Formatter:** `go test ./...` runs golden-file tests + corpus harness asserting idempotence
and token-stream equality before/after (no semantic change).
- **CLI:** `pgtidy fmt --check` returns non-zero on unformatted input, zero when clean.
- **Lint (v2):** fixture SQL with known violations → assert expected diagnostics; `--fix`
round-trips.
- **LSP/VSCode (v3):** load a `.sql` file in a dev-host VSCode, confirm format-on-save and
live diagnostics via the bundled binary.