from typing import Any, List, Optional, Literal, Union, overload from types import SimpleNamespace from datetime import datetime, date from sqlalchemy import text from sqlalchemy.engine import CursorResult from sqlalchemy.exc import SQLAlchemyError from actions.ui.ui import warn from database.firebird import Firebird class BaseRepositoryFirebird: # ------------------------------------------------------------------ # Sobrecargas # ------------------------------------------------------------------ @overload def _execute( self, sql: str, params: Optional[dict[str, Any]], fetch: Literal["all"] ) -> List[Any]: ... @overload def _execute( self, sql: str, params: Optional[dict[str, Any]], fetch: Literal["one"] ) -> Optional[Any]: ... @overload def _execute( self, sql: str, params: Optional[dict[str, Any]], fetch: Literal["none"] ) -> None: ... @overload def _execute( self, sql: str, params: Optional[dict[str, Any]], fetch: Literal["result"] ) -> CursorResult[Any]: ... # ------------------------------------------------------------------ # Sanitizador seguro de parâmetros (CORREÇÃO CRÍTICA) # ------------------------------------------------------------------ def _sanitize_params(self, params: Optional[dict[str, Any]]) -> dict[str, Any]: """ Sanitiza parâmetros antes de enviar ao Firebird. Trava datas inválidas (< 1900) e remove timezone para evitar overflow. """ if params is None: return {} safe: dict[str, Any] = {} for key, value in params.items(): # Permite None normalmente if value is None: safe[key] = None continue # --------------------------- # Tratamento de datetime # --------------------------- if isinstance(value, datetime): # Firebird explode com datas muito antigas if value.year < 1900: warn( f"⚠️ Data inválida detectada em '{key}': {value} " f"(ano < 1900). Definido como NULL para evitar overflow." ) safe[key] = None continue # Remove timezone se existir (evita timestamp negativo!) if value.tzinfo is not None: safe[key] = value.replace(tzinfo=None) else: safe[key] = value continue # --------------------------- # Tratamento de date # --------------------------- if isinstance(value, date): if value.year < 1900: warn( f"⚠️ Data de calendário inválida em '{key}': {value}. " f"Convertido para NULL." ) safe[key] = None else: safe[key] = value continue # Outros valores seguem direto safe[key] = value return safe # ------------------------------------------------------------------ # Execução de SQL # ------------------------------------------------------------------ def _execute( self, sql: str, params: Optional[dict[str, Any]] = None, fetch: Literal["all", "one", "none", "result"] = "result", ) -> Union[List[Any], Optional[Any], None, CursorResult[Any]]: engine = Firebird.get_engine() # 🔥 Sanitiza todos os parâmetros antes de enviar ao driver safe_params = self._sanitize_params(params) try: with engine.begin() as conn: result = conn.execute(text(sql), safe_params) # Lê BLOBs com segurança def _read_blob(value): if hasattr(value, "read"): try: return value.read() except Exception: return b"" return value # all if fetch == "all": rows = [] for row in result.mappings().all(): row_dict = {k.lower(): _read_blob(v) for k, v in row.items()} rows.append(SimpleNamespace(**row_dict)) return rows # one elif fetch == "one": row = result.mappings().first() if row: row_dict = {k.lower(): _read_blob(v) for k, v in row.items()} return SimpleNamespace(**row_dict) return None # none elif fetch == "none": return None # result return result except SQLAlchemyError as e: warn("⚠️ [ERRO SQL]: execução falhou") warn(f"SQL:\n{sql}") warn(f"Parâmetros SANITIZADOS enviados ao banco:\n{safe_params}") raise # ------------------------------------------------------------------ # Métodos utilitários públicos # ------------------------------------------------------------------ def query( self, sql: str, params: Optional[dict[str, Any]] = None ) -> CursorResult[Any]: return self._execute(sql, params, fetch="result") def fetch_all(self, sql: str, params: Optional[dict[str, Any]] = None) -> List[Any]: return self._execute(sql, params, fetch="all") def fetch_one( self, sql: str, params: Optional[dict[str, Any]] = None ) -> Optional[Any]: return self._execute(sql, params, fetch="one") def run(self, sql: str, params: Optional[dict[str, Any]] = None) -> None: self._execute(sql, params, fetch="none") def run_and_return( self, sql: str, params: Optional[dict[str, Any]] = None ) -> Optional[Any]: return self._execute(sql, params, fetch="one")