apache/iceberg · error · ValidationException

Operation must be IS_NULL, NOT_NULL, IS_NAN, or NOT_NAN

Error message

Operation must be IS_NULL, NOT_NULL, IS_NAN, or NOT_NAN

What it means

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.

Source

Thrown at api/src/main/java/org/apache/iceberg/expressions/UnboundPredicate.java:157

          return Expressions.alwaysTrue();
        } else if (boundTerm.type().equals(Types.UnknownType.get())) {
          return Expressions.alwaysFalse();
        }
        return new BoundUnaryPredicate<>(Operation.NOT_NULL, boundTerm);
      case IS_NAN:
        if (floatingType(boundTerm.type().typeId())) {
          return new BoundUnaryPredicate<>(Operation.IS_NAN, boundTerm);
        } else {
          throw new ValidationException("IsNaN cannot be used with a non-floating-point column");
        }
      case NOT_NAN:
        if (floatingType(boundTerm.type().typeId())) {
          return new BoundUnaryPredicate<>(Operation.NOT_NAN, boundTerm);
        } else {
          throw new ValidationException("NotNaN cannot be used with a non-floating-point column");
        }
      default:
        throw new ValidationException("Operation must be IS_NULL, NOT_NULL, IS_NAN, or NOT_NAN");
    }
  }

  private boolean allAncestorFieldsAreRequired(StructType struct, int fieldId) {
    return TypeUtil.ancestorFields(struct.asSchema(), fieldId).stream()
        .allMatch(Types.NestedField::isRequired);
  }

  private boolean floatingType(Type.TypeID typeID) {
    return Type.TypeID.DOUBLE.equals(typeID) || Type.TypeID.FLOAT.equals(typeID);
  }

  private Expression bindLiteralOperation(BoundTerm<T> boundTerm) {
    if (op() == Operation.STARTS_WITH || op() == Operation.NOT_STARTS_WITH) {
      ValidationException.check(
          boundTerm.type().equals(Types.StringType.get()),
          "Term for STARTS_WITH or NOT_STARTS_WITH must produce a string: %s: %s",
          boundTerm,

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Use the correct Expression factory: for EQ/LT/GT/IN etc. use Expressions.equal/lessThan/in rather than constructing UnboundPredicate directly
  2. Ensure your dispatch calls bindLiteralOperation (or the correct path) for binary operations and bindUnaryOperation only for the four unary ops
  3. Upgrade/check Iceberg version if you believe a supported unary op is rejected — verify the Operation enum value

Example fix

// before
UnboundPredicate<Object> p = new UnboundPredicate<>(Operation.LT, term, value); p.bind(struct, true); // may hit wrong path
// after
Expression p = Expressions.lessThan("col", value); // correct factory; binds via literal path
Defensive patterns

Strategy: type-guard

Validate before calling

Set<Operation> UNARY = Set.of(Operation.IS_NULL, Operation.NOT_NULL, Operation.IS_NAN, Operation.NOT_NAN);
if (!UNARY.contains(pred.op())) { /* route to literal (binary) bind path */ }

Type guard

static boolean isUnaryOp(Operation op) { return op == Operation.IS_NULL || op == Operation.NOT_NULL || op == Operation.IS_NAN || op == Operation.NOT_NAN; }

Try / catch

try { pred.bind(struct, caseSensitive); } catch (ValidationException e) { /* check which bind path the operation was routed through */ }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: Invalid enum value errors: "Unknown type", "Invalid scope", "must be one of" — when a string is not on the library's allowed list — this error's family across 23 libraries.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/cf6827278885eb85. Report an issue: GitHub.