"""Add encrypted, versioned external data-source credentials.""" from alembic import op revision = "20260718_50" down_revision = "20260716_40" branch_labels = None depends_on = None def upgrade() -> None: op.execute( """ CREATE TABLE IF NOT EXISTS public.datasource_credentials ( id UUID PRIMARY KEY, data_source_uid UUID NOT NULL, credential_version INTEGER NOT NULL CHECK (credential_version > 0), encrypted_payload BYTEA NOT NULL, nonce BYTEA NOT NULL CHECK (octet_length(nonce) = 12), key_version VARCHAR(40) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'retired', 'revoked')), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, retired_at TIMESTAMPTZ, UNIQUE (data_source_uid, credential_version) ); CREATE UNIQUE INDEX IF NOT EXISTS uq_datasource_credential_active ON public.datasource_credentials(data_source_uid) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS idx_datasource_credential_lookup ON public.datasource_credentials( data_source_uid, credential_version, status ); CREATE TABLE IF NOT EXISTS public.datasource_credential_audit_events ( id BIGSERIAL PRIMARY KEY, data_source_uid UUID NOT NULL, credential_version INTEGER, actor_uid UUID REFERENCES public.users(id) ON DELETE SET NULL, event_type VARCHAR(60) NOT NULL, success BOOLEAN NOT NULL, safe_detail VARCHAR(500), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_datasource_credential_audit_source ON public.datasource_credential_audit_events( data_source_uid, created_at DESC ); CREATE INDEX IF NOT EXISTS idx_datasource_credential_audit_actor ON public.datasource_credential_audit_events( actor_uid, created_at DESC ); """ ) def downgrade() -> None: # Credential and audit history remain available during application rollback. pass