fix(dwh): validate adapter limits strictly

This commit is contained in:
2026-07-11 20:19:05 +02:00
parent 216984aac8
commit f6302b31dd
7 changed files with 84 additions and 19 deletions
+8 -3
View File
@@ -2,14 +2,19 @@
from sqlalchemy import Engine
from tht.execute import ExecResult, PlanSummary, explain as _explain, run_controlled
from tht.execute import (
ExecResult,
PlanSummary,
explain as _explain,
require_positive_int,
run_controlled,
)
DEFAULT_TIMEOUT_MS = 30_000
def run_query(engine: Engine, sql: str, *, limit: int) -> ExecResult:
if limit <= 0:
raise ValueError("limit must be a positive integer")
limit = require_positive_int(limit, name="limit")
return run_controlled(
engine,
sql,
+5 -4
View File
@@ -4,6 +4,7 @@ from dataclasses import dataclass
from sqlalchemy import Engine, text
from tht.config import ExamplesConfig, LshConfig
from tht.execute import require_positive_int
from tht.mschema.models import Annotations, PhysicalSchema
from tht.ports.dwh import DistinctValues
@@ -26,8 +27,7 @@ def _quoted_top_values_query(engine: Engine, schema: str, table: str, column: st
def sample_column(
engine: Engine, schema: str, table: str, column: str, *, limit: int
) -> list[object]:
if limit <= 0:
raise ValueError("limit must be a positive integer")
limit = require_positive_int(limit, name="limit")
query = _quoted_top_values_query(engine, schema, table, column)
with engine.connect() as conn:
rows = conn.execute(query, {"lim": limit}).fetchall()
@@ -37,8 +37,7 @@ def sample_column(
def sample_column_rest(
client, schema: str, table: str, column: str, *, limit: int
) -> list[object]:
if limit <= 0:
raise ValueError("limit must be a positive integer")
limit = require_positive_int(limit, name="limit")
rows = client.top_values(schema, table, column, limit)
return [row["value"] for row in rows if row.get("value") is not None]
@@ -51,6 +50,7 @@ def distinct_values(
*,
max_values: int = DEFAULT_DISTINCT_VALUES_LIMIT,
) -> DistinctValues:
max_values = require_positive_int(max_values, name="max_values")
values = sample_column(engine, schema, table, column, limit=max_values + 1)
return DistinctValues(values=values[:max_values], truncated=len(values) > max_values)
@@ -63,6 +63,7 @@ def distinct_values_rest(
*,
max_values: int = DEFAULT_DISTINCT_VALUES_LIMIT,
) -> DistinctValues:
max_values = require_positive_int(max_values, name="max_values")
values = sample_column_rest(client, schema, table, column, limit=max_values + 1)
return DistinctValues(values=values[:max_values], truncated=len(values) > max_values)
+7
View File
@@ -26,6 +26,13 @@ class PlanSummary:
node_types: list[str]
def require_positive_int(value: object, *, name: str) -> int:
"""Return a validated positive integer, excluding booleans and numeric lookalikes."""
if type(value) is not int or value <= 0:
raise ValueError(f"{name} must be a positive integer")
return value
def _inject_limit(sql: str, limit: int) -> tuple[str, bool]:
"""Aggiunge LIMIT limit+1 se assente (il +1 serve a rilevare il troncamento).
Se la query ha gia' un suo LIMIT, lo si rispetta."""
+9 -3
View File
@@ -9,7 +9,14 @@ il client mantiene solo l'iniezione del LIMIT (per il rilevamento del troncament
import time
from tht.execute import ExecResult, ExecutionError, PlanSummary, _inject_limit, assert_read_only
from tht.execute import (
ExecResult,
ExecutionError,
PlanSummary,
_inject_limit,
assert_read_only,
require_positive_int,
)
from tht.rest.client import RestError
from tht.rest.explain import parse_text_plan
@@ -17,8 +24,7 @@ from tht.rest.explain import parse_text_plan
def run_controlled_rest(client, sql: str, *, limit: int) -> ExecResult:
# Guard read-only client-side anche sul path REST (D7): non delegare l'unica verifica
# al server. Stesso check strutturale del path diretto.
if limit <= 0:
raise ValueError("limit must be a positive integer")
limit = require_positive_int(limit, name="limit")
assert_read_only(sql)
final_sql, injected = _inject_limit(sql, limit)
start = time.monotonic()