"""Create normalized control-plane tables and workspace-bound foreign keys.""" from __future__ import annotations import sqlalchemy as sa from alembic import op revision = "0002_control_plane" down_revision = "0001_baseline" branch_labels = None depends_on = None def upgrade() -> None: """Create the control schema with composite workspace constraints.""" op.execute(sa.text("CREATE SCHEMA IF NOT EXISTS control")) op.create_table( "workspaces", sa.Column("id", sa.Uuid(), primary_key=True), sa.Column("name", sa.String(120), nullable=False), sa.Column("slug", sa.String(64), nullable=False), sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.UniqueConstraint("slug", name="uq_workspaces_slug"), schema="control", ) op.create_table( "applications", sa.Column("id", sa.Uuid(), primary_key=True), sa.Column("workspace_id", sa.Uuid(), nullable=False), sa.Column("name", sa.String(120), nullable=False), sa.Column("slug", sa.String(64), nullable=False), sa.Column("scopes", sa.JSON(), nullable=False), sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.ForeignKeyConstraint(["workspace_id"], ["control.workspaces.id"], ondelete="CASCADE"), sa.UniqueConstraint("id", "workspace_id", name="uq_applications_id_workspace"), sa.UniqueConstraint("workspace_id", "slug", name="uq_applications_workspace_slug"), schema="control", ) op.create_table( "spaces", sa.Column("id", sa.Uuid(), primary_key=True), sa.Column("workspace_id", sa.Uuid(), nullable=False), sa.Column("name", sa.String(120), nullable=False), sa.Column("slug", sa.String(64), nullable=False), sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column("trust_required", sa.Boolean(), nullable=False, server_default=sa.false()), sa.Column("retention_days", sa.Integer(), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.ForeignKeyConstraint(["workspace_id"], ["control.workspaces.id"], ondelete="CASCADE"), sa.UniqueConstraint("id", "workspace_id", name="uq_spaces_id_workspace"), sa.UniqueConstraint("workspace_id", "slug", name="uq_spaces_workspace_slug"), schema="control", ) op.create_table( "grants", sa.Column("id", sa.Uuid(), primary_key=True), sa.Column("workspace_id", sa.Uuid(), nullable=False), sa.Column("application_id", sa.Uuid(), nullable=False), sa.Column("knowledge_space_id", sa.Uuid(), nullable=False), sa.Column("role", sa.String(32), nullable=False), sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.ForeignKeyConstraint( ["application_id", "workspace_id"], ["control.applications.id", "control.applications.workspace_id"], ondelete="CASCADE", ), sa.ForeignKeyConstraint( ["knowledge_space_id", "workspace_id"], ["control.spaces.id", "control.spaces.workspace_id"], ondelete="CASCADE", ), sa.UniqueConstraint( "application_id", "knowledge_space_id", "role", name="uq_grants_application_space_role" ), schema="control", ) op.create_table( "policies", sa.Column("id", sa.Uuid(), primary_key=True), sa.Column("workspace_id", sa.Uuid(), nullable=False), sa.Column("knowledge_space_id", sa.Uuid(), nullable=True), sa.Column("operation", sa.String(120), nullable=False), sa.Column("required_scopes", sa.JSON(), nullable=False), sa.Column("active", sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column( "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() ), sa.ForeignKeyConstraint(["workspace_id"], ["control.workspaces.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint( ["knowledge_space_id", "workspace_id"], ["control.spaces.id", "control.spaces.workspace_id"], ondelete="CASCADE", ), schema="control", ) op.create_index( "uq_policies_workspace_operation", "policies", ["workspace_id", "operation"], unique=True, schema="control", postgresql_where=sa.text("knowledge_space_id IS NULL"), ) op.create_index( "uq_policies_workspace_space_operation", "policies", ["workspace_id", "knowledge_space_id", "operation"], unique=True, schema="control", postgresql_where=sa.text("knowledge_space_id IS NOT NULL"), ) def downgrade() -> None: """Remove the control schema created by this migration.""" op.execute(sa.text("DROP SCHEMA IF EXISTS control CASCADE"))