{"record":{"id":"33b9353fe375ad80","repo":"apache/iceberg","slug":"cannot-convert-bound-predicates-to-sql-33b935","errorCode":null,"errorMessage":"Cannot convert bound predicates to SQL","messagePattern":"Cannot convert bound predicates to SQL","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java","lineNumber":698,"sourceCode":"\n    @Override\n    public String not(String result) {\n      return \"NOT (\" + result + \")\";\n    }\n\n    @Override\n    public String and(String leftResult, String rightResult) {\n      return \"(\" + leftResult + \" AND \" + rightResult + \")\";\n    }\n\n    @Override\n    public String or(String leftResult, String rightResult) {\n      return \"(\" + leftResult + \" OR \" + rightResult + \")\";\n    }\n\n    @Override\n    public <T> String predicate(BoundPredicate<T> pred) {\n      throw new UnsupportedOperationException(\"Cannot convert bound predicates to SQL\");\n    }\n\n    @Override\n    public <T> String predicate(UnboundPredicate<T> pred) {\n      switch (pred.op()) {\n        case IS_NULL:\n          return sqlString(pred.term()) + \" IS NULL\";\n        case NOT_NULL:\n          return sqlString(pred.term()) + \" IS NOT NULL\";\n        case IS_NAN:\n          return \"is_nan(\" + sqlString(pred.term()) + \")\";\n        case NOT_NAN:\n          return \"not_nan(\" + sqlString(pred.term()) + \")\";\n        case LT:\n          return sqlString(pred.term()) + \" < \" + sqlString(pred.literal());\n        case LT_EQ:\n          return sqlString(pred.term()) + \" <= \" + sqlString(pred.literal());\n        case GT:","sourceCodeStart":680,"sourceCodeEnd":716,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/spark/v4.1/spark/src/main/java/org/apache/iceberg/spark/Spark3Util.java#L680-L716","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nExpression bound = Expressions.equal(schema.asStruct(), \"id\", 1).bind(schema.asStruct());\nString sql = Spark3Util.describe(schema, bound); // throws\n// after\nExpression unbound = Expressions.equal(\"id\", 1);\nString sql = Spark3Util.describe(schema, unbound);","handlingStrategy":"type-guard","validationCode":"if (expr instanceof BoundPredicate || expr instanceof BoundExpression) { throw new IllegalArgumentException(\"Pass an unbound expression to describe\"); }","typeGuard":"boolean isUnbound(Expression e) { return !(e instanceof BoundPredicate) && !(e instanceof BoundExpression); }","tryCatchPattern":"try { return Spark3Util.describe(schema, expr); } catch (UnsupportedOperationException e) { log.warn(\"Cannot describe bound expression: {}\", e.getMessage()); return expr.toString(); }","preventionTips":["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"],"tags":["spark","sql-translation","unsupported-operation"],"backgroundTag":"unsupported-operation","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}