From 57e0e58713a6104ad7a89c31aaa5d447d0e340e3 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Mon, 20 Jul 2026 01:53:17 +0200 Subject: [PATCH] fix(db): add missing offering columns to GetActivePurchaseHistory SELECT GetActivePurchaseHistory SELECTed 21 columns but shares scanPurchaseHistoryRow, which scans 24 destinations (offering_class, listing_id, listing_state were added by #808). pgx/v5 Rows.Scan requires field count == destination count, so every real-Postgres call to this query failed with "number of field descriptions must equal number of destinations, got 21 and 24". Impact (all broken on real Postgres, silently in some paths): - dashboard commitment KPIs (fetchCommitmentPurchases swallows the error and renders 0 active commitments / $0 committed monthly / $0 YTD), - GET /api/inventory/commitments and /api/inventory/coverage (return errors), - the analytics collector's Collect (fails every run). #808 (marketplace) added the three columns to the scanner and to GetPurchaseHistory / GetAllPurchaseHistory / GetPurchaseHistoryFiltered but missed GetActivePurchaseHistory. This is the same class of defect #1221 fixed for the 17-vs-21 case. Fix: append offering_class, listing_id, listing_state so the column list is identical to the other purchase-history queries and matches the scanner. Update the pgxmock regex to pin the corrected column list so a future drop of these columns fails the test. Note: pgxmock cannot reproduce the field-count mismatch (it fabricates result columns), so the regression is only guarded via the SQL-text regex here; a shared "emitted SELECT column list == purchaseHistoryCols" assertion across all purchase-history queries is a worthwhile follow-up. Found by the adversarial (Fable) review sweep of recently-merged PRs. --- internal/config/store_postgres.go | 8 +++++++- internal/config/store_postgres_pgxmock_test.go | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/internal/config/store_postgres.go b/internal/config/store_postgres.go index b8b0346c3..b1a1f4844 100644 --- a/internal/config/store_postgres.go +++ b/internal/config/store_postgres.go @@ -1810,11 +1810,17 @@ func (s *PostgresStore) GetActivePurchaseHistory(ctx context.Context, asOf time. args := []any{asOf} conds, args = appendAccountPredicate(conds, args, accountIDs, externalIDsByProvider) + // The column list MUST match scanPurchaseHistoryRow's scan destinations + // exactly (pgx errors on a field/destination count mismatch). #808 added + // offering_class, listing_id, listing_state to the scanner and every other + // purchase-history SELECT but missed this one, which broke every real-Postgres + // call here (dashboard KPIs, /api/inventory/*, analytics collector). query := fmt.Sprintf(` SELECT account_id, purchase_id, timestamp, provider, service, region, resource_type, count, term, payment, upfront_cost, monthly_cost, estimated_savings, plan_id, plan_name, ramp_step, cloud_account_id, - revocation_window_closes_at, revoked_at, revoked_via, support_case_id + revocation_window_closes_at, revoked_at, revoked_via, support_case_id, + offering_class, listing_id, listing_state FROM purchase_history WHERE %s ORDER BY timestamp DESC diff --git a/internal/config/store_postgres_pgxmock_test.go b/internal/config/store_postgres_pgxmock_test.go index 913e40943..9f66dc7e2 100644 --- a/internal/config/store_postgres_pgxmock_test.go +++ b/internal/config/store_postgres_pgxmock_test.go @@ -1046,7 +1046,7 @@ func TestPGXMock_GetActivePurchaseHistory_Unscoped(t *testing.T) { now := time.Now().Truncate(time.Second) rows := pgxmock.NewRows(purchaseHistoryCols).AddRow(purchaseHistoryRow(now, "aws", "acct-1")...) mock.ExpectQuery( - `SELECT account_id, purchase_id, .*revocation_window_closes_at, revoked_at, revoked_via, support_case_id FROM purchase_history WHERE term > 0 AND timestamp \+ make_interval\(hours => term \* 8760\) >= \$1 AND revoked_at IS NULL ORDER BY timestamp DESC$`, + `SELECT account_id, purchase_id, .*revocation_window_closes_at, revoked_at, revoked_via, support_case_id, offering_class, listing_id, listing_state FROM purchase_history WHERE term > 0 AND timestamp \+ make_interval\(hours => term \* 8760\) >= \$1 AND revoked_at IS NULL ORDER BY timestamp DESC$`, ).WithArgs(now).WillReturnRows(rows) records, err := store.GetActivePurchaseHistory(ctx, now, nil, nil)