pgexplain 0.1.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 +52 -0
- package/LICENSE +21 -0
- package/README.md +338 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +3205 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.js +2075 -0
- package/dist/index.js.map +1 -0
- package/package.json +92 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-06-29
|
|
11
|
+
|
|
12
|
+
Initial release.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`analyze`** (the default command): read a PostgreSQL `EXPLAIN (ANALYZE)`
|
|
17
|
+
plan from a file or stdin and produce a human-readable report. Point it at a
|
|
18
|
+
directory to analyze every plan in batch.
|
|
19
|
+
- **`run`**: connect to PostgreSQL, run `EXPLAIN` safely, and analyze the
|
|
20
|
+
result. Execution is wrapped in an auto-rolled-back, read-only transaction
|
|
21
|
+
with `statement_timeout` and `lock_timeout`; non-`SELECT` statements are
|
|
22
|
+
refused without `--force`. The `pg` driver is an optional, lazy-loaded
|
|
23
|
+
dependency.
|
|
24
|
+
- **`diff`**: compare two plans (`before` → `after`) and report regressions,
|
|
25
|
+
with CI gates via `--fail-on-slower` and `--fail-on-new-findings`.
|
|
26
|
+
- **`completion`**: print a shell completion script for bash, zsh, or fish.
|
|
27
|
+
- **16 advisor rules** with stable, greppable `PGX_*` codes: sequential scan on
|
|
28
|
+
a large table, nested loop with a large outer, high filter discard, sort
|
|
29
|
+
spill to disk, hash spill to disk, index-only heap fetches, lossy bitmap,
|
|
30
|
+
workers not launched, could-be-index-only, filter-could-be-index-condition,
|
|
31
|
+
correlated subplan, cartesian product, significant JIT time, trigger time,
|
|
32
|
+
row misestimate, and low cache hit ratio.
|
|
33
|
+
- **Actionable diagnostics**: every advisor finding and every operational error
|
|
34
|
+
tells you what happened, why, and exactly how to fix it — including
|
|
35
|
+
copy-pasteable SQL and shell commands and a link to the relevant PostgreSQL
|
|
36
|
+
docs. Operational errors come from a stable `PGX_*` catalog.
|
|
37
|
+
- **5 output formats**: terminal (color, heat, bars), markdown, json (stable,
|
|
38
|
+
`schemaVersion` 1), html (self-contained), and text. Controlled with
|
|
39
|
+
`-f/--format`, `-o/--output`, `--tldr`, `--redact`, `--ascii`,
|
|
40
|
+
`--color`/`--no-color`, and CI gates (`--fail-on`, `--strict`).
|
|
41
|
+
- **Safety wrapper**: credentials are scrubbed from all output; EXPLAIN ANALYZE
|
|
42
|
+
runs in a rolled-back, read-only transaction with timeouts; non-`SELECT` is
|
|
43
|
+
refused without `--force`; `--redact` strips literal values; no telemetry.
|
|
44
|
+
- **Configuration**: tune thresholds and per-rule `enabled`/`severity` via
|
|
45
|
+
`.pgexplainrc.json`, `.pgexplainrc`, or a `pgExplain` key in `package.json`.
|
|
46
|
+
- **Programmatic library API**: `import { analyze, render } from "pgexplain"`.
|
|
47
|
+
- **Stable exit codes** so scripts and CI can branch on the kind of failure:
|
|
48
|
+
`0` success, `1` CI gate tripped, `2` usage, `3` input, `4` parse,
|
|
49
|
+
`5` database, `70` internal, `130` interrupted.
|
|
50
|
+
|
|
51
|
+
[Unreleased]: https://github.com/OWNER/pgexplain/compare/v0.1.0...HEAD
|
|
52
|
+
[0.1.0]: https://github.com/OWNER/pgexplain/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pgexplain contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# pgexplain
|
|
2
|
+
|
|
3
|
+
Turn PostgreSQL `EXPLAIN (ANALYZE)` JSON into human-readable reports and detect plan anti-patterns — **every finding tells you what happened, why, and exactly how to fix it.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/pgexplain)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
The npm package is `pgexplain`; the command it installs is `pg-explain`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why pgexplain
|
|
14
|
+
|
|
15
|
+
Most EXPLAIN tools pretty-print the plan tree and stop there — you still have to know what a "lossy bitmap heap scan" or a 500× row misestimate *means* and what to do about it.
|
|
16
|
+
|
|
17
|
+
pgexplain goes further. It runs an **advisor** over the plan, flags anti-patterns by stable `PGX_*` code, and for each one prints three things:
|
|
18
|
+
|
|
19
|
+
- **What** happened (in plain language, with the real numbers from your plan).
|
|
20
|
+
- **Why** it matters.
|
|
21
|
+
- **Fix** — concrete steps and **copy-pasteable SQL/shell commands** (e.g. the exact `CREATE INDEX` or `ANALYZE` to run), plus a link to the relevant Postgres docs.
|
|
22
|
+
|
|
23
|
+
The same philosophy applies to operational errors: auth failures, timeouts, unreachable hosts, and malformed input all come back as actionable diagnostics rather than stack traces.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
# global
|
|
31
|
+
pnpm add -g pgexplain # or: npm install -g pgexplain
|
|
32
|
+
|
|
33
|
+
# one-off, no install
|
|
34
|
+
npx pgexplain plan.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires **Node.js >= 22**. The package is **ESM-only**.
|
|
38
|
+
|
|
39
|
+
> The `pg` driver is an **optional dependency**. You only need it for `pg-explain run` (connecting to a live database). Analyzing a saved plan from a file or stdin needs no driver. If `pg` is missing when you run `run`, pgexplain tells you exactly how to install it (`PGX_PG_DRIVER_MISSING`).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quickstart
|
|
44
|
+
|
|
45
|
+
Analyze a saved plan and write a Markdown report (the headline deliverable):
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pg-explain plan.json -o report.md -f markdown
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Pipe a plan straight from psql or a file:
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
psql -XqAt -c "EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) <query>" | pg-explain
|
|
55
|
+
pg-explain < plan.json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Connect to a database, run EXPLAIN safely, and analyze the result in one step:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
pg-explain run --query "SELECT * FROM orders WHERE status = 'shipped'" --dsn "$DATABASE_URL"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Point at a directory to analyze every plan in it (batch mode):
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
pg-explain ./plans/
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Example output
|
|
73
|
+
|
|
74
|
+
Running pgexplain on a plan with a sequential scan over a large table:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
$ pg-explain test/fixtures/seq-scan-large.json --no-color
|
|
78
|
+
|
|
79
|
+
pg-explain report
|
|
80
|
+
Verdict: 2 warnings, 2 notes — top cost: Seq Scan on orders (78% of time). Total 321.0 ms.
|
|
81
|
+
|
|
82
|
+
Plan tree
|
|
83
|
+
Aggregate ▇▇▁▁▁▁▁▁ rows=1 · self 70.5 ms (22%) · cache 2%
|
|
84
|
+
└─ Seq Scan on orders ▇▇▇▇▇▇▁▁ rows=500,000 (est 1,000, 500× under) · self 250.0 ms (78%) · cache 2%
|
|
85
|
+
|
|
86
|
+
Bottlenecks (by self time)
|
|
87
|
+
1. Seq Scan on orders — 250.0 ms (78%)
|
|
88
|
+
2. Aggregate — 70.5 ms (22%)
|
|
89
|
+
|
|
90
|
+
Findings
|
|
91
|
+
|
|
92
|
+
[WARNING] Sequential scan on orders (500,000 rows) PGX_SEQ_SCAN_LARGE
|
|
93
|
+
What: Postgres read orders sequentially, scanning roughly 500,000 rows.
|
|
94
|
+
Why: A row filter ((status = 'shipped'::text)) is applied after reading every row, so no index narrowed the scan.
|
|
95
|
+
Fix: Add an index covering the WHERE/JOIN predicate on orders so Postgres can skip non-matching rows. If the query genuinely needs most of the table, the seq scan is correct — reduce the rows touched instead.
|
|
96
|
+
- Identify the selective columns in the WHERE/JOIN predicate.
|
|
97
|
+
- Ensure they are sargable (no function-wrapping or implicit casts on the column).
|
|
98
|
+
- If selectivity is low, a partial index (WHERE …) may be better.
|
|
99
|
+
Index the predicate columns: CREATE INDEX ON orders (<predicate columns>) -- columns from the filter above;
|
|
100
|
+
docs: https://www.postgresql.org/docs/current/indexes-intro.html
|
|
101
|
+
|
|
102
|
+
[WARNING] 500x row underestimate on orders PGX_ROW_MISESTIMATE
|
|
103
|
+
What: Postgres estimated 1,000 rows but 500,000 were produced — a 500x underestimate on orders.
|
|
104
|
+
Why: The planner's row estimate is based on statistics that are stale, missing, or too coarse for this predicate (e.g. correlated columns the planner treats as independent).
|
|
105
|
+
Fix: Refresh and sharpen statistics for orders: run ANALYZE orders, raise per-column statistics targets on the predicate columns, and add extended statistics for correlated columns so the planner estimates rows correctly. Underestimates feeding a nested loop or hash join are the highest priority — fix these first.
|
|
106
|
+
- Refresh table statistics first; this alone often fixes the estimate.
|
|
107
|
+
- If the column has a skewed/uneven distribution, raise its statistics target and re-ANALYZE.
|
|
108
|
+
- If the predicate spans multiple correlated columns, create extended statistics so the planner stops assuming independence.
|
|
109
|
+
Refresh statistics: ANALYZE orders;
|
|
110
|
+
Raise per-column statistics target: ALTER TABLE orders ALTER COLUMN <column> SET STATISTICS 1000;
|
|
111
|
+
ANALYZE orders;
|
|
112
|
+
Add extended statistics for correlated columns: CREATE STATISTICS <stats_name> (dependencies, ndistinct) ON <col_a>, <col_b> FROM orders;
|
|
113
|
+
ANALYZE orders;
|
|
114
|
+
docs: https://www.postgresql.org/docs/current/planner-stats.html
|
|
115
|
+
|
|
116
|
+
[NOTE] Low cache hit ratio at Aggregate (2.3%) PGX_LOW_CACHE_HIT
|
|
117
|
+
What: Aggregate served only 2.3% of its shared-buffer accesses from cache, reading 5,000 blk (39.1 MiB) from disk.
|
|
118
|
+
Why: The pages this node needed were not resident in shared_buffers, so PostgreSQL had to read them from disk. On a first run this is an expected cold cache; if it persists, the working set is larger than the cache or the scan touches more pages than necessary.
|
|
119
|
+
Fix: Re-run the query to check whether this is just a cold cache — the ratio should climb on a warm run. If it stays low, the working set exceeds shared_buffers: size shared_buffers/effective_cache_size to your RAM, or add a selective index on the scanned relation so far fewer pages are read.
|
|
120
|
+
- Run the same EXPLAIN (ANALYZE, BUFFERS) a second time; a much higher hit ratio means the first run was a cold cache and no action is needed.
|
|
121
|
+
- If the ratio stays low, check whether shared_buffers (and effective_cache_size for planner costing) are sized to the machine's RAM.
|
|
122
|
+
- If the node reads far more pages than the rows it returns, add a selective index so only matching pages are fetched.
|
|
123
|
+
Inspect current buffer-cache sizing: SHOW shared_buffers; SHOW effective_cache_size;
|
|
124
|
+
Reduce pages read with a selective index: CREATE INDEX ON <table> (<predicate columns>);
|
|
125
|
+
docs: https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS
|
|
126
|
+
|
|
127
|
+
[NOTE] Low cache hit ratio at Seq Scan on orders (2.3%) PGX_LOW_CACHE_HIT
|
|
128
|
+
What: Seq Scan on orders served only 2.3% of its shared-buffer accesses from cache, reading 5,000 blk (39.1 MiB) from disk.
|
|
129
|
+
Why: The pages this node needed were not resident in shared_buffers, so PostgreSQL had to read them from disk. On a first run this is an expected cold cache; if it persists, the working set is larger than the cache or the scan touches more pages than necessary.
|
|
130
|
+
Fix: Re-run the query to check whether this is just a cold cache — the ratio should climb on a warm run. If it stays low, the working set exceeds shared_buffers: size shared_buffers/effective_cache_size to your RAM, or add a selective index on orders so far fewer pages are read.
|
|
131
|
+
- Run the same EXPLAIN (ANALYZE, BUFFERS) a second time; a much higher hit ratio means the first run was a cold cache and no action is needed.
|
|
132
|
+
- If the ratio stays low, check whether shared_buffers (and effective_cache_size for planner costing) are sized to the machine's RAM.
|
|
133
|
+
- If the node reads far more pages than the rows it returns, add a selective index so only matching pages are fetched.
|
|
134
|
+
Inspect current buffer-cache sizing: SHOW shared_buffers; SHOW effective_cache_size;
|
|
135
|
+
Reduce pages read with a selective index: CREATE INDEX ON orders (<predicate columns>);
|
|
136
|
+
docs: https://www.postgresql.org/docs/current/runtime-config-resource.html#GUC-SHARED-BUFFERS
|
|
137
|
+
|
|
138
|
+
Total execution time: 321.0 ms
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
(In a real terminal the tree uses color, severity heat, and proportional self-time bars.)
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Commands
|
|
146
|
+
|
|
147
|
+
| Command | What it does |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `pg-explain [FILE]` | **Analyze** a plan from a file, `< stdin`, or every plan in a directory (batch mode). This is the default command. |
|
|
150
|
+
| `pg-explain run` | **Connect** to PostgreSQL, run `EXPLAIN` safely, and analyze the result. Needs the optional `pg` driver. |
|
|
151
|
+
| `pg-explain diff <before> <after>` | **Compare** two plan JSON files and report regressions. Designed as a CI gate. |
|
|
152
|
+
| `pg-explain completion <bash\|zsh\|fish>` | Print a shell **completion** script for the given shell. |
|
|
153
|
+
|
|
154
|
+
Run `pg-explain --help`, `pg-explain run --help`, or `pg-explain diff --help` for the full flag list.
|
|
155
|
+
|
|
156
|
+
### `pg-explain run` (selected flags)
|
|
157
|
+
|
|
158
|
+
| Flag | Purpose |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `--dsn` / `--host` `--port` `-d/--dbname` `-U/--user` | Connection target (or `PG*` env vars). |
|
|
161
|
+
| `--query` / `--file` | SQL to explain (a string or a `.sql` file). |
|
|
162
|
+
| `--statement <n>` | 1-based statement index when the file holds several. |
|
|
163
|
+
| `--param <v>` | Value for `$1`, `$2`, … (repeatable). |
|
|
164
|
+
| `--sslmode` `--sslrootcert` | `disable \| require \| verify-ca \| verify-full` and a CA bundle. |
|
|
165
|
+
| `--connect-timeout` `--statement-timeout` `--lock-timeout` | Time budgets (default `10s` / `30s` / `5s`). |
|
|
166
|
+
| `--force` | Allow a non-SELECT to execute (still auto-rolled-back). |
|
|
167
|
+
| `--no-rollback` | Do **not** wrap the run in a rolled-back transaction (dangerous). |
|
|
168
|
+
| `--no-analyze` | Plan estimates only — the query never executes. |
|
|
169
|
+
| `--no-buffers` `--explain-verbose` `--settings` `--wal` `--generic-plan` `--no-timing` `--no-costs` | Toggle individual EXPLAIN options. |
|
|
170
|
+
| `--compat` | Auto-omit EXPLAIN options the server is too old for. |
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Output formats
|
|
175
|
+
|
|
176
|
+
Choose with `-f`/`--format` (default `terminal`); write to a file with `-o`/`--output`.
|
|
177
|
+
|
|
178
|
+
| Format | Notes |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| `terminal` | Color, severity heat, and proportional self-time bars for interactive use. |
|
|
181
|
+
| `markdown` | The headline shareable deliverable — paste into a PR or ticket. |
|
|
182
|
+
| `json` | Stable, machine-readable (`schemaVersion = 1`). |
|
|
183
|
+
| `html` | Single self-contained file (no external assets). |
|
|
184
|
+
| `text` | Plain text, no escapes — good for logs. |
|
|
185
|
+
|
|
186
|
+
`diff` supports `terminal`, `markdown`, and `json`.
|
|
187
|
+
|
|
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`.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Safety
|
|
193
|
+
|
|
194
|
+
`EXPLAIN ANALYZE` **executes the query**, so pgexplain is conservative by default when talking to a live database:
|
|
195
|
+
|
|
196
|
+
- **Auto-rollback.** Every `run` is wrapped in a transaction that is **always rolled back** (`BEGIN … ROLLBACK`), so nothing is committed. Opt out only with `--no-rollback`.
|
|
197
|
+
- **Read-only by default.** A data-modifying statement (`INSERT`/`UPDATE`/`DELETE`/`MERGE`/DDL) is **refused** (`PGX_NON_SELECT_REFUSED`) unless you pass `--force` — and even then it runs inside the rolled-back transaction. Or drop ANALYZE (`--no-analyze`) for an estimate-only plan that never runs.
|
|
198
|
+
- **Timeouts.** `statement_timeout` and `lock_timeout` are set on the session (`--statement-timeout`, `--lock-timeout`) so a runaway query or a lock wait can't hang.
|
|
199
|
+
- **Credential scrubbing.** Connection strings, passwords, and other secrets are scrubbed from **all** output, including error messages and `--debug` stack traces.
|
|
200
|
+
- **`--redact`.** Strips literal values from expressions before analysis so a shared report leaks no data.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## The advisor
|
|
205
|
+
|
|
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:
|
|
207
|
+
|
|
208
|
+
| Code | Flags when… |
|
|
209
|
+
| --- | --- |
|
|
210
|
+
| `PGX_CARTESIAN_PRODUCT` | A nested loop has no join condition (accidental cross join). |
|
|
211
|
+
| `PGX_SEQ_SCAN_LARGE` | A sequential scan reads a large table that an index could narrow. |
|
|
212
|
+
| `PGX_NESTED_LOOP_LARGE_OUTER` | A nested loop is driven by a large outer side (re-probes inner repeatedly). |
|
|
213
|
+
| `PGX_HIGH_FILTER_DISCARD` | A node reads many rows then discards most of them via a filter. |
|
|
214
|
+
| `PGX_SORT_SPILL_DISK` | A sort spilled to disk instead of staying in `work_mem`. |
|
|
215
|
+
| `PGX_HASH_SPILL_DISK` | A hash join's build side spilled to disk (multiple batches). |
|
|
216
|
+
| `PGX_CORRELATED_SUBPLAN` | A correlated subplan is re-executed once per outer row. |
|
|
217
|
+
| `PGX_ROW_MISESTIMATE` | Estimated vs actual row counts diverge sharply (stale/missing stats). |
|
|
218
|
+
| `PGX_FILTER_COULD_BE_INDEX_COND` | A residual filter could be pushed into an index condition. |
|
|
219
|
+
| `PGX_COULD_BE_INDEX_ONLY` | An index scan could become index-only with a covering index. |
|
|
220
|
+
| `PGX_INDEX_ONLY_HEAP_FETCHES` | An index-only scan still did many heap fetches (visibility map cold). |
|
|
221
|
+
| `PGX_BITMAP_LOSSY` | A bitmap heap scan went lossy (rechecks whole pages; `work_mem` too small). |
|
|
222
|
+
| `PGX_WORKERS_NOT_LAUNCHED` | Parallel workers were planned but not all launched. |
|
|
223
|
+
| `PGX_LOW_CACHE_HIT` | A node's shared-buffer cache hit ratio is low (heavy disk reads). |
|
|
224
|
+
| `PGX_SIGNIFICANT_JIT` | JIT compilation consumed a significant share of execution time. |
|
|
225
|
+
| `PGX_TRIGGER_TIME` | Triggers consumed a significant share of execution time. |
|
|
226
|
+
|
|
227
|
+
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
|
+
|
|
229
|
+
> 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
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## CI usage
|
|
234
|
+
|
|
235
|
+
pgexplain is built to gate pull requests on plan health.
|
|
236
|
+
|
|
237
|
+
**Fail when a finding is too severe:**
|
|
238
|
+
|
|
239
|
+
```sh
|
|
240
|
+
# exit 1 if any finding at or above the given severity exists
|
|
241
|
+
pg-explain plan.json --fail-on warn
|
|
242
|
+
|
|
243
|
+
# shorthand for --fail-on warn
|
|
244
|
+
pg-explain plan.json --strict
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`--fail-on` takes `info`, `warn`, or `error`. Findings alone never change the exit code unless `--strict`/`--fail-on` is set.
|
|
248
|
+
|
|
249
|
+
**Fail on regressions between two plans:**
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
# exit 1 if execution time regresses by >= 20%, or if any new finding appears
|
|
253
|
+
pg-explain diff before.json after.json \
|
|
254
|
+
--fail-on-slower 20 \
|
|
255
|
+
--fail-on-new-findings
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Branch on the **kind** of failure without parsing text:
|
|
259
|
+
|
|
260
|
+
| Exit | Meaning |
|
|
261
|
+
| --- | --- |
|
|
262
|
+
| `0` | Success — report produced. |
|
|
263
|
+
| `1` | CI gate tripped (`--strict` / `--fail-on`, or a `diff` gate). |
|
|
264
|
+
| `2` | Usage error (bad flags, refused non-SELECT, unsupported EXPLAIN option). |
|
|
265
|
+
| `3` | Input error (empty stdin and no file, or an unreadable file). |
|
|
266
|
+
| `4` | Parse error (not valid EXPLAIN JSON, or the wrong shape). |
|
|
267
|
+
| `5` | Database error (connect / auth / permission / timeout / cancel). |
|
|
268
|
+
| `70` | Internal error — a bug in pgexplain. |
|
|
269
|
+
| `130` | Interrupted by SIGINT. |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Config file
|
|
274
|
+
|
|
275
|
+
pgexplain reads, in order, `.pgexplainrc.json`, `.pgexplainrc`, or a `pgExplain` key in `package.json` (override with `--config <path>`). Tune thresholds and enable/disable or re-severity individual rules by code:
|
|
276
|
+
|
|
277
|
+
```jsonc
|
|
278
|
+
{
|
|
279
|
+
"thresholds": {
|
|
280
|
+
// numeric knobs the rules read (e.g. minimum rows for a "large" seq scan)
|
|
281
|
+
},
|
|
282
|
+
"rules": {
|
|
283
|
+
"PGX_SEQ_SCAN_LARGE": { "severity": "error" },
|
|
284
|
+
"PGX_LOW_CACHE_HIT": { "enabled": false }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Each rule entry accepts `{ "enabled": boolean, "severity": "error" | "warn" | "info" }`.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Library usage
|
|
294
|
+
|
|
295
|
+
pgexplain is also a typed library (ESM):
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { analyze, render } from "pgexplain";
|
|
299
|
+
|
|
300
|
+
const explainJson = await fs.readFile("plan.json", "utf8");
|
|
301
|
+
|
|
302
|
+
const result = analyze(explainJson, { redact: true });
|
|
303
|
+
// result.diagnostics — findings with code/severity/what-why-fix
|
|
304
|
+
// result.worstSeverity — "error" | "warn" | "info" | null
|
|
305
|
+
|
|
306
|
+
const markdown = render(result, { format: "markdown" });
|
|
307
|
+
console.log(markdown);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`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
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Exit codes
|
|
315
|
+
|
|
316
|
+
See the [CI usage](#ci-usage) table above — `0` success, `1` CI gate, `2` usage, `3` input, `4` parse, `5` database, `70` internal, `130` SIGINT. These are stable; scripts can branch on them directly.
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Contributing
|
|
321
|
+
|
|
322
|
+
This project uses **pnpm 9** and **Node >= 22**.
|
|
323
|
+
|
|
324
|
+
```sh
|
|
325
|
+
pnpm install
|
|
326
|
+
pnpm build # tsup
|
|
327
|
+
pnpm test # vitest
|
|
328
|
+
pnpm lint # biome
|
|
329
|
+
pnpm typecheck # tsc --noEmit
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Issues and pull requests are welcome at <https://github.com/OWNER/pgexplain>.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## License
|
|
337
|
+
|
|
338
|
+
MIT © 2026 pgexplain contributors. See [LICENSE](./LICENSE).
|
package/dist/cli.d.ts
ADDED