Files
mtgonline/backend/app/models/mtg_models.py
T
akadmin 03f5433583 refactor: consolidate 10+ model files into 2 modules
- models.py: All application models (users, decks, sessions, groups, imports, etc.)
- mtg_models.py: All MTG card data models (sets, cards, mirrors, links)
- Removed: mirror_models.py, user_data.py, user_deck.py, card_import_batch.py,
  user_card_import.py, user_card_import_record.py, relationships.py
- Added __backup__/ directory with original files for reference
- DecklistFile.card_links cross-model relationship resolved via late import
2026-08-26 03:45:15 +00:00

185 lines
7.2 KiB
Python

"""
SQLAlchemy ORM models for MTG card data and mirrors.
Consolidated from mtg_models.py and mirror_models.py into a single module.
This file contains all MTG card data models:
- MtgSet, MtgCard: Canonical card/set data from mtgjson.com (mtg_data DB)
- MtgCardMirror: Mirrored card data copied to mtgonline DB for fast deckbuilding queries
- DeckCardLink: Junction table linking user decks to mirrored cards
Cross-model references to application models (e.g., DeckCardLink.deck → DecklistFile)
use string-based relationship() calls to avoid circular imports.
"""
from sqlalchemy import (
Column, Integer, String, Text, DateTime, ForeignKey, Index,
UniqueConstraint,
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
# ---------------------------------------------------------------------------
# MTG Set and Card (canonical data from mtgjson.com)
# ---------------------------------------------------------------------------
class MtgSet(Base):
"""MTG Set model."""
__tablename__ = "mtg_sets"
id = Column(Integer, primary_key=True, index=True)
code = Column(String(10), unique=True, nullable=False, index=True)
name = Column(String(255), nullable=True)
type = Column(String(100), nullable=True)
release_date = Column(DateTime, nullable=True)
base_set_size = Column(Integer, nullable=True)
total_size = Column(Integer, nullable=True)
is_foil_only = Column(Integer, nullable=True)
is_non_foil_only = Column(Integer, nullable=True)
digital = Column(Integer, nullable=True)
icon_svg_url = Column(Text, nullable=True)
parent_code = Column(String(10), nullable=True)
mtgo_code = Column(String(10), nullable=True)
image = Column(Text, nullable=True) # Card image URL from MTGJSON
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
cards = relationship("MtgCard", back_populates="set")
def __repr__(self) -> str:
return f"<MtgSet {self.code}: {self.name}>"
class MtgCard(Base):
"""MTG Card model."""
__tablename__ = "mtg_cards"
id = Column(Integer, primary_key=True, index=True)
set_id = Column(Integer, ForeignKey("mtg_sets.id"), nullable=True, index=True)
name = Column(String(255), nullable=True, index=True)
mana_cost = Column(String(255), nullable=True, index=True)
type_line = Column(String(255), nullable=True, index=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, index=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 from MTGJSON
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
set = relationship("MtgSet", back_populates="cards")
def __repr__(self) -> str:
return f"<MtgCard {self.name} ({self.set_id})>"
# ---------------------------------------------------------------------------
# Card Mirror and Deck Links (mtgonline DB)
# ---------------------------------------------------------------------------
class MtgCardMirror(Base):
"""
Mirrored card data for user decks.
Contains all relevant card fields copied from mtg_cards (mtg_data)
to avoid cross-DB joins during deckbuilding. Back-references source_id
to the canonical mtg_cards.id for sync tracking.
"""
__tablename__ = "mtg_cards_mirror"
id = Column(Integer, primary_key=True, index=True)
source_id = Column(
Integer,
nullable=True,
index=True,
comment="Logical reference to mtg_cards.id in mtg_data database "
"(cross-DB reference, no FK constraint)",
)
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 of all identifiers
images = Column(Text, nullable=True) # JSON string of image URLs
image = Column(Text, nullable=True) # Card image URL from MTGJSON
card_parts = Column(Text, nullable=True) # Comma-separated list of face names
keywords = Column(Text, nullable=True) # Comma-separated keywords
legalities = Column(Text, nullable=True) # JSON of format legality
set_code = Column(String(10), nullable=True, index=True)
set_name = Column(String(255), nullable=True)
# Sync tracking
synced_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
created_at = Column(DateTime, server_default=func.now())
# Relationships
deck_links = relationship(
"DeckCardLink",
back_populates="card",
cascade="all, delete-orphan",
)
def __repr__(self) -> str:
return f"<MtgCardMirror {self.name} (ID: {self.id}, source: {self.source_id})>"
class DeckCardLink(Base):
"""
Junction table linking user decks to mirrored cards.
Represents a specific quantity of a mirrored card in a specific zone
(main deck or sideboard) of a user's deck.
"""
__tablename__ = "deck_card_links"
id = Column(Integer, primary_key=True, index=True)
deck_id = Column(
Integer, ForeignKey("mtgonline_decklist_files.id", ondelete="CASCADE"),
nullable=False,
)
card_id = Column(
Integer, ForeignKey("mtg_cards_mirror.id"), nullable=False,
)
quantity = Column(Integer, nullable=False, default=1)
zone = Column(String(20), nullable=False, default="main") # 'main' or 'sideboard'
# Composite unique: a card can only appear once per zone in a deck
__table_args__ = (
UniqueConstraint("deck_id", "card_id", "zone", name="uq_deck_card_link"),
Index("idx_deck_card_deck", "deck_id"),
Index("idx_deck_card_card", "card_id"),
)
# Relationships
# String reference to DecklistFile in models.py to avoid circular imports
deck = relationship("DecklistFile", back_populates="card_links")
card = relationship("MtgCardMirror", back_populates="deck_links")
def __repr__(self) -> str:
return (
f"<DeckCardLink deck={self.deck_id} card={self.card_id} "
f"qty={self.quantity} zone={self.zone}>"
)
# ---------------------------------------------------------------------------
# Indexes for performance
# ---------------------------------------------------------------------------
Index("idx_mtg_cards_name_set", MtgCard.name, MtgCard.set_id)
Index("idx_mtg_cards_type", MtgCard.type_line)
Index("idx_mtg_cards_rarity", MtgCard.rarity)