""" SQLAlchemy ORM models for the MTG Online database. Mirrors the original MySQL schema with modern PostgreSQL features. """ from sqlalchemy import Column, Integer, String, BigInteger, Boolean, DateTime, Text, ForeignKey, Index from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.core.database import Base class User(Base): """User account model.""" __tablename__ = "mtgonline_users" id = Column(Integer, primary_key=True, index=True) username = Column(String(64), unique=True, nullable=False, index=True) password_hash = Column(String(128), nullable=False) # bcrypt hash salt = Column(String(128), nullable=False) # password salt email = Column(String(255), nullable=True, index=True) country = Column(String(2), nullable=True) real_name = Column(String(128), nullable=True) avatar_bmp = Column(Text, nullable=True) # Base64 encoded avatar privlevel = Column(String(50), nullable=True, default="User") # Account status is_active = Column(Boolean, default=True) is_banned = Column(Boolean, default=False) ban_reason = Column(Text, nullable=True) ban_ends = Column(DateTime, nullable=True) # VIP/Donator status vip_status = Column(Integer, default=0) # 0=normal, 1=vip, 2=donator vip_expiry = Column(DateTime, nullable=True) # Timestamps creation_date = Column(DateTime, server_default=func.now()) last_login = Column(DateTime, nullable=True) # Relationships decklist_files = relationship("DecklistFile", back_populates="owner", cascade="all, delete-orphan") decklist_folders = relationship("DecklistFolder", back_populates="owner", cascade="all, delete-orphan") def __repr__(self) -> str: return f"" class MtgonlineCard(Base): """ Local card data mirror for the mtgonline database. Mirrors data from mtg_cards (mtgdata database) to avoid cross-database joins during deckbuilding. Maintains source_id to reference the canonical mtg_cards.id for sync tracking. """ __tablename__ = "mtgonline_cards" id = Column(Integer, primary_key=True, index=True) source_id = Column(Integer, nullable=True, index=True) # References mtg_cards.id in mtgdata name = Column(String(255), nullable=False, index=True) mana_cost = Column(String(255), nullable=True) type_line = Column(String(255), nullable=True) oracle_text = Column(Text, nullable=True) power = Column(String(50), nullable=True) toughness = Column(String(50), nullable=True) rarity = Column(String(50), nullable=True) layout = Column(String(50), nullable=True) artist = Column(String(255), nullable=True) flavor_text = Column(Text, nullable=True) numbers = Column(String(100), nullable=True) identifiers = Column(Text, nullable=True) # JSON string images = Column(Text, nullable=True) # JSON string image = Column(Text, nullable=True) # Card image URL set_code = Column(String(10), nullable=True, index=True) set_name = Column(String(255), nullable=True) card_parts = Column(Text, nullable=True) # Comma-separated face names keywords = Column(Text, nullable=True) # Comma-separated keywords legalities = Column(Text, nullable=True) # JSON of format legality # Sync tracking synced_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) created_at = Column(DateTime, server_default=func.now()) # Relationships deck_cards = relationship( "UserDeckCard", back_populates="card", cascade="all, delete-orphan" ) # Indexes __table_args__ = ( Index('idx_mtgonline_cards_name', 'name'), Index('idx_mtgonline_cards_set', 'set_code'), ) def __repr__(self) -> str: return f"" class DecklistFolder(Base): """User deck folder.""" __tablename__ = "mtgonline_decklist_folders" id = Column(Integer, primary_key=True, index=True) owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False) name = Column(String(255), nullable=False) parent_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True) creation_date = Column(DateTime, server_default=func.now()) # Relationships owner = relationship("User", back_populates="decklist_folders") children = relationship("DecklistFolder", back_populates="parent", cascade="all, delete-orphan") parent = relationship("DecklistFolder", back_populates="children", remote_side=[id]) files = relationship("DecklistFile", back_populates="folder", cascade="all, delete-orphan") user_decks = relationship( "UserDeck", back_populates="folder", lazy="select" ) class DecklistFile(Base): """Deck file stored in a folder.""" __tablename__ = "mtgonline_decklist_files" id = Column(Integer, primary_key=True, index=True) folder_id = Column(Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True) owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False) name = Column(String(255), nullable=False) content = Column(Text, nullable=False) # Native XML or plain text deck format format = Column(String(50), default="native") # 'native' or 'plain' status = Column(String(20), default="DRAUGHT") # 'DRAUGHT' or 'FINAL' creation_date = Column(DateTime, server_default=func.now()) # Relationships folder = relationship("DecklistFolder", back_populates="files") owner = relationship("User", back_populates="decklist_files") def __repr__(self) -> str: return f"" class Room(Base): """Chat room.""" __tablename__ = "mtgonline_rooms" id = Column(Integer, primary_key=True, index=True) name = Column(String(100), unique=True, nullable=False) description = Column(Text, nullable=True) is_password_protected = Column(Boolean, default=False) password_hash = Column(String(128), nullable=True) creation_date = Column(DateTime, server_default=func.now()) # Game types available in this room game_types = relationship("RoomGameType", back_populates="room", cascade="all, delete-orphan") class RoomGameType(Base): """Game type definition for a room.""" __tablename__ = "mtgonline_rooms_gametypes" id = Column(Integer, primary_key=True, index=True) room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=False) name = Column(String(100), nullable=False) description = Column(Text, nullable=True) # Relationships room = relationship("Room", back_populates="game_types") class Ban(Base): """User ban record.""" __tablename__ = "mtgonline_bans" id = Column(Integer, primary_key=True, index=True) user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False) server_id = Column(Integer, nullable=True) reason = Column(Text, nullable=False) moderators = Column(String(255), nullable=True) # Admin usernames ip_address = Column(String(45), nullable=True) # IPv4 or IPv6 expiration_time = Column(DateTime, nullable=True) active = Column(Boolean, default=True) creation_date = Column(DateTime, server_default=func.now()) # Relationships user = relationship("User") def __repr__(self) -> str: return f"" class GameLog(Base): """Game log entry.""" __tablename__ = "mtgonline_log" id = Column(Integer, primary_key=True, index=True) room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=True) player_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True) message = Column(Text, nullable=False) timestamp = Column(DateTime, server_default=func.now()) # Relationships room = relationship("Room") player = relationship("User") class AuditLog(Base): """Audit trail for administrative actions.""" __tablename__ = "mtgonline_audit" id = Column(Integer, primary_key=True, index=True) admin_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False) action_type = Column(String(50), nullable=False) # 'ban', 'unban', 'warn', etc. target_user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True) details = Column(Text, nullable=True) ip_address = Column(String(45), nullable=True) timestamp = Column(DateTime, server_default=func.now()) # Relationships admin = relationship("User", foreign_keys=[admin_id]) target_user = relationship("User", foreign_keys=[target_user_id]) # Indexes for performance Index("idx_decks_owner", DecklistFile.owner_id) Index("idx_decks_folder", DecklistFile.folder_id) Index("idx_bans_active", Ban.active) Index("idx_log_timestamp", GameLog.timestamp)