Files
lottosight/db/database.py
T
2026-05-23 12:05:33 -04:00

165 lines
5.6 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
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),
]
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 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()