apache/iceberg · error · IllegalStateException

Found already bound aggregate:

Error message

Found already bound aggregate: 

What it means

Binder's aggregate method binds UnboundAggregate expressions; receiving an already-bound BoundAggregate means double binding, an internal caller bug. Binder throws this IllegalStateException immediately.

Source

Thrown at api/src/main/java/org/apache/iceberg/expressions/Binder.java:169

    @Override
    public <T> Expression predicate(BoundPredicate<T> pred) {
      throw new IllegalStateException("Found already bound predicate: " + pred);
    }

    @Override
    public <T> Expression predicate(UnboundPredicate<T> pred) {
      return pred.bind(struct, caseSensitive);
    }

    @Override
    public <T> Expression aggregate(UnboundAggregate<T> agg) {
      return agg.bind(struct, caseSensitive);
    }

    @Override
    public <T, C> Expression aggregate(BoundAggregate<T, C> agg) {
      throw new IllegalStateException("Found already bound aggregate: " + agg);
    }
  }

  private static class ReferenceVisitor extends ExpressionVisitor<Set<Integer>> {
    private final Set<Integer> references = Sets.newHashSet();

    @Override
    public Set<Integer> alwaysTrue() {
      return references;
    }

    @Override
    public Set<Integer> alwaysFalse() {
      return references;
    }

    @Override
    public Set<Integer> not(Set<Integer> result) {

View on GitHub (pinned to 86d9c8fc54)

Solutions

  1. Bind aggregate expressions once and store the BoundAggregate result for reuse
  2. Check Expression.isBound() (or use visitors) before binding
  3. Keep unbound template expressions immutable and produce fresh bound copies per use

Example fix

// before
BoundAggregate<?, ?> agg = (BoundAggregate<?, ?>) Binder.bind(schema, unboundAgg);
agg = (BoundAggregate<?, ?>) Binder.bind(schema, agg); // throws
// after
Expression agg = Binder.bind(schema, unboundAgg); // bind once, reuse
Defensive patterns

Strategy: type-guard

Validate before calling

if (expr.isBound()) { /* reuse as-is, do not re-bind */ }

Type guard

boolean needsBinding = agg instanceof UnboundAggregate;

Try / catch

try { bound = Binder.bind(schema, aggExpr, caseSensitive); } catch (IllegalStateException e) { if (e.getMessage().startsWith("Found already bound aggregate")) { bound = aggExpr; } else { throw e; } }

Prevention

When it happens

Trigger: Passing an already-bound aggregate (e.g. from a prior bind() result) into ExpressionBinder.bind() again; binding aggregate expressions twice in scan planning pipelines.

Common situations: Reusing cached aggregate expressions across planning phases; custom scan code that binds aggregates for metrics evaluation and then re-binds the same objects.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/65ae0c8b5826f9bd. Report an issue: GitHub.