fix(filter): reject unknown operator dicts on scalar columns

An unrecognized operator dict on a non-JSONB column (e.g.
{"session_id": {"operator": "null"}}) fell through to `column == value`,
binding a dict to a VARCHAR parameter. That compiles, then fails in the
driver at execute time with "cannot adapt type 'dict'" — an unhandled
500 for what is invalid input.

Raise FilterError (422) instead. The guard lives in the shared
_build_field_condition, so every route through apply_filter is covered.
It keys on the actual column type rather than the JSONB_COLUMNS name
list, so dict equality still works on JSONB columns reachable through
Document's raw-key fallback (e.g. source_ids), where the driver adapts
dicts fine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vineeth Voruganti 2026-07-28 14:42:58 -04:00
parent e7cbcc8432
commit 8eb2c8839b
2 changed files with 40 additions and 0 deletions

View File

@ -5,6 +5,7 @@ from typing import Any, TypeVar
from typing import cast as typing_cast
from sqlalchemy import ColumnElement, Select, and_, case, cast, literal, not_, or_
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.types import Numeric
from ..exceptions import FilterError
@ -345,6 +346,14 @@ def _build_field_condition(
# For JSONB fields (metadata, configuration), check if it contains nested comparison operators
if column_name in JSONB_COLUMNS:
return _build_nested_metadata_conditions(column, value) # pyright: ignore
elif not isinstance(column.type, JSONB):
# A dict against a scalar column compiles fine but fails in the
# driver at execute time ("cannot adapt type 'dict'") as a 500.
# Reject unknown operator dicts here as a 422 instead.
keys = sorted(typing_cast("dict[str, Any]", value))
raise FilterError(
f"Invalid filter for column '{key}': unsupported operator(s) {keys}. Expected one of {sorted(COMPARISON_OPERATORS)} or a scalar value."
)
else:
return column == value
else:

View File

@ -0,0 +1,31 @@
"""Unit tests for filter condition building."""
import pytest
from sqlalchemy import select
from src.exceptions import FilterError
from src.models import Document
from src.utils.filter import apply_filter
def test_unknown_operator_dict_on_scalar_column_raises():
"""An unrecognized operator dict must 422, not reach the driver as a 500.
Regression: {"session_id": {"operator": "null"}} compiled to
`session_name = %(param)s` with a dict bind, which psycopg rejected with
"cannot adapt type 'dict'" -> unhandled 500.
"""
with pytest.raises(FilterError):
apply_filter(select(Document), Document, {"session_id": {"operator": "null"}})
def test_known_operator_dict_on_scalar_column_still_works():
stmt = apply_filter(
select(Document), Document, {"session_id": {"in": ["s1", "s2"]}}
)
assert "session_name IN" in str(stmt).replace("documents.", "")
def test_dict_on_jsonb_column_still_works():
stmt = apply_filter(select(Document), Document, {"metadata": {"kind": "note"}})
assert "internal_metadata" in str(stmt)