""" db.py Dynamic schema SQLite. One prescription upload can create MULTIPLE records (one per visit/date). All records from the same image share the same source_filename. Columns are created automatically based on whatever fields are extracted from each prescription type. """ import sqlite3 import json import re from datetime import datetime from pathlib import Path from typing import List, Dict, Any, Optional DB_PATH = Path(__file__).parent / "data" / "prescriptions.db" BASE_SCHEMA = """ CREATE TABLE IF NOT EXISTS prescriptions ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_filename TEXT, document_type TEXT, hospital_name TEXT, patient_name TEXT, hospital_no TEXT, visit_date TEXT, ocr_engine TEXT, created_at TEXT ); CREATE TABLE IF NOT EXISTS medications ( id INTEGER PRIMARY KEY AUTOINCREMENT, prescription_id INTEGER NOT NULL, drug_name TEXT, dosage TEXT, frequency TEXT, route TEXT, FOREIGN KEY (prescription_id) REFERENCES prescriptions(id) ); """ RESERVED_COLS = { "id", "source_filename", "document_type", "hospital_name", "patient_name", "hospital_no", "visit_date", "ocr_engine", "created_at" } def _safe_col(name: str) -> str: name = name.strip().lower() name = re.sub(r"[^a-z0-9_]", "_", name) name = re.sub(r"_+", "_", name).strip("_") return name or "field" def get_conn() -> sqlite3.Connection: DB_PATH.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def init_db(): conn = get_conn() conn.executescript(BASE_SCHEMA) conn.commit() conn.close() def _existing_columns(conn: sqlite3.Connection) -> List[str]: return [r["name"] for r in conn.execute("PRAGMA table_info(prescriptions)").fetchall()] def _ensure_columns(conn: sqlite3.Connection, field_names: List[str]): existing = set(_existing_columns(conn)) for name in field_names: col = _safe_col(name) if col and col not in existing and col not in RESERVED_COLS: conn.execute(f'ALTER TABLE prescriptions ADD COLUMN "{col}" TEXT') existing.add(col) conn.commit() def save_record( visit_date: Optional[str], fields: Dict[str, Any], medications: List[Dict], meta: Dict, ocr_engine: str, source_filename: str, ) -> int: """Save one visit record. Called once per visit detected in the image.""" conn = get_conn() # Dynamic fields — exclude reserved and medications key dynamic = { _safe_col(k): str(v) for k, v in fields.items() if v and _safe_col(k) not in RESERVED_COLS and k != "medications" } _ensure_columns(conn, list(dynamic.keys())) row = { "source_filename": source_filename, "document_type": meta.get("document_type"), "hospital_name": meta.get("hospital_name"), "patient_name": meta.get("patient_name"), "hospital_no": meta.get("hospital_no"), "visit_date": visit_date, "ocr_engine": ocr_engine, "created_at": datetime.utcnow().isoformat(), **dynamic, } # Remove None values row = {k: v for k, v in row.items() if v is not None} cols = ", ".join(f'"{c}"' for c in row.keys()) placeholders = ", ".join(["?"] * len(row)) cur = conn.execute( f"INSERT INTO prescriptions ({cols}) VALUES ({placeholders})", list(row.values()), ) pid = cur.lastrowid for med in (medications or []): if not isinstance(med, dict): continue conn.execute( "INSERT INTO medications (prescription_id, drug_name, dosage, frequency, route) VALUES (?,?,?,?,?)", (pid, med.get("drug_name"), med.get("dosage"), med.get("frequency"), med.get("route")), ) conn.commit() conn.close() return pid def fetch_prescriptions() -> List[Dict]: conn = get_conn() rows = conn.execute("SELECT * FROM prescriptions ORDER BY id DESC").fetchall() conn.close() return [dict(r) for r in rows] def fetch_medications(prescription_id: int) -> List[Dict]: conn = get_conn() rows = conn.execute( "SELECT drug_name, dosage, frequency, route FROM medications WHERE prescription_id = ?", (prescription_id,) ).fetchall() conn.close() return [dict(r) for r in rows] def fetch_all_flat() -> List[Dict]: conn = get_conn() rows = conn.execute( """ SELECT p.*, m.drug_name, m.dosage, m.frequency, m.route FROM prescriptions p LEFT JOIN medications m ON m.prescription_id = p.id ORDER BY p.id DESC """ ).fetchall() conn.close() return [dict(r) for r in rows] def delete_prescription(prescription_id: int): conn = get_conn() conn.execute("DELETE FROM medications WHERE prescription_id = ?", (prescription_id,)) conn.execute("DELETE FROM prescriptions WHERE id = ?", (prescription_id,)) conn.commit() conn.close()