"""add chat tables (conversations, messages, message_reports) Revision ID: 0008_add_chat_tables Revises: 0007_add_social_graph_tables Create Date: 2026-09-15 00:00:00.000000 docs/planning/social.md US-4 / app/models/chat.py. Models + migration only -- text filtering, rate limiting, and send/report/moderation endpoints live in sibling tasks that depend on this one. """ 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 = "0008_add_chat_tables" down_revision: Union[str, None] = "0007_add_social_graph_tables" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: message_report_status = postgresql.ENUM( "open", "resolved", "dismissed", name="message_report_status" ) message_report_status.create(op.get_bind(), checkfirst=True) op.create_table( "conversations", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "user_a_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "user_b_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("user_a_id", "user_b_id", name="uq_conversations_pair"), sa.CheckConstraint("user_a_id < user_b_id", name="ck_conversations_ordered_pair"), ) op.create_index("ix_conversations_user_a_id", "conversations", ["user_a_id"]) op.create_index("ix_conversations_user_b_id", "conversations", ["user_b_id"]) op.create_table( "conversation_participants", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "conversation_id", sa.Integer(), sa.ForeignKey("conversations.id", ondelete="CASCADE"), nullable=False, ), sa.Column( "user_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.UniqueConstraint( "conversation_id", "user_id", name="uq_conversation_participants_pair" ), ) op.create_index( "ix_conversation_participants_conversation_id", "conversation_participants", ["conversation_id"], ) op.create_index( "ix_conversation_participants_user_id", "conversation_participants", ["user_id"] ) op.create_table( "messages", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "sender_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ), sa.Column( "conversation_id", sa.Integer(), sa.ForeignKey("conversations.id", ondelete="CASCADE"), nullable=True, ), sa.Column( "world_id", sa.Integer(), sa.ForeignKey("worlds.id", ondelete="CASCADE"), nullable=True, ), sa.Column( "room_id", sa.Integer(), sa.ForeignKey("world_rooms.id", ondelete="CASCADE"), nullable=True, ), sa.Column("body", sa.Text(), nullable=False), sa.Column("filtered", sa.Boolean(), nullable=False, server_default="false"), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), ) op.create_index("ix_messages_sender_id", "messages", ["sender_id"]) op.create_index( "ix_messages_conversation_created", "messages", ["conversation_id", "created_at"] ) op.create_index("ix_messages_world_created", "messages", ["world_id", "created_at"]) op.create_table( "message_reports", sa.Column("id", sa.Integer(), primary_key=True), sa.Column( "message_id", sa.Integer(), sa.ForeignKey("messages.id", ondelete="SET NULL"), nullable=True, ), sa.Column( "reporter_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ), sa.Column("reason", sa.Text(), nullable=False), sa.Column( "status", postgresql.ENUM( "open", "resolved", "dismissed", name="message_report_status", create_type=False ), nullable=False, server_default="open", ), sa.Column( "moderator_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True, ), sa.Column("resolved_at", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), ) op.create_index("ix_message_reports_message_id", "message_reports", ["message_id"]) op.create_index("ix_message_reports_reporter_id", "message_reports", ["reporter_id"]) op.create_index("ix_message_reports_moderator_id", "message_reports", ["moderator_id"]) def downgrade() -> None: op.drop_index("ix_message_reports_moderator_id", table_name="message_reports") op.drop_index("ix_message_reports_reporter_id", table_name="message_reports") op.drop_index("ix_message_reports_message_id", table_name="message_reports") op.drop_table("message_reports") op.drop_index("ix_messages_world_created", table_name="messages") op.drop_index("ix_messages_conversation_created", table_name="messages") op.drop_index("ix_messages_sender_id", table_name="messages") op.drop_table("messages") op.drop_index("ix_conversation_participants_user_id", table_name="conversation_participants") op.drop_index( "ix_conversation_participants_conversation_id", table_name="conversation_participants" ) op.drop_table("conversation_participants") op.drop_index("ix_conversations_user_b_id", table_name="conversations") op.drop_index("ix_conversations_user_a_id", table_name="conversations") op.drop_table("conversations") postgresql.ENUM(name="message_report_status").drop(op.get_bind(), checkfirst=True)