Rifqi Hafizuddin
[KM-644] harden query path: grouped-select rule, int-date trends, sample decode, trace guard
74ad526
Raw
History Blame
4.02 kB
"""Base64-sample-value decoding for catalogs affected by a dedorch bug.
Go's introspection JSON-marshals numeric sample bytes as base64 (a `[]byte`
serialization quirk), so every decimal/int-typed column's `sample_values`
currently arrive as base64 strings (e.g. ``'OTUuMA=='`` for ``"95.0"``)
instead of the plain numeric text the planner LLM expects. The planner then
sees gibberish instead of value ranges for exactly the columns it filters and
aggregates on. Until Go fixes the marshaling, decode these at catalog read
time.
Conservative by design (a wrong decode silently corrupts planner context):
- only numeric-typed columns are considered
- every non-null sample in the column must pass a strict base64 gate
(valid base64, decodes to printable ASCII, parses as a float) — a single
non-conforming entry leaves the WHOLE column untouched (mixed content is
suspicious, never guessed)
- columns with `sample_values is None` (e.g. PII-flagged columns, which
carry no samples by design) are skipped cleanly
- self-disabling: once Go ships real numeric samples (plain ``"95.0"`` or
actual numbers), the gate fails — plain digit strings are either not
base64-padded correctly or don't decode to printable numeric text — so
the pass becomes a no-op with no further changes needed here
"""
from __future__ import annotations
import base64
import binascii
from src.middlewares.logging import get_logger
from .models import Catalog
logger = get_logger("sample_decode")
_NUMERIC_TYPES = {
"int",
"integer",
"bigint",
"decimal",
"numeric",
"float",
"double",
"number",
}
def _decode_one(value: str) -> str | None:
"""Return the decoded numeric text for `value`, or None if it fails the gate."""
if len(value) < 2 or len(value) % 4 != 0:
return None
try:
decoded = base64.b64decode(value, validate=True)
except (binascii.Error, ValueError):
return None
try:
text = decoded.decode("ascii")
except UnicodeDecodeError:
return None
if not text.isprintable():
return None
try:
float(text)
except ValueError:
return None
return text
def _decode_column_samples(samples: list) -> tuple[list, int] | None:
"""Return (decoded list, count decoded) if every non-null entry passes the gate.
Returns None if any entry fails the gate (mixed content is left untouched).
"""
decoded_values = []
count = 0
for entry in samples:
if entry is None:
decoded_values.append(None)
continue
if not isinstance(entry, str):
return None
decoded = _decode_one(entry)
if decoded is None:
return None
count += 1
decoded_values.append(decoded)
if not count:
return None
return decoded_values, count
def decode_sample_values(catalog: Catalog) -> int:
"""Decode base64-encoded numeric sample_values in place. Returns count decoded.
Never raises: any unexpected shape (wrong types, malformed entries) leaves
the offending column's values untouched.
"""
total = 0
try:
for source in catalog.sources:
for table in source.tables:
for col in table.columns:
if col.data_type.lower() not in _NUMERIC_TYPES:
continue
samples = col.sample_values
if not samples:
continue
result = _decode_column_samples(samples)
if result is None:
continue
decoded_values, count = result
col.sample_values = decoded_values
total += count
except Exception as e:
logger.error("sample decode failed", error=repr(e))
return total
if total:
logger.info("decoded base64 sample values", user_id=catalog.user_id, count=total)
return total