{"record":{"id":"ee6fbb72b2e00765","repo":"apache/superset","slug":"operator-operator-enum-value-on-relationship-c","errorCode":null,"errorMessage":"Operator '{operator_enum.value}' on relationship column '{col_name}' requires a scalar value, got {type(value).__name__}. Use '{counterpart}' to match multiple related ids.","messagePattern":"Operator '(.+?)' on relationship column '(.+?)' requires a scalar value, got (.+?)\\. Use '(.+?)' to match multiple related ids\\.","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"superset/daos/base.py","lineNumber":680,"sourceCode":"        pk_cols = inspect(column.property.mapper).primary_key\n        if len(pk_cols) != 1:\n            # Composite PKs would need a tuple `.in_()` and per-operator\n            # tuple handling; no Superset model uses one today, so we\n            # fail loudly rather than silently drop the trailing columns.\n            raise ValueError(\n                f\"Relationship filter on '{col_name}' requires a \"\n                f\"single-column primary key on the related model; \"\n                f\"found {len(pk_cols)} columns.\"\n            )\n        related_pk = pk_cols[0]\n        if operator_enum in (ColumnOperatorEnum.eq, ColumnOperatorEnum.ne):\n            # `value` must be scalar for both eq and ne: a list/tuple would\n            # silently compile to `related_pk == [...]` (or `!= [...]`),\n            # which behaves unpredictably across backends instead of\n            # failing fast. Use `in`/`nin` to match multiple related ids.\n            if isinstance(value, (list, tuple)):\n                counterpart = \"in\" if operator_enum == ColumnOperatorEnum.eq else \"nin\"\n                raise ValueError(\n                    f\"Operator '{operator_enum.value}' on relationship \"\n                    f\"column '{col_name}' requires a scalar value, got \"\n                    f\"{type(value).__name__}. Use '{counterpart}' to match \"\n                    f\"multiple related ids.\"\n                )\n            if operator_enum == ColumnOperatorEnum.eq:\n                return query.filter(column.any(related_pk == value))\n            return query.filter(~column.any(related_pk == value))\n        if operator_enum == ColumnOperatorEnum.in_:\n            values = value if isinstance(value, (list, tuple)) else [value]\n            return query.filter(column.any(related_pk.in_(values)))\n        if operator_enum == ColumnOperatorEnum.nin:\n            values = value if isinstance(value, (list, tuple)) else [value]\n            return query.filter(~column.any(related_pk.in_(values)))\n        if operator_enum == ColumnOperatorEnum.is_null:\n            # \"has no related rows at all\"\n            return query.filter(~column.any())\n        if operator_enum == ColumnOperatorEnum.is_not_null:","sourceCodeStart":662,"sourceCodeEnd":698,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/daos/base.py#L662-L698","documentation":"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.","triggerScenarios":"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.","commonSituations":"Reusing a scalar-column filter payload (where eq + list sometimes 'works') against a relationship column; MCP/tool clients that always wrap values in lists.","solutions":["Switch the operator: use {\"opr\": \"in\", \"value\": [1,2,3]} to match any of the ids, or {\"opr\": \"nin\", \"value\": [...]} to exclude them.","Pass a scalar with eq/ne when you mean exactly one id.","For negated membership remember nin means 'has NO related row with these ids'."],"exampleFix":"# before\nfilters=[{\"col\": \"roles\", \"opr\": \"eq\", \"value\": [1, 2]}]\n\n# after\nfilters=[{\"col\": \"roles\", \"opr\": \"in\", \"value\": [1, 2]}]","handlingStrategy":"validation","validationCode":"REL_SCALAR_OPS = {\"eq\", \"ne\", \"in\", \"nin\", \"is_null\", \"is_not_null\"}\n\ndef normalize_rel_filter(f: dict) -> dict:\n    if f[\"opr\"] == \"eq\" and isinstance(f.get(\"value\"), (list, tuple)):\n        return {**f, \"opr\": \"in\"}\n    if f[\"opr\"] == \"ne\" and isinstance(f.get(\"value\"), (list, tuple)):\n        return {**f, \"opr\": \"nin\"}\n    return f","typeGuard":"def is_scalar_relationship_filter(f: dict) -> bool:\n    return not (f[\"opr\"] in {\"eq\", \"ne\"} and isinstance(f.get(\"value\"), (list, tuple)))","tryCatchPattern":"try:\n    dao.find(query=\", filters=filters)\nexcept ValueError as ex:\n    if 'requires a scalar value' in str(ex):\n        filters = [normalize_rel_filter(f) for f in filters]\n        results = dao.find(query=\", filters=filters)","preventionTips":["Map 'match any of these ids' to in and 'match none of these' to nin from the start.","Keep value types consistent: scalar for eq/ne, list for in/nin.","Validate filter payloads against get_filterable_columns_and_operators() output."],"tags":["dao","filters","relationships","validation"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}