apache/superset · error · ValueError

Operator '{operator_enum.value}' on relationship column '{co

Error message

Operator '{operator_enum.value}' on relationship column '{col_name}' requires a scalar value, got {type(value).__name__}. Use '{counterpart}' to match multiple related ids.

What it means

Raised by BaseDAO._apply_relationship_filter() when eq or ne is used on a collection relationship with a list/tuple value. `column.any(pk == [..])` would compile to nonsense and behave unpredictably across backends, so the code fails fast and tells you to use `in` (for eq) or `nin` (for ne) to match multiple related ids.

Source

Thrown at superset/daos/base.py:680

        pk_cols = inspect(column.property.mapper).primary_key
        if len(pk_cols) != 1:
            # Composite PKs would need a tuple `.in_()` and per-operator
            # tuple handling; no Superset model uses one today, so we
            # fail loudly rather than silently drop the trailing columns.
            raise ValueError(
                f"Relationship filter on '{col_name}' requires a "
                f"single-column primary key on the related model; "
                f"found {len(pk_cols)} columns."
            )
        related_pk = pk_cols[0]
        if operator_enum in (ColumnOperatorEnum.eq, ColumnOperatorEnum.ne):
            # `value` must be scalar for both eq and ne: a list/tuple would
            # silently compile to `related_pk == [...]` (or `!= [...]`),
            # which behaves unpredictably across backends instead of
            # failing fast. Use `in`/`nin` to match multiple related ids.
            if isinstance(value, (list, tuple)):
                counterpart = "in" if operator_enum == ColumnOperatorEnum.eq else "nin"
                raise ValueError(
                    f"Operator '{operator_enum.value}' on relationship "
                    f"column '{col_name}' requires a scalar value, got "
                    f"{type(value).__name__}. Use '{counterpart}' to match "
                    f"multiple related ids."
                )
            if operator_enum == ColumnOperatorEnum.eq:
                return query.filter(column.any(related_pk == value))
            return query.filter(~column.any(related_pk == value))
        if operator_enum == ColumnOperatorEnum.in_:
            values = value if isinstance(value, (list, tuple)) else [value]
            return query.filter(column.any(related_pk.in_(values)))
        if operator_enum == ColumnOperatorEnum.nin:
            values = value if isinstance(value, (list, tuple)) else [value]
            return query.filter(~column.any(related_pk.in_(values)))
        if operator_enum == ColumnOperatorEnum.is_null:
            # "has no related rows at all"
            return query.filter(~column.any())
        if operator_enum == ColumnOperatorEnum.is_not_null:

View on GitHub (pinned to f4587218dd)

Solutions

  1. Switch the operator: use {"opr": "in", "value": [1,2,3]} to match any of the ids, or {"opr": "nin", "value": [...]} to exclude them.
  2. Pass a scalar with eq/ne when you mean exactly one id.
  3. For negated membership remember nin means 'has NO related row with these ids'.

Example fix

# before
filters=[{"col": "roles", "opr": "eq", "value": [1, 2]}]

# after
filters=[{"col": "roles", "opr": "in", "value": [1, 2]}]
Defensive patterns

Strategy: validation

Validate before calling

REL_SCALAR_OPS = {"eq", "ne", "in", "nin", "is_null", "is_not_null"}

def normalize_rel_filter(f: dict) -> dict:
    if f["opr"] == "eq" and isinstance(f.get("value"), (list, tuple)):
        return {**f, "opr": "in"}
    if f["opr"] == "ne" and isinstance(f.get("value"), (list, tuple)):
        return {**f, "opr": "nin"}
    return f

Type guard

def is_scalar_relationship_filter(f: dict) -> bool:
    return not (f["opr"] in {"eq", "ne"} and isinstance(f.get("value"), (list, tuple)))

Try / catch

try:
    dao.find(query=", filters=filters)
except ValueError as ex:
    if 'requires a scalar value' in str(ex):
        filters = [normalize_rel_filter(f) for f in filters]
        results = dao.find(query=", filters=filters)

Prevention

When it happens

Trigger: filters=[{"col": "<uselist relationship>", "opr": "eq", "value": [1,2,3]}] or the same with "opr": "ne" against any DAO; scalar eq/ne values are fine, only list/tuple values trip it.

Common situations: Reusing a scalar-column filter payload (where eq + list sometimes 'works') against a relationship column; MCP/tool clients that always wrap values in lists.

Related errors


AI-assisted analysis of apache/superset@f4587218dd (2026-08-14). Data as JSON: /api/errors/ee6fbb72b2e00765. Report an issue: GitHub.