Files
social-app/backend/alembic/versions/20260226_0003_social_graph_tables.py
T

325 lines
11 KiB
Python
Raw Normal View History

"""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")