6.4 KiB
6.4 KiB
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/ — LSP4IJ integration (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
diagnosticstype.
Core invariants (the formatter must never violate these)
- Lossless lex:
emit(lex(src)) == srcbyte-for-byte. The lexer keeps all trivia. - Semantic equivalence: formatting only changes trivia/layout. Verify by re-lexing the output and comparing the non-trivia token stream against the input.
- Idempotence:
fmt(fmt(x)) == fmt(x). - Graceful degradation: any span the parser cannot handle is passed through verbatim rather than corrupted.
Commands
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;
ASthen$$on its own line; body; closing$$;on its own line. - PL/pgSQL:
DECLAREalone, vars 2-space indented;--Block--comment markers preserved;BEGIN/ENDat 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)
- 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)) == srcbyte-for-byte across the corpus. - CST + parser (
pkg/cst,pkg/parser): recursive descent for DML (SELECT/INSERT/ UPDATE/DELETE/CTE), DDL (CREATE FUNCTION/PROCEDURE/TABLE/INDEX/TRIGGER, ALTER, DO). - PL/pgSQL body parser: DECLARE/BEGIN/END, IF/CASE/LOOP, assignments, nested SQL.
- Printer (
pkg/format): Doc-IR (group/indent/line/softline) driven bystyleconfig. Graceful degradation — unparsed spans pass through verbatim. - CLI (
cmd/pgtidy fmt):--check,--write/-w, stdin→stdout,--diff; config discovery walking up to.pgtidy.yaml; CI-friendly exit codes. - Config (
pkg/config): load/merge style config; defaults = house style above.
V2 — Linter
pkg/pgastgo-pgquery (WASM) wrapper;pkg/lintrule 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 lintsubcommand;--fixfor autofixable rules.
V3 — LSP + VSCode
pkg/lsp:textDocument/formatting+ range formatting,publishDiagnostics,codeActionquick-fixes — all reusing the core.editors/vscode: TS extension usingvscode-languageclient, launches bundledpgtidy lsp. Per-platform VSIX (win32/linux/darwin × x64/arm64) built in CI matrix.
V4 — DataGrip
editors/datagrip: integrate via LSP4IJ (free, works across JetBrains editions incl. DataGrip). No core changes expected.
Verification
- Formatter:
go test ./...runs golden-file tests + corpus harness asserting idempotence and token-stream equality before/after (no semantic change). - CLI:
pgtidy fmt --checkreturns non-zero on unformatted input, zero when clean. - Lint (v2): fixture SQL with known violations → assert expected diagnostics;
--fixround-trips. - LSP/VSCode (v3): load a
.sqlfile in a dev-host VSCode, confirm format-on-save and live diagnostics via the bundled binary.