05/23 Phase 1
This commit is contained in:
+164
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
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
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# DB file lives in lottosight/data/lottosight.db
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DB_PATH = os.path.join(BASE_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()
|
||||
Reference in New Issue
Block a user