{"record":{"id":"cf6827278885eb85","repo":"apache/iceberg","slug":"operation-must-be-is-null-not-null-is-nan-or-no","errorCode":null,"errorMessage":"Operation must be IS_NULL, NOT_NULL, IS_NAN, or NOT_NAN","messagePattern":"Operation must be IS_NULL, NOT_NULL, IS_NAN, or NOT_NAN","errorType":"validation","errorClass":"ValidationException","httpStatus":null,"severity":"error","filePath":"api/src/main/java/org/apache/iceberg/expressions/UnboundPredicate.java","lineNumber":157,"sourceCode":"          return Expressions.alwaysTrue();\n        } else if (boundTerm.type().equals(Types.UnknownType.get())) {\n          return Expressions.alwaysFalse();\n        }\n        return new BoundUnaryPredicate<>(Operation.NOT_NULL, boundTerm);\n      case IS_NAN:\n        if (floatingType(boundTerm.type().typeId())) {\n          return new BoundUnaryPredicate<>(Operation.IS_NAN, boundTerm);\n        } else {\n          throw new ValidationException(\"IsNaN cannot be used with a non-floating-point column\");\n        }\n      case NOT_NAN:\n        if (floatingType(boundTerm.type().typeId())) {\n          return new BoundUnaryPredicate<>(Operation.NOT_NAN, boundTerm);\n        } else {\n          throw new ValidationException(\"NotNaN cannot be used with a non-floating-point column\");\n        }\n      default:\n        throw new ValidationException(\"Operation must be IS_NULL, NOT_NULL, IS_NAN, or NOT_NAN\");\n    }\n  }\n\n  private boolean allAncestorFieldsAreRequired(StructType struct, int fieldId) {\n    return TypeUtil.ancestorFields(struct.asSchema(), fieldId).stream()\n        .allMatch(Types.NestedField::isRequired);\n  }\n\n  private boolean floatingType(Type.TypeID typeID) {\n    return Type.TypeID.DOUBLE.equals(typeID) || Type.TypeID.FLOAT.equals(typeID);\n  }\n\n  private Expression bindLiteralOperation(BoundTerm<T> boundTerm) {\n    if (op() == Operation.STARTS_WITH || op() == Operation.NOT_STARTS_WITH) {\n      ValidationException.check(\n          boundTerm.type().equals(Types.StringType.get()),\n          \"Term for STARTS_WITH or NOT_STARTS_WITH must produce a string: %s: %s\",\n          boundTerm,","sourceCodeStart":139,"sourceCodeEnd":175,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/api/src/main/java/org/apache/iceberg/expressions/UnboundPredicate.java#L139-L175","documentation":"bindUnaryOperation is only written to handle the four unary predicate operations IS_NULL, NOT_NULL, IS_NAN, and NOT_NAN. If an UnboundPredicate with any other operation reaches this switch's default branch, it indicates the operation was dispatched to the wrong bind path, and a ValidationException is thrown listing the valid operations.","triggerScenarios":"Calling bind() on an UnboundPredicate whose Operation is binary (EQ, LT, IN, STARTS_WITH, etc.) but which internally routes into bindUnaryOperation — normally only reachable via misuse of the class API or a bug in a custom expression implementation.","commonSituations":"Custom expression frameworks constructing UnboundPredicate with the wrong arity of operation (e.g. unary dispatcher fed a binary op); internal dispatch bugs when subclassing or wrapping UnboundPredicate.","solutions":["Use the correct Expression factory: for EQ/LT/GT/IN etc. use Expressions.equal/lessThan/in rather than constructing UnboundPredicate directly","Ensure your dispatch calls bindLiteralOperation (or the correct path) for binary operations and bindUnaryOperation only for the four unary ops","Upgrade/check Iceberg version if you believe a supported unary op is rejected — verify the Operation enum value"],"exampleFix":"// before\nUnboundPredicate<Object> p = new UnboundPredicate<>(Operation.LT, term, value); p.bind(struct, true); // may hit wrong path\n// after\nExpression p = Expressions.lessThan(\"col\", value); // correct factory; binds via literal path","handlingStrategy":"type-guard","validationCode":"Set<Operation> UNARY = Set.of(Operation.IS_NULL, Operation.NOT_NULL, Operation.IS_NAN, Operation.NOT_NAN);\nif (!UNARY.contains(pred.op())) { /* route to literal (binary) bind path */ }","typeGuard":"static boolean isUnaryOp(Operation op) { return op == Operation.IS_NULL || op == Operation.NOT_NULL || op == Operation.IS_NAN || op == Operation.NOT_NAN; }","tryCatchPattern":"try { pred.bind(struct, caseSensitive); } catch (ValidationException e) { /* check which bind path the operation was routed through */ }","preventionTips":["Dispatch predicates by operation arity before binding","Construct predicates via Expressions factories instead of direct UnboundPredicate construction","Add assertions in custom dispatch code mapping operations to bind paths"],"tags":["expressions","predicate","internal","validation"],"backgroundTag":"invalid-enum-value","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}