"""initial schema part 4: collaboration tables Revision ID: 202602260004 Revises: 202602260003 Create Date: 2026-02-26 20:13:00 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa from sqlalchemy.dialects import postgresql revision: str = "202602260004" down_revision: Union[str, Sequence[str], None] = "202602260003" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "schedule_items", sa.Column("id", sa.UUID(), nullable=False), sa.Column("owner_id", sa.UUID(), nullable=False), sa.Column("title", sa.String(length=255), nullable=False), sa.Column("description", sa.Text(), nullable=True), sa.Column("start_at", sa.DateTime(timezone=True), nullable=False), sa.Column("end_at", sa.DateTime(timezone=True), nullable=True), sa.Column("timezone", sa.String(length=50), nullable=False), sa.Column( "metadata", postgresql.JSONB(astext_type=sa.Text()), server_default="{}", nullable=False, ), sa.Column("recurrence_rule", sa.String(length=255), nullable=True), sa.Column("source_type", sa.String(length=20), nullable=False), sa.Column("status", sa.String(length=20), nullable=False), sa.Column("created_by", sa.UUID(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), sa.PrimaryKeyConstraint("id"), ) op.create_index( "ix_schedule_items_owner_start", "schedule_items", ["owner_id", "start_at"], unique=False, ) op.create_index( "ix_schedule_items_status_start", "schedule_items", ["status", "start_at"], unique=False, ) op.execute( "ALTER TABLE schedule_items ADD CONSTRAINT chk_schedule_item_source_type CHECK (source_type IN ('manual', 'imported', 'agent_generated'))" ) op.execute( "ALTER TABLE schedule_items ADD CONSTRAINT chk_schedule_item_status CHECK (status IN ('active', 'completed', 'canceled', 'archived'))" ) op.create_foreign_key( "fk_schedule_items_owner_id", "schedule_items", "users", ["owner_id"], ["id"], referent_schema="auth", ondelete="CASCADE", ) op.create_foreign_key( "fk_schedule_items_created_by", "schedule_items", "users", ["created_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) _enable_rls("schedule_items") op.create_table( "schedule_subscriptions", sa.Column("id", sa.UUID(), nullable=False), sa.Column("item_id", sa.UUID(), nullable=False), sa.Column("subscriber_id", sa.UUID(), nullable=False), sa.Column( "permission", sa.Integer(), nullable=False, server_default=sa.text("1") ), sa.Column( "notify_level", sa.String(length=20), nullable=False, server_default=sa.text("'all'"), ), sa.Column( "status", sa.String(length=20), nullable=False, server_default=sa.text("'active'"), ), sa.Column("created_by", sa.UUID(), 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.PrimaryKeyConstraint("id"), sa.UniqueConstraint( "item_id", "subscriber_id", name="uq_schedule_subscriptions_item_subscriber" ), ) op.create_index( "ix_schedule_subscribers_subscriber_status", "schedule_subscriptions", ["subscriber_id", "status"], unique=False, ) op.create_index( "ix_schedule_subscribers_item_status", "schedule_subscriptions", ["item_id", "status"], unique=False, ) op.execute( "ALTER TABLE schedule_subscriptions ADD CONSTRAINT chk_schedule_subscription_permission CHECK (permission BETWEEN 0 AND 7)" ) op.execute( "ALTER TABLE schedule_subscriptions ADD CONSTRAINT chk_schedule_subscription_notify_level CHECK (notify_level IN ('all', 'mentions', 'none'))" ) op.execute( "ALTER TABLE schedule_subscriptions ADD CONSTRAINT chk_schedule_subscription_status CHECK (status IN ('active', 'paused', 'unsubscribed'))" ) op.create_foreign_key( "fk_schedule_subscriptions_item_id", "schedule_subscriptions", "schedule_items", ["item_id"], ["id"], ondelete="CASCADE", ) op.create_foreign_key( "fk_schedule_subscriptions_subscriber_id", "schedule_subscriptions", "users", ["subscriber_id"], ["id"], referent_schema="auth", ondelete="CASCADE", ) op.create_foreign_key( "fk_schedule_subscriptions_created_by", "schedule_subscriptions", "users", ["created_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) _enable_rls("schedule_subscriptions") op.create_table( "todos", sa.Column("id", sa.UUID(), nullable=False), sa.Column("owner_id", sa.UUID(), nullable=False), sa.Column("title", sa.String(length=255), nullable=False), sa.Column("description", sa.String(length=1000), nullable=True), sa.Column("due_at", sa.DateTime(timezone=True), nullable=True), sa.Column("priority", sa.Integer(), nullable=False), sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), sa.Column("status", sa.String(length=20), nullable=False), sa.Column("created_by", sa.UUID(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True), sa.PrimaryKeyConstraint("id"), ) op.create_index( "ix_todos_owner_status_due", "todos", ["owner_id", "status", "due_at"], unique=False, ) op.create_index( "ix_todos_owner_created", "todos", ["owner_id", "created_at"], unique=False ) op.execute( "ALTER TABLE todos ADD CONSTRAINT chk_todos_status CHECK (status IN ('pending', 'done', 'canceled'))" ) op.execute( "ALTER TABLE todos ADD CONSTRAINT chk_todos_priority CHECK (priority BETWEEN 1 AND 4)" ) op.create_foreign_key( "fk_todos_owner_id", "todos", "users", ["owner_id"], ["id"], referent_schema="auth", ondelete="CASCADE", ) op.create_foreign_key( "fk_todos_created_by", "todos", "users", ["created_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) _enable_rls("todos") op.create_table( "todo_sources", sa.Column("id", sa.UUID(), nullable=False), sa.Column("todo_id", sa.UUID(), nullable=False), sa.Column("schedule_item_id", sa.UUID(), nullable=False), 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.PrimaryKeyConstraint("id"), sa.UniqueConstraint( "todo_id", "schedule_item_id", name="uq_todo_sources_todo_schedule" ), ) op.create_index("ix_todo_sources_todo", "todo_sources", ["todo_id"], unique=False) op.create_index( "ix_todo_sources_schedule_item", "todo_sources", ["schedule_item_id"], unique=False, ) op.create_foreign_key( "fk_todo_sources_todo_id", "todo_sources", "todos", ["todo_id"], ["id"], ondelete="CASCADE", ) op.create_foreign_key( "fk_todo_sources_schedule_item_id", "todo_sources", "schedule_items", ["schedule_item_id"], ["id"], ondelete="CASCADE", ) _enable_rls("todo_sources") op.create_table( "inbox_messages", sa.Column("id", sa.UUID(), nullable=False), sa.Column("recipient_id", sa.UUID(), nullable=False), sa.Column("sender_id", sa.UUID(), nullable=True), sa.Column("message_type", sa.String(length=20), nullable=False), sa.Column("friendship_id", sa.UUID(), nullable=True), sa.Column("schedule_item_id", sa.UUID(), nullable=True), sa.Column("group_id", sa.UUID(), nullable=True), sa.Column("content", sa.Text(), nullable=True), sa.Column( "is_read", sa.Boolean(), nullable=False, server_default=sa.text("false") ), sa.Column("status", sa.String(length=20), nullable=False), sa.Column("created_by", sa.UUID(), 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.PrimaryKeyConstraint("id"), ) op.create_index( "ix_inbox_messages_recipient_status_created", "inbox_messages", ["recipient_id", "status", "created_at"], unique=False, ) op.execute( "ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_type CHECK (message_type IN ('friend_request', 'calendar', 'system', 'group'))" ) op.execute( "ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_status CHECK (status IN ('pending', 'accepted', 'rejected', 'dismissed'))" ) op.execute( "ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_sender CHECK ((message_type = 'system' AND sender_id IS NULL) OR (message_type <> 'system' AND sender_id IS NOT NULL))" ) op.execute( "ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_friendship CHECK ((message_type = 'friend_request' AND friendship_id IS NOT NULL) OR (message_type <> 'friend_request' AND friendship_id IS NULL))" ) op.execute( "ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_schedule_item CHECK ((message_type = 'calendar' AND schedule_item_id IS NOT NULL) OR (message_type <> 'calendar' AND schedule_item_id IS NULL))" ) op.execute( "ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_group CHECK ((message_type = 'group' AND group_id IS NOT NULL) OR (message_type <> 'group' AND group_id IS NULL))" ) op.execute( "ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_system_fields CHECK ((message_type = 'system' AND friendship_id IS NULL AND schedule_item_id IS NULL AND group_id IS NULL) OR (message_type <> 'system' AND (friendship_id IS NOT NULL OR schedule_item_id IS NOT NULL OR group_id IS NOT NULL)))" ) op.create_foreign_key( "fk_inbox_messages_recipient_id", "inbox_messages", "users", ["recipient_id"], ["id"], referent_schema="auth", ondelete="CASCADE", ) op.create_foreign_key( "fk_inbox_messages_sender_id", "inbox_messages", "users", ["sender_id"], ["id"], referent_schema="auth", ondelete="SET NULL", ) op.create_foreign_key( "fk_inbox_messages_friendship_id", "inbox_messages", "friendships", ["friendship_id"], ["id"], ondelete="CASCADE", ) op.create_foreign_key( "fk_inbox_messages_schedule_item_id", "inbox_messages", "schedule_items", ["schedule_item_id"], ["id"], ondelete="CASCADE", ) op.create_foreign_key( "fk_inbox_messages_group_id", "inbox_messages", "groups", ["group_id"], ["id"], ondelete="CASCADE", ) op.create_foreign_key( "fk_inbox_messages_created_by", "inbox_messages", "users", ["created_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) _enable_rls("inbox_messages") def downgrade() -> None: _drop_rls("inbox_messages") op.drop_constraint( "fk_inbox_messages_created_by", "inbox_messages", type_="foreignkey" ) op.drop_constraint( "fk_inbox_messages_group_id", "inbox_messages", type_="foreignkey" ) op.drop_constraint( "fk_inbox_messages_schedule_item_id", "inbox_messages", type_="foreignkey" ) op.drop_constraint( "fk_inbox_messages_friendship_id", "inbox_messages", type_="foreignkey" ) op.drop_constraint( "fk_inbox_messages_sender_id", "inbox_messages", type_="foreignkey" ) op.drop_constraint( "fk_inbox_messages_recipient_id", "inbox_messages", type_="foreignkey" ) op.drop_table("inbox_messages") _drop_rls("todo_sources") op.drop_constraint( "fk_todo_sources_schedule_item_id", "todo_sources", type_="foreignkey" ) op.drop_constraint("fk_todo_sources_todo_id", "todo_sources", type_="foreignkey") op.drop_table("todo_sources") _drop_rls("todos") op.drop_constraint("fk_todos_created_by", "todos", type_="foreignkey") op.drop_constraint("fk_todos_owner_id", "todos", type_="foreignkey") op.drop_table("todos") _drop_rls("schedule_subscriptions") op.drop_constraint( "fk_schedule_subscriptions_created_by", "schedule_subscriptions", type_="foreignkey", ) op.drop_constraint( "fk_schedule_subscriptions_subscriber_id", "schedule_subscriptions", type_="foreignkey", ) op.drop_constraint( "fk_schedule_subscriptions_item_id", "schedule_subscriptions", type_="foreignkey", ) op.drop_table("schedule_subscriptions") _drop_rls("schedule_items") op.drop_constraint( "fk_schedule_items_created_by", "schedule_items", type_="foreignkey" ) op.drop_constraint( "fk_schedule_items_owner_id", "schedule_items", type_="foreignkey" ) op.drop_table("schedule_items") def _enable_rls(table_name: str) -> None: for role in ["anon", "authenticated"]: for action in ["select", "insert", "update", "delete"]: op.execute( f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}" ) op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY") for role in ["anon", "authenticated"]: op.execute( f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)" ) op.execute( f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)" ) op.execute( f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)" ) op.execute( f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)" ) def _drop_rls(table_name: str) -> None: for role in ["anon", "authenticated"]: op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}") op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}") op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}") op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}") op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")