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

  1. Pass the original unbound Expression to Spark3Util.describe / DescribeExpressionVisitor instead of a bound one
  2. Unbind by reconstructing the expression from its specification (e.g. rebuild from parsed SQL or the table's parsed filter) before describing
  3. 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

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


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)