"""Add DLQ columns to event_outbox and fix consumer_inbox RLS policy. Phase 5: Dead-Letter-Queue support. - Adds error_message TEXT and failed_at TIMESTAMPTZ to event_outbox - Adds partial index for failed events - Fixes consumer_inbox RLS policy (previous 0085 policy referenced tenant_id column which does not exist on consumer_inbox; the correct policy uses the event_id FK to event_outbox.tenant_id) Revision ID: 0092 Revises: 0091 """ from __future__ import annotations from alembic import op revision = "0092" down_revision = "0091" branch_labels = None depends_on = None def upgrade() -> None: # 1. Add DLQ columns to event_outbox op.execute( "ALTER TABLE IF EXISTS event_outbox " "ADD COLUMN IF NOT EXISTS error_message TEXT" ) op.execute( "ALTER TABLE IF EXISTS event_outbox " "ADD COLUMN IF NOT EXISTS failed_at TIMESTAMPTZ" ) # 2. Partial index for efficient failed-event queries op.execute( "CREATE INDEX IF NOT EXISTS ix_outbox_failed " "ON event_outbox (status, failed_at) WHERE status = 'failed'" ) # 3. Fix consumer_inbox RLS policy # Migration 0085 created a policy using tenant_id, but consumer_inbox # has no tenant_id column. Drop the broken policy and create one # that follows the same pattern as outbox_deliveries (0075): use the # event_id FK to check event_outbox.tenant_id. op.execute( "DROP POLICY IF EXISTS consumer_inbox_tenant_isolation ON consumer_inbox" ) op.execute("DROP POLICY IF EXISTS tenant_isolation ON consumer_inbox") op.execute("ALTER TABLE consumer_inbox ENABLE ROW LEVEL SECURITY") op.execute("ALTER TABLE consumer_inbox FORCE ROW LEVEL SECURITY") op.execute( "CREATE POLICY consumer_inbox_tenant_isolation ON consumer_inbox " "FOR ALL TO crm_api, crm_worker " "USING (EXISTS (SELECT 1 FROM event_outbox " "WHERE event_outbox.id = consumer_inbox.event_id " "AND event_outbox.tenant_id = " "NULLIF(current_setting('app.current_tenant_id', true), '')::uuid)) " "WITH CHECK (EXISTS (SELECT 1 FROM event_outbox " "WHERE event_outbox.id = consumer_inbox.event_id " "AND event_outbox.tenant_id = " "NULLIF(current_setting('app.current_tenant_id', true), '')::uuid))" ) # Ensure grants are in place op.execute( "GRANT SELECT, INSERT, UPDATE, DELETE ON consumer_inbox TO crm_api, crm_worker" ) def downgrade() -> None: op.execute("DROP INDEX IF EXISTS ix_outbox_failed") op.execute("ALTER TABLE event_outbox DROP COLUMN IF EXISTS failed_at") op.execute("ALTER TABLE event_outbox DROP COLUMN IF EXISTS error_message") # Restore the broken policy state (consumer_inbox RLS remains enabled) op.execute( "DROP POLICY IF EXISTS consumer_inbox_tenant_isolation ON consumer_inbox" )