Skip to content

release: fix rollup_1h grain honesty - #182

Merged
NikolayS merged 5 commits into
mainfrom
agent/w2-b1
Jul 27, 2026
Merged

release: fix rollup_1h grain honesty#182
NikolayS merged 5 commits into
mainfrom
agent/w2-b1

Conversation

@NikolayS

@NikolayS NikolayS commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes the rollup_1h grain collapse by separating requested/effective buckets from the grain actually retained, exposing provenance, and returning NULL instead of fabricated peak/p99 values when retained data is too coarse.

  • makes ash.aas(), ash.timeline(), ash.top(), and ash.compare() follow the same grain-honesty rule
  • routes the database dimension through valid per-database minute_counts
  • labels upgraded/synthesized arrays as rollup_1h_flat
  • rejects incomplete or sum-inconsistent minute_counts in both rollup_hour() and the v1.5 → 2.0 backfill
  • snaps hour-only dimensional windows outward and exposes effective bounds instead of raising
  • gives ash.summary() separate headline and wait/query drill provenance so widened drill values cannot inherit narrower labels
  • corrects the N8 catalog claim and points the coverage-model limitation to Design blocker: persist sampling cadence for trustworthy AAS #137
  • keeps zero-argument and now()-relative APIs working

Fixes #161.
Fixes #168.
Fixes #175.
Fixes #130.

This supersedes #131; I recommend closing #131 because this branch satisfies #130 without its hard-error/default-call and database-precision regressions.

This PR needs a fresh rebase onto origin/main immediately before merge. It is intentionally a draft and must not be merged as-is later without that rebase.

Audit fixture behavior

Case Before After
B1a overall aas() avg/peak/p99 1.15 / 10.00 / 4.69 unchanged
B1a top('wait_event') 0.77 / 0.77 / 0.77 avg 0.77; peak/p99 NULL because the read is hourly
B1a top('database') 1.15 / 1.15 / 1.15 1.15 / 10.00 / 4.69 from the per-database minute array
B1b filtered IO avg/peak/p99 2.30 / 2.30 / 2.30 at a requested minute bucket avg 2.30, seconds 8280.00, effective bucket 1 hour, peak/p99 NULL; an explicit one-hour request remains 2.30 / 2.30
B1c identical dimensional windows no source columns; avg delta 0.00 but peaks 10.98 / 600.00 and p99 10.98 / 246.59 sources rollup_1h / rollup_1m, averages 10.98 / 10.98, delta 0.00, and both peak/p99 pairs NULL because retained grains differ
B1c identical overall windows no source columns; averages 10.98 / 10.98, peaks 600 / 600, p99 246.59 / 246.59 same exact metrics plus source/effective-bucket provenance
B1d real v1.5 legacy hour timeline fabricated 60 minute rows, source rollup_1h, points 60, avg 0.17, 60 non-NULL extrema one row, source rollup_1h_flat, points 1, avg 0.17, no extrema; aas() also reports flat provenance and no extrema
N1 partial backfill 60-slot array with minute sum 1800 vs hourly sum 3600 minute_counts IS NULL
N1 complete backfill not protected by a sum invariant 60 slots, minute sum 4140 equals hourly sum 4140
#130 partial dimensional window clipped partial-hour denominator could fabricate AAS hour-only waits snap outward and disclose bounds; database reads remain minute-precise
summary() asymmetric edge load headline [h+30m,h+90m) avg 0.00, but hour-only drill values had no separate plan metadata headline stays exact; drill discloses rollup_1h, [h,h+2h), bucket 1h, and top IO avg 5.00

For #130 specifically, the half-hour IO aas() request now discloses [h, h+1h), avg 1.00, seconds 3600.00, peak/p99 NULL; the crossing wait window discloses [h, h+2h), avg 0.50, seconds 3600.00, peak/p99 NULL; database keeps [h, h+30m), avg/peak/p99 1.00 / 1.00 / 1.00, seconds 1800.00.

RED (captured before implementation)

B1a

ERROR:  B1a aas avg=1.15 peak=10.00 p99=4.69; top wait avg=0.77 peak=0.77 p99=0.77; database avg=1.15 peak=1.15 p99=1.15
CONTEXT:  PL/pgSQL function inline_code_block line 54 at ASSERT

B1b

ERROR:  B1b unfiltered avg=2.32 peak=21.00 p99=9.79; IO minute avg=2.30 seconds=8280.00 peak=2.30 p99=2.30; IO hour peak=2.30 p99=2.30
CONTEXT:  PL/pgSQL function inline_code_block line 51 at ASSERT

B1c

ERROR:  B1c overall source=<missing>/<missing> avg=10.98/10.98 delta=0.00 peak=600.00/600.00 p99=246.59/246.59; dimension source=<missing>/<missing> avg=10.98/10.98 delta=0.00 peak=10.98/600.00 p99=10.98/246.59
CONTEXT:  PL/pgSQL function inline_code_block line 65 at ASSERT

B1d (real v1.5 install upgraded through sql/migrations/ash-1.5-to-2.0.sql)

ERROR:  B1d legacy timeline rows=60 source=rollup_1h..rollup_1h points=60 avg=0.17..0.17 nonnull_peak=60 nonnull_p99=60
CONTEXT:  PL/pgSQL function inline_code_block line 22 at ASSERT

N1 (same real v1.5 upgrade)

ERROR:  N1 partial backfill minute_counts cardinality=60 null_slots=30 minute_sum=1800 hour_sum=3600
CONTEXT:  PL/pgSQL function inline_code_block line 18 at ASSERT

The overlapping #130 assertion also failed before implementation:

ERROR:  #130 aas period=2026-06-12 15:00:00+00..2026-06-12 15:30:00+00 avg=2.00 seconds=3600.00 peak=2.00 p99=2.00; wait period=<missing>..<missing> avg= seconds= peak= p99=; database period=<missing>..<missing> avg=2.00 seconds=3600.00 peak=2.00 p99=2.00
CONTEXT:  PL/pgSQL function inline_code_block line 69 at ASSERT

Post-review summary provenance RED:

ERROR:  summary headline=2026-06-12 17:30:00+00..2026-06-12 18:30:00+00 avg=0.00; drill= .. bucket= top=IO:DataFileRead (avg_aas 5.00, 66.67%)
CONTEXT:  PL/pgSQL function inline_code_block line 80 at ASSERT

GREEN (verbatim)

Focused reader block:

NOTICE:  ash.chart: source rollup_1h uses effective window [2026-06-12 17:00:00+00 to 2026-06-12 19:00:00+00) and bucket 01:00:00
NOTICE:  ash.chart: source rollup_1h uses effective window [2026-07-27 16:00:00+00 to 2026-07-27 18:00:00+00) and bucket 01:00:00
NOTICE:  rollup_1h grain honesty, partial-hour, and default-call tests PASSED

Real-release upgrade block:

NOTICE:  B1d GREEN timeline rows=1 source=rollup_1h_flat points=1 avg=0.17 nonnull_peak=0 nonnull_p99=0; aas source=rollup_1h_flat buckets=1/1 avg=0.17 peak=NULL p99=NULL seconds=600.00
NOTICE:  N1 GREEN partial minute_counts=NULL hour_sum=3600
NOTICE:  N1 GREEN complete cardinality=60 null_slots=0 first=600 quiet_slots=59 minute_sum=4140 hour_sum=4140
NOTICE:  real v1.5 rollup_1h legacy and backfill honesty PASSED

Summary provenance GREEN values:

         metric         |                 value
------------------------+----------------------------------------
 period_start           | 2026-06-12 17:30:00+00
 period_end             | 2026-06-12 18:30:00+00
 avg_aas                | 0.00
 drill_source           | rollup_1h
 drill_period_start     | 2026-06-12 17:00:00+00
 drill_period_end       | 2026-06-12 19:00:00+00
 drill_effective_bucket | 01:00:00
 top_wait_1             | IO:DataFileRead (avg_aas 5.00, 66.67%)
(8 rows)

Full post-rebase local job:

All tests passed for PostgreSQL 17 (pg_cron=off)

Validation

  • final HEAD 2af229a44095aac4a983f38f5a07311f61c08158, rebased onto origin/main at 64265009e25acc8f23187e3e2f40e3754b7724af
  • PostgreSQL 17.10: full 58-step local CI-equivalent no-cron job passed, including real release upgrades, reapply, schema equivalence, and degraded modes
  • PostgreSQL 19beta2: fresh install, reapply, B1/Bug: partial-hour dimensional reads fabricate rollup_1h AAS #130, summary provenance, real v1.5 upgrade, and fix: defer hourly rollup until minute coverage #133 completion/lock behavior passed
  • pre-rebase PostgreSQL 14.23, 15.18, 16.14, 18.4, and 19beta2 focused matrices passed
  • GitHub Actions: docs-lint, PostgreSQL 14/15/16/17/18/19beta1, PG17 no-cron, and CodeQL all pass on final HEAD
  • workflow YAML parsed; documentation checks and git diff --check passed

API/documentation changes

blueprints/AAS_API.md now documents exact return schemas and the new provenance/effective-window fields, requested versus effective bounds, upward bucket widening, hour-only dimensional snapping, database minute precision, flat legacy hours, cross-grain compare suppression, summary headline-versus-drill provenance, chart NOTICE behavior, and the #137 idle-versus-uncovered limitation. Related examples, user stories, spec, README, and catalog comments use the same contract.

Deliberately not attempted

This does not add coverage markers capable of distinguishing a sampled idle minute from an uncovered minute. The current storage model cannot represent that distinction; #137 is the architectural follow-up. Wait/query dimensions also cannot recover minute extrema once only hourly aggregates remain, so these readers now return exact averages/totals but honestly return NULL extrema instead of inventing detail.

@NikolayS NikolayS changed the title fix: preserve rollup_1h grain honesty release: fix rollup_1h grain honesty Jul 27, 2026
@NikolayS NikolayS closed this Jul 27, 2026
@NikolayS NikolayS reopened this Jul 27, 2026
samo-agent added a commit to samo-agent/pg_ash that referenced this pull request Jul 27, 2026
Document that retained AAS is weighted by the current sampler interval and that stored activity cannot prove sampler coverage. Coordinate timeline and rollup wording with NikolayS#175 and PR NikolayS#182 without changing reader behavior.

Copy link
Copy Markdown
Owner Author

REV Code Review Report

  • PR: NikolayS/pg_ash#182 — release: fix rollup_1h grain honesty
  • Branch: agent/w2-b1
  • Reviewed head: 2af229a44095aac4a983f38f5a07311f61c08158
  • CI: ✅ docs-lint, PostgreSQL 14/15/16/17/18/19beta1, PG17 no-cron, and CodeQL all pass

Result: PASSED

No outstanding security, correctness, test, guidelines, or documentation findings remain.

Findings addressed during review

  1. MEDIUM — summary drill provenance: ash.summary() exposed the minute-precise headline plan but discarded the wider hour-only wait/query plan. Added drill_source, drill_period_start, drill_period_end, and drill_effective_bucket, plus an asymmetric exact RED/GREEN fixture (headline avg=0.00, widened IO drill avg=5.00).
  2. LOW — no-data source wording: corrected AAS_API.md; an uncovered window keeps its planned source with zero covered buckets, while source = none means all aggregate source tables are globally empty.
  3. LOW — documentation style: corrected three unspaced em dashes.

Review coverage

Area Outstanding Result
Security / privilege boundaries 0 PASS
SQL behavior / edge cases 0 PASS
Tests / migrations 0 PASS
Repository guidelines / SQL style 0 PASS
API docs / catalog comments 0 PASS

The post-rebase audit also confirmed that the concurrently landed #127, #129, #133, and #152 behavior remains intact, including #133’s blocking hourly rollup lock and minute-watermark completeness guard.

REV execution note

The installed GitHub REV command skips draft PRs by design, and this task explicitly forbids marking the PR ready. A read-only full-diff claude -p run was therefore attempted with draft skipping overridden; it remained active but emitted no buffered report within a 15-minute ceiling and was terminated with exit 143, without file or GitHub writes. I then ran REV’s documented review areas as independent parallel full-diff reviews and consolidated their results above. This note is included so the audit trail does not misrepresent the timed-out wrapper as a successful stock invocation.


REV-compatible parallel review using the postgres-ai/rev rubric; transparent fallback after the stock draft-aware wrapper timed out.

@NikolayS
NikolayS marked this pull request as ready for review July 27, 2026 20:49
Compare the bare rollup_1h.ts column against bigint bounds in _rollup_1h_has_flat(). This preserves overflow safety while allowing the (ts, datid) primary key to serve narrow historical windows instead of scanning the full archive.

@NikolayS NikolayS left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex review (standing in for REV) — PASS

  • Reviewed head: 4fa269b2f2e8934017282e07eb51d4b66bc61901
  • Rebased onto: 86e0ca834d6cdcb21af0a5deea1f1250b1040df9 (#189)
  • Verdict: no remaining correctness, security, migration, documentation, or test blocker.

Audit contract

The B1 audit required: “NULL peak_aas/p99_aas whenever the read grain exceeds the requested bucket; add source_1/source_2 (or an effective-bucket column) to ash.compare(); route the database dimension through rollup_1h_minutes; and mark legacy flat hours.” It also required the N1 backfill invariant to reject partial minute_counts.

This head satisfies that contract and genuinely covers all of #130: filtered aas, filtered timeline, every top dimension, dimensional compare, chart, and summary avoid clipped-hour denominators. Hour-only paths snap outward and disclose their effective plan; unfiltered/database-only paths retain exact minute precision. Zero-argument and now()-relative calls remain usable, so #131 is superseded without importing its regressions.

Rebase and collision audit

The four source commits rebased textually cleanly, then were checked semantically:

  • #189 survives: _raw_oldest_sample() remains the physical evidence boundary, _raw_retention_start() remains the logical planning/loss boundary, source selection and report attribution use physical evidence, and the API/examples retain that distinction.
  • #181 survives: the dynamic reader bundle grants all 43/43 current non-admin functions (the prior 41 plus _minute_counts_valid() and _rollup_1h_has_flat()), including _raw_oldest_sample(); _admin_funcs() remains denied.
  • #185/#193 rotation and deterministic-upgrade fixtures remain intact.

RED / GREEN

Exact RED on current main:

ERROR: B1a aas avg=1.15 peak=10.00 p99=4.69; top wait avg=0.77 peak=0.77 p99=0.77; database avg=1.15 peak=1.15 p99=1.15

Exact rebased GREEN:

  • complete B1/#130/default-call block: rollup_1h grain honesty, partial-hour, and default-call tests PASSED
  • real v1.5 upgrade: B1d returns one rollup_1h_flat datum with NULL extrema; N1 partial stays NULL; N1 complete has 60 slots and minute/hour sums both 4140; re-apply repairs an earlier partial array
  • supplemental partial-window wait_event, query_id, and dimensional compare() probe passed with widened bounds and NULL extrema
  • #189 raw-boundary block, rollup-source/seam blocks, and #181 reader-helper closure passed

Codex finding resolved

The initial full-diff Codex review found one P2: _rollup_1h_has_flat() cast the indexed rollup_1h.ts column, forcing a full-archive scan. Commit 4fa269b moves overflow-safe arithmetic to bigint bounds while leaving the indexed column bare. PostgreSQL now reports:

Index Scan using rollup_1h_pkey
Index Cond: ((ts < ...::bigint) AND (ts > ...::bigint))

A follow-up Codex review confirmed mathematical equivalence, overflow safety, and index eligibility, with no remaining finding.

CI

Fresh rebased CI is green: docs-lint and PostgreSQL 14/15/16/17/18/19beta1 plus PG17 no-cron in Test run 30304779707; CodeQL is green in run 30304776247.

REV itself was not run; this is the required codex review substitute, labelled explicitly.

@NikolayS
NikolayS merged commit a1f0a30 into main Jul 27, 2026
11 checks passed
@NikolayS
NikolayS deleted the agent/w2-b1 branch July 27, 2026 21:14
NikolayS pushed a commit that referenced this pull request Jul 27, 2026
Keep #180's until-relative compare defaults exactly once. The #182 reader expansion carried an older duplicate declaration block whose now-relative defaults would both fail function compilation and discard the B5 contract.

Use #189's _raw_retention_start() and _active_slots() helpers for #180's alternate-coverage NOTICE instead of reintroducing the pre-B3 retention formula. This keeps the corrected logical/physical raw boundary split and excluded-slot rule intact.

Adapt the sparse-hour C1 gate to #182's honest rollup_1h_flat plan: aggregate readers disclose one widened hourly datum, while report still returns silent NULL because exact per-minute coverage is not derivable.
NikolayS added a commit that referenced this pull request Jul 27, 2026
* fix: reject inverted reader windows

* docs: clarify reader and scheduler diagnostics

* fix: preserve reader contracts after rebase

Keep #180's until-relative compare defaults exactly once. The #182 reader expansion carried an older duplicate declaration block whose now-relative defaults would both fail function compilation and discard the B5 contract.

Use #189's _raw_retention_start() and _active_slots() helpers for #180's alternate-coverage NOTICE instead of reintroducing the pre-B3 retention formula. This keeps the corrected logical/physical raw boundary split and excluded-slot rule intact.

Adapt the sparse-hour C1 gate to #182's honest rollup_1h_flat plan: aggregate readers disclose one widened hourly datum, while report still returns silent NULL because exact per-minute coverage is not derivable.

* fix: make diagnostic source guidance actionable

When cron.database_name is unset or names the current database, explain how to configure pg_cron or use the external scheduler. Recommend installing pg_ash in another database only when the setting names a real, different target.

Document ash.report's rollup_1m-only contract in the README and guard both disclosures in CI. Exercise mutable cron.database_name placeholders only on the cron-off axis because preloaded pg_cron registers the GUC as postmaster-only. This closes the Codex pre-merge review findings without changing report behavior.

---------

Co-authored-by: samo-agent <280144521+samo-agent@users.noreply.github.com>
samo-agent added a commit to samo-agent/pg_ash that referenced this pull request Jul 27, 2026
Document that retained AAS is weighted by the current sampler interval, successful idle ticks are not persisted, and intervals above one minute can overstate minute extrema. Mark the affected user stories partial and expose the limitation in the README, release notes, blueprints, and sample_interval catalog comment.

Hand-resolve against current main by retaining NikolayS#182's final grain/provenance contracts, NikolayS#180's until-only window and report disclosures, NikolayS#189's raw-boundary wording, and NikolayS#187's retired-benchmark caveat.
NikolayS added a commit that referenced this pull request Jul 27, 2026
Document that retained AAS is weighted by the current sampler interval, successful idle ticks are not persisted, and intervals above one minute can overstate minute extrema. Mark the affected user stories partial and expose the limitation in the README, release notes, blueprints, and sample_interval catalog comment.

Hand-resolve against current main by retaining #182's final grain/provenance contracts, #180's until-only window and report disclosures, #189's raw-boundary wording, and #187's retired-benchmark caveat.

Co-authored-by: samo-agent <280144521+samo-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants