""" SQLAlchemy ORM models for the MTG Online application. Consolidated from 9 fragmented model files (models.py, user_deck.py, user_data.py, card_import_batch.py, user_card_import.py, relationships.py, and others) into a single module for clarity and maintainability. This file contains all application-level models: - Core models: User, DecklistFile, DecklistFolder, Room, RoomGameType, Ban, GameLog, AuditLog - Card mirror: MtgonlineCard (local copy of mtg_cards for deckbuilding) - User decks: UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion - User data: UserSession, DeckVersion, GameReplay, ReplayPlayer, GameOutcome, UserStatistics, UserCardCollection, CardWishlist, UserGroup, GroupMember, GroupChatMessage, UserNetwork, NetworkMember, UserPreference, UserActivityLog - Card imports: CardImportBatch, UserCardImportRecord, UserCardImport Cross-model relationships (e.g., DecklistFile.card_links → DeckCardLink) are resolved at the bottom of this file after all classes are defined. """ from sqlalchemy import ( Column, Integer, String, BigInteger, Boolean, DateTime, Text, ForeignKey, Index, UniqueConstraint, Float, JSON, ) from sqlalchemy.orm import relationship from sqlalchemy.sql import func from app.core.database import Base # --------------------------------------------------------------------------- # Core Models # --------------------------------------------------------------------------- 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 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 (card_links is added below after DeckCardLink is defined) 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]) # --------------------------------------------------------------------------- # Card Mirror (local copy for deckbuilding) # --------------------------------------------------------------------------- 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"" # --------------------------------------------------------------------------- # User Deck Models # --------------------------------------------------------------------------- class UserDeck(Base): """Per-user deck storage in the primary mtgonline database.""" __tablename__ = "user_decks" id = Column(BigInteger, primary_key=True, autoincrement=True) user_id = Column( Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True, ) name = Column(String(255), nullable=False, index=True) status = Column( String(20), nullable=False, default="DRAFT", index=True, ) # DRAFT or FINAL folder_id = Column( Integer, ForeignKey("mtgonline_decklist_folders.id"), nullable=True, ) format = Column( String(50), nullable=True, default="standard", ) # Standard, Modern, etc. notes = Column(Text, nullable=True) is_precedent = Column(Boolean, default=False, index=True) # Mark as deck precedent/template precedent_name = Column(String(255), nullable=True) # Name for precedent (if applicable) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) # Relationships user = relationship("User", backref="user_decks") folder = relationship("DecklistFolder", backref="user_decks") cards = relationship( "UserDeckCard", back_populates="deck", cascade="all, delete-orphan", order_by="UserDeckCard.id", ) def __repr__(self) -> str: return f"" class UserDeckCard(Base): """ Junction table for user deck cards. Stores individual card entries in a user's deck with quantity and zone. Card references point to mtg_cards.id in the mtgdata database. """ __tablename__ = "user_deck_cards" id = Column(BigInteger, primary_key=True, autoincrement=True) deck_id = Column( BigInteger, ForeignKey("user_decks.id", ondelete="CASCADE"), nullable=False, index=True, ) card_id = Column( Integer, ForeignKey("mtgonline_cards.id"), nullable=False, index=True, ) quantity = Column(Integer, nullable=False, default=1) zone = Column(String(20), nullable=False, default="main") # 'main' or 'sideboard' position = Column(Integer, nullable=True) # Optional ordering within zone __table_args__ = ( UniqueConstraint("deck_id", "card_id", "zone", name="uq_deck_card_unique"), Index("idx_deck_cards_deck", "deck_id"), Index("idx_deck_cards_card", "card_id"), ) # Relationships deck = relationship("UserDeck", back_populates="cards") card = relationship("MtgonlineCard", back_populates="deck_cards") def __repr__(self) -> str: return f"" class DeckPrecedent(Base): """ Deck precedent (template) storage. Precedents are pre-built deck templates that users can clone to start building their own decks. """ __tablename__ = "deck_precedents" id = Column(BigInteger, primary_key=True, autoincrement=True) name = Column(String(255), nullable=False, index=True) description = Column(Text, nullable=True) format = Column(String(50), nullable=True, default="standard") is_public = Column(Boolean, default=True, index=True) created_by = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=True, ) # None = system created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) # Relationships creator = relationship("User", foreign_keys=[created_by]) cards = relationship( "DeckPrecedentCard", back_populates="precedent", cascade="all, delete-orphan", ) def __repr__(self) -> str: return f"" class DeckPrecedentCard(Base): """Junction table for deck precedent cards.""" __tablename__ = "deck_precedent_cards" id = Column(BigInteger, primary_key=True, autoincrement=True) precedent_id = Column( BigInteger, ForeignKey("deck_precedents.id", ondelete="CASCADE"), nullable=False, index=True, ) card_id = Column(Integer, nullable=False, index=True) quantity = Column(Integer, nullable=False, default=1) zone = Column(String(20), nullable=False, default="main") __table_args__ = ( UniqueConstraint( "precedent_id", "card_id", "zone", name="uq_precedent_card_unique", ), ) precedent = relationship("DeckPrecedent", back_populates="cards") def __repr__(self) -> str: return f"" class CardSuggestion(Base): """ Card suggestion storage. Stores suggested cards for a deck based on similarity, pairing patterns, or manual curation. """ __tablename__ = "card_suggestions" id = Column(BigInteger, primary_key=True, autoincrement=True) deck_id = Column( BigInteger, ForeignKey("user_decks.id", ondelete="CASCADE"), nullable=False, index=True, ) card_id = Column(Integer, nullable=False, index=True) # Suggested card source_card_id = Column(Integer, nullable=True) # Card that triggered the suggestion suggestion_type = Column( String(50), nullable=False, default="SIMILAR", ) # SIMILAR, PAIRING, ALTERNATIVE confidence = Column(Float, nullable=True) # 0.0 to 1.0 notes = Column(Text, nullable=True) created_at = Column(DateTime, server_default=func.now()) deck = relationship("UserDeck", backref="suggestions") __table_args__ = ( UniqueConstraint( "deck_id", "card_id", "source_card_id", name="uq_suggestion_unique", ), ) def __repr__(self) -> str: return ( f"" ) # --------------------------------------------------------------------------- # User Data Models # --------------------------------------------------------------------------- class UserSession(Base): """User authentication session.""" __tablename__ = "user_sessions" id = Column(BigInteger, primary_key=True) user_id = Column( Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True, ) session_token_hash = Column( String(255), unique=True, nullable=False, index=True, ) ip_address = Column(String(45), nullable=True) user_agent = Column(Text, nullable=True) created_at = Column(DateTime, server_default=func.now()) expires_at = Column(DateTime, nullable=False) is_active = Column(Boolean, default=True) user = relationship("User", backref="sessions") def __repr__(self) -> str: return f"" class DeckVersion(Base): """Deck version history.""" __tablename__ = "deck_versions" id = Column(BigInteger, primary_key=True) deck_id = Column( Integer, ForeignKey("mtgonline_decklist_files.id", ondelete="CASCADE"), nullable=False, index=True, ) version_number = Column(Integer, nullable=False) content = Column(Text, nullable=False) status = Column(String(20), server_default="DRAFT") comment = Column(Text, nullable=True) created_at = Column(DateTime, server_default=func.now()) deck = relationship("DecklistFile", backref="versions") def __repr__(self) -> str: return f"" class GameReplay(Base): """Game replay recording.""" __tablename__ = "game_replays" id = Column(BigInteger, primary_key=True) game_uuid = Column(String(36), unique=True, nullable=False) room_id = Column( Integer, ForeignKey("mtgonline_rooms.id"), nullable=True, index=True, ) game_type = Column(String(50), nullable=True) format = Column(String(50), nullable=True) duration_seconds = Column(Integer, nullable=True) start_time = Column(DateTime, nullable=False) end_time = Column(DateTime, nullable=True) status = Column(String(20), server_default="IN_PROGRESS") replay_data = Column(JSON, nullable=True) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) players = relationship( "ReplayPlayer", back_populates="replay", cascade="all, delete-orphan", ) outcomes = relationship( "GameOutcome", back_populates="replay", cascade="all, delete-orphan", ) def __repr__(self) -> str: return f"" class ReplayPlayer(Base): """Player in a game replay.""" __tablename__ = "replay_players" id = Column(BigInteger, primary_key=True) replay_id = Column( BigInteger, ForeignKey("game_replays.id", ondelete="CASCADE"), nullable=False, index=True, ) user_id = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True, ) position = Column(Integer, nullable=True) deck_id = Column( Integer, ForeignKey("mtgonline_decklist_files.id"), nullable=True, ) won = Column(Boolean, nullable=True) lost = Column(Boolean, nullable=True) concession = Column(Boolean, default=False) turn_one = Column(Boolean, default=False) created_at = Column(DateTime, server_default=func.now()) replay = relationship("GameReplay", back_populates="players") user = relationship("User") deck = relationship("DecklistFile") def __repr__(self) -> str: return f"" class GameOutcome(Base): """Game outcome record.""" __tablename__ = "game_outcomes" id = Column(BigInteger, primary_key=True) user_id = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True, ) game_uuid = Column( String(36), ForeignKey("game_replays.game_uuid"), nullable=False, index=True, ) outcome = Column(String(20), nullable=False, index=True) opponent_id = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=True, ) format = Column(String(50), nullable=True) rating_before = Column(Integer, nullable=True) rating_after = Column(Integer, nullable=True) rating_change = Column(Integer, nullable=True) created_at = Column(DateTime, server_default=func.now()) replay = relationship("GameReplay", back_populates="outcomes") user = relationship("User", foreign_keys=[user_id]) opponent = relationship("User", foreign_keys=[opponent_id]) def __repr__(self) -> str: return f"" class UserStatistics(Base): """User game statistics summary.""" __tablename__ = "user_statistics" user_id = Column( Integer, ForeignKey("mtgonline_users.id"), primary_key=True, ) total_games = Column(Integer, default=0) total_wins = Column(Integer, default=0) total_losses = Column(Integer, default=0) total_concessions = Column(Integer, default=0) win_rate = Column(Float, default=0.0) current_streak = Column(Integer, default=0) best_streak = Column(Integer, default=0) average_rating = Column(Float, default=0.0) last_game_date = Column(DateTime, nullable=True) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) user = relationship("User") def __repr__(self) -> str: return ( f"" ) class UserCardCollection(Base): """User card collection.""" __tablename__ = "user_card_collection" id = Column(BigInteger, primary_key=True) user_id = Column( Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True, ) card_id = Column(Integer, nullable=False, index=True) quantity = Column(Integer, default=1) condition = Column(String(20), server_default="NEAR_MINT") language = Column(String(5), server_default="EN") is_foil = Column(Boolean, default=False) is_alt_art = Column(Boolean, default=False) acquired_date = Column(DateTime, server_default=func.now()) acquisition_method = Column(String(50), nullable=True) notes = Column(Text, nullable=True) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) __table_args__ = ( UniqueConstraint( "user_id", "card_id", "is_foil", "is_alt_art", name="uq_collection_unique", ), Index("idx_collection_user_card", "user_id", "card_id"), ) user = relationship("User", backref="card_collection") def __repr__(self) -> str: return ( f"" ) class CardWishlist(Base): """User card wishlist.""" __tablename__ = "card_wishlist" id = Column(BigInteger, primary_key=True) user_id = Column( Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, ) card_id = Column(Integer, nullable=False) max_price = Column(Float, nullable=True) notes = Column(Text, nullable=True) created_at = Column(DateTime, server_default=func.now()) __table_args__ = ( UniqueConstraint("user_id", "card_id", name="uq_wishlist_user_card"), ) user = relationship("User", backref="wishlist") def __repr__(self) -> str: return f"" class UserGroup(Base): """User group.""" __tablename__ = "user_groups" id = Column(BigInteger, primary_key=True) name = Column(String(100), nullable=False) description = Column(Text, nullable=True) owner_id = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True, ) is_public = Column(Boolean, default=True) max_members = Column(Integer, default=50) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) owner = relationship("User", foreign_keys=[owner_id]) members = relationship( "GroupMember", back_populates="group", cascade="all, delete-orphan", ) messages = relationship( "GroupChatMessage", back_populates="group", cascade="all, delete-orphan", ) def __repr__(self) -> str: return f"" class GroupMember(Base): """Group member.""" __tablename__ = "group_members" id = Column(BigInteger, primary_key=True) group_id = Column( BigInteger, ForeignKey("user_groups.id", ondelete="CASCADE"), nullable=False, index=True, ) user_id = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True, ) role = Column(String(20), server_default="MEMBER") joined_at = Column(DateTime, server_default=func.now()) group = relationship("UserGroup", back_populates="members") user = relationship("User") __table_args__ = ( UniqueConstraint("group_id", "user_id", name="uq_group_member"), ) def __repr__(self) -> str: return ( f"" ) class GroupChatMessage(Base): """Group chat message.""" __tablename__ = "group_chat_messages" id = Column(BigInteger, primary_key=True) group_id = Column( BigInteger, ForeignKey("user_groups.id", ondelete="CASCADE"), nullable=False, index=True, ) sender_id = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True, ) message = Column(Text, nullable=False) created_at = Column(DateTime, server_default=func.now(), index=True) group = relationship("UserGroup", back_populates="messages") sender = relationship("User", foreign_keys=[sender_id]) def __repr__(self) -> str: return f"" class UserNetwork(Base): """User network (extended social connection).""" __tablename__ = "user_networks" id = Column(BigInteger, primary_key=True) name = Column(String(100), nullable=False) description = Column(Text, nullable=True) creator_id = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=False, ) is_public = Column(Boolean, default=True) created_at = Column(DateTime, server_default=func.now()) creator = relationship("User", foreign_keys=[creator_id]) members = relationship( "NetworkMember", back_populates="network", cascade="all, delete-orphan", ) def __repr__(self) -> str: return f"" class NetworkMember(Base): """Network member.""" __tablename__ = "network_members" id = Column(BigInteger, primary_key=True) network_id = Column( BigInteger, ForeignKey("user_networks.id", ondelete="CASCADE"), nullable=False, index=True, ) user_id = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True, ) role = Column(String(20), server_default="MEMBER") joined_at = Column(DateTime, server_default=func.now()) network = relationship("UserNetwork", back_populates="members") user = relationship("User") __table_args__ = ( UniqueConstraint("network_id", "user_id", name="uq_network_member"), ) def __repr__(self) -> str: return ( f"" ) class UserPreference(Base): """User preferences and settings.""" __tablename__ = "user_preferences" user_id = Column( Integer, ForeignKey("mtgonline_users.id"), primary_key=True, ) theme = Column(String(20), server_default="light") notifications_enabled = Column(Boolean, default=True) email_notifications = Column(Boolean, default=True) auto_save_decks = Column(Boolean, default=True) default_format = Column(String(50), server_default="standard") language = Column(String(5), server_default="EN") updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) user = relationship("User") def __repr__(self) -> str: return f"" class UserActivityLog(Base): """User activity log.""" __tablename__ = "user_activity_log" id = Column(BigInteger, primary_key=True) user_id = Column( Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True, ) activity_type = Column(String(50), nullable=False, index=True) activity_data = Column(JSON, nullable=True) ip_address = Column(String(45), nullable=True) created_at = Column(DateTime, server_default=func.now(), index=True) user = relationship("User", backref="activity_logs") def __repr__(self) -> str: return f"" # --------------------------------------------------------------------------- # Card Import Models # --------------------------------------------------------------------------- class CardImportBatch(Base): """ Card import batch tracking. Tracks a batch of card imports from a file, including matching results and error information. """ __tablename__ = "card_import_batches" id = Column(BigInteger, primary_key=True, autoincrement=True) user_id = Column( Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, ) filename = Column(String(255), nullable=False) file_type = Column(String(50), nullable=True) # 'mtgjson', 'csv', etc. file_size = Column(BigInteger, nullable=True) # Size in bytes status = Column( String(20), default="PENDING", ) # PENDING, PROCESSING, COMPLETED, FAILED total_cards = Column(Integer, default=0) matched_cards = Column(Integer, default=0) unmatched_cards = Column(Integer, default=0) match_results = Column(Text, nullable=True) # JSON of match details error_message = Column(Text, nullable=True) created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) # Relationships user = relationship("User", backref="card_import_batches") records = relationship( "UserCardImportRecord", back_populates="batch", cascade="all, delete-orphan", ) def __repr__(self) -> str: return f"" class UserCardImportRecord(Base): """ Individual card import record within a batch. Tracks whether each card in an import batch was confirmed and when the confirmation happened. """ __tablename__ = "user_card_imports_confirmed" id = Column(BigInteger, primary_key=True, autoincrement=True) user_id = Column( Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, ) batch_id = Column( BigInteger, ForeignKey("card_import_batches.id", ondelete="CASCADE"), nullable=False, ) is_confirmed = Column(Boolean, default=False) confirmed_at = Column(DateTime, nullable=True) # Relationships batch = relationship("CardImportBatch", back_populates="records") __table_args__ = ( UniqueConstraint( "user_id", "batch_id", name="uq_user_card_import_batch", ), ) def __repr__(self) -> str: return ( f"" ) class UserCardImport(Base): """ User card import record. Stores a user's imported card collection as a JSON array of card names. This table was created in migration 004 and may be superseded by CardImportBatch and UserCardImportRecord in migration 005. """ __tablename__ = "user_card_imports" id = Column(Integer, primary_key=True, autoincrement=True) user_id = Column( Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, ) card_names_json = Column(Text, nullable=False) # JSON array of card names created_at = Column(DateTime, server_default=func.now()) updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now()) # Relationships user = relationship("User", backref="card_imports") __table_args__ = ( UniqueConstraint( "user_id", name="uq_user_card_imports_user_id", ), ) def __repr__(self) -> str: return f"" # --------------------------------------------------------------------------- # Cross-Model Relationships # --------------------------------------------------------------------------- # Add the back-reference from DecklistFile to DeckCardLink. # DeckCardLink is defined in mtg_models.py; we import it here after all # classes in this file are defined to avoid circular imports. from app.models.mtg_models import DeckCardLink # noqa: E402 DecklistFile.card_links = relationship( "DeckCardLink", back_populates="deck", cascade="all, delete-orphan", order_by="DeckCardLink.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)