Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/invvd/mtg-cheaper-deck/llms.txt

Use this file to discover all available pages before exploring further.

The db module is the data layer for MTG Cheaper Deck. It declares the cards table using SQLAlchemy Core, manages the engine singleton that all other modules share, and provides init_db() to create the schema on first run. The default backend is a local SQLite file that requires no external infrastructure. Switching to PostgreSQL requires only a DATABASE_URL change and installing the appropriate driver.

DATABASE_URL

DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///./data/cards.db")
The SQLAlchemy connection URL, read from the environment at import time via python-dotenv. If the variable is not set, the default is a SQLite database stored at ./data/cards.db relative to the working directory. To use PostgreSQL, set the environment variable before starting any process:
DATABASE_URL=postgresql+psycopg://user:pass@localhost/mtg python main.py
The scoring module currently requires SQLite because it uses json_extract for legality filtering. See the warning in the scoring reference before changing DATABASE_URL to a PostgreSQL URL.

cards Table

The cards SQLAlchemy Table object is the schema declaration for the main card store. It maps one-to-one with the Scryfall oracle card data loaded by the sync process.
ColumnTypeConstraintsDescription
oracle_idStringPrimary KeyScryfall oracle ID — unique per distinct card regardless of printing
scryfall_idStringScryfall ID of the representative printing stored in the database
nameStringNOT NULLCard name (English)
color_identityJSONNOT NULL, default []List of colour symbols, e.g. ["U", "B"]
color_identity_maskIntegerNOT NULL, default 0Bitmask: W=1, U=2, B=4, R=8, G=16. Used for fast colour-subset filtering
type_lineStringFull Scryfall type line, e.g. "Legendary Creature — Elf Druid"
cmcFloatConverted mana cost
keywordsJSONNOT NULL, default []Scryfall evergreen keyword list, e.g. ["Flying", "Deathtouch"]
oracle_textTextRules text of the oracle card
powerStringPower (creatures only), stored as string to preserve * values
toughnessStringToughness (creatures only), stored as string to preserve * values
price_usdFloatCurrent USD price from Scryfall (may be NULL for unpurchaseable cards)
legalitiesJSONNOT NULL, default {}Dict of format → legality status, e.g. {"commander": "legal", "standard": "not_legal"}
edhrec_rankIntegerEDHREC rank (lower = more popular). NULL for cards without a rank
updated_atStringISO-8601 timestamp of the last sync update for this row

Indexes

Three indexes are created by init_db():
IndexColumnPurpose
idx_cards_name_lowerlower(name)Supports the case-insensitive name lookup in matching.py
idx_cards_priceprice_usdSpeeds up price-range scans in CandidatePool.cheaper_than()
idx_cards_color_maskcolor_identity_maskSpeeds up bitmask colour-subset filtering in suggest_alternatives()

get_engine

Returns the global SQLAlchemy Engine singleton, creating it on the first call. Returns Engine — the lazily-initialised engine. Subsequent calls return the same instance. For SQLite connections the engine is created with check_same_thread=False so that the same connection can be used from multiple threads (relevant when the pipeline runs background tasks).
from db import get_engine
from sqlalchemy import select, text

engine = get_engine()
with engine.connect() as conn:
    result = conn.execute(text("SELECT COUNT(*) FROM cards"))
    print(result.scalar())  # e.g. 27000

init_db

Creates the data/ directory (and any intermediate directories) if it does not exist, then calls metadata.create_all() to create all tables and indexes that are not already present in the database. Safe to call on every startup — it is a no-op when the schema is already up to date. Returns None.
from db import init_db, get_engine, cards
from sqlalchemy import select

# Safe to call multiple times — idempotent
init_db()

# Query after init
engine = get_engine()
with engine.connect() as conn:
    rows = conn.execute(select(cards).limit(5)).mappings().all()
    for row in rows:
        print(row["name"], row["price_usd"])
Calling init_db() is the recommended first step in any script that might run against a fresh environment (e.g. a new clone or a CI job that starts with an empty data/ directory).

Build docs developers (and LLMs) love