- 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
185 lines
7.2 KiB
Python
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)
|