""" db/database.py -------------- SQLite initialization, schema creation, and default game seeding. All table definitions live here. Call init_db() once on app startup. """ import sqlite3 import logging import os import shutil from datetime import datetime from core.paths import user_data_dir logger = logging.getLogger(__name__) DB_PATH = os.path.join(user_data_dir(), "data", "lottosight.db") def get_connection(): """Return a sqlite3 connection with foreign keys enabled.""" conn = sqlite3.connect(DB_PATH) conn.execute("PRAGMA foreign_keys = ON") conn.row_factory = sqlite3.Row return conn def init_db(): """ Initialize the database — create all tables if they don't exist, then seed default game configs. Safe to call on every app launch (uses IF NOT EXISTS). """ logger.info("[DB] Initializing database at %s", DB_PATH) # Ensure data/ directory exists os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) conn = get_connection() try: cursor = conn.cursor() # ── games ────────────────────────────────────────────────────────── cursor.execute(""" CREATE TABLE IF NOT EXISTS games ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, main_count INTEGER NOT NULL, main_max INTEGER NOT NULL, bonus_count INTEGER NOT NULL DEFAULT 0, bonus_max INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 1 ) """) # ── draws ────────────────────────────────────────────────────────── cursor.execute(""" CREATE TABLE IF NOT EXISTS draws ( id INTEGER PRIMARY KEY AUTOINCREMENT, game_id INTEGER NOT NULL REFERENCES games(id), draw_date TEXT NOT NULL, numbers TEXT NOT NULL, bonus TEXT, multiplier TEXT, source TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) # Unique constraint: one record per game per draw date cursor.execute(""" CREATE UNIQUE INDEX IF NOT EXISTS idx_draws_game_date ON draws (game_id, draw_date) """) # Index for fast date-range queries cursor.execute(""" CREATE INDEX IF NOT EXISTS idx_draws_date ON draws (draw_date) """) # ── predictions ──────────────────────────────────────────────────── cursor.execute(""" CREATE TABLE IF NOT EXISTS predictions ( id INTEGER PRIMARY KEY AUTOINCREMENT, game_id INTEGER NOT NULL REFERENCES games(id), strategy TEXT NOT NULL, numbers TEXT NOT NULL, bonus TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')) ) """) # ── fetch_log ────────────────────────────────────────────────────── cursor.execute(""" CREATE TABLE IF NOT EXISTS fetch_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, source TEXT NOT NULL, fetched_at TEXT NOT NULL DEFAULT (datetime('now')), added INTEGER NOT NULL DEFAULT 0, skipped INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'success', message TEXT ) """) conn.commit() logger.info("[DB] Tables created/verified OK") _seed_games(cursor, conn) except Exception as e: conn.rollback() logger.error("[ERROR] DB init failed: %s", e, exc_info=True) raise finally: conn.close() def _seed_games(cursor, conn): """ Insert default game configs if they don't already exist. Uses INSERT OR IGNORE to be idempotent on every launch. """ defaults = [ # name main_count main_max bonus_count bonus_max active ("Powerball", 5, 69, 1, 26, 1), ("Mega Millions", 5, 70, 1, 25, 1), ("Cash 5", 5, 45, 0, 0, 1), ("Millionaire for Life", 5, 58, 1, 5, 1), ("Bank a Million", 6, 40, 1, 40, 1), ] for row in defaults: cursor.execute(""" INSERT OR IGNORE INTO games (name, main_count, main_max, bonus_count, bonus_max, active) VALUES (?, ?, ?, ?, ?, ?) """, row) inserted = conn.total_changes conn.commit() if inserted > 0: logger.info("[DB] Seeded %d default game(s)", inserted) else: logger.info("[DB] Default games already seeded — skipped") def backup_db(dest_dir: str | None = None) -> str: """Copy the live DB to dest_dir and return the backup file path.""" if dest_dir is None: dest_dir = os.path.join(user_data_dir(), "exports") os.makedirs(dest_dir, exist_ok=True) ts = datetime.now().strftime("%Y%m%d_%H%M%S") dest = os.path.join(dest_dir, f"lottosight_backup_{ts}.db") shutil.copy2(DB_PATH, dest) logger.info("[DB] Backup created: %s", dest) return dest def restore_db(source_path: str) -> None: """Overwrite the live DB with a backup file.""" if not os.path.isfile(source_path): raise FileNotFoundError(f"Backup file not found: {source_path}") os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) shutil.copy2(source_path, DB_PATH) logger.info("[DB] Database restored from: %s", source_path) def get_db_stats(): """ Return a dict of basic DB stats for display in Settings screen. {game_name: draw_count, ...} """ conn = get_connection() try: cursor = conn.cursor() cursor.execute(""" SELECT g.name, COUNT(d.id) as draw_count FROM games g LEFT JOIN draws d ON d.game_id = g.id GROUP BY g.id ORDER BY g.name """) rows = cursor.fetchall() return {row["name"]: row["draw_count"] for row in rows} finally: conn.close()