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
- 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
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
- 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
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
- IsNaN cannot be used with a non-floating-point column
- NotNaN cannot be used with a non-floating-point column
- Invalid value for conversion to type %s: %s (%s)
- Visitor %s does not support non-reference: %s
- Cannot replace transform in unknown predicate: + pred
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/cf6827278885eb85.
Report an issue: GitHub.