207 lines
7.4 KiB
Python
207 lines
7.4 KiB
Python
"""
|
|
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,
|
|
top_prize TEXT NOT NULL DEFAULT ''
|
|
)
|
|
""")
|
|
|
|
# ── 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
|
|
)
|
|
""")
|
|
|
|
# Migration: add top_prize column to existing databases
|
|
cursor.execute("PRAGMA table_info(games)")
|
|
cols = {r["name"] for r in cursor.fetchall()}
|
|
if "top_prize" not in cols:
|
|
cursor.execute(
|
|
"ALTER TABLE games ADD COLUMN top_prize TEXT NOT NULL DEFAULT ''"
|
|
)
|
|
logger.info("[DB] Migrated: added top_prize column to games")
|
|
|
|
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.
|
|
Also back-fills top_prize for any existing rows where it is empty.
|
|
"""
|
|
defaults = [
|
|
# name mc mm bc bm active top_prize
|
|
("Powerball", 5, 69, 1, 26, 1, "Jackpot (variable)"),
|
|
("Mega Millions", 5, 70, 1, 25, 1, "Jackpot (variable)"),
|
|
("Cash 5", 5, 45, 0, 0, 1, "Jackpot from $200K"),
|
|
("Millionaire for Life", 5, 58, 1, 5, 1, "$1M/yr for life"),
|
|
("Bank a Million", 6, 40, 1, 40, 1, "$1M after taxes"),
|
|
]
|
|
|
|
for row in defaults:
|
|
cursor.execute("""
|
|
INSERT OR IGNORE INTO games
|
|
(name, main_count, main_max, bonus_count, bonus_max, active, top_prize)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
""", row)
|
|
# Back-fill top_prize for rows inserted before this column existed
|
|
cursor.execute(
|
|
"UPDATE games SET top_prize = ? WHERE name = ? AND top_prize = ''",
|
|
(row[6], row[0]),
|
|
)
|
|
|
|
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()
|