{"record":{"id":"65ae0c8b5826f9bd","repo":"apache/iceberg","slug":"found-already-bound-aggregate","errorCode":null,"errorMessage":"Found already bound aggregate: ","messagePattern":"Found already bound aggregate: ","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"api/src/main/java/org/apache/iceberg/expressions/Binder.java","lineNumber":169,"sourceCode":"\n    @Override\n    public <T> Expression predicate(BoundPredicate<T> pred) {\n      throw new IllegalStateException(\"Found already bound predicate: \" + pred);\n    }\n\n    @Override\n    public <T> Expression predicate(UnboundPredicate<T> pred) {\n      return pred.bind(struct, caseSensitive);\n    }\n\n    @Override\n    public <T> Expression aggregate(UnboundAggregate<T> agg) {\n      return agg.bind(struct, caseSensitive);\n    }\n\n    @Override\n    public <T, C> Expression aggregate(BoundAggregate<T, C> agg) {\n      throw new IllegalStateException(\"Found already bound aggregate: \" + agg);\n    }\n  }\n\n  private static class ReferenceVisitor extends ExpressionVisitor<Set<Integer>> {\n    private final Set<Integer> references = Sets.newHashSet();\n\n    @Override\n    public Set<Integer> alwaysTrue() {\n      return references;\n    }\n\n    @Override\n    public Set<Integer> alwaysFalse() {\n      return references;\n    }\n\n    @Override\n    public Set<Integer> not(Set<Integer> result) {","sourceCodeStart":151,"sourceCodeEnd":187,"githubUrl":"https://github.com/apache/iceberg/blob/86d9c8fc543e7c56c9f624eb725f76c9baff9570/api/src/main/java/org/apache/iceberg/expressions/Binder.java#L151-L187","documentation":"Binder's aggregate method binds UnboundAggregate expressions; receiving an already-bound BoundAggregate means double binding, an internal caller bug. Binder throws this IllegalStateException immediately.","triggerScenarios":"Passing an already-bound aggregate (e.g. from a prior bind() result) into ExpressionBinder.bind() again; binding aggregate expressions twice in scan planning pipelines.","commonSituations":"Reusing cached aggregate expressions across planning phases; custom scan code that binds aggregates for metrics evaluation and then re-binds the same objects.","solutions":["Bind aggregate expressions once and store the BoundAggregate result for reuse","Check Expression.isBound() (or use visitors) before binding","Keep unbound template expressions immutable and produce fresh bound copies per use"],"exampleFix":"// before\nBoundAggregate<?, ?> agg = (BoundAggregate<?, ?>) Binder.bind(schema, unboundAgg);\nagg = (BoundAggregate<?, ?>) Binder.bind(schema, agg); // throws\n// after\nExpression agg = Binder.bind(schema, unboundAgg); // bind once, reuse","handlingStrategy":"type-guard","validationCode":"if (expr.isBound()) { /* reuse as-is, do not re-bind */ }","typeGuard":"boolean needsBinding = agg instanceof UnboundAggregate;","tryCatchPattern":"try { bound = Binder.bind(schema, aggExpr, caseSensitive); } catch (IllegalStateException e) { if (e.getMessage().startsWith(\"Found already bound aggregate\")) { bound = aggExpr; } else { throw e; } }","preventionTips":["Bind aggregate expressions once at planning time","Guard binding with isBound() checks","Store bound aggregates explicitly rather than re-binding cached templates"],"tags":["expressions","binding","aggregates"],"backgroundTag":"invalid-state-transition","analyzedSha":"86d9c8fc543e7c56c9f624eb725f76c9baff9570","analyzedAt":"2026-09-12T00:46:39.097Z","contentChangedAt":"2026-09-12T00:46:39.097Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}