177 lines
6 KiB
Python
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")
|