"""Add band_storage table for provider-agnostic, encrypted storage configs. Each band can have one active storage provider (Nextcloud, Google Drive, etc.). Credentials are Fernet-encrypted at the application layer — never stored in plaintext. A partial unique index enforces at most one active config per band at the DB level. Revision ID: 0007_band_storage Revises: 0006_waveform_peaks_in_db Create Date: 2026-04-10 """ from alembic import op import sqlalchemy as sa from sqlalchemy.dialects.postgresql import UUID revision = "0007_band_storage" down_revision = "0006_waveform_peaks_in_db" branch_labels = None depends_on = None def upgrade() -> None: op.create_table( "band_storage", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column( "band_id", UUID(as_uuid=True), sa.ForeignKey("bands.id", ondelete="CASCADE"), nullable=False, ), sa.Column("provider", sa.String(20), nullable=False), sa.Column("label", sa.String(255), nullable=True), sa.Column("is_active", sa.Boolean, nullable=False, server_default="false"), sa.Column("root_path", sa.Text, nullable=True), # Fernet-encrypted JSON — never plaintext sa.Column("credentials", sa.Text, nullable=False), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False, ), ) # Index for fast per-band lookups op.create_index("ix_band_storage_band_id", "band_storage", ["band_id"]) # Partial unique index: at most one active storage per band op.execute( """ CREATE UNIQUE INDEX uq_band_active_storage ON band_storage (band_id) WHERE is_active = true """ ) def downgrade() -> None: op.execute("DROP INDEX IF EXISTS uq_band_active_storage") op.drop_index("ix_band_storage_band_id", table_name="band_storage") op.drop_table("band_storage")