Skip to content

fix(analytics): keep unknown usage and commitment unknown in the savings rollups - #1287

Draft
IAmJSD wants to merge 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-151
Draft

IAmJSD wants to merge 2 commits into
LeanerCloud:mainfrom
IAmJSD:fix/issue-151

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

This is the backend SQL slice of #151. Two analytics materialized views stop turning an unknown cost into a definite number. Every backend instance in the issue was re-checked against current main (a14dc8f). The table below gives each one's status. Frontend items are listed for the UI owner at the end and are not changed here.

Root cause

savings_snapshots.total_usage (nullable since 000067, H2) and savings_snapshots.total_commitment (nullable since 000101, #450) store NULL when the provider gave no figure. For example, an all-upfront purchase has no recurring cost to derive usage from. The collector writes NULL on purpose, because a placeholder 0 would read as a real $0.

monthly_savings_summary (000078) and provider_savings_summary (000074) rolled these columns up with SUM and AVG, and both skip NULL. At one collection timestamp, "us-east-1 $4000 + eu-west-1 unknown" therefore became a definite $4000, with no sign that a region was missing. The same happened over time. If a month had one known instant ($100) and one unknown instant, it averaged to $100.

The issue said total_commitment was unaffected because it was NOT NULL. That is no longer true: 000101 made it nullable too, so it is fixed alongside total_usage.

Fix

Migration 000147_savings_rollups_propagate_unknown drops and recreates both views:

  • The inner per-timestamp total is CASE WHEN COUNT(col) = COUNT(*) THEN SUM(col) END. One unknown row makes that instant's total unknown. This is the same pattern the history totals already use for upfront_cost (internal/api/analytics_postgres.go:180).
  • The outer per-period average is CASE WHEN COUNT(ts_col) = COUNT(*) THEN AVG(ts_col) END. One unknown instant makes the period unknown. Averaging only the known instants would understate a period in which a purchase with no figure became active.
  • A known 0 stays 0. Fully known data gives the same H5 sum-then-average result as before.
  • Unchanged: total_savings (NOT NULL), avg_coverage (a percentage averaged over the rows that have one, which is deliberate per H2), snapshot_count, and daily_savings_trend, which aggregates no nullable cost column.
  • Column names and types are unchanged (a CASE with no ELSE keeps the numeric type). The unique indexes are recreated as 000076 left them (plain columns, NULLS NOT DISTINCT), and both views are created populated, so refresh_savings_materialized_views() can keep refreshing them CONCURRENTLY.
  • The down migration restores the 000078 / 000074 definitions exactly. The test compares pg_get_viewdef and the index definitions before and after a round trip.

No Go code changes. No API reads these columns: QueryMonthlyTotals selects only total_savings, avg_coverage and snapshot_count from monthly_savings_summary, and nothing reads provider_savings_summary. Only direct SQL consumers of the views see the change.

Concurrency. The plausible race is a collector run calling refresh_savings_materialized_views() while the migration is dropping and recreating the views during a deploy. TestMigration_SavingsRollupsPropagateUnknown_ConcurrentRefresh sets this up deterministically. A reader holding provider_savings_summary pauses the migration after it has replaced monthly_savings_summary and while it still holds that view's lock. A refresh is then started and confirmed (via pg_stat_activity) to be waiting on the lock. The reader is then released. The migration commits, and the queued refresh succeeds against the new views instead of failing on the dropped OID.

Migration number: 000147. main is at 000134. Open PRs and our unopened fork branches use 000135 to 000146.

Refs #151

Per-instance status

# Instance (from the issue) Status
1 APIPermissionConstraint.MaxAmount omitempty (internal/auth/service_api.go:59) No change. Superseded by the owner decision on #133. Absent and 0 both mean "no cap" in every reader. An explicit max_amount: 0 is rejected with 400 on group writes (#983, merged in acb9182) and on API-key create (our open #1159, which covers that instance). A stored 0 therefore can never be a cap, and the round trip is already lossless. Dropping omitempty would make it worse: every uncapped constraint would GET as max_amount: 0, and a client that echoes it back would get the #983 400.
2 PermissionConstraints.MaxPurchaseAmount omitempty (internal/auth/types.go:76) No change, for the same reason as 1.
3 monthly_savings_summary inner SUM(total_usage) / outer AVG(ts_usage) (000078:48, :34) Fixed (000147).
4 "Same shape for daily_savings_trend" (000067:101, 000074:193) Covered by 3. Those lines are the older flat AVG(total_usage) definitions of monthly_savings_summary, which 000078 replaced. The current daily_savings_trend aggregates no nullable cost column.
5 (found while re-verifying) total_commitment in monthly_savings_summary and provider_savings_summary. The issue called it NOT NULL, but it has been nullable since 000101. Fixed (000147).
6 A02-013: accounts API Enabled fallback / AWSIsOrgRoot *bool (internal/api/handler_accounts.go updateAccount, cloudAccountFromRequest) Deferred. The fix edits the updateAccount lines around account := cloudAccountFromRequest(req), and our open #1141 (provider-immutable check) and #1248 (annotateAccountNotices) both edit those same lines. Follow up after they merge.
7 A06-027: assembleLadderPlan fabricates existingTotal = 0 (internal/server/handler_ladder.go:600-607) Deferred. Our open #1238 rewrites the same existingTotal block (on-demand-equivalent conversion). Follow up after it merges.
8-11 groupModals.ts, settings.ts, numeric filter extractors, ramp progress / recommendations.ts Frontend. See below.

JSON / API output changes

None. No omitempty tag is changed, for the reasons in rows 1 and 2. The only output that changes is SQL: monthly_savings_summary.total_usage, monthly_savings_summary.total_commitment and provider_savings_summary.total_commitment can now be NULL where they used to hold a partial figure. No API endpoint or UI reads them today.

Verification

All evidence comes from local runs on Linux against a real PostgreSQL 16 testcontainer. These are integration tests on fixture rows, not production data. The migrations package's integration tests each start their own container (testhelpers.RequirePostgresContainer, like the other migration tests there). That package has no TestMain for the shared-container helper, which only internal/config uses.

New internal/database/postgres/migrations/000147_savings_rollups_propagate_unknown_test.go (integration tag):

  • TestMigration_SavingsRollupsPropagateUnknown migrates to the previous version and inserts four fixture accounts: mixed known/unknown regions at one instant, known zeros, fully known data across two instants, and known-then-unknown across two instants. It asserts the old views report the partial figures (4000, 100), then migrates up and calls refresh_savings_materialized_views() (the CONCURRENTLY path). It then asserts:

    • the mixed and known-then-unknown accounts are NULL in all three columns;
    • the known-zero account is 0, not NULL;
    • the fully known account is 225.

    Finally it migrates down, asserts the view and index definitions equal the originals and the old partial figure is back, and migrates up again.

  • TestMigration_SavingsRollupsPropagateUnknown_ConcurrentRefresh runs the refresh-during-migration race described above.

Fail-before / pass-after:

  • Fail before (mutation standing in for main): the 000147 up file was replaced with the old definitions from its down file. Both tests fail: every "must be unknown (NULL)" assertion fails for the mixed, known-then-unknown, up-again and racing-refresh cases (12 assertions).
  • Mutation, outer level only: the outer CASE ... AVG was reverted to a plain AVG. The known-then-unknown case fails (3 assertions), so both levels are needed.
  • Pass after: both tests pass. The existing TestAnalyticsNestedRollup_COR02 (000078, which migrates down past 000147 and back) also passes.

Commands, all passing (run with AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true):

go build ./...
go vet ./...
go vet -tags integration ./...
go test -count=1 ./internal/database/postgres/migrations/
go test -count=1 ./internal/mocks/
go test -tags integration -count=1 -timeout 60m -run 'TestMigration_SavingsRollupsPropagateUnknown' ./internal/database/postgres/migrations/
go test -tags integration -count=1 -timeout 60m -run 'TestAnalyticsNestedRollup_COR02' ./internal/database/postgres/migrations/
golangci-lint run --allow-parallel-runners --build-tags integration ./internal/database/postgres/migrations/...   # v2.10.1: 0 issues

gocyclo: no non-test Go files changed. The fixtures use acct-* strings, not 12-digit account IDs.

Combined-tree verification. This branch was tested together with the other queued branches on a local, never-pushed tree: queue/platform @ 936996ad = origin/main a14dc8f2 plus these heads, merged in this order:

Branch Tested head
fix/issue-337 ff3420c
fix/issue-169 7d75eb5
fix/issue-342 66b221f
fix/issue-310 5e11c21
fix/issue-326 5f0ea59
fix/issue-243 365e959
fix/issue-343 6c94036
fix/issue-312 d2848a8
fix/issue-316 acd60dd
fix/issue-339 75ea9b9
fix/issue-339-commitment-type 73bdb19
fix/issue-353 5d25fcd
fix/issue-313 3156bc4
fix/issue-357 53d9e58
fix/issue-368 0ba727d
fix/issue-419 61648c9
fix/issue-111 b04c372
fix/issue-686 0ad90b7
fix/issue-674 793095d
fix/issue-1055 4ec96f1
fix/issue-559 dfa9525
fix/issue-558 034eed4
fix/issue-874 1720539
fix/issue-666 1e120da
fix/issue-668 fb26345
fix/issue-568 ce04173
fix/issue-311 e54f250
fix/issue-177 0cbb88a
fix/issue-151 c21377c
fix/issue-166 41fb77b
fix/issue-27 2d67622
fix/issue-323 69288ae
fix/issue-59 36463e8
fix/issue-43 35e4069
fix/issue-158 6f76f8a
fix/issue-104 bb00491
fix/issue-55 971ef1b
fix/issue-361 89ac220
fix/issue-120 b458719
fix/issue-36 42bac6e
fix/issue-525 57c97dd
fix/issue-712 e5bcda0
fix/issue-72 6cc63ed

fix/issue-72 and fix/issue-312 conflict on one line in internal/api/handler_dashboard.go (both change the calculateCommitmentMetrics call). The tree used #72's side with #312's validated provider variable; whichever PR merges second takes that same resolution.

Linux, Go 1.26.9, AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null AWS_EC2_METADATA_DISABLED=true; Docker Postgres via testcontainers:

  • Passed at 936996ad: go build ./..., go vet ./..., go vet -tags integration ./... and the full go test -race -short -count=1 ./....
  • Static checks at 936996ad: golangci-lint v2.10.1 found 0 issues on every changed package, with and without --build-tags integration. tests/e2e is a separate module, so it was vetted under its own e2e tag. gocyclo -over 10 is silent on the changed non-test files.
  • Integration tests, every changed package, one package at a time (-tags integration -count=1, 60-minute local timeout because the VM was shared):
    • At 936996ad, these pass: internal/server, internal/credentials, internal/scheduler, internal/database/postgres/migrations, internal/purchase and internal/api.
    • internal/config passes except TestPostgresStoreDB_ResolveStaleRIExchange_ValidatesCloudCheck, which fails the same way on unmodified main (fixed by fix(config): compare resolve checked_at at the database's microsecond precision #1252).
    • These were run on the first build of this tree (26795376, same branches at earlier heads): internal/analytics, internal/auth, internal/commitmentopts, internal/database and cmd/lambda/clear-rate-limit all pass. The later fixes don't touch those packages.
  • What the combined run caught, each fixed on its own branch before this PR opened:
    • a compile break between fix/issue-104's test and fix/issue-326's httpToLambdaRequest signature change;
    • fix/issue-316 rejecting legacy-keyed plans;
    • fix/issue-120 versus main's global-filter test.
  • Secrets: git-secrets, using the repo's scripts/setup-git-secrets.sh patterns in a fresh repo with an isolated HOME, found nothing in the changed files, and a planted fake key was flagged. scripts/test-git-secrets-allowlist.sh: 137 PASS, 0 FAIL.
  • Nothing was applied to any cloud.

Out of scope

  • A02-013 and A06-027 are deferred for the overlap reasons in the table.
  • avg_coverage keeps averaging over the rows that have a coverage figure. That is an average of a percentage, not a sum, and H2 chose it deliberately.
  • fix(purchase): preserve unknown provider upfront costs #450 described the view commitment columns as "known-sample statistics without a completeness guarantee". This PR changes that: the views now report NULL rather than a known-sample figure. If the maintainer prefers the known-sample meaning for total_commitment, the alternative is a companion known-ness flag column.

Frontend follow-up for the UI owner

Status on main a14dc8f. Nothing under frontend/ is changed here.

  • frontend/src/groups/groupModals.ts:293 (populate) and :497 (collect): already fixed. They use ?? '' and .trim() !== '', consistent with the fix(frontend): a max_amount spend cap of 0 is silently erased when a group is edited #133 decision that 0 is refused rather than stored.
  • frontend/src/settings.ts:3208 and :3216 (default_coverage): already fixed by fix(frontend/settings): keep a stored default_coverage of 0 on load #510 (??).
  • frontend/src/plans.ts:251-252: the numeric filter returns p.upfront_cost ?? 0 and p.estimated_savings ?? 0. The types are non-null, but for a step with has_recommendations === false these are placeholders (platform#609), so a =0 filter matches them. It should return Number.NaN for placeholder or absent values.
  • frontend/src/history.ts:1045 and :1734: p.estimated_savings ?? 0. Its upfront_cost neighbors at :1044 / :1733 already return Number.NaN. Return Number.NaN here too for consistency.
  • frontend/src/riexchange.ts:686-687: source_count ?? 0 / target_count ?? 0. target_count is optional (api/types.ts:802, :834), so an absent count matches =0. Return Number.NaN, as the neighbors at :688-690 do.
  • frontend/src/plans.ts:1096 and :1148: the ramp default { current_step: 0, total_steps: 1 } and ${current_step || 0}/${total_steps || 1} steps still render missing step data as "0/1 steps". Render -- when total_steps is not a positive number.
  • frontend/src/recommendations.ts:881: the plr.savingsMin === null || plr.savingsMax === null branch is unreachable, because pageLevelRange (:1257 onward) initializes both to 0 and PageLevelRange types them as number (:1216-1217). Either propagate nullability out of pageLevelRange or delete the dead branch.

Labels

A maintainer, please mirror the issue's triage labels onto this PR: priority/p2, severity/high, urgency/this-quarter, impact/many, effort/l, type/bug, triaged.

🤖 Generated with Claude Code

…ngs rollups

savings_snapshots.total_usage and total_commitment are nullable on purpose:
NULL means the provider gave no figure. monthly_savings_summary and
provider_savings_summary rolled them up with SUM and AVG, which skip NULL,
so "us-east-1 $4000 + eu-west-1 unknown" became a definite $4000.

Migration 000147 recreates both views so a timestamp's total is NULL when
any of its rows is unknown, and the period average is NULL when any of its
timestamps is. A known 0 stays 0. Column names and types, the unique
indexes and the CONCURRENTLY refresh path are unchanged; the down migration
restores the previous definitions.

Integration tests on real Postgres cover unknown, known zero, fully known,
unknown-at-one-instant, down/up, and a refresh_savings_materialized_views()
call queued behind the migration's locks.

Refs LeanerCloud#151

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added priority/p2 Backlog-worthy effort/l Weeks triaged Item has been triaged severity/high Significant harm urgency/this-quarter Within the quarter impact/many Affects most users type/bug Defect labels Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

Brings in main's CI fixes (LeanerCloud#1236, LeanerCloud#1252) and CI changes since, so this PR gets a fresh CI run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): VERDICT: APPROVE d5518d9200ec0daa7fa7f333247e1420db6bce36

Blocking:

  • none

Non-blocking:

Attacked:

  • Read the up and down SQL against 000078 (monthly), 000074 (provider) and 000076 (plain-column NULLS NOT DISTINCT indexes). The down file is byte-for-byte the 000078 monthly definition and the 000074/000076 provider definition. The two-level COUNT(col) = COUNT(*) guard is correct at both levels, and a CASE with no ELSE keeps the numeric type. CASCADE drops nothing extra: the only objects that reference these views are these migrations and the plpgsql refresh_savings_materialized_views(), which resolves names at runtime. No GRANTs are lost (none exist on these views). The only Go reader is internal/analytics/postgres_analytics.go:310, which doesn't select the changed columns.
  • Fail-before: I copied the down SQL over the 000147 up file. TestMigration_SavingsRollupsPropagateUnknown then failed with 9 "must be unknown (NULL)" assertions (mixed / later / up-again). With the fix restored, it passes.
  • Ran: go build ./..., go vet ./..., go vet -tags integration ./... and tests/e2e go vet -tags e2e are clean. go test -race -short -count=1 ./internal/database/postgres/migrations/ passes. go test -race -tags integration -count=1 passes for TestMigration_SavingsRollupsPropagateUnknown, ..._ConcurrentRefresh and TestAnalyticsNestedRollup_COR02. The first two runs hit a testcontainers startup timeout on this heavily loaded VM (load around 30); the clean rerun passed. golangci-lint v2.10.1 with --build-tags integration on the migrations package reports 0 issues.

Body-stale:

  • "main is at 000134. Open PRs and our unopened fork branches use 000135 to 000146": main is now at 000139, and the 140-146 slots belong to the open PRs listed above.
  • The per-instance table and the frontend notes cite "current main (a14dc8f)" line numbers. Main has moved since, so those anchors may have drifted.

Reviewer: rB1. Bar while the maintainers are away (set by IAmJSD as acting owner): one approving independent review on the head, two for money, auth and security paths.

@cristim

cristim commented Oct 26, 2026

Copy link
Copy Markdown
Member

Independent review 1 (cold, fresh context):

VERDICT: APPROVE d5518d9
PR is OPEN. Reviewed tree: git archive of the head (merge-base with origin/main 503eb2e). The only non-merge commit is c21377c, which adds three files (000147 up/down/test).

Findings:

Unknown-value trace (producer -> SQL -> API -> UI):

  • Producer: collector.go buildSnapshots:214-238 writes a nil *float64 for unknown commitment/usage. The store inserts the pointer (postgres_analytics.go:117-132, :171-208) and reads it back through sql.Null into *float64 (:272-282). JSON is total_usage,omitempty (interfaces.go:18), so an unknown is absent, never 0.
  • Views: after 000147, the inner CASE COUNT(col)=COUNT() and the outer CASE COUNT(ts)=COUNT() yield NULL. A known 0 stays 0, and fully known data gives the same H5 result (225 fixture).
  • API: the only view reader is QueryMonthlyTotals (postgres_analytics.go:311-313), which selects total_savings, avg_coverage and snapshot_count. MonthlySummary (interfaces.go:50-62) has no usage or commitment field. Nothing reads provider_savings_summary.
  • Frontend: no total_usage reference and no reader of these view columns (only a comment in frontend/src/api/analytics.ts:6). No sum, average, chart or CSV path consumes the changed columns, so nothing downstream turns NULL back into 0. The frontend ?? 0 items in the body are out of scope and listed as follow-ups.

Attacked:

  • Diff vs base: the down migration matches 000078 (monthly) and 000074 (provider) query-for-query. Provider is created populated instead of WITH NO DATA + REFRESH, which ends in the same state. The test's pg_get_viewdef + index comparison confirms the round trip. Indexes match 000076 (plain columns, NULLS NOT DISTINCT), so CONCURRENTLY refresh keeps working.
  • CASCADE drop: no view, function or GRANT depends on either matview (git grep of all migrations). The refresh function references them by name, not OID, and the concurrent-refresh test covers that.
  • Transactionality: migrate.go:353 (all migrations transactional, enforced by TestMigrationsAreTransactional, which passes).
  • Collision with fix(analytics): label snapshot commitment types from provider and service #1302 (000145 ALTERs savings_snapshots constraints only, no view dependency): none.
  • Tests run on macOS (Docker PG): go test -race ./internal/database/postgres/migrations/ (27 pass), go vet -tags integration, the integration tests TestMigration_SavingsRollupsPropagateUnknown, its _ConcurrentRefresh variant and TestAnalyticsNestedRollup_COR02 (all PASS), and the full migrations integration package (ok, 30.8s).
  • Mutation probes (scratch copy; each restored and cmp'd byte-identical to the head blob):
    P1 monthly inner ts_usage CASE -> plain SUM: FAIL ("mixed" and "up again, mixed" monthly total_usage must be NULL).
    P2 provider outer total_commitment CASE -> plain AVG: FAIL ("later" provider total_commitment must be NULL).
    P3 provider inner ts_commitment CASE -> plain SUM: FAIL ("mixed" and "up again, mixed" provider total_commitment).
  • Refuted: a numeric type change from CASE without ELSE (no: the type stays numeric and readers cast to float8); the down not restoring the original (refuted by the viewdef equality test); the test month partition missing at month start (t1 is the 1st at 01:00 UTC of the current month and passes the 90-day window either way).

@IAmJSD the open migration PRs now span 000140-000149: #1239 (140/141), #1240 (142), #1241 (143), #1253 (144), #1302 (145), #1268 (146), #1287 (147), #1116 (149). Merging them in number order (or renumbering whichever lands later above main's highest) keeps golang-migrate from skipping any. I'm adding a guard on our merge side that refuses a PR whose migration number is not above main's highest.

@cristim

cristim commented Oct 26, 2026

Copy link
Copy Markdown
Member

Independent review 2 (claude-fleet, cold): APPROVE at d5518d9

Blocking (file:line)

  • none

Non-blocking (file:line)

  • Migration order: internal/database/postgres/migrations/000147_savings_rollups_propagate_unknown.up.sql is numbered 147, and origin/main (f316add) is now at 000139. The head merged older main, so its export stops at 000134. If 147 deploys before 140 to 146 land, golang-migrate will never apply those lower numbers. Either merge this after them or renumber it at merge time. Up and down are symmetric: the down file matches 000078 up :24-54 and 000074 up :237-258, plus the 000076 indexes; the test checks this with pg_get_viewdef.
  • An existing bug in the same class, outside this diff: internal/analytics/collector.go:191-194. Inside a single snapshot row, usage is the sum of the known MonthlyCost values only, and it counts as known once any purchase has one. A row that mixes nil and non-nil MonthlyCost therefore stores a partial figure as known, and 000147 cannot detect it. Commitment in the same function (:183-186) fails closed. Worth a follow-up issue, or an entry in known-issues.md.
  • Product semantics: one NULL anywhere in a month makes that month NULL. That covers one all-upfront purchase with a nil upfront_cost, or one missing usage figure. No reader exists today, so this is harmless now, but the body's known-ness flag column would read better once someone exposes these columns.
  • Body is stale: "main is at 000134" is now 000139, and the a14dc8f line anchors may have drifted.

Is the fix wired in? Yes. The fix lives entirely in SQL (new migration files), and that is why the diff deletes nothing. migrate.go:468/507/540 loads every file in the migrations dir via file://, so 000147 runs on deploy. It drops and recreates both matviews, which replaces the 000078/000074 definitions at the DB level. No old collapsing definition is left live after up. No Go code changes, and none is needed.

Collapse-point table (site / before / after / test)

  • monthly inner ts_usage (up.sql:54) / SUM skips NULL / CASE COUNT=COUNT(*) / P1 killed
  • monthly outer total_usage (up.sql:40) / AVG skips NULL / CASE / P2, P7 killed
  • monthly inner ts_commitment (up.sql:53) / SUM / CASE / P5 killed
  • monthly outer total_commitment (up.sql:39) / AVG / CASE / P6 killed
  • provider inner ts_commitment (up.sql:86) / SUM / CASE / P3 killed
  • provider outer total_commitment (up.sql:77) / AVG / CASE / P4 killed
  • collector row usage (collector.go:191-194) / partial sum treated as known / unchanged / none
  • daily_savings_trend (000074:205-228) / only total_savings (NOT NULL) and coverage / unchanged / n/a

Consumer table

  • internal/analytics/postgres_analytics.go:310-336 QueryMonthlyTotals selects only total_savings, avg_coverage and snapshot_count. MonthlySummary (interfaces.go:50-62) has no usage or commitment field, so it is unaffected.
  • provider_savings_summary: there are no Go, TS or Python readers. refresh_savings_materialized_views (postgres_analytics.go:511) still works, including CONCURRENTLY, and the test covers it.
  • frontend/src/api/analytics.ts:6 is a comment only. No email, CSV or dashboard reads these views (searched the whole tree, including docs, specs and runbooks).
  • No JSON or omitempty change, so no consumer can now receive a null it does not expect.

Semantics check

  • Sum: one unknown makes the instant unknown (strict). Average: one unknown instant makes the period unknown. A known 0 stays 0, and fully known data matches the prior sum-then-average result (225). The provider view applies the same rule across services. This matches the existing upfront pattern at internal/api/analytics_postgres.go:180.
  • avg_coverage stays a known-only average, which is deliberate and documented in the up.sql header comment.
  • The rules are consistent across monthly, per-account and per-provider rollups, and are documented in the up.sql header.
  • No ratio has an unknown denominator. There are no currency buckets in these views.
  • No placeholder zero anywhere in the diff.
  • Test gaps (non-blocking): the fixtures use one service, have no all-unknown account, and set usage and commitment to nil together. Each column is still asserted on its own.

Probes (on the head export, each restored with cp, cmp OK, FINAL_RESTORED)

  • P0: up.sql replaced with the base definitions. 12 NULL assertions fail.
  • P1: COALESCE(SUM(usage),0) in the monthly inner query. 3 failures.
  • P2: plain outer AVG for usage. Fails the "later" case.
  • P3: plain SUM in the provider inner query only (one rollup only). 2 failures.
  • P4: plain AVG in the provider outer query. 1 failure.
  • P5: ELSE 0 on the monthly inner commitment. 3 failures.
  • P6: COALESCE(...,0) on the monthly outer commitment. 3 failures.
  • P7: NULLIF(...,0), which turns a known zero into unknown. Fails "zero must be known".
  • "Drop the unknown flag from the API response": not applicable, because no API field carries these columns.

Commands (GOTOOLCHAIN=go1.26.10, AWS config/creds set to /dev/null, AWS_PROFILE empty)

  • In the head export: go build ./... passed. go vet migrations passed. go vet -tags integration on migrations and analytics passed.
  • go test -count=1 on migrations and analytics: ok. gofmt -l on the test file: clean.
  • golangci-lint 2.11.4 on migrations/..., with and without --build-tags integration: no issues.
  • Under the heavy lock: go test -tags integration -run 'TestMigration_SavingsRollupsPropagateUnknown|TestAnalyticsNestedRollup_COR02'. All 3 tests pass.
  • Fail-before: the head test copied into the base export fails both tests (version 147 not found, and the lock-wait condition is never satisfied). Replacing up.sql with the old SQL (P0) is the more meaningful fail-before.
  • git merge-tree --write-tree origin/main d5518d9 gives 1a803030 cleanly. On that tree, build, vet -tags integration and the same 3 integration tests pass, with previous version 139.
  • git show --remerge-diff d5518d9 is empty, so the merge needed no hand resolution.
  • Frontend: not run, because nothing under frontend/ changed and nothing in it renders these columns.

Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/l Weeks impact/many Affects most users priority/p2 Backlog-worthy severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants