apache/beam · error · java.lang.IllegalArgumentException

WindowFns must match for a bounded-vs-bounded/unbounded-vs-u

Error message

WindowFns must match for a bounded-vs-bounded/unbounded-vs-unbounded join.

What it means

BeamCoGBKJoinRel.expand joins two PCollections via CoGroupByKey, which requires both inputs to use compatible windowing. It calls WindowFn.verifyCompatibility on the left and right window functions and, if they are incompatible, throws IllegalArgumentException explaining that window functions must match for a join. This guarantees elements land in the same windows on both sides so the join is well-defined.

Source

Thrown at sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamCoGBKJoinRel.java:128

                  "right_TimestampCombiner",
                  Window.<Row>configure().withTimestampCombiner(TimestampCombiner.EARLIEST));

      // extract the join fields
      List<Pair<RexNode, RexNode>> pairs = extractJoinRexNodes(condition);
      int leftRowColumnCount = BeamSqlRelUtils.getBeamRelInput(left).getRowType().getFieldCount();

      FieldAccessDescriptor leftKeyFields =
          BeamJoinTransforms.getJoinColumns(true, pairs, 0, leftSchema);
      FieldAccessDescriptor rightKeyFields =
          BeamJoinTransforms.getJoinColumns(false, pairs, leftRowColumnCount, rightSchema);

      WindowFn leftWinFn = leftRows.getWindowingStrategy().getWindowFn();
      WindowFn rightWinFn = rightRows.getWindowingStrategy().getWindowFn();

      try {
        leftWinFn.verifyCompatibility(rightWinFn);
      } catch (IncompatibleWindowException e) {
        throw new IllegalArgumentException(
            "WindowFns must match for a bounded-vs-bounded/unbounded-vs-unbounded join.", e);
      }

      verifySupportedTrigger(leftRows);
      verifySupportedTrigger(rightRows);

      return standardJoin(leftRows, rightRows, leftKeyFields, rightKeyFields);
    }
  }

  private <T> void verifySupportedTrigger(PCollection<T> pCollection) {
    WindowingStrategy windowingStrategy = pCollection.getWindowingStrategy();

    if (UNBOUNDED.equals(pCollection.isBounded()) && !triggersOncePerWindow(windowingStrategy)) {
      throw new UnsupportedOperationException(
          "Joining unbounded PCollections is currently only supported for "
              + "non-global windows with triggers that are known to produce output once per window,"
              + "such as the default trigger with zero allowed lateness. "

View on GitHub (pinned to 12126d8942)

Solutions

  1. Apply the same WindowFn to both join inputs before the join (e.g. Window.into(FixedWindows.of(Duration.standardMinutes(1))) on both sides)
  2. If one side is a bounded lookup table, window it with the same WindowFn as the streaming side (or convert the join to use global windows only if both are bounded and it's semantically safe)
  3. Reorder the pipeline so windowing is applied before the SQL JOIN transform
  4. Check WindowingStrategy of both PCollections (getWindowingStrategy().getWindowFn()) and make verifyCompatibility pass before invoking the SQL join

Example fix

// before
PCollection<Row> right = rows.apply(Window.into(FixedWindows.of(Duration.standardMinutes(5))));
// after (match the left side)
PCollection<Row> right = rows.apply(Window.into(FixedWindows.of(Duration.standardMinutes(1))));
Defensive patterns

Strategy: validation

Validate before calling

try {
  left.getWindowingStrategy().getWindowFn().verifyCompatibility(right.getWindowingStrategy().getWindowFn());
} catch (IncompatibleWindowException e) {
  throw new IllegalStateException("Join inputs use incompatible WindowFns: " + e.getMessage());
}

Try / catch

try {
  return sqlEnv.executeJoin(left, right);
} catch (IllegalArgumentException e) {
  if (e.getMessage().contains("WindowFns must match")) {
    // re-window both sides with the same WindowFn and retry
  } else { throw e; }
}

Prevention

When it happens

Trigger: Executing a Beam SQL JOIN where one side of the join uses a different WindowFn than the other — e.g. fixed windows of 1 minute vs 5 minutes, or sliding vs fixed windows, or global window vs fixed windows — on bounded or unbounded PCollections.

Common situations: Joining a streaming table (windowed with WithTimestamps/Window.into) with a bounded side table that stayed in the GlobalWindows; two streams windowed with different durations or window types being joined in SQL; changing one input's windowing after building the pipeline.

Related errors


AI-assisted analysis of apache/beam@12126d8942 (2026-09-13). Data as JSON: /api/errors/884f4118583db7c6. Report an issue: GitHub.