"""Esquema y gestión de la base de datos SQLite.""" import sqlite3 from pathlib import Path from typing import Optional from datetime import datetime from ..constants import DB_PATH class DatabaseManager: """Gestor de la base de datos SQLite.""" def __init__(self, db_path: Optional[Path] = None): """ Inicializa el gestor de base de datos. Args: db_path: Ruta a la base de datos (usa DB_PATH por defecto) """ self.db_path = db_path or DB_PATH self.db_path.parent.mkdir(parents=True, exist_ok=True) self._initialize_schema() def get_connection(self) -> sqlite3.Connection: """Obtiene una conexión a la base de datos.""" conn = sqlite3.connect(str(self.db_path), check_same_thread=False) conn.row_factory = sqlite3.Row return conn def _initialize_schema(self): """Crea las tablas si no existen.""" with self.get_connection() as conn: cursor = conn.cursor() # Tabla de jobs cursor.execute(""" CREATE TABLE IF NOT EXISTS jobs ( job_id TEXT PRIMARY KEY, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, source_path TEXT NOT NULL, source_name TEXT NOT NULL, source_hash TEXT, node_name TEXT, db_name TEXT, status TEXT NOT NULL, attempts INTEGER DEFAULT 0, last_error TEXT, started_at TEXT, finished_at TEXT, total_ms INTEGER, extract_ms INTEGER, restore_ms INTEGER, filelist_ms INTEGER ) """) # Tabla de pasos de jobs cursor.execute(""" CREATE TABLE IF NOT EXISTS job_steps ( id INTEGER PRIMARY KEY AUTOINCREMENT, job_id TEXT NOT NULL, step TEXT NOT NULL, started_at TEXT NOT NULL, finished_at TEXT, duration_ms INTEGER, exit_code INTEGER, stdout TEXT, stderr TEXT, error TEXT, FOREIGN KEY (job_id) REFERENCES jobs(job_id) ) """) # Tabla de nodos (mapeo node_name -> db_name) cursor.execute(""" CREATE TABLE IF NOT EXISTS nodes ( node_name TEXT PRIMARY KEY, db_name TEXT NOT NULL, active INTEGER DEFAULT 1, notes TEXT, updated_at TEXT NOT NULL ) """) # Tabla de eventos cursor.execute(""" CREATE TABLE IF NOT EXISTS events ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_at TEXT NOT NULL, level TEXT NOT NULL, job_id TEXT, message TEXT NOT NULL, FOREIGN KEY (job_id) REFERENCES jobs(job_id) ) """) # Tabla de configuración cursor.execute(""" CREATE TABLE IF NOT EXISTS config ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT NOT NULL ) """) # Índices cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_jobs_created ON jobs(created_at)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_events_created ON events(created_at)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_events_job ON events(job_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_job_steps_job ON job_steps(job_id)") conn.commit() def execute(self, query: str, params: tuple = ()) -> sqlite3.Cursor: """Ejecuta una consulta SQL.""" with self.get_connection() as conn: cursor = conn.cursor() cursor.execute(query, params) conn.commit() return cursor def fetchone(self, query: str, params: tuple = ()) -> Optional[sqlite3.Row]: """Ejecuta una consulta y devuelve una fila.""" with self.get_connection() as conn: cursor = conn.cursor() cursor.execute(query, params) return cursor.fetchone() def fetchall(self, query: str, params: tuple = ()) -> list[sqlite3.Row]: """Ejecuta una consulta y devuelve todas las filas.""" with self.get_connection() as conn: cursor = conn.cursor() cursor.execute(query, params) return cursor.fetchall() # Instancia global db = DatabaseManager()