{"record":{"id":"187199f168a2c907","repo":"apache/superset","slug":"relationship-filter-on-col-name-requires-a-sin","errorCode":null,"errorMessage":"Relationship filter on '{col_name}' requires a single-column primary key on the related model; found {len(pk_cols)} columns.","messagePattern":"Relationship filter on '(.+?)' requires a single-column primary key on the related model; found (.+?) columns\\.","errorType":"validation","errorClass":"ValueError","httpStatus":400,"severity":"error","filePath":"superset/daos/base.py","lineNumber":667,"sourceCode":"        col_name: str,\n        operator_enum: \"ColumnOperatorEnum\",\n        value: Any,\n    ) -> Any:\n        \"\"\"Apply a filter on a many-to-many or one-to-many relationship column.\n\n        Translates the caller's operator into a SQLAlchemy ``.any()``\n        expression against the related model's primary key. Supports\n        eq / ne / in / nin / is_null / is_not_null. Other operators\n        (sw, like, gt, etc.) don't make sense on a collection of related\n        rows and raise a clear ValueError instead of producing a\n        cryptic SQLAlchemy error at query time.\n        \"\"\"\n        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                )","sourceCodeStart":649,"sourceCodeEnd":685,"githubUrl":"https://github.com/apache/superset/blob/f4587218dd19d046c3e4d00063e7d27f8a2ed354/superset/daos/base.py#L649-L685","documentation":"Raised by BaseDAO._apply_relationship_filter() when filtering on a collection relationship (many-to-many / one-to-many detected via RelationshipProperty.uselist) whose related model has a composite (multi-column) primary key. The implementation translates the operator into `.any(related_pk ...)` and needs exactly one PK column; it fails loudly with a ValueError rather than silently dropping trailing PK columns.","triggerScenarios":"Passing filters like {\"col\": \"<relationship_name>\", \"opr\": \"eq\", \"value\": <id>} to any DAO where the relationship target model defines a composite primary key (len(inspect(mapper).primary_key) != 1).","commonSituations":"Custom Superset forks/plugins that add association models with composite PKs and try to filter them through the generic DAO filter machinery; MCP-driven dynamic filtering that assumes single-column PKs on every related model.","solutions":["Filter on a scalar column instead of the collection relationship (e.g. the FK column on this model).","Give the related model a single-column primary key (all stock Superset models have one) — or query the association table directly.","If you control the code path, build the filter manually with tuple `.in_()` handling for composite keys."],"exampleFix":"# before\nfilters=[{\"col\": \"composite_pk_children\", \"opr\": \"eq\", \"value\": 5}]\n\n# after\nfilters=[{\"col\": \"parent_id\", \"opr\": \"eq\", \"value\": 5}]","handlingStrategy":"validation","validationCode":"from sqlalchemy.orm import RelationshipProperty, inspect\n\ndef relationship_has_single_pk(model_cls, rel_name: str) -> bool:\n    col = getattr(model_cls, rel_name, None)\n    if col is None or not hasattr(col, \"property\"):\n        return False\n    if not isinstance(col.property, RelationshipProperty) or not col.property.uselist:\n        return True  # scalar path unaffected\n    return len(inspect(col.property.mapper).primary_key) == 1","typeGuard":null,"tryCatchPattern":"try:\n    dao.find(query=\", filters=filters)\nexcept ValueError as ex:\n    if 'single-column primary key' in str(ex):\n        # rewrite filter against a scalar FK column on this model\n        ...","preventionTips":["Prefer filtering on the local FK column over collection relationships.","Keep custom association models on single-column PKs when they'll be filtered via DAOs.","Test filter payloads against all target models in a matrix, not just one."],"tags":["dao","filters","relationships","sqlalchemy","primary-key"],"backgroundTag":null,"analyzedSha":"f4587218dd19d046c3e4d00063e7d27f8a2ed354","analyzedAt":"2026-08-14T22:39:27.425Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}