apache/iceberg · error · UnsupportedOperationException
Cannot convert bound predicates to SQL
Error message
Cannot convert bound predicates to SQL
What it means
Spark3Util's DescribeExpressionVisitor implements the two-phase ExpressionVisitor pattern: unbound expressions are visited first, and bound predicates are never expected. If a BoundPredicate reaches this visitor it means the expression was already bound to a schema, which the SQL-description path does not support, so it throws UnsupportedOperationException.
Solutions
- Pass the original unbound Expression to Spark3Util.describe / DescribeExpressionVisitor instead of a bound one
- Unbind by reconstructing the expression from its specification (e.g. rebuild from parsed SQL or the table's parsed filter) before describing
- If only a bound predicate is available, implement a custom ExpressionVisitor that handles BoundPredicate and renders SQL from it
Example fix
// before
Expression bound = Expressions.equal(schema.asStruct(), "id", 1).bind(schema.asStruct());
String sql = Spark3Util.describe(schema, bound); // throws
// after
Expression unbound = Expressions.equal("id", 1);
String sql = Spark3Util.describe(schema, unbound); Defensive patterns
Strategy: type-guard
Validate before calling
if (expr instanceof BoundPredicate || expr instanceof BoundExpression) { throw new IllegalArgumentException("Pass an unbound expression to describe"); } Type guard
boolean isUnbound(Expression e) { return !(e instanceof BoundPredicate) && !(e instanceof BoundExpression); } Try / catch
try { return Spark3Util.describe(schema, expr); } catch (UnsupportedOperationException e) { log.warn("Cannot describe bound expression: {}", e.getMessage()); return expr.toString(); } Prevention
- Keep the original unbound Expression around alongside any bound copy for reporting paths
- Never feed residual/bound filters from scans or row-level ops into describe utilities
- Add a unit test describing every expression you surface to users
When it happens
Trigger: Calling Spark3Util.describe(Schema, Expression) (or otherwise applying DescribeExpressionVisitor) with an expression that has been bound via bind(FullKeyExpression) or similar, causing predicate(BoundPredicate<T>) to be dispatched instead of the unbound overload.
Common situations: Passing expressions extracted from an already-bound context (e.g. a row-level operation's residual filter or a bound scan residual) into describe/describeExpr instead of the original unbound expression; custom code reusing DescribeExpressionVisitor on bound trees.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- Cannot convert term to SQL
- Altering a view is not supported by catalog:
- Altering a view is not supported by catalog
- Altering a view is not supported by catalog
- Altering a view is not supported by catalog
AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12).
Data as JSON: /api/errors/33b9353fe375ad80.
Report an issue: GitHub.
Appendix: source
Thrown at spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java:698
@Override
public String not(String result) {
return "NOT (" + result + ")";
}
@Override
public String and(String leftResult, String rightResult) {
return "(" + leftResult + " AND " + rightResult + ")";
}
@Override
public String or(String leftResult, String rightResult) {
return "(" + leftResult + " OR " + rightResult + ")";
}
@Override
public <T> String predicate(BoundPredicate<T> pred) {
throw new UnsupportedOperationException("Cannot convert bound predicates to SQL");
}
@Override
public <T> String predicate(UnboundPredicate<T> pred) {
switch (pred.op()) {
case IS_NULL:
return sqlString(pred.term()) + " IS NULL";
case NOT_NULL:
return sqlString(pred.term()) + " IS NOT NULL";
case IS_NAN:
return "is_nan(" + sqlString(pred.term()) + ")";
case NOT_NAN:
return "not_nan(" + sqlString(pred.term()) + ")";
case LT:
return sqlString(pred.term()) + " < " + sqlString(pred.literal());
case LT_EQ:
return sqlString(pred.term()) + " <= " + sqlString(pred.literal());
case GT:View on GitHub (pinned to 86d9c8fc54)