"""SQLAlchemy models for realtime messaging Tables: - messages: Stores all messages sent in incident rooms - message_reactions: User reactions to messages (emoji) - message_edit_history: Audit trail for message edits """ from sqlalchemy import Column, Integer, String, Text, DateTime, Enum, ForeignKey, UniqueConstraint, Index, BigInteger, JSON from sqlalchemy.orm import relationship from datetime import datetime import enum import uuid from app.core.database import Base class MessageType(str, enum.Enum): """Types of messages in incident rooms""" TEXT = "text" IMAGE_REF = "image_ref" FILE_REF = "file_ref" SYSTEM = "system" INCIDENT_DATA = "incident_data" class Message(Base): """Message model for incident room communications""" __tablename__ = "messages" message_id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) room_id = Column(String(36), ForeignKey("incident_rooms.room_id", ondelete="CASCADE"), nullable=False) sender_id = Column(String(255), nullable=False) # User email/ID content = Column(Text, nullable=False) message_type = Column(Enum(MessageType), default=MessageType.TEXT, nullable=False) # Message metadata for structured data, mentions, file references, etc. message_metadata = Column(JSON) # Timestamps created_at = Column(DateTime, default=datetime.utcnow, nullable=False) edited_at = Column(DateTime) # Last edit timestamp deleted_at = Column(DateTime) # Soft delete timestamp # Sequence number for FIFO ordering within a room # Note: Autoincrement doesn't work for non-PK in SQLite, will be set in service layer sequence_number = Column(BigInteger, nullable=False) # Relationships reactions = relationship("MessageReaction", back_populates="message", cascade="all, delete-orphan") edit_history = relationship("MessageEditHistory", back_populates="message", cascade="all, delete-orphan") # Indexes for common queries __table_args__ = ( Index("ix_messages_room_created", "room_id", "created_at"), Index("ix_messages_room_sequence", "room_id", "sequence_number"), Index("ix_messages_sender", "sender_id"), # PostgreSQL full-text search index on content (commented for SQLite compatibility) # Note: Uncomment when using PostgreSQL with pg_trgm extension enabled # Index("ix_messages_content_search", "content", postgresql_using='gin', postgresql_ops={'content': 'gin_trgm_ops'}), ) class MessageReaction(Base): """Message reaction model for emoji reactions""" __tablename__ = "message_reactions" reaction_id = Column(Integer, primary_key=True, autoincrement=True) message_id = Column(String(36), ForeignKey("messages.message_id", ondelete="CASCADE"), nullable=False) user_id = Column(String(255), nullable=False) # User email/ID who reacted emoji = Column(String(10), nullable=False) # Emoji character or code # Timestamp created_at = Column(DateTime, default=datetime.utcnow, nullable=False) # Relationships message = relationship("Message", back_populates="reactions") # Constraints and indexes __table_args__ = ( # Ensure unique reaction per user per message UniqueConstraint("message_id", "user_id", "emoji", name="uq_message_reaction"), Index("ix_message_reactions_message", "message_id"), ) class MessageEditHistory(Base): """Message edit history model for audit trail""" __tablename__ = "message_edit_history" edit_id = Column(Integer, primary_key=True, autoincrement=True) message_id = Column(String(36), ForeignKey("messages.message_id", ondelete="CASCADE"), nullable=False) original_content = Column(Text, nullable=False) # Content before edit edited_by = Column(String(255), nullable=False) # User who made the edit # Timestamp edited_at = Column(DateTime, default=datetime.utcnow, nullable=False) # Relationships message = relationship("Message", back_populates="edit_history") # Indexes __table_args__ = ( Index("ix_message_edit_history_message", "message_id", "edited_at"), )