{"record":{"id":"bf680df319e29e2a","repo":"makeplane/plane","slug":"multiple-logical-operators","errorCode":"multiple_logical_operators","errorMessage":"A filter object cannot contain multiple logical operators at the same level","messagePattern":"A filter object cannot contain multiple logical operators at the same level","errorType":"validation","errorClass":"DRFValidationError","httpStatus":400,"severity":"error","filePath":"apps/api/plane/utils/filters/filter_backend.py","lineNumber":353,"sourceCode":"            raise DRFValidationError(\n                {\n                    \"message\": \"Each filter node must be a JSON object\",\n                    \"code\": \"invalid_filter_node\",\n                }\n            )\n\n        if not node:\n            raise DRFValidationError(\n                {\n                    \"message\": \"Filter objects must not be empty\",\n                    \"code\": \"empty_filter_object\",\n                }\n            )\n\n        logical_keys = [k for k in node.keys() if isinstance(k, str) and k.lower() in (\"or\", \"and\", \"not\")]\n\n        if len(logical_keys) > 1:\n            raise DRFValidationError(\n                {\n                    \"message\": (\"A filter object cannot contain multiple logical operators at the same level\"),\n                    \"code\": \"multiple_logical_operators\",\n                }\n            )\n\n        if len(logical_keys) == 1:\n            op_key = logical_keys[0]\n            # must not mix operator with other keys\n            if len(node) != 1:\n                raise DRFValidationError(\n                    {\n                        \"message\": (f\"Cannot mix logical operator '{op_key}' with field keys at the same level\"),\n                        \"code\": \"mixed_operator_and_fields\",\n                    }\n                )\n\n            op = op_key.lower()","sourceCodeStart":335,"sourceCodeEnd":371,"githubUrl":"https://github.com/makeplane/plane/blob/1c8a60f858d8472aa56e29994ec1c7926da2c6ce/apps/api/plane/utils/filters/filter_backend.py#L335-L371","documentation":"Raised by _validate_structure when a single node contains more than one logical operator key (case-insensitive). The grammar permits at most one of or/and/not per object; mixing them — e.g. {'or':[...], 'and':[...]} — is ambiguous and rejected so the backend never has to choose an evaluation order.","triggerScenarios":"GET .../?filters={\"or\":[...],\"and\":[...]} ; GET .../?filters={\"AND\":{...},\"not\":{...}} (case-insensitive match); clients that combine operators in one object intending SQL-style precedence.","commonSituations":"Translating an SQL WHERE (a OR b AND c) directly into a single object instead of nesting; merging two filter fragments with dict.update(); schema misunderstanding after migrating from a legacy AND/OR-flat format.","solutions":["Nest operators so each object has exactly one: {\"and\":[{\"or\":[...]},{...}]} instead of {\"or\":[...],\"and\":[...]}.","When merging filter fragments, wrap each in an explicit and/or group rather than dict-merging them together.","Use lower-case keys consistently; remember matching is case-insensitive so 'OR' and 'or' still collide."],"exampleFix":"// before\n?filters={\"or\":[{\"state\":\"open\"}],\"and\":[{\"priority\":\"high\"}]}\n// after\n?filters={\"and\":[{\"or\":[{\"state\":\"open\"}]},{\"priority\":\"high\"}]}","handlingStrategy":"validation","validationCode":"def count_operators(node):\n    return sum(1 for k in node if isinstance(k, str) and k.lower() in ('or', 'and', 'not'))\n\ndef assert_single_operator(node):\n    if count_operators(node) > 1:\n        raise ValueError(f'node has multiple operators: {[k for k in node if k.lower() in (\"or\",\"and\",\"not\")]}')","typeGuard":"function singleOperatorOnly(node) {\n  const ops = Object.keys(node).filter(k => ['or','and','not'].includes(k.toLowerCase()));\n  return ops.length <= 1;\n}","tryCatchPattern":null,"preventionTips":["Never dict-merge two operator fragments; nest them under and/or instead.","Operator key matching is case-insensitive — pick one casing and stay consistent."],"tags":["filters","schema","validation","api"],"backgroundTag":null,"analyzedSha":"1c8a60f858d8472aa56e29994ec1c7926da2c6ce","analyzedAt":"2026-08-12T14:44:31.636Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}