144 lines
5.4 KiB
Python
144 lines
5.4 KiB
Python
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)
|