9661b9a81c
Models + Alembic migration (0007, head) only, per docs/planning/social.md US-1 -- the friends/follow API router is a separate sibling task. - friendships: requester_id/addressee_id (FK users, CASCADE), status (pending/accepted native enum), unique pair constraint, indexed FKs. - follows: follower_id/followed_id (FK users, CASCADE), unique pair. - blocks: blocker_id/blocked_id (FK users, CASCADE), unique pair. - Relationships wired onto User for both directions of each table. - tests/test_social_models.py exercises request/accept, the unique-pair constraint, follow, block, and DB-level CASCADE delete against real Postgres. Full backend suite (152 tests) still green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2FESCaMXeJpC9gSctiEeD
119 lines
4.4 KiB
Python
119 lines
4.4 KiB
Python
"""add social graph tables (friendships, follows, blocks)
|
|
|
|
Revision ID: 0007_add_social_graph_tables
|
|
Revises: 0006_add_presence_columns
|
|
Create Date: 2026-09-15 00:00:00.000000
|
|
|
|
docs/planning/social.md US-1 / app/models/social.py. Models + migration
|
|
only -- the request/accept/block business logic lives in the sibling
|
|
"Friends & follow API router" task.
|
|
"""
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = "0007_add_social_graph_tables"
|
|
down_revision: Union[str, None] = "0006_add_presence_columns"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
friendship_status = postgresql.ENUM(
|
|
"pending", "accepted", name="friendship_status"
|
|
)
|
|
friendship_status.create(op.get_bind(), checkfirst=True)
|
|
|
|
op.create_table(
|
|
"friendships",
|
|
sa.Column("id", sa.Integer(), primary_key=True),
|
|
sa.Column(
|
|
"requester_id",
|
|
sa.Integer(),
|
|
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"addressee_id",
|
|
sa.Integer(),
|
|
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"status",
|
|
postgresql.ENUM(
|
|
"pending", "accepted", name="friendship_status", create_type=False
|
|
),
|
|
nullable=False,
|
|
server_default="pending",
|
|
),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.UniqueConstraint("requester_id", "addressee_id", name="uq_friendships_pair"),
|
|
)
|
|
op.create_index("ix_friendships_requester_id", "friendships", ["requester_id"])
|
|
op.create_index("ix_friendships_addressee_id", "friendships", ["addressee_id"])
|
|
|
|
op.create_table(
|
|
"follows",
|
|
sa.Column("id", sa.Integer(), primary_key=True),
|
|
sa.Column(
|
|
"follower_id",
|
|
sa.Integer(),
|
|
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"followed_id",
|
|
sa.Integer(),
|
|
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.UniqueConstraint("follower_id", "followed_id", name="uq_follows_pair"),
|
|
)
|
|
op.create_index("ix_follows_follower_id", "follows", ["follower_id"])
|
|
op.create_index("ix_follows_followed_id", "follows", ["followed_id"])
|
|
|
|
op.create_table(
|
|
"blocks",
|
|
sa.Column("id", sa.Integer(), primary_key=True),
|
|
sa.Column(
|
|
"blocker_id",
|
|
sa.Integer(),
|
|
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column(
|
|
"blocked_id",
|
|
sa.Integer(),
|
|
sa.ForeignKey("users.id", ondelete="CASCADE"),
|
|
nullable=False,
|
|
),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False),
|
|
sa.UniqueConstraint("blocker_id", "blocked_id", name="uq_blocks_pair"),
|
|
)
|
|
op.create_index("ix_blocks_blocker_id", "blocks", ["blocker_id"])
|
|
op.create_index("ix_blocks_blocked_id", "blocks", ["blocked_id"])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("ix_blocks_blocked_id", table_name="blocks")
|
|
op.drop_index("ix_blocks_blocker_id", table_name="blocks")
|
|
op.drop_table("blocks")
|
|
|
|
op.drop_index("ix_follows_followed_id", table_name="follows")
|
|
op.drop_index("ix_follows_follower_id", table_name="follows")
|
|
op.drop_table("follows")
|
|
|
|
op.drop_index("ix_friendships_addressee_id", table_name="friendships")
|
|
op.drop_index("ix_friendships_requester_id", table_name="friendships")
|
|
op.drop_table("friendships")
|
|
|
|
postgresql.ENUM(name="friendship_status").drop(op.get_bind(), checkfirst=True)
|