diff --git a/tests/trace/test_saved_view.py b/tests/trace/test_saved_view.py index 80a4fd98f18d..3e2434d4f308 100644 --- a/tests/trace/test_saved_view.py +++ b/tests/trace/test_saved_view.py @@ -6,6 +6,7 @@ from weave.flow.saved_view import ( Filter, Filters, + filter_to_clause, filters_to_query, query_to_filters, to_seconds, @@ -124,6 +125,27 @@ def test_roundtrip_operators(): assert filters == filters2 +def test_number_filter_skips_float_convert_for_typed_columns(): + """Typed columns (started_at, latency_ms) skip the `$convert` to double; JSON fields keep it.""" + assert filter_to_clause( + Filter(field="started_at", operator="(number): >=", value=1777593600) + ) == {"$gte": [{"$getField": "started_at"}, {"$literal": 1777593600.0}]} + assert filter_to_clause( + Filter(field="started_at", operator="(number): <=", value=1778889599) + ) == {"$not": [{"$gt": [{"$getField": "started_at"}, {"$literal": 1778889599.0}]}]} + assert filter_to_clause( + Filter(field="summary.weave.latency_ms", operator="(number): >", value=100) + ) == {"$gt": [{"$getField": "summary.weave.latency_ms"}, {"$literal": 100.0}]} + assert filter_to_clause( + Filter(field="inputs.score", operator="(number): >", value=5) + ) == { + "$gt": [ + {"$convert": {"input": {"$getField": "inputs.score"}, "to": "double"}}, + {"$literal": 5.0}, + ] + } + + def test_filter_op_without_client(): """If we haven't done an init, we don't know what entity/project a non-qualified op name is in.""" with pytest.raises( diff --git a/weave/flow/saved_view.py b/weave/flow/saved_view.py index c7f47710f87e..f34754f9868f 100644 --- a/weave/flow/saved_view.py +++ b/weave/flow/saved_view.py @@ -154,11 +154,9 @@ def make_eval_op_name(entity: str, project: str) -> str: VALUELESS_OPERATORS = {"(any): isEmpty", "(any): isNotEmpty"} -# We return a real float from the backend! do not convert to double! -# When option clicking latency, because we are generating this field -# manually on the backend, it is actually a float, not a string stored -# in json, so we need to omit the conversion params from the filter -FIELDS_NO_FLOAT_CONVERT = {"summary.weave.latency_ms"} +# Real typed columns (a backend float, a DateTime64), not JSON strings, so they +# skip the `to: double` cast. toFloat64OrNull on started_at is rejected by ClickHouse. +FIELDS_NO_FLOAT_CONVERT = {"summary.weave.latency_ms", "started_at"} def py_to_api_filter(filter: tsi.CallsFilter | None) -> tsi.CallsFilter | None: