From e8c5f448571f1870ed6ddc7ba54b8a926f3a92e9 Mon Sep 17 00:00:00 2001 From: Alexey Lesovsky Date: Mon, 22 Jun 2026 08:15:18 +0500 Subject: [PATCH 01/21] draft(userspec): create user-spec for pg-stat-statements-jit Co-Authored-By: Claude Opus 4.8 (1M context) --- ...at-pg-stat-statements-jit-code-research.md | 509 ++++++++++++++++++ ...-feat-pg-stat-statements-jit-interview.yml | 149 +++++ ...7-feat-pg-stat-statements-jit-metrics.json | 34 ++ .../007-feat-pg-stat-statements-jit.md | 225 ++++++++ 4 files changed, 917 insertions(+) create mode 100644 docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-code-research.md create mode 100644 docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-interview.yml create mode 100644 docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-metrics.json create mode 100644 docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-code-research.md b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-code-research.md new file mode 100644 index 0000000..65afd48 --- /dev/null +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-code-research.md @@ -0,0 +1,509 @@ +# Code Research — Feature 007: pg_stat_statements JIT sub-screen + +New 7th pg_stat_statements sub-screen (`statements_jit`, under the `X` menu, hotkey `x` +cycle) showing JIT compilation metrics per (user, database, queryid) row. PG15+ feature; +PG17 adds `jit_deform_count` / `jit_deform_time`. TUI-only, `NotRecordable`. Built by +analogy with the existing `statements_io` sub-screen. + +Research date: 2026-06-21. Model: opus-4-8[1m]. + +--- + +## 1. pg_stat_statements JIT column schema (exact, by PG version) + +Verified against official PostgreSQL docs: +- PG17: https://www.postgresql.org/docs/17/pgstatstatements.html +- PG15: https://www.postgresql.org/docs/15/pgstatstatements.html +- Context7 `/websites/postgresql_17` release notes (E.11.3.11.1): PG17 added the JIT + `deform_counter`, plus `local_blk_read_time`/`local_blk_write_time`, + `stats_since`/`minmax_stats_since`. + +### PG15 / PG16 base set (8 columns) + +| Column | SQL type | Diffable? | Kind | +|--------------------------|--------------------|-----------|----------| +| `jit_functions` | `bigint` | yes | count | +| `jit_generation_time` | `double precision` | yes (ms) | time, ms | +| `jit_inlining_count` | `bigint` | yes | count | +| `jit_inlining_time` | `double precision` | yes (ms) | time, ms | +| `jit_optimization_count` | `bigint` | yes | count | +| `jit_optimization_time` | `double precision` | yes (ms) | time, ms | +| `jit_emission_count` | `bigint` | yes | count | +| `jit_emission_time` | `double precision` | yes (ms) | time, ms | + +### PG17+ additions (2 columns) + +| Column | SQL type | Diffable? | Kind | +|--------------------|--------------------|-----------|----------| +| `jit_deform_count` | `bigint` | yes | count | +| `jit_deform_time` | `double precision` | yes (ms) | time, ms | + +`jit_deform_count` / `jit_deform_time` do NOT exist in PG15/PG16 — confirmed by the PG15 +docs (Table F.20 lacks them). All counts are cumulative `bigint` (safe to diff); all times +are cumulative `double precision` milliseconds. The existing pgss timing queries round such +ms values with `round(p.)` and present them as both a cumulative `text` interval and an +interval-diffable `,ms` column (see `PgStatStatementsTimingPG13`, statements.go:8). + +There is no PG18 JIT column change relative to PG17 in the release notes consulted — PG17 and +PG18 share the same JIT column set. So a two-branch selector (PG15/16 base; PG17+ adds +deform) covers PG15-18. + +--- + +## 2. Reference sub-screen pattern (`internal/query/statements.go`) + +File: `/home/lesovsky/Git/github.com/lesovsky/pgcenter/internal/query/statements.go` + +### Const naming convention + +`PgStatStatements` where suffix is `Default` (covers all / latest), +`PG13`, or `PG12`. Examples in file: +- `PgStatStatementsTimingPG12` (statements.go:21) — PG ≤12 +- `PgStatStatementsTimingPG13` (statements.go:8) — PG 13–16 +- `PgStatStatementsTimingDefault` (statements.go:34) — PG 17+ +- `PgStatStatementsGeneralDefault`, `PgStatStatementsIoDefault`, `PgStatStatementsTempDefault`, + `PgStatStatementsLocalDefault`, `PgStatStatementsWalDefault` — single-version modes. + +Plan: add `PgStatStatementsJITDefault` (PG15/16 base set) and +`PgStatStatementsJITPG17` (adds deform columns), or invert (`...PG15` base + `...Default` +for PG17+). Match the timing precedent which uses `Default` for the newest shape: +`PgStatStatementsJITPG15` (base) + `PgStatStatementsJITDefault` (PG17+). + +### SELECT shape (canonical pgss row layout) + +Every pgss "diffable" mode query follows this exact column order (see `PgStatStatementsIoDefault`, +statements.go:56, the closest analog): + +``` +pg_get_userbyid(p.userid) AS user, -- col 0 (UniqueKey component, displayed) +d.datname AS database, -- col 1 + AS *_total / "*_total,unit", -- cumulative snapshot (shown, not diffed) + AS * / "*,unit", -- DIFFED columns (DiffIntvl range) +p.calls AS calls, -- col after interval block +left(md5(p.userid::text || p.dbid::text || p.queryid::text), 10) AS queryid, -- synthetic key +regexp_replace({{.PgSSQueryLenFn}}, E'\\s+', ' ', 'g') AS query -- last col +FROM {{.PGSSSchema}}.pg_stat_statements p JOIN pg_database d ON d.oid=p.dbid +``` + +`statements_io` column count breakdown (Ncols=13): user(0), database(1), 4 totals(2-5), +4 intervals(6-9), calls(10), queryid(11), query(12). `DiffIntvl: [2]int{6,10}` (view.go:221) +— note the diff range upper bound is `calls` index (10), i.e. it spans the 4 interval +columns 6-9 plus `calls` at 10. (`statements_wal` uses `DiffIntvl {3,6}` with only 2 interval +cols + records/fpi + calls; layout differs per mode.) + +### Templating tokens + +- `{{.PGSSSchema}}` — schema where pg_stat_statements is installed (`p.ExtPGSSSchema`). +- `{{.PgSSQueryLenFn}}` — expands to a query-text expression honoring the configured query + length (e.g. `left(p.query, N)` or `p.query`). Resolved by `query.Format(tmpl, opts)`. +- Both are resolved in `view.Views.Configure()` → `query.Format()` (view.go:395-402). + +The `Default` (PG17+) templates use a raw backtick string with `'\s+'` (single backslash); +the older concatenated `+ "..."` style uses `E'\\s+'`. Either works — match neighbours. + +### Version-branch selector functions + +Two selectors live at the bottom of statements.go: +- `SelectStatStatementsTimingQuery(version int) string` (statements.go:306) — branches: + `version < 130000` → PG12; `version >= 170000` → Default; else → PG13. +- `SelectQueryReportQuery(version int) string` (statements.go:318) — same 3-way branch. + +Note: `statements_general`, `statements_io`, `statements_temp`, `statements_local`, +`statements_wal` have NO selector — they use a single `*Default` const wired directly as +`QueryTmpl` in view.go and are never re-selected in `Configure()`. Only `statements_timings` +goes through a selector (view.go:373-375). + +### Plan for `SelectStatStatementsJITQuery` + +```go +// SelectStatStatementsJITQuery returns proper statements_jit query depending on Postgres version. +func SelectStatStatementsJITQuery(version int) string { + switch { + case version >= 170000: + return PgStatStatementsJITDefault // base 8 + jit_deform_count/time + default: + return PgStatStatementsJITPG15 // base 8 columns (PG15/16) + } +} +``` + +The view's `MinRequiredVersion: query.PostgresV15` gate (see §3) means this selector is only +ever called with version ≥ 150000, so the two-way branch is sufficient — no PG<15 case +needed. `PostgresV15 = 150000` is confirmed present (query.go:18); also `PostgresV16`, +`PostgresV17`, `PostgresV18` all exist (query.go:19-21). + +Wire it in `view.Views.Configure()` next to the timings case (view.go:373): +```go +case "statements_jit": + view.QueryTmpl, view.Ncols, view.DiffIntvl = query.SelectStatStatementsJITQuery(opts.Version) + v[k] = view +``` +Because Ncols/DiffIntvl differ between PG15/16 (8 JIT metrics) and PG17+ (10 JIT metrics), +the selector should ALSO return Ncols and DiffIntvl (like `SelectStatWALQuery`, +`SelectStatIOQuery` which return `(string, int, [2]int)`), not just the string like the +timing selector. This is the cleanest fit — see §3 for the UniqueKey constraint that forces +two different column SETs rather than hiding columns. + +--- + +## 3. View registration (`internal/view/view.go`) + +File: `/home/lesovsky/Git/github.com/lesovsky/pgcenter/internal/view/view.go` + +### `View` struct fields (view.go:9-31) + +`Name, MinRequiredVersion, QueryTmpl, Query, DiffIntvl [2]int, Cols []string, Ncols int, +OrderKey int, OrderDesc bool, UniqueKey int, ColsWidth map[int]int, Aligned bool, Msg string, +Filters map[int]*regexp.Regexp, Refresh, ShowExtra, CollectExtra, IOAvailable, +DelayAcctAvailable, NotRecordable bool`. + +### `statements_io` entry (view.go:218-229) — primary template to copy + +```go +"statements_io": { + Name: "statements_io", + QueryTmpl: query.PgStatStatementsIoDefault, + DiffIntvl: [2]int{6, 10}, + Ncols: 13, + OrderKey: 0, + OrderDesc: true, + UniqueKey: 11, + ColsWidth: map[int]int{}, + Msg: "Show statements IO statistics", + Filters: map[int]*regexp.Regexp{}, +}, +``` + +### `statements_wal` entry (view.go:254-266) — has MinRequiredVersion + NotRecordable precedent + +```go +"statements_wal": { + Name: "statements_wal", + MinRequiredVersion: query.PostgresV13, + QueryTmpl: query.PgStatStatementsWalDefault, + DiffIntvl: [2]int{3, 6}, + Ncols: 9, + OrderKey: 0, + OrderDesc: true, + UniqueKey: 7, + ColsWidth: map[int]int{}, + Msg: "Show statements WAL statistics", + Filters: map[int]*regexp.Regexp{}, +}, +``` + +(`statements_wal` is NOT NotRecordable — it predates the TUI-first principle. Our new view +MUST set `NotRecordable: true`; the closest NotRecordable+MinRequiredVersion+UniqueKey +precedent is `stat_io` view.go:166-179.) + +### Field meanings + +- **`DiffIntvl [2]int`** — `[start, end]` inclusive column-index range that the diff engine + treats as deltas (per-interval values). Picks the "interval" block of the SELECT (and + often includes `calls`). `statements_io` `{6,10}` covers interval cols 6-9 + calls 10. +- **`UniqueKey int`** — index of the synthetic `md5(...) queryid` column used to match rows + across diff snapshots. For pgss it is the `queryid` column (`statements_io` → 11). The + `user`/`database` text columns are NOT the unique key — the md5 is, because the same query + appears once per (user,db) and the md5 folds all three. +- **`Ncols`** — total columns returned; right border for `OrderKey` wrap (config_view.go:37). +- **`OrderKey` / `OrderDesc`** — initial sort column / descending. pgss screens use + `OrderKey: 0` (user) DESC. (stat_io uses `OrderKey: 4`, the first diffed counter.) +- **`ColsWidth: map[int]int{}`**, **`Filters: map[int]*regexp.Regexp{}`** — initialized empty. +- **`Cols`** — populated at runtime from the result header (top/stat.go:314), not set here. + +### Synthetic md5 queryid → columns cannot be hidden per version + +The `UniqueKey` points at the md5 `queryid` column whose index is fixed by the SELECT column +count. The diff/align machinery enforces a minimum column width of 8 on the md5 (10-char +hash) and treats the layout positionally. Therefore you cannot hide a column for an older PG +version while keeping the same query — the indices (and hence UniqueKey/DiffIntvl/Ncols) +would shift. The established pattern is to ship a DIFFERENT query (different column SET) per +version and have the selector return matching `Ncols`/`DiffIntvl`/`UniqueKey`. + +How other pgss screens differ across versions: only `statements_timings` differs across +versions (PG12 vs PG13 vs PG17+), and it does so by swapping the WHOLE query +(`SelectStatStatementsTimingQuery`) — NOT by hiding columns. Its column COUNT happens to stay +constant (13) across those variants, so the static `Ncols/DiffIntvl/UniqueKey` in view.go +(view.go:197-201) remain valid and `Configure()` only swaps `QueryTmpl`. For JIT the column +count DOES change (8 vs 10 JIT metrics → ~Ncols differs), so the selector must also return +`Ncols`/`DiffIntvl` (and UniqueKey must be recomputed). This is exactly the `stat_io` model +(`SelectStatIOQuery` returns `(string, int, [2]int)`, view.go:386) — follow it, and set the +view's static `UniqueKey: 0`-style placeholder while the Configure path patches the rest. + +Recommended JIT layouts (proposal, with `user`=0, `database`=1): + +PG15/16 (8 JIT metrics, counts+times). Suggested compact set: show the 4 count metrics + +4 time metrics as interval columns. Mirroring statements_io's total+interval doubling would +make 8 totals + 8 intervals = very wide; given no horizontal scroll (see §7), prefer a +single interval block (like the timings `,ms` columns) rather than total+interval doubling. +Final column set is a tech-spec decision — but keep UniqueKey aligned to the md5 column index +and DiffIntvl spanning the count/time block + calls. + +--- + +## 4. The two count-tests that break when adding a view + +### (a) `internal/view/view_test.go::TestNew` (view_test.go:9-12) + +```go +func TestNew(t *testing.T) { + v := New() + assert.Equal(t, 26, len(v)) // 26 is the total number of views have to be returned +} +``` +**Fix:** bump `26` → `27` and update the comment. Adding `statements_jit` to `view.New()` +raises the map size by one. + +### (b) `record/record_test.go::Test_filterViews` (record_test.go:101-136) + +```go +testcases := []struct { + version int + pgssSchema string + wantN int // filtered-out count + wantV int // remaining count +}{ + {version: 140000, pgssSchema: "", wantN: 10, wantV: 16}, + {version: 140000, pgssSchema: "public", wantN: 4, wantV: 22}, + {version: 130000, pgssSchema: "public", wantN: 7, wantV: 19}, + {version: 120000, pgssSchema: "public", wantN: 10, wantV: 16}, + {version: 110000, pgssSchema: "public", wantN: 12, wantV: 14}, + {version: 100000, pgssSchema: "public", wantN: 12, wantV: 14}, +} +``` +The new view is `MinRequiredVersion: query.PostgresV15` (150000) AND `NotRecordable: true`. +In `filterViews` (record.go:200-233) the `NotRecordable` branch (record.go:208) fires BEFORE +the version gate — so on EVERY test row the new view counts as filtered-out: `wantN += 1`, +`wantV` unchanged (it never joins the remaining set), regardless of version or pgssSchema. + +**Fix — bump every row's `wantN` by exactly 1** (`wantV` stays the same on all 6 rows): + +| version | pgssSchema | wantN old → new | wantV | +|---------|-----------|-----------------|-------| +| 140000 | "" | 10 → 11 | 16 | +| 140000 | public | 4 → 5 | 22 | +| 130000 | public | 7 → 8 | 19 | +| 120000 | public | 10 → 11 | 16 | +| 110000 | public | 12 → 13 | 14 | +| 100000 | public | 12 → 13 | 14 | + +There is NO separate name-map that must gain an entry in `Test_filterViews`. The other +`record_test.go` tests (`TestFilterViews_NotRecordable`, `TestFilterViews_dropsExplicitNotRecordable`) +build their own ad-hoc `view.Views{}` literals and are NOT affected. + +Note: `Test_filterViews` cases stop at version 140000 (no 150000 row), but because the +`NotRecordable` drop is version-independent, the +1 applies uniformly anyway. (If a 150000+ +row were added later it would also be +1.) + +--- + +## 5. Menu + cycle wiring + +### (a) Uppercase menu `X` — `top/menu.go` + +File: `/home/lesovsky/Git/github.com/lesovsky/pgcenter/top/menu.go` + +`menuPgss` items list (`selectMenuStyle`, menu.go:53-60) currently has 6 entries (indices +0-5). **Add a 7th**, index 6: +```go +" pg_stat_statements WAL usage", +" pg_stat_statements JIT compilation", // NEW index 6 +``` + +`menuSelect` `case menuPgss` switch (menu.go:155-172) currently handles cy 0-5 + default. +**Add `case 6`** before `default`: +```go +case 5: + viewSwitchHandler(app.config, "statements_wal") +case 6: + viewSwitchHandler(app.config, "statements_jit") // NEW +default: + viewSwitchHandler(app.config, "statements_timings") +``` + +The `X` keybinding already opens this menu: `{"sysstat", 'X', menuOpen(menuPgss, app.config, +app.postgresProps.ExtPGSSSchema)}` (keybindings.go:45). The menu height auto-sizes to +`len(s.items)` (menu.go:115) — no manual height edit needed. + +### (b) Lowercase cycle `x` — `top/config_view.go` + +File: `/home/lesovsky/Git/github.com/lesovsky/pgcenter/top/config_view.go` + +`statementsNextView` (config_view.go:160-180) currently cycles +`...wal → timings`. **Insert `statements_jit`** between wal and timings: +```go +case "statements_wal": + next = "statements_jit" // CHANGED (was statements_timings) +case "statements_jit": // NEW + next = "statements_timings" +default: + next = "statements_timings" +``` + +The `x` keybinding: `{"sysstat", 'x', switchViewTo(app, "statements")}` (keybindings.go:40). +`switchViewTo` → `statementsNextView(app.config.view.Name)` (config_view.go:115). It already +guards `ExtPGSSSchema == ""` (config_view.go:105). No keybindings.go edit required for `x`. + +### (c) Version-aware availability note + +`statements_jit` has `MinRequiredVersion: query.PostgresV15`. On PG<15 the view is filtered +out of the recordable set, but in the live TUI the menu/cycle target it unconditionally. Check +how `viewSwitchHandler` (config_view.go:206-210) behaves when switching to a view absent from +`config.views` on PG<15 — confirm `view.New()` / the top-level view filter (top/top.go setup) +removes sub-PG15 views from `config.views`, otherwise switching to `statements_jit` on PG14 +yields a zero-value `View{}`. The tech-spec must verify the TUI's view-availability filter +(separate from record's `filterViews`) drops `statements_jit` on PG<15 and that the menu/cycle +degrade gracefully (skip it). Search target: where `top` builds `config.views` and applies +`VersionOK`. + +--- + +## 6. NotRecordable mechanism + +- Field: `NotRecordable bool` on `view.View` (view.go:30): "When true, + record/record.go:filterViews() skips this view." +- Enforcement: `record/record.go:filterViews()` (record.go:200), the `if v.NotRecordable` + branch at record.go:208 — `delete(views, k); filtered++; continue`. Fires BEFORE the + version gate (record.go:214) and the pgss-schema gate (record.go:221). +- Precedent setters (all `NotRecordable: true`): + - `bgwriter` (view.go:151) — feature 004. + - `replslots` (view.go:164) — feature 005. + - `stat_io` (view.go:178), `stat_io_time` (view.go:192) — feature 006. +- `statements_*` views are normally recordable (none set the flag) — our new + `statements_jit` MUST set `NotRecordable: true` per the 0.11.0 TUI-first principle + (`docs/roadmap-0.11.0.md:86-92`, ADR `[004-feat-bgwriter-checkpointer]` + `docs/decisions-log.md:223-233`). +- **report.go**: `doDescribe` (report/report.go:604) maps view-name → description text + (report.go:607+; includes `statements_io`, `statements_wal`, etc.). NotRecordable views + are not recorded, so there is no recorded data to report — `statements_jit` does NOT need a + description entry here. Confirm by precedent: bgwriter/replslots/stat_io (all NotRecordable) + have NO entry in this map. Skip the report.go description for the JIT view. + +--- + +## 7. jit=off / zero-JIT row filtering + +- **`statements_io` / `statements_wal` / all current pgss screens do NOT filter zero rows.** + Their SELECTs end at `FROM {{.PGSSSchema}}.pg_stat_statements p JOIN pg_database d ON + d.oid=p.dbid` with NO `WHERE`/`HAVING` (statements.go:67, 98). Every statement is shown. +- **`pg_stat_io` (feature 006) DOES filter all-zero rows** via a count-based `WHERE`: + `... WHERE coalesce(reads,0)+coalesce(writes,0)+...+coalesce(fsyncs,0) > 0` + (io.go:35, io.go:58, io.go:79). The comment (io.go:17-19) explains the count-based WHERE + keeps the screen compact and makes the count and time sub-screens share an identical + row-set. This was ADR-backed (decisions-log §[006...]). +- **Recommendation for JIT:** filter to rows with actual JIT activity. With `jit=on` (default) + the vast majority of normalized statements never trigger JIT (only large-cost plans do), so + WITHOUT a filter the screen would be dominated by all-zero rows. Add a count-based + `WHERE jit_functions > 0` (or + `WHERE coalesce(jit_functions,0)+coalesce(jit_inlining_count,0)+coalesce(jit_optimization_count,0)+coalesce(jit_emission_count,0) > 0`) + following the `pg_stat_io` precedent. This also gracefully handles `jit=off` (all-zero → + empty screen) — consider an optional cmdline hint "no JIT activity (jit=off?)" when empty, + matching the interview's "optional jit=off hint" open question. Using only `jit_functions + > 0` is simplest and sufficient: a statement with any JIT work always has + `jit_functions > 0`. This is a tech-spec decision but the strong recommendation is: FILTER. + +--- + +## 8. Exact files to touch (with current line anchors) + +| File | Change | Anchor | +|------|--------|--------| +| `internal/query/statements.go` | Add `PgStatStatementsJITPG15` + `PgStatStatementsJITDefault` consts; add `SelectStatStatementsJITQuery(version) (string, int, [2]int)` selector | consts block ends ~statements.go:303; selectors at statements.go:305-327 | +| `internal/view/view.go` | Add `"statements_jit"` View entry (MinRequiredVersion PostgresV15, NotRecordable true, UniqueKey=md5 idx); add `case "statements_jit":` in `Configure()` | view entries view.go:194-266; Configure switch view.go:373-391 | +| `top/menu.go` | Add 7th `menuPgss` item (index 6); add `case 6` in `menuSelect`/`case menuPgss` | items menu.go:53-60; switch menu.go:156-171 | +| `top/config_view.go` | Insert `statements_jit` into `statementsNextView` cycle (wal→jit→timings) | config_view.go:160-180 | +| `top/keybindings.go` | NO CHANGE (`x` at keybindings.go:40 and `X` at keybindings.go:45 already wired) | — | +| `report/report.go` | NO CHANGE (NotRecordable → no description entry; matches bgwriter/replslots/stat_io) | doDescribe map report.go:604-624 | + +### Test files to touch + +| File | Change | Anchor | +|------|--------|--------| +| `internal/view/view_test.go` | Bump `TestNew` count `26 → 27`; optionally add `TestNew_StatementsJITView` guard (mirror `TestNew_StatIOView` view_test.go:17) | view_test.go:9-12 | +| `record/record_test.go` | Bump `Test_filterViews` `wantN` +1 on all 6 rows (table in §4) | record_test.go:123-128 | +| `internal/query/statements_test.go` | Add `TestSelectStatStatementsJITQuery` (mirror `TestSelectStatStatementsTimingQuery` statements_test.go:10) + add JIT exec sub-test loop over versions 150000-180000 (mirror `Test_StatStatementsQueries` statements_test.go:34, gated PG15+ like the WAL PG13+ loop at statements_test.go:86) | statements_test.go:10-103 | + +### PG version constants (confirmed present, `internal/query/query.go`) + +`PostgresV13=130000` (query.go:16), `PostgresV14=140000` (17), `PostgresV15=150000` (18), +`PostgresV16=160000` (19), `PostgresV17=170000` (20), `PostgresV18=180000` (21). Use +`query.PostgresV15` for the gate. + +--- + +## 9. Integration points & runtime data flow + +- `view.Views.Configure(opts query.Options)` (view.go:357) runs the per-view selector switch + then `query.Format(view.QueryTmpl, opts)` for every view (view.go:395). `opts.Version` + drives JIT branch; `opts.ExtPGSSSchema` / query-len feed `{{.PGSSSchema}}` / + `{{.PgSSQueryLenFn}}`. +- `top/stat.go:alignViewToResult` (stat.go:303) populates `view.Cols` from the live result + header (stat.go:314) and computes `ColsWidth` — JIT columns are auto-aligned; the md5 + `queryid` min width is enforced there. +- Diff/ordering uses `DiffIntvl`, `UniqueKey`, `OrderKey`, `Ncols` — all must be internally + consistent with the returned column count (see §3). + +--- + +## 10. Potential problems / constraints + +1. **No horizontal column scroll.** Columns past terminal width are silently truncated + (decisions-log §[006...] context, decisions-log:355). With 10 JIT metrics on PG17+ plus + user/database/calls/queryid/query, a total+interval doubling (like statements_io) would be + far too wide. Prefer a SINGLE interval block (counts + times), like the timings `,ms` + columns. This is the main column-design constraint for the tech-spec. + +2. **Ncols changes across versions (8 vs 10 JIT metrics).** Because of the synthetic md5 + `UniqueKey` (§3), the JIT selector MUST return `(string, Ncols, DiffIntvl)` and the + Configure case must patch all three (follow `SelectStatIOQuery` model, view.go:386 — + NOT the timings model which keeps Ncols constant). Getting Ncols/UniqueKey/DiffIntvl out + of sync with the actual column count is the highest-risk bug; covered only by an exec test + against a real PG (CI matrix PG15-18) — local runs without PG skip these (`t.Skipf`, + statements_test.go:53). + +3. **Count-test breakage masked locally.** `TestNew` and `Test_filterViews` (§4) are the two + count assertions that will fail. They run without a PG instance, so `make test` catches + them locally — but they are easy to forget. Both are pure integer bumps. + +4. **PG<15 TUI view availability.** The record-side `filterViews` handles PG<15 via + `MinRequiredVersion`, but the live TUI menu/cycle (§5c) must also drop `statements_jit` on + PG<15. Verify the `top` view-availability filter (separate from record's `filterViews`) + removes sub-PG15 views from `config.views`; otherwise selecting JIT on PG14 yields a + zero-value `View{}`. This is the one item needing live verification in the tech-spec. + +5. **jit=off / no-activity empty screen.** With the recommended `WHERE jit_functions > 0` + filter, `jit=off` and low-activity systems show an empty screen. Decide whether to surface + an explanatory cmdline hint (interview open question). Not a blocker. + +### Tech debt / ADRs in scope + +- `docs/tech-debt.md` Active Debt: no items touch statements.go / view.go / menu.go / + config_view.go (grep found none) — no debt to flag. +- ADRs that constrain this feature (settled — do not re-litigate): + - `[004-feat-bgwriter-checkpointer]` (decisions-log:223) — TUI-first / `NotRecordable: true` + rationale. Apply directly: set `NotRecordable: true`, no record/report wiring. + - `[006-feat-pg-stat-io]` synthetic-md5-key + count-based zero-row WHERE (decisions-log:355, + 373) — direct precedent for the JIT `WHERE jit_functions > 0` filter and the + UniqueKey-on-md5 layout. + +--- + +## Summary + +- **JIT schema (confirmed vs PG official docs):** PG15/16 base = 8 columns — + `jit_functions`(bigint), `jit_generation_time`(double), `jit_inlining_count`(bigint), + `jit_inlining_time`(double), `jit_optimization_count`(bigint), `jit_optimization_time`(double), + `jit_emission_count`(bigint), `jit_emission_time`(double). PG17+ adds 2 — + `jit_deform_count`(bigint), `jit_deform_time`(double). All `*_count`/`jit_functions` are + cumulative bigint counts (diffable); all `*_time` are cumulative double-precision ms + (round + diff, like the timing screen). PG18 == PG17 column set. +- **Version-branch plan:** two query consts (PG15 base / PG17+ Default) + selector + `SelectStatStatementsJITQuery(version) (string, int, [2]int)` returning query + Ncols + + DiffIntvl (stat_io model, because Ncols differs); gate `MinRequiredVersion: + query.PostgresV15`; two-way branch (≥170000 → Default, else base) is sufficient. +- **Two count-test fixes:** `view_test.go::TestNew` `26 → 27`; `record_test.go::Test_filterViews` + `wantN +1` on all 6 rows (wantV unchanged), because NotRecordable drops it on every version. +- **Zero-row filtering:** RECOMMEND filtering — add `WHERE jit_functions > 0` (pg_stat_io + precedent). Current statements_io/wal do NOT filter, but JIT rows are overwhelmingly all-zero + under default `jit=on`, so an unfiltered JIT screen would be near-useless; filtering also + cleanly handles `jit=off`. diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-interview.yml b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-interview.yml new file mode 100644 index 0000000..a24c033 --- /dev/null +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-interview.yml @@ -0,0 +1,149 @@ +# Interview Plan for Feature 007 — pg_stat_statements JIT screen + +interview_metadata: + feature_name: "pg-stat-statements-jit" + feature_base: "docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit" + work_type: "feature" + started: "2026-06-21" + last_updated: "2026-06-21" + status: "in_progress" + can_resume: true + current_question_num: 0 + feature_size: "S" + +conversation_history: [] + +phase1_feature_overview: + feature_description: + score: 85 + status: "complete" + value: > + New 7th pg_stat_statements sub-screen under the X menu showing JIT compilation + metrics per (user, database, queryid): jit_functions, generation/inlining/ + optimization/emission counts+times (PG15+); PG17 adds deform_count/deform_time. + Built by analogy with statements_io. TUI-only, NotRecordable. Closes release 0.11.0. + gaps: "" + required: true + user_problem: + score: 80 + status: "complete" + value: > + No way in pgcenter to see JIT compilation cost per query. With jit=on (default), + JIT overhead can dominate planning/execution for short queries; a DBA needs to spot + which normalized statements pay heavy generation/optimization/emission time. + gaps: "" + required: true + target_users: + score: 80 + status: "complete" + value: "Practicing PostgreSQL DBA troubleshooting query latency / JIT overhead." + required: false + success_criteria: + score: 60 + status: "partial" + value: "JIT sub-screen reachable via X menu and x-cycle; correct columns per PG version; CI green PG14-18." + gaps: "Confirm exact acceptance criteria and zero-JIT row handling" + required: true + constraints: + score: 85 + status: "complete" + value: > + PG15+ feature (view absent <15). TUI-only, NotRecordable. pgss extension required. + Single query per version (PG15/16 base, PG17+ adds deform_*). Synthetic md5 queryid + UniqueKey -> columns cannot be hidden, must use per-version column set. + gaps: "" + required: true + testing_strategy: + score: 70 + status: "partial" + value: > + S feature. Unit: SelectStatStatementsJITQuery version branching (query_test.go pattern). + Fix the two count-tests broken by adding a view (view_test.go::TestNew, + record/record_test.go::Test_filterViews). Full PG14-18 gate in CI. + gaps: "Confirm whether config_view/menu need tests" + required: true + +phase2_user_experience: + user_stories: + score: 50 + status: "partial" + value: "" + gaps: "Need concrete step-by-step scenarios" + required: true + acceptance_criteria: + score: 50 + status: "partial" + value: "" + gaps: "Testable checklist" + required: true + edge_cases: + score: 40 + status: "partial" + value: "" + gaps: "jit=off (all-zero), PG<15, zero-JIT queries, deform on PG17+" + required: true + error_scenarios: + score: 60 + status: "partial" + value: "pgss absent -> 'pg_stat_statements not found' (existing X-menu behavior)." + gaps: "PG<15 behavior" + required: true + verification_strategy: + score: 50 + status: "partial" + value: "" + gaps: "Local PG17 manual check + CI matrix" + required: true + ui_ux_requirements: + score: 55 + status: "partial" + value: "7th menu item; x-cycle wal->jit->timings; columns TBD; optional jit=off hint." + gaps: "Exact column set/order, hint decision, zero-row filtering" + required: true + risks: + score: 60 + status: "partial" + value: "Lowest-risk feature. Main pitfall: count-test breakage masked locally (no PG)." + gaps: "" + required: true + technical_decisions: + score: 60 + status: "partial" + value: "" + gaps: "Column set, hint, zero-row filter, total vs interval columns" + required: false + +phase3_integration: + integration_points: + score: 80 + status: "complete" + value: > + internal/query/statements.go (new query const + SelectStatStatementsJITQuery selector), + internal/view/view.go (new statements_jit view + DiffIntvl/UniqueKey/Ncols), + top/menu.go (menuPgss item + menuSelect case), top/config_view.go (statementsNextView link). + gaps: "" + required: true + dependencies: + score: 80 + status: "complete" + value: "pg_stat_statements extension; existing pgss view/menu/cycle infrastructure." + required: false + data_requirements: + score: 70 + status: "partial" + value: "JIT columns from pg_stat_statements; counts diffable, times rounded ms; md5 queryid." + gaps: "Confirm exact column list via code research" + required: false + migration_needs: + score: 100 + status: "complete" + value: "None — additive TUI feature." + required: false + +phase4_completion: + userspec_file: "docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md" + status: "created" + +decision_rules: + required_threshold: 85 + optional_threshold: 50 diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-metrics.json b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-metrics.json new file mode 100644 index 0000000..e62aed9 --- /dev/null +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-metrics.json @@ -0,0 +1,34 @@ +{ + "meta": { + "schema_version": "1.0", + "feature_id": "007", + "feature_name": "pg-stat-statements-jit", + "feature_base": "docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit", + "project": "pgcenter", + "feature_size": "S", + "model": "claude-opus-4-8[1m]", + "date_started": "2026-06-21", + "date_completed": "" + }, + "phases": { + "user_spec": null, + "tech_spec": null, + "task_decomposition": null, + "feature_execution": null, + "done": null + }, + "quality": { + "validation_rounds": {}, + "validation_findings": { "critical": 0, "major": 0, "minor": 0 }, + "review_rounds": {}, + "review_findings": { "critical": 0, "major": 0, "minor": 0 }, + "first_pass_rate_pct": null + }, + "volume": { + "interview_questions": 0, + "tasks_count": 0, + "waves_count": 0, + "agents_spawned": 0, + "commits_count": 0 + } +} diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md new file mode 100644 index 0000000..3d33fc3 --- /dev/null +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md @@ -0,0 +1,225 @@ +--- +# Creation date (YYYY-MM-DD) +created: 2026-06-22 + +# Status: draft | approved +status: draft + +# Work type: feature | bug | refactoring +type: feature + +# Feature size: S (1-3 files, local fix) | M (several components) | L (new architecture) +size: S +--- + +# User Spec: pg_stat_statements JIT screen + +## Что делаем +Добавляем 7-й под-экран в меню `pg_stat_statements` (`X`) — **JIT**, показывающий стоимость +JIT-компиляции по нормализованным запросам: кумулятивные времена фаз генерации/инлайнинга/ +оптимизации/эмиссии (и деформации на PG17+) + интервальные дельты этих времён и число +JIT-компилированных функций. Под-экран строится по образцу `statements_timings`. Только TUI, +`NotRecordable`. Это последняя фича релиза 0.11.0. + +## Зачем +Сейчас в pgcenter нет способа увидеть стоимость JIT-компиляции по запросам. При `jit=on` +(дефолт начиная с PG12) накладные расходы JIT для коротких/частых запросов могут превышать +выигрыш от компиляции — это классическая причина «непонятной» латентности. DBA нужно быстро +найти, какие нормализованные запросы платят тяжёлым временем генерации/оптимизации/эмиссии, +чтобы решить — поднять `jit_above_cost`/`jit_optimize_above_cost` или выключить JIT для +нагрузки. Данные есть в `pg_stat_statements`, но достаются только ручным SQL. + +## Пользовательские истории + +- Как DBA, я хочу открыть JIT-под-экран `pg_stat_statements`, чтобы увидеть запросы с + наибольшим суммарным временем JIT-компиляции, не уходя в `psql`. +- Как DBA, я хочу видеть разбивку времени JIT по фазам (генерация, инлайнинг, оптимизация, + эмиссия, деформация), чтобы понять, какая фаза доминирует и какой GUC крутить. +- Как DBA, я хочу видеть интервальные дельты (что JIT-компилируется прямо сейчас) рядом с + кумулятивными итогами, чтобы отличить историческую нагрузку от текущей активности. +- Как DBA на сервере с `jit=off`, я хочу понятный сигнал, что JIT-активности нет, а не пустой + экран без объяснения. + +### Пользовательские сценарии + +**Сценарий 1: Найти запросы с дорогой JIT-компиляцией** +1. Пользователь нажимает `X` — открывается меню `pg_stat_statements`, в нём 7-й пункт + `pg_stat_statements JIT`. +2. Пользователь выбирает JIT (или циклически листает под-экраны клавишей `x` до JIT). +3. Система показывает таблицу, отсортированную по `gen_total` (суммарное время генерации) по + убыванию — самые «дорогие по JIT» запросы сверху. +4. Колонки: `user`, `database`, кумулятивные `gen_total/inline_total/opt_total/emit_total` + (`+deform_total` на PG17+), интервальные `gen_ms/inline_ms/opt_ms/emit_ms` + (`+deform_ms` на PG17+), `functions` (число JIT-функций за интервал), `queryid`, `query`. +5. Результат: DBA видит, какие запросы и в какой фазе тратят время на JIT. + +**Сценарий 2: Переключение фазы доминирования и сортировка** +1. На JIT-экране пользователь стрелками меняет колонку сортировки (например на `opt_ms`). +2. Система пересортировывает строки; текстовые итоги-длительности сортируются численно + (через duration-aware сортировку `internal/stat/postgres.go`, корректно для `HH:MM:SS` + с 3+ значными часами и `N days ...`). +3. Пользователь фильтрует по `query`/`database` клавишей `/`. +4. Результат: DBA изолирует конкретные запросы и фазу. + +**Сценарий 3: Сервер без JIT-активности** +1. Пользователь открывает JIT-под-экран на сервере с `jit=off` (или где ни один запрос не + превысил `jit_above_cost`). +2. Так как SQL фильтрует строки `WHERE jit_functions > 0`, таблица пуста. +3. В командной строке показывается хинт о том, что JIT-активности нет (см. UX-поведение). +4. Результат: DBA понимает, что пусто — это норма, а не баг. + +## Дизайн и интерфейс + +### Страницы / экраны +- **JIT-под-экран `pg_stat_statements` (`statements_jit`):** многострочная таблица, одна строка + на `(user, database, queryid)`. Седьмой режим в меню `X`, в одном ряду с timings/general/io/ + temp/local/wal. Доступен и циклом `x` (порядок: …→ wal → **jit** → timings → …). + +### Ключевые компоненты +- **Меню `X` (menuPgss):** добавляется 7-й пункт `" pg_stat_statements JIT"`. +- **Таблица JIT:** колонки зависят от версии PostgreSQL. + + **PG15/16 (13 колонок):** + + | idx | колонка | смысл | diff | + |-----|---------|-------|------| + | 0 | `user` | владелец запроса | — | + | 1 | `database` | БД | — | + | 2 | `gen_total` | кумулятивное время генерации (текст-длительность) | — | + | 3 | `inline_total` | кумулятивное время инлайнинга | — | + | 4 | `opt_total` | кумулятивное время оптимизации | — | + | 5 | `emit_total` | кумулятивное время эмиссии | — | + | 6 | `gen_ms` | время генерации за интервал, мс | ✓ | + | 7 | `inline_ms` | время инлайнинга за интервал, мс | ✓ | + | 8 | `opt_ms` | время оптимизации за интервал, мс | ✓ | + | 9 | `emit_ms` | время эмиссии за интервал, мс | ✓ | + | 10 | `functions` | число JIT-функций за интервал | ✓ | + | 11 | `queryid` | синтетический md5 (UniqueKey) | — | + | 12 | `query` | нормализованный текст запроса | — | + + `DiffIntvl {6,10}`, `Ncols 13`, `UniqueKey 11`, `OrderKey 2` (desc). + + **PG17+ (15 колонок):** добавляются `deform_total` (idx 6) и `deform_ms` (idx 11, diff); + индексы queryid/query сдвигаются. `DiffIntvl {7,12}`, `Ncols 15`, `UniqueKey 13`, + `OrderKey 2` (desc). + +### Формы и ввод данных +Форм нет (read-only TUI). Ввод — существующие клавиши: `X` (меню), `x` (цикл под-экранов), +стрелки (сортировка), `/` (фильтр). + +### UX-поведение +- **Навигация:** идентична остальным pgss-под-экранам — `X` открывает меню с выбором, `x` + циклически переключает. +- **Пустое состояние / `jit=off`:** строки фильтруются `WHERE jit_functions > 0`, поэтому без + JIT-активности экран пуст. При открытии показывается командный хинт, что JIT-активности нет + (механика хинта — по образцу существующего хинта про `track_io_timing` у `statements_io`; + точную реализацию определит tech-spec). +- **pg_stat_statements не установлен:** меню `X` уже корректно отвечает + `NOTICE: pg_stat_statements not found` — поведение не меняется. +- **Сортировка:** дефолт по `gen_total` (desc) — стабильный «самые тяжёлые по генерации + сверху»; кумулятив не мерцает между обновлениями. + +## Как должно работать + +### Основной сценарий +1. Пользователь на PG15+ с установленным `pg_stat_statements` и `jit=on` нажимает `X` → JIT. +2. pgcenter выполняет версионно-выбранный запрос к `pg_stat_statements`, диффит интервальные + колонки, сортирует по `gen_total`. +3. Пользователь видит запросы с JIT-активностью и разбивку по фазам. + +### Граничные случаи +- **PG < 15:** под-экран недоступен (`MinRequiredVersion PostgresV15`); меню/цикл деградируют + корректно (не отдают пустой `View{}`) — поведение должно совпадать с тем, как `statements_wal` + ведёт себя ниже своей `MinRequiredVersion`. +- **PG17 vs PG15/16:** число колонок различается (15 vs 13) — обрабатывается отдельным набором + колонок в запросе (а не скрытием: синтетический md5 queryid как UniqueKey не позволяет прятать + колонки, min align width 8). +- **`jit=off` / нет запросов выше `jit_above_cost`:** пустой экран + хинт. +- **pg_stat_statements отсутствует:** существующий `NOTICE`. + +## Критерии приёмки +- [ ] В меню `X` появился 7-й пункт `pg_stat_statements JIT`; выбор открывает `statements_jit`. +- [ ] Цикл `x` проходит через JIT в порядке `… wal → jit → timings …`. +- [ ] На PG15/16 показываются 13 колонок согласно таблице; на PG17/18 — 15 (с `deform_total`/ + `deform_ms`). +- [ ] Интервальные колонки (`*_ms`, `functions`) диффятся; `*_total` — кумулятивные текст-итоги. +- [ ] Строки с `jit_functions = 0` не показываются (SQL-фильтр). +- [ ] Дефолтная сортировка — по `gen_total` (desc); смена колонки сортировки и фильтр `/` работают. +- [ ] На PG < 15 под-экран недоступен, приложение не падает, меню/цикл деградируют корректно. +- [ ] При `jit=off`/пустом наборе показывается хинт об отсутствии JIT-активности. +- [ ] View помечен `NotRecordable: true` — `pgcenter record`/`report` его пропускают. +- [ ] Юнит-тест на `SelectStatStatementsJITQuery(version)` покрывает обе версионные ветки. +- [ ] Обновлены count-тесты: `view_test.go::TestNew` (26→27) и + `record/record_test.go::Test_filterViews` (+1 к `wantN` во всех версиях). +- [ ] `make test` и `make lint` зелёные локально (PG17); CI-матрица PG14–18 зелёная. + +## Ограничения +- **PG15+** — JIT-колонки `pg_stat_statements` появились в PG15; `deform_*` — в PG17. +- **Только TUI (top).** View `NotRecordable: true` — record/report отложены (TUI-first принцип + релиза 0.11.0, см. фичи 004–006). Поддержка record/report — отдельная будущая фича. +- **Требуется расширение `pg_stat_statements`** — как для всех под-экранов `X`. +- **Без горизонтального скролла:** набор колонок подобран так, чтобы укладываться в ширину + (13/15 колонок, как у `timings`), поэтому `*_count` фазы (inlining/optimization/emission) + опущены — оставлен единственный счётчик `functions`; ценность экрана — времена фаз. + +## Риски +- **Риск 1:** добавление view ломает два count-теста (`TestNew`, `Test_filterViews`), и локально + без PostgreSQL это маскируется connection-refused — ловит только CI (урок фичи 006). + **Митигация:** правки этих тестов включены в критерии приёмки и явно проверяются; полагаемся на + CI-матрицу PG14–18. +- **Риск 2:** механика хинта `jit=off` может отличаться от хинта `track_io_timing`. + **Митигация:** tech-spec сверяется с реальной реализацией хинта `statements_io` перед закладкой. +- **Риск 3:** деградация под-экрана на PG < 15 (меню/цикл могут вернуть пустой `View{}`). + **Митигация:** tech-spec проверяет фильтр доступности view в TUI (отдельный от record + `filterViews`), сценарий покрыт критерием приёмки. + +## Технические решения +- Мы решили строить JIT по образцу `statements_timings` (пары `*_total` текст-длительность + + `*_ms` интервал), потому что это привычная DBA раскладка и она укладывается в ширину экрана. +- Мы решили показывать только `functions` как единственный счётчик и опустить + `inlining/optimization/emission_count`, потому что 9–11 метрик × (total+interval) не влезают + без горизонтального скролла, а ценность — времена фаз. +- Мы решили фильтровать `WHERE jit_functions > 0`, потому что при `jit=on` подавляющее + большинство нормализованных запросов не триггерят JIT и нефильтрованный экран был бы бесполезен + (тот же подход, что у `pg_stat_io`). +- Мы решили использовать селектор `SelectStatStatementsJITQuery(version)` (модель + `SelectStatIOQuery`, возвращающий query + Ncols + DiffIntvl + UniqueKey), потому что между + версиями меняется **число** колонок, а не только имена. +- Мы решили сортировать по `gen_total` (desc) по умолчанию, потому что генерация обычно + доминирует, а кумулятив стабилен между обновлениями (в отличие от мерцающего интервала). +- Мы решили пометить view `NotRecordable: true` и не трогать record/report, потому что это + TUI-first принцип релиза 0.11.0. + +## Тестирование + +**Unit-тесты:** делаются всегда. Покрыть `SelectStatStatementsJITQuery(version)` (обе ветки: +PG15/16 → базовый запрос/Ncols 13/DiffIntvl{6,10}; PG17+ → расширенный/Ncols 15/DiffIntvl{7,12}). +Поправить count-тесты `view_test.go::TestNew` и `record/record_test.go::Test_filterViews`. + +**Интеграционные тесты:** не делаем отдельно — версионный гейт PG14–18 покрывается существующей +CI-матрицей (фактический запрос к `pg_stat_statements` на каждой версии). + +**E2E тесты:** не делаем — у проекта нет E2E-слоя для TUI; ручная проверка на локальном PG17. + +## Как проверить + +### Агент проверяет + +| Шаг | Инструмент | Ожидаемый результат | +|-----|-----------|-------------------| +| Сборка | `make build` | бинарь собирается без ошибок | +| Юнит + гонки | `make test` | зелено, новый тест селектора проходит, count-тесты обновлены | +| Линт | `make lint` | без новых замечаний | +| Версионные ветки запроса | `go test ./internal/query/...` | обе ветки возвращают ожидаемые Ncols/DiffIntvl | +| CI-матрица | push в ветку | джобы PG14–18 зелёные (PG14 — под-экран недоступен, PG17/18 — deform-колонки) | + +### Пользователь проверяет +- На локальном PG17 с `pg_stat_statements` и `jit=on`: открыть `X` → JIT, прогнать + JIT-генерящий запрос (например тяжёлый аналитический с `SET jit_above_cost=0`), убедиться, что + строка появляется, времена фаз растут, сортировка/фильтр работают, и `x`-цикл проходит через JIT. + +## Post-implementation + + From 71ff18236c283e1f414c667f8920a1cec552f4c3 Mon Sep 17 00:00:00 2001 From: Alexey Lesovsky Date: Mon, 22 Jun 2026 08:17:37 +0500 Subject: [PATCH 02/21] =?UTF-8?q?chore(userspec):=20validation=20round=201?= =?UTF-8?q?=20=E2=80=94=20move=20impl=20detail=20to=20tech-spec,=20add=20d?= =?UTF-8?q?uration-sort=20AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- ...g-stat-statements-jit-adequacy-review.json | 21 ++++++ ...pg-stat-statements-jit-quality-review.json | 49 +++++++++++++ .../007-feat-pg-stat-statements-jit.md | 71 ++++++++----------- 3 files changed, 100 insertions(+), 41 deletions(-) create mode 100644 docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-adequacy-review.json create mode 100644 docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-quality-review.json diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-adequacy-review.json b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-adequacy-review.json new file mode 100644 index 0000000..2f3cab7 --- /dev/null +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-adequacy-review.json @@ -0,0 +1,21 @@ +{ + "status": "approved", + "findings": [ + { + "category": "overengineering", + "severity": "major", + "issue": "The user-spec embeds tech-spec-level implementation detail: exact column-index tables with idx/DiffIntvl/Ncols/UniqueKey/OrderKey values (lines 82-104), the internal selector function name and signature SelectStatStatementsJITQuery(version) (lines 151, 185-187), specific source file paths (internal/stat/postgres.go, statements_io reference), and the precise test-count edits view_test.go::TestNew (26->27) and record/record_test.go::Test_filterViews +1 (lines 153, 197).", + "why_matters": "User-spec defines WHAT and WHY; HOW (column indices, function names, file paths, Ncols/DiffIntvl values, test-count deltas) belongs in tech-spec. Pinning indices in the user-spec couples the requirement to one implementation, and if the tech-spec chooses a slightly different layout the acceptance criteria become wrong/contradictory. It also blurs the review boundary — these numbers should be derived and validated during tech-spec, not pre-committed here.", + "fix": "Move the index/Ncols/DiffIntvl/UniqueKey tables, the selector name+signature, file paths, and the exact test-count edits into the tech-spec (they are already captured in code-research.md). In the user-spec keep behavioural acceptance criteria: which columns appear per PG version (by meaning, not index), default sort by generation time desc, jit_functions=0 rows hidden, PG<15 unavailable, NotRecordable, hint on empty. Restate AC lines 151/153 as behaviour, not as code edits." + }, + { + "category": "underengineering", + "severity": "minor", + "issue": "Edge cases are listed for the main flows (jit=off / empty set, PG<15 degradation, pg_stat_statements absent, sort/filter), but the duration-aware numeric sort of *_total text columns is mentioned only as an assumption (line 59-60) rather than as an explicit acceptance criterion, and there is no edge case for very large cumulative times (3+ digit hours / 'N days ...') beyond a parenthetical note.", + "why_matters": "The *_total columns are text durations; sorting them numerically (HH:MM:SS with 3+ digit hours and 'N days') is a known sharp edge in internal/stat/postgres.go. Without an explicit AC, a regression there would pass all listed criteria.", + "fix": "Add an acceptance criterion: sorting by a *_total column orders rows numerically by duration (correct for 100+ hour and multi-day values), not lexically. This is behaviour, so it belongs in the user-spec." + } + ], + "worst_category": "overengineering", + "summary": "The idea is sound, feasible, and correctly right-sized as S: a 7th pgss sub-screen built strictly by analogy with established, version-aware NotRecordable views (statements_timings / statements_io / stat_io / replslots) using only the existing stack (pgx/v5, gocui, version selectors). No new dependencies, infrastructure, or architectural conflicts; the chosen single-interval-block layout and WHERE jit_functions>0 filter are the simplest viable approach and match prior ADRs. Approved with no blocking concerns — main improvement is pulling implementation detail (column indices, selector signature, file paths, test-count edits) out of the user-spec into the tech-spec." +} diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-quality-review.json b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-quality-review.json new file mode 100644 index 0000000..a4778ef --- /dev/null +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-quality-review.json @@ -0,0 +1,49 @@ +{ + "status": "approved", + "checks": { + "completeness": "pass", + "edge_cases": "pass", + "acceptance_criteria": "pass", + "contradictions": "pass", + "template_compliance": "pass", + "size_check": "warning", + "clarity": "pass" + }, + "findings": [ + { + "check": "size_check", + "severity": "minor", + "issue": "Заявлен size S, но число критериев приёмки (12) превышает порог >10. Спек по объёму и детализации (таблицы колонок по версиям, 3 сценария, 3 риска) тяготеет к нижней границе M. Фактический объём кода действительно мал (1-3 файла + правки count-тестов), поэтому это не противоречие, а пограничный случай.", + "location": "frontmatter (size: S) / Критерии приёмки", + "fix": "Оставить S оправданно (мало файлов, аддитивная фича), либо явно отметить, что часть критериев — это обязательные правки count-тестов, а не отдельные пользовательские флоу. Действия не требуется, информативно." + }, + { + "check": "acceptance_criteria", + "severity": "minor", + "issue": "Критерий «смена колонки сортировки и фильтр / работают» формулирует ожидаемый результат через слово «работают» без конкретного наблюдаемого исхода. Контекст (дефолт gen_total desc, duration-aware сортировка из сценария 2) делает его проверяемым, поэтому не critical/major.", + "location": "Критерии приёмки (6-й пункт)", + "fix": "Уточнить ожидаемый результат: «смена колонки сортировки пересортировывает строки численно (включая текст-длительности HH:MM:SS), фильтр / сужает набор по query/database»." + } + ], + "interview_coverage": { + "covered": [ + "feature_description (7-й pgss под-экран JIT, TUI-only, NotRecordable, закрывает 0.11.0)", + "user_problem (нет способа увидеть стоимость JIT-компиляции по запросам, jit=on overhead)", + "target_users (DBA, troubleshooting латентности/JIT overhead)", + "constraints (PG15+, deform_* на PG17, pgss required, синтетический md5 queryid → per-version column set)", + "testing_strategy (unit на SelectStatStatementsJITQuery, правки count-тестов, CI PG14-18)", + "user_stories / scenarios (4 истории + 3 пошаговых сценария)", + "acceptance_criteria (12 тестируемых критериев)", + "edge_cases (jit=off, PG<15, нет запросов выше jit_above_cost, deform на PG17+)", + "error_scenarios (pgss отсутствует → NOTICE; PG<15 деградация)", + "verification_strategy (агент: make build/test/lint, версионные ветки, CI; пользователь: локальный PG17)", + "ui_ux_requirements (7-й пункт меню, x-цикл wal→jit→timings, набор колонок по версиям, хинт jit=off, фильтр jit_functions>0)", + "risks (count-тесты ломаются и маскируются локально; механика хинта; деградация на PG<15)", + "technical_decisions (образец statements_timings, только functions без *_count, фильтр jit_functions>0, селектор SelectStatStatementsJITQuery, сортировка gen_total, NotRecordable)", + "integration_points (statements.go, view.go, top/menu.go, top/config_view.go)", + "data_requirements (JIT-колонки pgss, диффы интервальных, md5 queryid)" + ], + "missing": [] + }, + "summary": "Спек качественный и готов к утверждению: все обязательные секции заполнены содержательно, критерии тестируемы (есть и негативные — PG<15 не падает, хинт jit=off), риски имеют митигации, решения по тестам обоснованы, темы интервью покрыты полностью. Единственное — size_check warning: 12 критериев превышают порог >10 для S, но фактический объём кода мал, так что это пограничный, а не блокирующий случай." +} diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md index 3d33fc3..9e11dc0 100644 --- a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md @@ -76,32 +76,19 @@ JIT-компилированных функций. Под-экран строи temp/local/wal. Доступен и циклом `x` (порядок: …→ wal → **jit** → timings → …). ### Ключевые компоненты -- **Меню `X` (menuPgss):** добавляется 7-й пункт `" pg_stat_statements JIT"`. -- **Таблица JIT:** колонки зависят от версии PostgreSQL. - - **PG15/16 (13 колонок):** - - | idx | колонка | смысл | diff | - |-----|---------|-------|------| - | 0 | `user` | владелец запроса | — | - | 1 | `database` | БД | — | - | 2 | `gen_total` | кумулятивное время генерации (текст-длительность) | — | - | 3 | `inline_total` | кумулятивное время инлайнинга | — | - | 4 | `opt_total` | кумулятивное время оптимизации | — | - | 5 | `emit_total` | кумулятивное время эмиссии | — | - | 6 | `gen_ms` | время генерации за интервал, мс | ✓ | - | 7 | `inline_ms` | время инлайнинга за интервал, мс | ✓ | - | 8 | `opt_ms` | время оптимизации за интервал, мс | ✓ | - | 9 | `emit_ms` | время эмиссии за интервал, мс | ✓ | - | 10 | `functions` | число JIT-функций за интервал | ✓ | - | 11 | `queryid` | синтетический md5 (UniqueKey) | — | - | 12 | `query` | нормализованный текст запроса | — | - - `DiffIntvl {6,10}`, `Ncols 13`, `UniqueKey 11`, `OrderKey 2` (desc). - - **PG17+ (15 колонок):** добавляются `deform_total` (idx 6) и `deform_ms` (idx 11, diff); - индексы queryid/query сдвигаются. `DiffIntvl {7,12}`, `Ncols 15`, `UniqueKey 13`, - `OrderKey 2` (desc). +- **Меню `X`:** добавляется 7-й пункт `pg_stat_statements JIT`. +- **Таблица JIT:** одна строка на нормализованный запрос. Колонки (в порядке слева направо): + - `user`, `database` — владелец запроса и БД. + - **Кумулятивные итоги (текст-длительности):** `gen_total` (генерация), `inline_total` + (инлайнинг), `opt_total` (оптимизация), `emit_total` (эмиссия). На **PG17+** добавляется + `deform_total` (деформация кортежей). + - **Интервальные дельты:** `gen_ms`, `inline_ms`, `opt_ms`, `emit_ms` (время фаз за интервал, + мс). На **PG17+** добавляется `deform_ms`. `functions` — число JIT-функций за интервал. + - `queryid` — короткий идентификатор запроса; `query` — нормализованный текст. + + То есть на PG15/16 показывается базовый набор фаз, на PG17/18 — тот же набор плюс пара колонок + деформации. Конкретная раскладка колонок (индексы, диапазоны диффа) — деталь реализации, + фиксируется в tech-spec; здесь важен видимый пользователю состав и смысл. ### Формы и ввод данных Форм нет (read-only TUI). Ввод — существующие клавиши: `X` (меню), `x` (цикл под-экранов), @@ -131,26 +118,29 @@ JIT-компилированных функций. Под-экран строи - **PG < 15:** под-экран недоступен (`MinRequiredVersion PostgresV15`); меню/цикл деградируют корректно (не отдают пустой `View{}`) — поведение должно совпадать с тем, как `statements_wal` ведёт себя ниже своей `MinRequiredVersion`. -- **PG17 vs PG15/16:** число колонок различается (15 vs 13) — обрабатывается отдельным набором - колонок в запросе (а не скрытием: синтетический md5 queryid как UniqueKey не позволяет прятать - колонки, min align width 8). +- **PG17 vs PG15/16:** на PG17+ показываются две дополнительные колонки деформации; набор колонок + выбирается под версию (не скрытием отдельных колонок). - **`jit=off` / нет запросов выше `jit_above_cost`:** пустой экран + хинт. - **pg_stat_statements отсутствует:** существующий `NOTICE`. ## Критерии приёмки - [ ] В меню `X` появился 7-й пункт `pg_stat_statements JIT`; выбор открывает `statements_jit`. - [ ] Цикл `x` проходит через JIT в порядке `… wal → jit → timings …`. -- [ ] На PG15/16 показываются 13 колонок согласно таблице; на PG17/18 — 15 (с `deform_total`/ - `deform_ms`). +- [ ] На PG15/16 показывается базовый набор фаз; на PG17/18 — он же плюс `deform_total`/ + `deform_ms`. - [ ] Интервальные колонки (`*_ms`, `functions`) диффятся; `*_total` — кумулятивные текст-итоги. - [ ] Строки с `jit_functions = 0` не показываются (SQL-фильтр). -- [ ] Дефолтная сортировка — по `gen_total` (desc); смена колонки сортировки и фильтр `/` работают. +- [ ] Дефолтная сортировка — по `gen_total` (desc). +- [ ] Смена колонки сортировки переупорядочивает строки; для текст-колонок `*_total` сортировка + численная по длительности (3+ значные часы и `N days ...` сортируются корректно, не + лексикографически). +- [ ] Фильтр `/` по `query`/`database` сужает набор строк. - [ ] На PG < 15 под-экран недоступен, приложение не падает, меню/цикл деградируют корректно. - [ ] При `jit=off`/пустом наборе показывается хинт об отсутствии JIT-активности. - [ ] View помечен `NotRecordable: true` — `pgcenter record`/`report` его пропускают. -- [ ] Юнит-тест на `SelectStatStatementsJITQuery(version)` покрывает обе версионные ветки. -- [ ] Обновлены count-тесты: `view_test.go::TestNew` (26→27) и - `record/record_test.go::Test_filterViews` (+1 к `wantN` во всех версиях). +- [ ] Юнит-тест на версионный селектор запроса покрывает обе ветки (PG15/16 и PG17+). +- [ ] Обновлены count-тесты, ломающиеся при добавлении view (`view_test.go::TestNew` и + `record/record_test.go::Test_filterViews`). - [ ] `make test` и `make lint` зелёные локально (PG17); CI-матрица PG14–18 зелёная. ## Ограничения @@ -182,9 +172,8 @@ JIT-компилированных функций. Под-экран строи - Мы решили фильтровать `WHERE jit_functions > 0`, потому что при `jit=on` подавляющее большинство нормализованных запросов не триггерят JIT и нефильтрованный экран был бы бесполезен (тот же подход, что у `pg_stat_io`). -- Мы решили использовать селектор `SelectStatStatementsJITQuery(version)` (модель - `SelectStatIOQuery`, возвращающий query + Ncols + DiffIntvl + UniqueKey), потому что между - версиями меняется **число** колонок, а не только имена. +- Мы решили выбирать запрос под версию через версионный селектор (как у `pg_stat_io`), а не + скрывать колонки, потому что между версиями меняется **число** колонок, а не только имена. - Мы решили сортировать по `gen_total` (desc) по умолчанию, потому что генерация обычно доминирует, а кумулятив стабилен между обновлениями (в отличие от мерцающего интервала). - Мы решили пометить view `NotRecordable: true` и не трогать record/report, потому что это @@ -192,9 +181,9 @@ JIT-компилированных функций. Под-экран строи ## Тестирование -**Unit-тесты:** делаются всегда. Покрыть `SelectStatStatementsJITQuery(version)` (обе ветки: -PG15/16 → базовый запрос/Ncols 13/DiffIntvl{6,10}; PG17+ → расширенный/Ncols 15/DiffIntvl{7,12}). -Поправить count-тесты `view_test.go::TestNew` и `record/record_test.go::Test_filterViews`. +**Unit-тесты:** делаются всегда. Покрыть версионный селектор запроса (обе ветки: базовый набор +PG15/16 и расширенный PG17+). Поправить count-тесты `view_test.go::TestNew` и +`record/record_test.go::Test_filterViews`, ломающиеся при добавлении view. **Интеграционные тесты:** не делаем отдельно — версионный гейт PG14–18 покрывается существующей CI-матрицей (фактический запрос к `pg_stat_statements` на каждой версии). From 74d4e8f7309f16e5b4352ce257fa5d33e1f1769f Mon Sep 17 00:00:00 2001 From: Alexey Lesovsky Date: Mon, 22 Jun 2026 08:18:42 +0500 Subject: [PATCH 03/21] chore(userspec): approve user-spec for pg-stat-statements-jit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../007-feat-pg-stat-statements-jit-interview.yml | 4 ++-- .../007-feat-pg-stat-statements-jit.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-interview.yml b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-interview.yml index a24c033..0462b4c 100644 --- a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-interview.yml +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-interview.yml @@ -6,7 +6,7 @@ interview_metadata: work_type: "feature" started: "2026-06-21" last_updated: "2026-06-21" - status: "in_progress" + status: "completed" can_resume: true current_question_num: 0 feature_size: "S" @@ -142,7 +142,7 @@ phase3_integration: phase4_completion: userspec_file: "docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md" - status: "created" + status: "approved" decision_rules: required_threshold: 85 diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md index 9e11dc0..c16cfcd 100644 --- a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit.md @@ -3,7 +3,7 @@ created: 2026-06-22 # Status: draft | approved -status: draft +status: approved # Work type: feature | bug | refactoring type: feature From 26cd2f096d2d53521808b8840b95371b49004104 Mon Sep 17 00:00:00 2001 From: Alexey Lesovsky Date: Mon, 22 Jun 2026 08:22:04 +0500 Subject: [PATCH 04/21] draft(techspec): create tech-spec for pg-stat-statements-jit Co-Authored-By: Claude Opus 4.8 (1M context) --- ...7-feat-pg-stat-statements-jit-tech-spec.md | 285 ++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-tech-spec.md diff --git a/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-tech-spec.md b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-tech-spec.md new file mode 100644 index 0000000..e367c74 --- /dev/null +++ b/docs/features/007-feat-pg-stat-statements-jit/007-feat-pg-stat-statements-jit-tech-spec.md @@ -0,0 +1,285 @@ +--- +created: 2026-06-22 +status: draft +branch: feature/pg-stat-statements-jit +size: S +--- + +# Tech Spec: pg_stat_statements JIT screen + +## Solution + +Add a 7th `pg_stat_statements` sub-screen (`statements_jit`) showing per-statement JIT +compilation cost, modeled on the existing `statements_timings` sub-screen. The screen is built +from three layers, all reusing established pgcenter patterns: + +1. **Query layer** (`internal/query/statements.go`) — two version-specific query consts + (PG15/16 base, PG17+ with deform columns) and a `SelectStatStatementsJITQuery(version)` + selector returning query + `Ncols` + `DiffIntvl` + `UniqueKey` (the `SelectStatIOQuery` + model, because the JIT column *count* — not just names — changes across versions). The + SELECT follows the canonical pgss row layout (`user, database, <*_total cumulative>, + <*_ms + functions interval>, queryid, query`) and filters to rows with JIT activity via + `WHERE jit_functions > 0`. +2. **View registration** (`internal/view/view.go`) — a `statements_jit` view entry gated by + `MinRequiredVersion: query.PostgresV15`, marked `NotRecordable: true`, sorted by `gen_total` + desc, with a `Msg` that doubles as the empty-screen hint; plus a `Configure()` case that + patches `QueryTmpl/Ncols/DiffIntvl/UniqueKey` from the selector per detected version. +3. **TUI wiring** (`top/menu.go`, `top/config_view.go`) — a 7th `menuPgss` item + `menuSelect` + case, and insertion of `statements_jit` into the `x`-cycle (`… wal → jit → timings …`). + +No record/report wiring (TUI-first principle, `NotRecordable`). No keybinding changes (`X`/`x` +already route to the pgss menu/cycle). PG<15 degrades gracefully via the existing runtime +`VersionOK` guard. + +## Architecture + +### What we're building/modifying + +- **`internal/query/statements.go`** — add `PgStatStatementsJITPG15` and + `PgStatStatementsJITDefault` consts + `SelectStatStatementsJITQuery(version) (string, int, + [2]int, int)` selector. Purpose: produce the version-correct JIT query and its layout + metadata. +- **`internal/view/view.go`** — add the `statements_jit` view entry and a `Configure()` case. + Purpose: register the screen, gate it to PG15+, mark it non-recordable, and bind the + version-selected query/layout at startup. +- **`top/menu.go`** — add the menu item + select case. Purpose: make JIT reachable from the + `X` menu. +- **`top/config_view.go`** — extend `statementsNextView`. Purpose: include JIT in the `x` + cycle. +- **Test files** — `internal/query/statements_test.go` (selector + exec coverage), + `internal/view/view_test.go` (count bump + optional view guard), `record/record_test.go` + (filtered-count bump). + +### How it works + +1. At startup `view.New()` registers `statements_jit` in the view map (regardless of PG + version). `view.Views.Configure(opts)` runs the `statements_jit` case, calls + `SelectStatStatementsJITQuery(opts.Version)`, and patches the view's `QueryTmpl`, `Ncols`, + `DiffIntvl`, `UniqueKey`; then `query.Format()` resolves `{{.PGSSSchema}}` / + `{{.PgSSQueryLenFn}}`. +2. The DBA presses `X` → selects "pg_stat_statements JIT" (or cycles with `x`). + `viewSwitchHandler` swaps the active view; `printCmdline` shows the view `Msg`. +3. The collector runs `view.VersionOK(version)` first — on PG<15 it returns "selected + statistics is not supported by current version of Postgres" and never queries (same as + `statements_wal` on PG<13). On PG15+ it runs `view.Query`, diffs the `DiffIntvl` columns, + matches rows across samples on the `UniqueKey` (md5 queryid), and sorts by `OrderKey` (2, + `gen_total`, desc) — the duration-aware branch in `internal/stat/postgres.go::sort` orders + the `*_total` text durations numerically. +4. Rows are pre-filtered in SQL by `WHERE jit_functions > 0`, so under `jit=off`/low activity + the screen is empty; the `Msg` text explains why. + +## Decisions + +### Decision 1: Selector returns layout metadata (query + Ncols + DiffIntvl + UniqueKey) +**Decision:** `SelectStatStatementsJITQuery(version int) (string, int, [2]int, int)` returns the +query template, `Ncols`, `DiffIntvl`, and `UniqueKey`; `Configure()` patches all four onto the +view. Two-way branch: `version >= 170000` → `PgStatStatementsJITDefault` (15 cols), else → +`PgStatStatementsJITPG15` (13 cols). +**Rationale:** Unlike `statements_timings` (whose column *count* stays 13 across PG12/13/17 +variants, so only `QueryTmpl` is swapped), the JIT column count changes (13 vs 15) because PG17 +adds `deform_total`/`deform_ms`. With the synthetic md5 `queryid` as `UniqueKey`, columns cannot +be hidden (the align path floors every column at width 8 and is positional — ADR +`[006-feat-pg-stat-io]`), so each version needs a distinct column *set*, and `Ncols`/`DiffIntvl`/ +`UniqueKey` must move with it. Returning all four is explicit and mirrors `SelectStatIOQuery` +(which returns `(string, int, [2]int)`; we add `UniqueKey` because, unlike `stat_io`'s key at a +fixed col 0, the JIT key sits at the end and shifts with `Ncols`). +**Alternatives considered:** (a) Return only the query (timings model) — rejected: leaves stale +`Ncols`/`DiffIntvl`/`UniqueKey` for PG17. (b) Return 3-tuple and compute `UniqueKey = Ncols-2` +in `Configure()` — works but hides a layout invariant in arithmetic; explicit return is clearer +and test-checkable. (c) Hide deform columns on PG15 via `ColsWidth` — impossible (ADR [006]). + +### Decision 2: Column layout — single cumulative-total + single interval block (no doubling) +**Decision:** Columns: `user(0), database(1), gen_total, inline_total, opt_total, emit_total +[, deform_total], gen_ms*, inline_ms*, opt_ms*, emit_ms* [, deform_ms*], functions*, queryid, +query`. `*_total` are cumulative text durations (`date_trunc('seconds', round(