pgexplain 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 5a1be66: - **New rule `PGX_MEMOIZE_EVICTIONS`**: flags a thrashing Memoize cache (evictions outpacing hits, or cache overflows) with a `work_mem` / `hash_mem_multiplier` remediation. The parser now normalizes Memoize cache counters (`Cache Hits/Misses/Evictions/Overflows`).
8
+ - **Studio component tests**: React Testing Library + happy-dom cover FindingCard, the side-by-side DiffPanel, and toasts; the web test project runs in CI via `pnpm test`.
9
+ - **Fix `PGX_CARTESIAN_PRODUCT` false positive**: the rule now looks through Memoize/Materialize to the real inner scan, so `Nested Loop → Memoize → Index Scan (parameterized)` is no longer misreported as a cross join.
10
+ - 5a1be66: New analysis capabilities:
11
+
12
+ - **New rule `PGX_LIMIT_LARGE_OFFSET`**: flags OFFSET-style pagination where the plan generates and discards a large row prefix; recommends keyset pagination. Tunable via `limitDiscardRows`.
13
+ - **New check `PGX_STALE_STATISTICS`** (run path only): flags tables in the plan that were never analyzed or churned past `staleStatsModRatio` (default 20%) since their last ANALYZE — the usual root cause behind row misestimates.
14
+ - **New command `pg-explain locks`**: live lock-contention snapshot (who is blocked, by whom, for how long) with cancel/terminate remediation; `--fail-on-blocked` exits 1 for scripting; terminal and JSON output.
15
+ - **Studio: side-by-side plan diff** — the diff view now renders both plan trees with slower/faster/added/removed nodes highlighted.
16
+ - **Studio: shareable run URLs** — every stored run gets a `#run=<id>` deep link plus a copy-link button.
17
+ - Shell completion now includes the `locks` and `studio` subcommands.
18
+
19
+ ### Patch Changes
20
+
21
+ - 5a1be66: Studio & DX quality pass:
22
+
23
+ - Studio: toast notifications — export failures and settings saves are no longer silent
24
+ - Studio: keyboard shortcuts (⌘/Ctrl+K focus editor, ⇧⌘/Ctrl+F format SQL, `?` help overlay) and ARIA tablist/landmark roles
25
+ - Studio: collapsible sidebar and history filter box
26
+ - Library: export `severityAtLeast` for CI-gate scripting; README library examples expanded
27
+ - Tests: snapshot coverage for all five render formats, command-flow tests for `analyze`/`diff` exit codes, and a new `web` vitest project covering studio helpers
28
+ - Dev: `pnpm dev:studio` runs core rebuild + API restart + Vite HMR in one terminal
29
+
3
30
  All notable changes to this project will be documented in this file.
4
31
 
5
32
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
package/README.md CHANGED
@@ -24,6 +24,31 @@ The same philosophy applies to operational errors: auth failures, timeouts, unre
24
24
 
25
25
  ---
26
26
 
27
+ ## Features
28
+
29
+ **Advisor & analysis**
30
+ - **18 plan-anti-pattern rules** (`PGX_*`) — cartesian products, large seq scans, disk-spilling sorts/hashes, thrashing Memoize caches, misestimates, lossy bitmaps, OFFSET pagination, low cache-hit ratio, JIT/trigger overhead, and more.
31
+ - **`PGX_STALE_STATISTICS`** — on the `run` path, flags tables that were never analyzed or have churned since their last `ANALYZE`, explaining the usual root cause behind misestimates.
32
+ - **Lock advisor** — static warnings for risky DDL/DML (table rewrites, missing `CONCURRENTLY`, unbounded `FOR UPDATE`, …) plus a **live lock-contention** snapshot (who is blocked, by whom).
33
+ - Every finding ships **what / why / fix**, with copy-pasteable SQL/shell commands and a Postgres docs link.
34
+ - Config file (`.pgexplainrc[.json]` or `package.json#pgExplain`) to tune thresholds and enable/disable/re-sever individual rules.
35
+
36
+ **CLI**
37
+ - `pg-explain [FILE]` — analyze a plan from a file, `< stdin`, or a whole directory (batch mode).
38
+ - `pg-explain run` — connect, `EXPLAIN` safely (rolled back, read-only unless `--force`), analyze.
39
+ - `pg-explain diff` — compare two plans and gate CI on regressions or new findings.
40
+ - `pg-explain locks` — snapshot live lock contention from the terminal; `--fail-on-blocked` for scripting.
41
+ - `pg-explain studio` — launch the local web UI.
42
+ - 5 output formats (terminal, markdown, HTML, JSON, plain text), stable exit codes for scripting, shell completion.
43
+
44
+ **Studio (local web UI)** — see screenshots below.
45
+ - Run a query or paste a plan; interactive **plan graph** (heat-colored by time/rows/cost/buffers) with per-node detail, or a text tree view.
46
+ - **History** of every run, **side-by-side plan diff** between any two, and **shareable `#run=` links**.
47
+ - **Stats** by node type / table / index, table catalog with autocomplete, **Settings** to tune thresholds live.
48
+ - Keyboard shortcuts (`⌘/Ctrl+Enter` run, `⌘/Ctrl+K` focus editor, `⇧⌘/Ctrl+F` format, `?` for help), light/dark theme, collapsible sidebar with history search.
49
+
50
+ ---
51
+
27
52
  ## Install
28
53
 
29
54
  ```sh
@@ -67,6 +92,81 @@ Point at a directory to analyze every plan in it (batch mode):
67
92
  pg-explain ./plans/
68
93
  ```
69
94
 
95
+ Or launch the local **Studio** — a GUI for everything the CLI does:
96
+
97
+ ```sh
98
+ pg-explain studio # opens http://127.0.0.1:5177 in your browser
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Studio (local web UI)
104
+
105
+ `pg-explain studio` starts a local, single-user web app (binds `127.0.0.1`, no auth) that
106
+ mirrors the CLI with a friendlier surface. It ships inside the package, so `npx pgexplain studio`
107
+ just works — the PostgreSQL driver and the UI are only loaded on demand.
108
+
109
+ - **Analyze** a pasted `EXPLAIN (FORMAT JSON)` plan, or **Run** a query (connect → EXPLAIN-only,
110
+ rollback-wrapped, read-only; non-`SELECT` refused unless forced).
111
+ - **Findings** as plain-language cards (what / why / fix + copy-paste commands + docs links).
112
+ - **Lock advisor** — static warnings (rewrites, missing `CONCURRENTLY`, unindexed `UPDATE/DELETE`,
113
+ unbounded `FOR UPDATE`, …) plus a **live locks** view of current blocking chains.
114
+ - Interactive **plan graph** (heat-colored by time/rows/cost/buffers, click a node for full detail)
115
+ or a plain-text tree, **Stats** by node type/table/index, table catalog with autocomplete.
116
+ - **History** of every run (SQLite under `~/.pgexplain`), a **side-by-side diff** between any two
117
+ runs with regressed/improved/added/removed nodes highlighted, and **shareable `#run=` links**.
118
+ - **Export** to Markdown / HTML / JSON, and a **Settings** page to tune advisor thresholds live.
119
+ - Light/dark theme, keyboard shortcuts (`?` for the full list), collapsible sidebar with history search.
120
+
121
+ Flags: `pg-explain studio [--port 5177] [--host 127.0.0.1] [--no-open] [--unsafe-host]`.
122
+ Binding a non-loopback host requires `--unsafe-host` (the studio can reach arbitrary databases,
123
+ so exposing it is an SSRF/credential risk). Set `PGEXPLAIN_DATA_DIR` to relocate the local store.
124
+
125
+ <table>
126
+ <tr>
127
+ <td width="50%">
128
+
129
+ **Findings** — what / why / fix, with copy-pasteable SQL
130
+ <img src="docs/screenshots/findings-light.png" alt="Studio findings tab showing plain-language diagnostics with remediation SQL">
131
+
132
+ </td>
133
+ <td width="50%">
134
+
135
+ **Plan graph** — heat-colored, click a node for full detail
136
+ <img src="docs/screenshots/plan-graph.png" alt="Studio interactive plan graph with a node detail panel open">
137
+
138
+ </td>
139
+ </tr>
140
+ <tr>
141
+ <td width="50%">
142
+
143
+ **Side-by-side diff** — before/after plans with regressions highlighted
144
+ <img src="docs/screenshots/diff-side-by-side.png" alt="Studio diff view comparing two runs with a side-by-side plan tree">
145
+
146
+ </td>
147
+ <td width="50%">
148
+
149
+ **Stats** — self-time rolled up by node type, table, and index
150
+ <img src="docs/screenshots/stats-tab.png" alt="Studio stats tab with self-time breakdowns">
151
+
152
+ </td>
153
+ </tr>
154
+ <tr>
155
+ <td width="50%">
156
+
157
+ **Dark mode**
158
+ <img src="docs/screenshots/plan-graph-dark.png" alt="Studio in dark mode showing the plan graph">
159
+
160
+ </td>
161
+ <td width="50%">
162
+
163
+ **Keyboard shortcuts** (press `?`)
164
+ <img src="docs/screenshots/keyboard-shortcuts.png" alt="Studio keyboard shortcuts overlay">
165
+
166
+ </td>
167
+ </tr>
168
+ </table>
169
+
70
170
  ---
71
171
 
72
172
  ## Example output
@@ -149,6 +249,8 @@ Total execution time: 321.0 ms
149
249
  | `pg-explain [FILE]` | **Analyze** a plan from a file, `< stdin`, or every plan in a directory (batch mode). This is the default command. |
150
250
  | `pg-explain run` | **Connect** to PostgreSQL, run `EXPLAIN` safely, and analyze the result. Needs the optional `pg` driver. |
151
251
  | `pg-explain diff <before> <after>` | **Compare** two plan JSON files and report regressions. Designed as a CI gate. |
252
+ | `pg-explain locks` | Snapshot **live lock contention**: who is blocked and by whom (`pg_blocking_pids`), with a cancel/terminate remediation. `--fail-on-blocked` exits 1 for scripting. Needs the optional `pg` driver. |
253
+ | `pg-explain studio` | Launch the local **web UI** (history, plan graph, diff, live locks). |
152
254
  | `pg-explain completion <bash\|zsh\|fish>` | Print a shell **completion** script for the given shell. |
153
255
 
154
256
  Run `pg-explain --help`, `pg-explain run --help`, or `pg-explain diff --help` for the full flag list.
@@ -180,12 +282,20 @@ Choose with `-f`/`--format` (default `terminal`); write to a file with `-o`/`--o
180
282
  | `terminal` | Color, severity heat, and proportional self-time bars for interactive use. |
181
283
  | `markdown` | The headline shareable deliverable — paste into a PR or ticket. |
182
284
  | `json` | Stable, machine-readable (`schemaVersion = 1`). |
183
- | `html` | Single self-contained file (no external assets). |
285
+ | `html` | Single self-contained file (no external assets). Auto-opens in your browser when run interactively. |
184
286
  | `text` | Plain text, no escapes — good for logs. |
185
287
 
186
288
  `diff` supports `terminal`, `markdown`, and `json`.
187
289
 
188
- Shared output flags: `--tldr` (summary + findings, no plan tree), `--redact` (strip literal values so the report is safe to share), `--ascii` (ASCII tree glyphs), `--color auto|always|never` / `--no-color`, `--compact` (compact JSON), `--config <path>`, `-q/--quiet`, `--verbose`, `--debug`.
290
+ When you run `-f html` in an interactive terminal, the report opens in your default browser automatically: with `-o report.html` that file is opened, otherwise a temp file is written and opened. Auto-open is off when output is piped/redirected or `CI` is set; use `--open` to force it (e.g. into a file in CI) and `--no-open` to disable it.
291
+
292
+ ```bash
293
+ pg-explain plan.json -f html -o report.html # writes the file and opens it
294
+ pg-explain plan.json -f html # writes a temp file and opens it
295
+ pg-explain plan.json -f html > report.html # redirected → no open, HTML to stdout
296
+ ```
297
+
298
+ Shared output flags: `--tldr` (summary + findings, no plan tree), `--redact` (strip literal values so the report is safe to share), `--open` / `--no-open` (HTML browser opening), `--ascii` (ASCII tree glyphs), `--color auto|always|never` / `--no-color`, `--compact` (compact JSON), `--config <path>`, `-q/--quiet`, `--verbose`, `--debug`.
189
299
 
190
300
  ---
191
301
 
@@ -203,7 +313,7 @@ Shared output flags: `--tldr` (summary + findings, no plan tree), `--redact` (st
203
313
 
204
314
  ## The advisor
205
315
 
206
- The advisor ships **16 rules**, each identified by a stable, greppable `PGX_*` code (the rule id is the diagnostic code, and the config key). They run in roughly most-actionable-first order:
316
+ The advisor ships **18 rules**, each identified by a stable, greppable `PGX_*` code (the rule id is the diagnostic code, and the config key). They run in roughly most-actionable-first order:
207
317
 
208
318
  | Code | Flags when… |
209
319
  | --- | --- |
@@ -211,8 +321,10 @@ The advisor ships **16 rules**, each identified by a stable, greppable `PGX_*` c
211
321
  | `PGX_SEQ_SCAN_LARGE` | A sequential scan reads a large table that an index could narrow. |
212
322
  | `PGX_NESTED_LOOP_LARGE_OUTER` | A nested loop is driven by a large outer side (re-probes inner repeatedly). |
213
323
  | `PGX_HIGH_FILTER_DISCARD` | A node reads many rows then discards most of them via a filter. |
324
+ | `PGX_LIMIT_LARGE_OFFSET` | A `LIMIT` discards a large generated prefix (OFFSET pagination — use keyset). |
214
325
  | `PGX_SORT_SPILL_DISK` | A sort spilled to disk instead of staying in `work_mem`. |
215
326
  | `PGX_HASH_SPILL_DISK` | A hash join's build side spilled to disk (multiple batches). |
327
+ | `PGX_MEMOIZE_EVICTIONS` | A Memoize cache is thrashing (evictions outpace hits, or entries overflow `work_mem`). |
216
328
  | `PGX_CORRELATED_SUBPLAN` | A correlated subplan is re-executed once per outer row. |
217
329
  | `PGX_ROW_MISESTIMATE` | Estimated vs actual row counts diverge sharply (stale/missing stats). |
218
330
  | `PGX_FILTER_COULD_BE_INDEX_COND` | A residual filter could be pushed into an index condition. |
@@ -226,6 +338,8 @@ The advisor ships **16 rules**, each identified by a stable, greppable `PGX_*` c
226
338
 
227
339
  Every finding includes the *what / why / fix* triad shown in the example above. Rules can be tuned or disabled per project (see [Config](#config-file)).
228
340
 
341
+ One additional check runs only on the `run` path (it needs a live connection, not just a plan): `PGX_STALE_STATISTICS` flags tables in the plan that were never analyzed or have churned past `staleStatsModRatio` (default 20%) since their last ANALYZE — the most common root cause behind `PGX_ROW_MISESTIMATE`. It is configured like any other rule.
342
+
229
343
  > pgexplain also has an **operational error catalog** of stable `PGX_*` codes — auth failures, unreachable hosts, SSL problems, timeouts, malformed/empty input, missing driver, and more — each with a title, cause, remediation, and Postgres docs link.
230
344
 
231
345
  ---
@@ -309,6 +423,24 @@ console.log(markdown);
309
423
 
310
424
  `analyze(input, options?)` parses the EXPLAIN JSON, optionally redacts literals, computes metrics, and runs the advisor. `render(result, options?)` emits any supported format. Other exports include `runAdvisor`, `parseExplainJson`, `computeMetrics`, `DEFAULT_CONFIG`, `FORMATS`, `JSON_SCHEMA_VERSION`, `ExitCode`, and the full type set.
311
425
 
426
+ For finer control — e.g. custom thresholds or gating a deploy script on severity:
427
+
428
+ ```ts
429
+ import { analyze, DEFAULT_CONFIG, severityAtLeast } from "pgexplain";
430
+
431
+ const result = analyze(explainJson, {
432
+ config: {
433
+ ...DEFAULT_CONFIG,
434
+ thresholds: { ...DEFAULT_CONFIG.thresholds, seqScanRows: 10_000 },
435
+ rules: { PGX_LOW_CACHE_HIT: { enabled: false } },
436
+ },
437
+ });
438
+
439
+ if (result.worstSeverity && severityAtLeast(result.worstSeverity, "warn")) {
440
+ process.exit(1); // same behaviour as `pg-explain --fail-on warn`
441
+ }
442
+ ```
443
+
312
444
  ---
313
445
 
314
446
  ## Exit codes