"""initial schema part 3: social graph tables Revision ID: 202602260003 Revises: 202602260002 Create Date: 2026-02-26 20:12:00 """ from typing import Sequence, Union from alembic import op import sqlalchemy as sa revision: str = "202602260003" down_revision: Union[str, Sequence[str], None] = "202602260002" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: op.create_table( "friendships", sa.Column("id", sa.UUID(), nullable=False), sa.Column("user_low_id", sa.UUID(), nullable=False), sa.Column("user_high_id", sa.UUID(), nullable=False), sa.Column("initiator_id", sa.UUID(), nullable=True), sa.Column("status", sa.String(length=20), nullable=False), sa.Column("requested_at", sa.DateTime(timezone=True), nullable=True), sa.Column("accepted_at", sa.DateTime(timezone=True), nullable=True), sa.Column("blocked_by", sa.UUID(), nullable=True), sa.Column("created_by", sa.UUID(), nullable=True), sa.Column("updated_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"), sa.UniqueConstraint("user_low_id", "user_high_id", name="uq_friendships_users"), ) op.create_index( "ix_friendships_user_low_status", "friendships", ["user_low_id", "status"], unique=False, ) op.create_index( "ix_friendships_user_high_status", "friendships", ["user_high_id", "status"], unique=False, ) op.execute( "CREATE INDEX ix_friendships_pending ON friendships (status) WHERE status = 'pending'" ) op.execute( "ALTER TABLE friendships ADD CONSTRAINT chk_user_low_less_than_high CHECK (user_low_id < user_high_id)" ) op.execute( "ALTER TABLE friendships ADD CONSTRAINT chk_initiator_id_valid CHECK (initiator_id IN (user_low_id, user_high_id))" ) op.execute( "ALTER TABLE friendships ADD CONSTRAINT chk_user_ids_different CHECK (user_low_id <> user_high_id)" ) op.create_foreign_key( "fk_friendships_user_low_id", "friendships", "users", ["user_low_id"], ["id"], referent_schema="auth", ondelete="CASCADE", ) op.create_foreign_key( "fk_friendships_user_high_id", "friendships", "users", ["user_high_id"], ["id"], referent_schema="auth", ondelete="CASCADE", ) op.create_foreign_key( "fk_friendships_initiator_id", "friendships", "users", ["initiator_id"], ["id"], referent_schema="auth", ondelete="SET NULL", ) op.create_foreign_key( "fk_friendships_created_by", "friendships", "users", ["created_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) op.create_foreign_key( "fk_friendships_updated_by", "friendships", "users", ["updated_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) _enable_rls("friendships") op.create_table( "groups", sa.Column("id", sa.UUID(), nullable=False), sa.Column("name", sa.String(length=100), nullable=False), sa.Column("description", sa.Text(), nullable=True), sa.Column("owner_id", sa.UUID(), nullable=False), sa.Column("status", sa.String(length=20), nullable=False), sa.Column("created_by", sa.UUID(), nullable=True), sa.Column("updated_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_groups_owner_status", "groups", ["owner_id", "status"], unique=False ) op.create_foreign_key( "fk_groups_owner_id", "groups", "users", ["owner_id"], ["id"], referent_schema="auth", ondelete="CASCADE", ) op.create_foreign_key( "fk_groups_created_by", "groups", "users", ["created_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) op.create_foreign_key( "fk_groups_updated_by", "groups", "users", ["updated_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) _enable_rls("groups") op.create_table( "group_members", sa.Column("id", sa.UUID(), nullable=False), sa.Column("group_id", sa.UUID(), nullable=False), sa.Column("user_id", sa.UUID(), nullable=False), sa.Column("role", sa.String(length=20), nullable=False), sa.Column("join_source", sa.String(length=20), nullable=False), sa.Column("invited_by", sa.UUID(), nullable=True), sa.Column("joined_at", sa.DateTime(timezone=True), nullable=True), sa.Column("removed_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("updated_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"), sa.UniqueConstraint("group_id", "user_id", name="uq_group_members_group_user"), ) op.create_index( "ix_group_members_group_role_status", "group_members", ["group_id", "role", "status"], unique=False, ) op.create_index( "ix_group_members_user_status", "group_members", ["user_id", "status"], unique=False, ) op.execute( "ALTER TABLE group_members ADD CONSTRAINT chk_group_member_role CHECK (role IN ('owner', 'admin', 'member'))" ) op.create_foreign_key( "fk_group_members_group_id", "group_members", "groups", ["group_id"], ["id"], ondelete="CASCADE", ) op.create_foreign_key( "fk_group_members_user_id", "group_members", "users", ["user_id"], ["id"], referent_schema="auth", ondelete="CASCADE", ) op.create_foreign_key( "fk_group_members_invited_by", "group_members", "users", ["invited_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) op.create_foreign_key( "fk_group_members_created_by", "group_members", "users", ["created_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) op.create_foreign_key( "fk_group_members_updated_by", "group_members", "users", ["updated_by"], ["id"], referent_schema="auth", ondelete="SET NULL", ) _enable_rls("group_members") def downgrade() -> None: _drop_rls("group_members") op.drop_constraint( "fk_group_members_updated_by", "group_members", type_="foreignkey" ) op.drop_constraint( "fk_group_members_created_by", "group_members", type_="foreignkey" ) op.drop_constraint( "fk_group_members_invited_by", "group_members", type_="foreignkey" ) op.drop_constraint("fk_group_members_user_id", "group_members", type_="foreignkey") op.drop_constraint("fk_group_members_group_id", "group_members", type_="foreignkey") op.drop_table("group_members") _drop_rls("groups") op.drop_constraint("fk_groups_updated_by", "groups", type_="foreignkey") op.drop_constraint("fk_groups_created_by", "groups", type_="foreignkey") op.drop_constraint("fk_groups_owner_id", "groups", type_="foreignkey") op.drop_table("groups") _drop_rls("friendships") op.drop_constraint("fk_friendships_updated_by", "friendships", type_="foreignkey") op.drop_constraint("fk_friendships_created_by", "friendships", type_="foreignkey") op.drop_constraint("fk_friendships_initiator_id", "friendships", type_="foreignkey") op.drop_constraint("fk_friendships_user_high_id", "friendships", type_="foreignkey") op.drop_constraint("fk_friendships_user_low_id", "friendships", type_="foreignkey") op.drop_table("friendships") 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")