"""add config profiles, includes, mounts, and tool instance profile selection Revision ID: 0013_add_config_profiles Revises: 0012_default_port_req Create Date: 2026-05-24 12:00:00.000000 """ from collections.abc import Sequence from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision: str = "0013_add_config_profiles" down_revision: str | None = "0012_default_port_req" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def _table_exists(table_name: str) -> bool: return sa.inspect(op.get_bind()).has_table(table_name) def _column_exists(table_name: str, column_name: str) -> bool: if not _table_exists(table_name): return False return column_name in { column["name"] for column in sa.inspect(op.get_bind()).get_columns(table_name) } def _index_exists(table_name: str, index_name: str) -> bool: if not _table_exists(table_name): return False return index_name in { index["name"] for index in sa.inspect(op.get_bind()).get_indexes(table_name) } def _foreign_key_exists( table_name: str, constrained_columns: list[str], referred_table: str, ) -> bool: if not _table_exists(table_name): return False for foreign_key in sa.inspect(op.get_bind()).get_foreign_keys(table_name): if ( foreign_key.get("constrained_columns") == constrained_columns and foreign_key.get("referred_table") == referred_table ): return True return False def upgrade() -> None: # Earlier branches may already have created config_profiles. Keep this # migration defensive so databases can converge onto the current graph. if not _table_exists("config_profiles"): op.create_table( "config_profiles", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("user_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("name", sa.String(length=255), nullable=False), sa.Column("description", sa.Text(), nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False, ), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint( "user_id", "name", name="uq_config_profiles_user_name" ), ) if not _index_exists("config_profiles", "idx_config_profiles_user"): op.create_index("idx_config_profiles_user", "config_profiles", ["user_id"]) if not _table_exists("config_includes"): op.create_table( "config_includes", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("profile_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column( "included_profile_id", postgresql.UUID(as_uuid=True), nullable=False ), sa.Column("order_index", sa.Integer(), nullable=False, server_default="0"), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False, ), sa.ForeignKeyConstraint( ["profile_id"], ["config_profiles.id"], ondelete="CASCADE" ), sa.ForeignKeyConstraint( ["included_profile_id"], ["config_profiles.id"], ondelete="CASCADE", ), sa.PrimaryKeyConstraint("id"), sa.UniqueConstraint( "profile_id", "included_profile_id", name="uq_config_includes_pair" ), ) if not _index_exists("config_includes", "idx_config_includes_profile"): op.create_index("idx_config_includes_profile", "config_includes", ["profile_id"]) if not _index_exists("config_includes", "idx_config_includes_included"): op.create_index( "idx_config_includes_included", "config_includes", ["included_profile_id"] ) if not _table_exists("config_mounts"): op.create_table( "config_mounts", sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("profile_id", postgresql.UUID(as_uuid=True), nullable=False), sa.Column("mount_path", sa.String(length=1024), nullable=False), sa.Column("content", sa.Text(), nullable=True), sa.Column("source_profile_id", postgresql.UUID(as_uuid=True), nullable=True), sa.Column("order_index", sa.Integer(), nullable=False, server_default="0"), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.text("NOW()"), nullable=False, ), sa.ForeignKeyConstraint( ["profile_id"], ["config_profiles.id"], ondelete="CASCADE" ), sa.ForeignKeyConstraint( ["source_profile_id"], ["config_profiles.id"], ondelete="SET NULL" ), sa.PrimaryKeyConstraint("id"), ) if not _index_exists("config_mounts", "idx_config_mounts_profile"): op.create_index("idx_config_mounts_profile", "config_mounts", ["profile_id"]) if not _column_exists("tool_instances", "selected_profile_id"): op.add_column( "tool_instances", sa.Column("selected_profile_id", postgresql.UUID(as_uuid=True), nullable=True), ) if not _foreign_key_exists( "tool_instances", ["selected_profile_id"], "config_profiles" ): op.create_foreign_key( "fk_tool_instances_selected_profile", "tool_instances", "config_profiles", ["selected_profile_id"], ["id"], ondelete="SET NULL", ) if not _index_exists("tool_instances", "idx_tool_instances_selected_profile"): op.create_index( "idx_tool_instances_selected_profile", "tool_instances", ["selected_profile_id"], ) def downgrade() -> None: # Remove selected_profile_id from tool_instances op.drop_index("idx_tool_instances_selected_profile", table_name="tool_instances") op.drop_constraint( "fk_tool_instances_selected_profile", "tool_instances", type_="foreignkey" ) op.drop_column("tool_instances", "selected_profile_id") # Drop config_mounts op.drop_index("idx_config_mounts_profile", table_name="config_mounts") op.drop_table("config_mounts") # Drop config_includes op.drop_index("idx_config_includes_included", table_name="config_includes") op.drop_index("idx_config_includes_profile", table_name="config_includes") op.drop_table("config_includes") # Drop config_profiles op.drop_index("idx_config_profiles_user", table_name="config_profiles") op.drop_table("config_profiles")