Ferramentas/AjustaFundos/abstracts/repository_firebird.py

177 lines
6 KiB
Python

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")