126 lines
6.6 KiB
Markdown
126 lines
6.6 KiB
Markdown
## 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.
|
||
|
||
## 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.
|