Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion internal/config/store_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -2103,6 +2103,9 @@ func scanExecutionRows(rows pgx.Rows) ([]PurchaseExecution, error) {
// approval_token_hash is NULL on rows migration 000100 found with no
// raw token (every canceled row among them).
var approvalTokenHash sql.NullString
// error is nullable (migration 000001); one NULL row must not fail the
// whole list scan (issue #1465). exec.Error stays "" for those.
var errText sql.NullString

err := rows.Scan(
&planID,
Expand All @@ -2116,7 +2119,7 @@ func scanExecutionRows(rows pgx.Rows) ([]PurchaseExecution, error) {
&exec.TotalUpfrontCost,
&exec.EstimatedSavings,
&completedAt,
&exec.Error,
&errText,
&expiresAt,
&exec.CloudAccountID,
&exec.Source,
Expand Down Expand Up @@ -2146,6 +2149,7 @@ func scanExecutionRows(rows pgx.Rows) ([]PurchaseExecution, error) {
exec.IdempotencyKey = idempotencyKey.String
}
exec.ApprovalToken = approvalTokenHash.String
exec.Error = errText.String

// Unmarshal recommendations
if err := json.Unmarshal(recommendationsJSON, &exec.Recommendations); err != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//go:build integration
// +build integration

package config

import (
"context"
"testing"
"time"

"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestIntegration_ExecutionReaders_NullErrorColumn is the issue #1465
// regression. purchase_executions.error is nullable, and one row with a NULL
// there used to fail the scan for the whole result set, hiding every
// actionable execution from the history page and approval queue.
func TestIntegration_ExecutionReaders_NullErrorColumn(t *testing.T) {
ctx := context.Background()
store := setupRampStepStore(ctx, t)

base := time.Now().Add(-time.Hour)
saveClean := func(offset int, status string) string {
exec := &PurchaseExecution{
ExecutionID: uuid.New().String(),
IdempotencyKey: uuid.New().String(),
Status: status,
ScheduledDate: base.Add(time.Duration(offset) * time.Minute),
}
require.NoError(t, store.SavePurchaseExecution(ctx, exec))
return exec.ExecutionID
}
other := saveClean(1, "pending")
nullErr := saveClean(2, "pending")
last := saveClean(3, "notified")
_, err := store.db.Exec(ctx, `UPDATE purchase_executions SET error = NULL WHERE execution_id = $1`, nullErr)
require.NoError(t, err)

wantIDs := []string{last, nullErr, other}
assertAll := func(t *testing.T, got []PurchaseExecution) {
t.Helper()
ids := make([]string, len(got))
for i := range got {
ids[i] = got[i].ExecutionID
assert.Empty(t, got[i].Error, "NULL error scans as empty string")
}
assert.Equal(t, wantIDs, ids)
}

statuses := []string{"pending", "notified"}
byStatuses, err := store.GetExecutionsByStatuses(ctx, statuses, 100)
require.NoError(t, err)
assertAll(t, byStatuses)

page, err := store.ListHistoryExecutionsPage(ctx, HistoryExecutionsPageQuery{Statuses: statuses, Limit: 100})
require.NoError(t, err)
assertAll(t, page)

pending, err := store.GetPendingExecutions(ctx)
require.NoError(t, err)
require.Len(t, pending, 3)
for i := range pending {
assert.Empty(t, pending[i].Error)
}

single, err := store.GetExecutionByID(ctx, nullErr)
require.NoError(t, err)
assert.Empty(t, single.Error)
}
Loading