Repository navigation
fix(config): tolerate a NULL error when scanning purchase executions - #1473
Conversation
purchase_executions.error is nullable, but scanExecutionRows scanned it straight into a string. One row with a NULL error failed the scan for the whole result set, so the history page and approval queue lost every actionable execution for every viewer. Scan it through sql.NullString; every execution reader goes through this scanner, so one change covers them all. Writes are unchanged. Adds a real-Postgres regression test covering GetExecutionsByStatuses, ListHistoryExecutionsPage, GetPendingExecutions and GetExecutionByID with a NULL-error row. Closes #1465 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 2 billable files and costs up to $0.50.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 28 minutes for your next included review. View limit details
Comment |
|
Independent review 1 (cold, fresh context): VERDICT: APPROVE 9abec09
|
|
Independent review 2 (claude-fleet, cold): APPROVE at 9abec09 Blocking findings: none. Non-blocking findings:
Scan-site table (site / columns / NULL-safe?):
Nullable-column table (column / type / nullable / destination / NULL reachable? / scratch result):
Probes (in the head export, each restored with cp and checked with cmp):
Commands (GOTOOLCHAIN=go1.26.10, cloud credentials nulled):
Reviewed by an independent agent with no author context (second reviewer). Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
Local verification at
|
…r NOT NULL (#1505) Both columns have been nullable since migration 000001, but the Go readers scan them into plain values (ramp_step into an int; error into a string, tolerated since #1473). The writers never store NULL, so a NULL only comes from a legacy or hand-written row, and one such purchase_history row fails the scan of the whole history listing. Migration 000146 backfills NULL to 0 / '' and sets both columns NOT NULL with that default. Both tables are locked ACCESS EXCLUSIVE before the backfill so a NULL committed by a concurrent older writer cannot slip in between the UPDATE and SET NOT NULL, and the purchase_executions updated_at trigger is disabled for the backfill statement only, so the lifecycle clock of legacy rows is not reset. Down makes both columns nullable again. Adds real-Postgres migration tests (backfill, rows survive, updated_at kept, defaults, NULL rejected, GetAllPurchaseHistory lists the formerly-NULL row, idempotent up, down, and a concurrent in-flight NULL writer). The #1465 regression test now relaxes the constraint in its own database to keep covering the scanner's NULL tolerance. Closes #1484 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #1465.
Problem:
scanExecutionRows(internal/config/store_postgres.go) scanned the nullablepurchase_executions.errorcolumn straight into the string fieldexec.Error. One row with a NULLerrorfailed the scan for the whole result set (cannot scan NULL into *string). The history page then lost every actionable execution (pending, notified, paused, scheduled) for every viewer, and the approval queue went empty.Change: the scan goes through a
sql.NullStringand setsexec.Errorto""when the column is NULL. Every purchase-execution reader uses this scanner:GetExecutionsByStatuses,ListHistoryExecutionsPage,GetPendingExecutions,GetExecutionByID, and the ramp and scheduled readers. The only othererrorscan, for RI exchanges, already usesNullString. Writes are unchanged, and there is no migration.Test: a real-Postgres integration test sets one execution's
errorto NULL with raw SQL. It then checks that all four readers return every row, withErrorset to""for the NULL one. Before the fix it fails withfailed to scan execution: can't scan into dest[11] (col: error): cannot scan NULL into *string.Local:
go build ./...passes, andgo test -racepasses on config and api.-tags integrationpasses on config and api (testcontainers Postgres).🤖 Generated with Claude Code