-- v1.0.9 Item G — subscription pending_payment state machine. -- -- The user_subscriptions.status column is a free-text VARCHAR(30) with -- no DB-level enum, so the new 'pending_payment' value introduced by -- the Go const StatusPendingPayment requires no DDL. This migration is -- documentation + an index that the future reconciliation worker -- (Phase 2) needs to find rows stuck in pending_payment past the -- webhook-arrival window. -- -- Index strategy : a partial index on (created_at) WHERE status = -- 'pending_payment' is the smallest possible footprint that still -- gives the reconciler an O(log N) scan for "rows older than 30m -- that haven't transitioned yet". A non-partial composite index would -- pay storage cost for every row in the table; partial keeps it -- proportional to the in-flight set. -- -- CONCURRENTLY is intentionally not used: the migration runner wraps -- each migration in a single transaction (database.go:390) and -- Postgres forbids CREATE INDEX CONCURRENTLY inside a transaction -- block. The partial WHERE clause keeps this index tiny (only rows -- currently in pending_payment), so the brief AccessExclusiveLock -- taken by the non-concurrent variant resolves in milliseconds — -- comfortably shorter than any human-noticeable migration window. -- If pending_payment cardinality grows large enough to matter, switch -- to a manual two-step deploy (DDL outside the migration runner). CREATE INDEX IF NOT EXISTS idx_user_subscriptions_pending_payment ON user_subscriptions (created_at) WHERE status = 'pending_payment'; COMMENT ON INDEX idx_user_subscriptions_pending_payment IS 'v1.0.9 Item G — feeds the subscription reconciliation sweep that catches rows stuck in pending_payment past the webhook deadline.';