Files
PgTidy/docs/todo.md
T
warkanum 7fb76bae3d feat(lint): add linter rules for migration and naming conventions
* Implement migration safety rules:
  - MIG001: Warn on CREATE INDEX without CONCURRENT.
  - MIG002: Warn on ALTER TABLE ADD COLUMN NOT NULL without DEFAULT.
  - MIG003: Warn on ALTER TABLE ADD CONSTRAINT without NOT VALID.
* Implement naming conventions rules:
  - NAM001: Warn on non-snake_case table names.
  - NAM002: Warn on non-snake_case column names in CREATE TABLE.
  - NAM003: Warn on non-snake_case function names.
* Add test fixtures for all new rules.
2026-06-27 22:15:02 +02:00

8.3 KiB
Raw Blame History

PgTidy — TODO / Progress

Tracks what is done and what remains. See plan.md for the full design and rationale.

Legend: done · 🚧 in progress · not started


V1 — Formatter + CLI (current milestone)

Scaffold module + repo hygiene

  • go.mod (module github.com/hein/pgtidy, go 1.26).
  • Replaced WkMailSync boilerplate: AGENTS.md (PgTidy architecture + invariants), CLAUDE.md, Makefile (build/test/vet/fmt/lint/clean).
  • Directory layout created (cmd/, pkg/..., testdata/).
  • Copied 4 real-world procedures into testdata/corpus/*.pgsql (safety harness).

Lossless lexer — pkg/lexer

  • token.go: Kind enum (trivia, words, literals, operators, punctuation) + Token.
  • lexer.go: full PG token coverage — dollar-quoted strings ($tag$, nested-tag aware), -- / nestable /* */ comments, standard/escape/bit/hex/unicode strings, numbers (decimal, exponent, leading dot, 0x/0o/0b, _ separators), positional params ($1), operator runs with PostgreSQL's trailing +/- rule, :: / := / :, punctuation. Tracks line/col per token.
  • lexer_test.go: unit tests (kinds, operator trailing rule, dollar quotes, line/col) + corpus round-trip asserting emit(Lex(src)) == src byte-for-byte.
  • Status: all tests pass; round-trips all 4 corpus files. Invariant #1 satisfied.

CST node model + parser — pkg/cst, pkg/parser

  • pkg/cst: lossless node model — Tok (significant token + leading trivia), Attach, File.Source() (byte-exact reconstruction), Raw (verbatim fallback), CreateFunction (Head/Name/Params/Options/As/Body/Tail/Semi), Param (with separator comma).
  • pkg/parser: statement splitting at top-level ;; structures CREATE FUNCTION/PROCEDURE (head, qualified name, comma-split params, option clauses split by keyword, AS + body); everything else → Raw. Graceful degradation is a property of the data model.
  • parser_test.go: small round-trips, CreateFunction shape assertions, Raw fallback, and corpus round-trip — reconstructs all 4 files byte-for-byte; structures all 4 functions.
  • Status: all tests pass.
  • Still TODO (later): DML/other-DDL structuring (currently Raw) for full formatting.

🚧 PL/pgSQL body parser ← NEXT (the remaining V1 piece)

DECLARE section — pkg/format/body.go

  • formatBody splits the dollar-quote tag, calls formatBodyInner.
  • formatBodyInner locates DECLARE and BEGIN at depth 0, formats the declare block, then emits BEGIN onwards verbatim.
  • formatDeclareVars: each variable declaration collapsed to one line ( name type [= expr];), --Block-- comment markers preserved on their own lines, mid-declaration block comments trigger verbatim fallback.
  • needSpace fixed for LBracket — no space before [ after ident/closing bracket (fixes citext[], array subscripts).
  • semanticallyEqual in tests updated to recurse into dollar-quoted body tokens so whitespace normalization inside the body does not falsely fail the semantic check.
  • Added testdata/corpus/test_a_broken.pgsql — a CRLF corpus file with intentionally broken layout (split-line variables + split-line body statements) used as a formatting target; testdata/corpus/test_a.pgsql is the golden output.
  • Status: all tests pass; DECLARE section formats correctly.

Body statement formatter — pkg/format/body.go

  • formatBodyStatements: line-by-line formatter for the BEGIN … END block.
  • Block-depth tracker: BEGIN/END, IF/THEN/ELSIF/ELSE/END IF, LOOP/END LOOP, EXCEPTION.
  • Split-line join: col-0 lines at paren-depth 0 with non-clause first token are joined to preceding line. SQL clause keywords (SELECT, FROM, WHERE, INTO, WITH, HAVING, GROUP, ORDER, RETURNING, SET, joins) stay on own lines.
  • Base-indent normalisation: first logical line of each statement gets blockDepth × st.Indent; subsequent lines preserve their original indentation (relative indentation maintained for multi-line expressions).
  • Blank-line count preservation: blank lines between statements kept as-is.
  • Verbatim-indent mode after EXCEPTION: original leading whitespace preserved to avoid style conflicts between functions that put WHEN at col-0 vs indented.
  • sqlClauseKw map; firstBodyKeyword, leadingWhitespace helpers.
  • TestFormatBodyBroken: golden-file test — format(test_a_broken.pgsql) must equal test_a.pgsql.
  • Updated test_a.pgsql to match actual formatter output.
  • Status: all tests pass; idempotence verified.

Printer + style config — pkg/format, pkg/config

  • pkg/config: Style struct + Default() = house style. Load(startDir) walks up the directory tree to find .pgtidy.yaml and merges its fields over the defaults. Supported keys: indent, newline, keyword_case, ident_case, type_case, commas. Dependency: gopkg.in/yaml.v3.
  • pkg/format: formats CREATE FUNCTION/PROCEDURE headers to house style (params one-per-line leading-comma, option clauses each on own line, AS/$$ own lines); DECLARE section formatted (see body parser entry); Raw statements emitted verbatim. Spacing engine (needSpace, tight ops :: : -> ->>, array []) + casing (keywords/typeNames sets). Comment-safety: verbatim fallback if a header carries comments it cannot relocate.
  • pkg/format/body.go: DECLARE section formatter (see body parser entry).
  • Tests: golden header, idempotence, corpus idempotence + semantic equivalence (updated to recurse into dollar-quoted body tokens).
  • Note: not a full Wadler Doc-IR yet — fixed-layout printer. Doc-IR for width-based expression wrapping can come when DML structuring lands.

CLI fmt + safety harness — cmd/pgtidy

  • pgtidy fmt (gofmt model): default stdin→stdout; -w/--write, -l/--list, --check (CI exit codes); -d/--diff (unified diff output); version/help.
  • Config discovery: walks up from cwd to find .pgtidy.yaml; applied before formatting.
  • diff.go: in-house unified diff (LCS-based, zero additional deps).
  • fmt_test.go: stdin, --check (un/formatted), -w idempotence, unknown-command.
  • Safety invariants #2 (semantic equivalence), #3 (idempotence), #4 (graceful degradation) are tested in pkg/format over the corpus.

V2 — Linter

  • pkg/pgast: go-pgquery (WASM, no cgo) wrapper → real PG AST. FirstTokenOffset skips leading whitespace/comments for accurate line numbers.
  • pkg/diagnostics: Diagnostic{RuleID, Severity, Message, File, Line, Col}.
  • pkg/lint: Engine, Rule interface, New() with all built-ins:
    • MIG001 CREATE INDEX without CONCURRENT
    • MIG002 ALTER TABLE ADD COLUMN NOT NULL without DEFAULT
    • MIG003 ALTER TABLE ADD CONSTRAINT FK/CHECK without NOT VALID
    • COR001 SELECT * | COR002 UPDATE without WHERE | COR003 DELETE without WHERE
    • NAM001/2/3 table/column/function names not snake_case (quoted identifiers only)
  • pgtidy lint [--only=ID,...] [files...]; exits 1 on findings, 2 on error.
  • Fixture SQL in testdata/lint/; 6 tests covering violations + clean fixtures.
  • --fix for autofixable rules: future.

V3 — LSP + VSCode (later)

  • pkg/lsp: formatting + range formatting, publishDiagnostics, codeAction quick-fixes.
  • editors/vscode: TS extension (vscode-languageclient) launching bundled pgtidy lsp; per-platform VSIX matrix in CI (rust-analyzer model) + target-less fallback.

V4 — DataGrip (later)

  • editors/datagrip: integrate via free LSP4IJ plugin.

Build / release (cross-cutting)

  • Add goreleaser for the multi-platform binary matrix (clean: no cgo).
  • make_release.sh retained from boilerplate (generic version tagging).

Core invariants (must always hold — tested)

  1. Lossless lex: emit(Lex(src)) == src (corpus round-trip).
  2. Semantic equivalence: formatting changes only trivia/layout (corpus token-stream check).
  3. Idempotence: fmt(fmt(x)) == fmt(x) (corpus + CLI tests).
  4. Graceful degradation: unparsable spans pass through verbatim (Raw nodes + verbatim body).

Open risks

  • Lossless PL/pgSQL recursive-descent parser is the largest effort; pass-through fallback bounds risk and allows shipping construct-by-construct.
  • go-pgquery tracks PG17 (not PG18) — fine for lint; irrelevant to formatter path.
  • Leading-comma + one-per-line is a first-class style option, not an afterthought.