Files
TicketTracker/tickettracker/db/schema.py

128 lines
4.2 KiB
Python
Raw Normal View History

"""
Schéma SQLite pour TicketTracker.
Ce module gère uniquement le DDL (création des tables, vues et index).
Il ne contient pas de logique métier.
Tables :
receipts un ticket de courses par ligne
items articles, liés à leur ticket par FK
Vue :
price_history jointure items × receipts pour comparer les prix dans le temps
"""
import sqlite3
from pathlib import Path
# Chemin par défaut : data/tickettracker.db à la racine du projet
DEFAULT_DB_PATH = Path(__file__).parent.parent.parent / "data" / "tickettracker.db"
# ---------------------------------------------------------------------------
# Instructions DDL (CREATE TABLE / INDEX / VIEW)
# ---------------------------------------------------------------------------
_SQL_CREATE_RECEIPTS = """
CREATE TABLE IF NOT EXISTS receipts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
store TEXT NOT NULL,
date TEXT NOT NULL, -- format ISO 8601 : "2026-02-14"
total REAL NOT NULL,
delivery_fee REAL, -- NULL pour Leclerc (magasin physique)
order_id TEXT, -- NULL si non disponible
raw_json TEXT NOT NULL, -- résultat de receipt.to_json() pour debug
created_at TEXT NOT NULL -- datetime UTC ISO au moment de l'insertion
);
"""
_SQL_CREATE_RECEIPTS_IDX = """
CREATE INDEX IF NOT EXISTS idx_receipts_dedup
ON receipts (store, date, total);
"""
_SQL_CREATE_ITEMS = """
CREATE TABLE IF NOT EXISTS items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
receipt_id INTEGER NOT NULL REFERENCES receipts(id),
name_raw TEXT NOT NULL, -- nom tel que sorti du parser
name_normalized TEXT, -- NULL jusqu'au Sprint 3 (normalisation LLM)
category TEXT, -- NULL pour Picnic (pas de catégories dans le mail)
quantity REAL NOT NULL,
unit TEXT NOT NULL,
unit_price REAL NOT NULL,
total_price REAL NOT NULL
);
"""
_SQL_CREATE_ITEMS_IDX = """
CREATE INDEX IF NOT EXISTS idx_items_receipt_id
ON items (receipt_id);
"""
_SQL_CREATE_ITEMS_NORM_IDX = """
CREATE INDEX IF NOT EXISTS idx_items_name_normalized
ON items (name_normalized);
"""
_SQL_CREATE_PRICE_HISTORY = """
CREATE VIEW IF NOT EXISTS price_history AS
SELECT
i.name_normalized,
r.store,
r.date,
i.unit_price,
i.total_price,
i.quantity,
i.unit,
i.category
FROM items i
JOIN receipts r ON i.receipt_id = r.id;
"""
# ---------------------------------------------------------------------------
# Fonctions publiques
# ---------------------------------------------------------------------------
def get_connection(db_path: str | Path = DEFAULT_DB_PATH) -> sqlite3.Connection:
"""Ouvre une connexion SQLite avec les pragmas requis.
Active les clés étrangères (désactivées par défaut dans SQLite
le pragma doit être réappliqué à chaque nouvelle connexion).
Configure row_factory = sqlite3.Row pour accéder aux colonnes par nom.
Args:
db_path: Chemin vers le fichier .db (créé automatiquement si absent).
Returns:
Connexion sqlite3 configurée.
"""
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON")
return conn
def init_db(db_path: str | Path = DEFAULT_DB_PATH) -> None:
"""Crée les tables, index et vues s'ils n'existent pas encore.
Idempotent : peut être appelé plusieurs fois sans erreur grâce aux
clauses CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS.
Crée le dossier parent (data/) s'il n'existe pas.
Args:
db_path: Chemin vers le fichier .db.
Raises:
PermissionError: Si le système de fichiers refuse la création du dossier.
"""
db_path = Path(db_path)
db_path.parent.mkdir(parents=True, exist_ok=True)
with get_connection(db_path) as conn:
conn.execute(_SQL_CREATE_RECEIPTS)
conn.execute(_SQL_CREATE_RECEIPTS_IDX)
conn.execute(_SQL_CREATE_ITEMS)
conn.execute(_SQL_CREATE_ITEMS_IDX)
conn.execute(_SQL_CREATE_ITEMS_NORM_IDX)
conn.execute(_SQL_CREATE_PRICE_HISTORY)