import sqlite3 import contextlib from typing import Generator from app import config def get_db_connection() -> sqlite3.Connection: conn = sqlite3.connect(config.DB_PATH, timeout=15.0) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL;") conn.execute("PRAGMA synchronous=NORMAL;") conn.execute("PRAGMA foreign_keys=ON;") return conn @contextlib.contextmanager def get_db() -> Generator[sqlite3.Connection, None, None]: conn = get_db_connection() try: yield conn conn.commit() except Exception: conn.rollback() raise finally: conn.close() def _migrate(conn: sqlite3.Connection): """Safe schema migrations: add new columns/tables without touching existing data.""" # --- vocabulary_cards new columns --- existing_cols = { row[1] for row in conn.execute("PRAGMA table_info(vocabulary_cards)").fetchall() } card_migrations = [ ("collection", "TEXT NOT NULL DEFAULT 'General'"), ("srs_reps", "INTEGER DEFAULT 0"), ("srs_interval", "INTEGER DEFAULT 0"), ("srs_ease", "REAL DEFAULT 2.5"), ("srs_next_review", "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"), ("srs_last_review", "TIMESTAMP"), ("srs_state", "TEXT DEFAULT 'new'"), ("error_count", "INTEGER DEFAULT 0"), ] for col, definition in card_migrations: if col not in existing_cols: conn.execute(f"ALTER TABLE vocabulary_cards ADD COLUMN {col} {definition}") # Ensure srs_logs table exists conn.execute(""" CREATE TABLE IF NOT EXISTS srs_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, card_id INTEGER NOT NULL, rating INTEGER NOT NULL, interval INTEGER NOT NULL, ease REAL NOT NULL, reviewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (card_id) REFERENCES vocabulary_cards(id) ON DELETE CASCADE ); """) conn.execute("CREATE INDEX IF NOT EXISTS idx_srs_user ON srs_logs(user_id);") conn.execute("CREATE INDEX IF NOT EXISTS idx_srs_reviewed ON srs_logs(user_id, reviewed_at);") def init_db(): config.DB_PATH.parent.mkdir(parents=True, exist_ok=True) with get_db() as conn: conn.executescript(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL COLLATE NOCASE, password_hash TEXT NOT NULL, salt TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS sessions ( token TEXT PRIMARY KEY, user_id INTEGER NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP NOT NULL, user_agent TEXT, ip TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE TABLE IF NOT EXISTS vocabulary_cards ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, term TEXT NOT NULL, translation TEXT NOT NULL, context_sentence TEXT DEFAULT '', phonetic TEXT DEFAULT '', audio_url TEXT DEFAULT '', part_of_speech TEXT DEFAULT '', definition_en TEXT DEFAULT '', tags TEXT DEFAULT 'general', notes TEXT DEFAULT '', collection TEXT NOT NULL DEFAULT 'General', srs_reps INTEGER DEFAULT 0, srs_interval INTEGER DEFAULT 0, srs_ease REAL DEFAULT 2.5, srs_next_review TIMESTAMP DEFAULT CURRENT_TIMESTAMP, srs_last_review TIMESTAMP, srs_state TEXT DEFAULT 'new', error_count INTEGER DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_vocab_user ON vocabulary_cards(user_id); CREATE INDEX IF NOT EXISTS idx_vocab_term ON vocabulary_cards(term); CREATE INDEX IF NOT EXISTS idx_vocab_created ON vocabulary_cards(created_at DESC); CREATE INDEX IF NOT EXISTS idx_vocab_collection ON vocabulary_cards(user_id, collection); CREATE INDEX IF NOT EXISTS idx_vocab_srs_queue ON vocabulary_cards(user_id, srs_next_review, srs_state); CREATE TABLE IF NOT EXISTS srs_logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, card_id INTEGER NOT NULL, rating INTEGER NOT NULL, interval INTEGER NOT NULL, ease REAL NOT NULL, reviewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (card_id) REFERENCES vocabulary_cards(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_srs_user ON srs_logs(user_id); CREATE INDEX IF NOT EXISTS idx_srs_reviewed ON srs_logs(user_id, reviewed_at); CREATE TABLE IF NOT EXISTS dictionary_cache ( query TEXT PRIMARY KEY COLLATE NOCASE, response_json TEXT NOT NULL, cached_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); """) # Apply migrations for pre-existing databases _migrate(conn)