Skip to content

fix(server): handle boolean metadata filters in span DSL#11805

Merged
RogerHYang merged 3 commits intoArize-ai:mainfrom
pandego:fix/11743-bool-metadata-filter
Mar 9, 2026
Merged

fix(server): handle boolean metadata filters in span DSL#11805
RogerHYang merged 3 commits intoArize-ai:mainfrom
pandego:fix/11743-bool-metadata-filter

Conversation

@pandego
Copy link
Contributor

@pandego pandego commented Feb 28, 2026

fixes #11743

The spans filter DSL currently leaves metadata lookups as raw JSON when compared to booleans. In Postgres this can fail at runtime because JSON values are compared directly against boolean literals.

This change updates the filter translator to cast attribute/metadata JSON lookups to as_boolean() when the other side of the comparison is a boolean literal. Existing string/float behavior is unchanged.

Validation:

  • uv run ruff check src/phoenix/trace/dsl/filter.py tests/unit/trace/dsl/test_filter.py
  • uv run pytest tests/unit/trace/dsl/test_filter.py

Note

Medium Risk
Changes the filter AST translation logic for comparisons involving JSON attributes, which can affect generated SQL and query results. Scope is targeted to boolean literal comparisons and covered by new unit tests, but still impacts runtime filtering behavior.

Overview
Fixes span filter DSL boolean comparisons by detecting boolean literals and translating metadata[...]/attributes[...] lookups to as_boolean() instead of default string casting.

Tightens literal typing by excluding bool from _is_float_constant and adds unit tests covering ==, reversed operand order, and is/is not boolean comparisons to ensure correct translation.

Written by Cursor Bugbot for commit e36ccf9. This will update automatically on new commits. Configure here.

@pandego pandego requested a review from a team as a code owner February 28, 2026 17:06
@github-project-automation github-project-automation bot moved this to 📘 Todo in phoenix Feb 28, 2026
@dosubot dosubot bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Feb 28, 2026
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

@axiomofjoy
Copy link
Contributor

Hey @pandego, I actually wasn't able to reproduce this issue on the latest version of Phoenix. Have you been able to reproduce?

@pandego
Copy link
Contributor Author

pandego commented Mar 6, 2026

Thanks for checking.

Here is the exact repro I used:

  1. Create a span with metadata:
    {
    "is_empty": true
    }
  2. In Project -> Spans, click that metadata value (or manually apply):
    metadata['is_empty'] == True
  3. The spans query fails with an unexpected GraphQL error instead of returning filtered results.

If you are not seeing it on latest Phoenix, can you share the exact version/commit you tested?
I can retest against that and if this is already fixed upstream, I am happy to close this PR.

@RogerHYang
Copy link
Contributor

Yes I can repro the issue.

(sqlalchemy.dialects.postgresql.asyncpg.ProgrammingError) <class 'asyncpg.exceptions.UndefinedFunctionError'>: operator does not exist: double precision = boolean
HINT:  No operator matches the given name and argument types. You might need to add explicit type casts.
[SQL: SELECT spans.id, spans.start_time AS "startTime_span_sort_column" 
FROM spans JOIN traces ON traces.id = spans.trace_rowid 
WHERE traces.project_rowid = $1::INTEGER AND spans.start_time >= $2::TIMESTAMP WITH TIME ZONE AND CAST((spans.attributes #>> $3) AS FLOAT) = true AND spans.parent_id IS NULL ORDER BY "startTime_span_sort_column" DESC NULLS LAST, spans.id DESC 
 LIMIT $4::INTEGER]
[parameters: (3, datetime.datetime(2026, 2, 27, 16, 0, tzinfo=datetime.timezone.utc), ['metadata', 'is_empty'], 31)]
(Background on this error at: https://sqlalche.me/e/20/f405)

GraphQL request:76:3
76 |   spans(first: $first, after: $after, sort: $sort, rootSpansOnly: $rootSpansOnly
   |   ^
   | , filterCondition: $filterCondition, orphanSpanAsRootSpan: $orphanSpanAsRootSpan
Traceback (most recent call last):
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 526, in _prepare_and_execute
    prepared_stmt, attributes = await adapt_connection._prepare(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 773, in _prepare
    prepared_stmt = await self._connection.prepare(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/asyncpg/connection.py", line 638, in prepare
    return await self._prepare(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/asyncpg/connection.py", line 657, in _prepare
    stmt = await self._get_statement(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/asyncpg/connection.py", line 443, in _get_statement
    statement = await self._protocol.prepare(
  File "asyncpg/protocol/protocol.pyx", line 165, in prepare
asyncpg.exceptions.UndefinedFunctionError: operator does not exist: double precision = boolean
HINT:  No operator matches the given name and argument types. You might need to add explicit type casts.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context
    self.dialect.do_execute(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
    cursor.execute(statement, parameters)
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 585, in execute
    self._adapt_connection.await_(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 132, in await_only
    return current.parent.switch(awaitable)  # type: ignore[no-any-return,attr-defined] # noqa: E501
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 196, in greenlet_spawn
    value = await result
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 563, in _prepare_and_execute
    self._handle_exception(error)
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 513, in _handle_exception
    self._adapt_connection._handle_exception(error)
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 797, in _handle_exception
    raise translated_error from error
sqlalchemy.dialects.postgresql.asyncpg.AsyncAdapt_asyncpg_dbapi.ProgrammingError: <class 'asyncpg.exceptions.UndefinedFunctionError'>: operator does not exist: double precision = boolean
HINT:  No operator matches the given name and argument types. You might need to add explicit type casts.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/graphql/execution/execute.py", line 530, in await_result
    return_type, field_nodes, info, path, await result
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/strawberry/schema/schema_converter.py", line 787, in _async_resolver
    return await await_maybe(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/strawberry/utils/await_maybe.py", line 13, in await_maybe
    return await value
  File "/Users/rogeryang/phoenix/src/phoenix/server/api/types/Project.py", line 393, in spans
    span_records = await session.stream(stmt)
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/ext/asyncio/session.py", line 681, in stream
    result = await greenlet_spawn(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 201, in greenlet_spawn
    result = context.throw(*sys.exc_info())
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2351, in execute
    return self._execute_internal(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/orm/session.py", line 2249, in _execute_internal
    result: Result[Any] = compile_state_cls.orm_execute_statement(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/orm/context.py", line 306, in orm_execute_statement
    result = conn.execute(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1419, in execute
    return meth(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 527, in _execute_on_connection
    return connection._execute_clauseelement(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1641, in _execute_clauseelement
    ret = self._execute_context(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1846, in _execute_context
    return self._exec_single_context(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1986, in _exec_single_context
    self._handle_dbapi_exception(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 2363, in _handle_dbapi_exception
    raise sqlalchemy_exception.with_traceback(exc_info[2]) from e
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/engine/base.py", line 1967, in _exec_single_context
    self.dialect.do_execute(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/engine/default.py", line 952, in do_execute
    cursor.execute(statement, parameters)
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 585, in execute
    self._adapt_connection.await_(
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 132, in await_only
    return current.parent.switch(awaitable)  # type: ignore[no-any-return,attr-defined] # noqa: E501
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 196, in greenlet_spawn
    value = await result
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 563, in _prepare_and_execute
    self._handle_exception(error)
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 513, in _handle_exception
    self._adapt_connection._handle_exception(error)
  File "/Users/rogeryang/phoenix/.venv/lib/python3.10/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py", line 797, in _handle_exception
    raise translated_error from error
sqlalchemy.exc.ProgrammingError: (sqlalchemy.dialects.postgresql.asyncpg.ProgrammingError) <class 'asyncpg.exceptions.UndefinedFunctionError'>: operator does not exist: double precision = boolean
HINT:  No operator matches the given name and argument types. You might need to add explicit type casts.
[SQL: SELECT spans.id, spans.start_time AS "startTime_span_sort_column" 
FROM spans JOIN traces ON traces.id = spans.trace_rowid 
WHERE traces.project_rowid = $1::INTEGER AND spans.start_time >= $2::TIMESTAMP WITH TIME ZONE AND CAST((spans.attributes #>> $3) AS FLOAT) = true AND spans.parent_id IS NULL ORDER BY "startTime_span_sort_column" DESC NULLS LAST, spans.id DESC 
 LIMIT $4::INTEGER]
[parameters: (3, datetime.datetime(2026, 2, 27, 16, 0, tzinfo=datetime.timezone.utc), ['metadata', 'is_empty'], 31)]

@pandego
Copy link
Contributor Author

pandego commented Mar 6, 2026

Quick confirmation from my side on latest main (d2617f2): I can still reproduce.

I tested metadata['is_empty'] == True and it translates to:
attributes[['metadata', 'is_empty']].as_float() == True

So boolean metadata is still being cast as float in the filter translator.

@RogerHYang RogerHYang moved this from 📘 Todo to 👨‍💻 In progress in phoenix Mar 6, 2026
@github-project-automation github-project-automation bot moved this from 👨‍💻 In progress to 👍 Approved in phoenix Mar 9, 2026
@RogerHYang RogerHYang merged commit deb84a6 into Arize-ai:main Mar 9, 2026
42 of 43 checks passed
@github-project-automation github-project-automation bot moved this from 👍 Approved to ✅ Done in phoenix Mar 9, 2026
@RogerHYang
Copy link
Contributor

Thank you for your contribution!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

[BUG] can't filter by bool variable in metadata

3 participants