{"record":{"id":"bf2cbed34e13fb1f","repo":"apache/beam","slug":"side-input-join-can-only-be-used-if-one-table-is-bounded","errorCode":null,"errorMessage":"Side input join can only be used if one table is bounded.","messagePattern":"Side input join can only be used if one table is bounded\\.","errorType":"exception","errorClass":"java.lang.UnsupportedOperationException","httpStatus":null,"severity":"error","filePath":"sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSideInputJoinRel.java","lineNumber":118,"sourceCode":"    // the unbounded & RIGHT OUTER JOIN where right side of the join must be the unbounded\n    if (joinType == JoinRelType.FULL) {\n      throw new UnsupportedOperationException(\n          \"FULL OUTER JOIN is not supported when join \"\n              + \"a bounded table with an unbounded table.\");\n    }\n\n    BeamRelNode leftRelNode = BeamSqlRelUtils.getBeamRelInput(left);\n    BeamRelNode rightRelNode = BeamSqlRelUtils.getBeamRelInput(right);\n\n    if ((joinType == JoinRelType.LEFT && leftRelNode.isBounded() == PCollection.IsBounded.BOUNDED)\n        || (joinType == JoinRelType.RIGHT\n            && rightRelNode.isBounded() == PCollection.IsBounded.BOUNDED)) {\n      throw new UnsupportedOperationException(\n          String.format(\"%s side of an OUTER JOIN must be Unbounded table.\", joinType.name()));\n    }\n    if (leftRelNode.isBounded() == IsBounded.UNBOUNDED\n        && rightRelNode.isBounded() == IsBounded.UNBOUNDED) {\n      throw new UnsupportedOperationException(\n          \"Side input join can only be used if one table is bounded.\");\n    }\n    return new SideInputJoin();\n  }\n\n  private class SideInputJoin extends PTransform<PCollectionList<Row>, PCollection<Row>> {\n\n    @Override\n    public PCollection<Row> expand(PCollectionList<Row> pinput) {\n      Schema leftSchema = pinput.get(0).getSchema();\n      Schema rightSchema = pinput.get(1).getSchema();\n      PCollection<Row> leftRows =\n          pinput\n              .get(0)\n              .apply(\n                  \"left_TimestampCombiner\",\n                  Window.<Row>configure().withTimestampCombiner(TimestampCombiner.EARLIEST));\n      PCollection<Row> rightRows =","sourceCodeStart":100,"sourceCodeEnd":136,"githubUrl":"https://github.com/apache/beam/blob/12126d8942aaf848030c478b4c6a28c6af861c66/sdks/java/extensions/sql/src/main/java/org/apache/beam/sdk/extensions/sql/impl/rel/BeamSideInputJoinRel.java#L100-L136","documentation":"The side-input join implementation requires exactly one side of the join to be bounded: the bounded side is materialized as a side input while the other side streams. If both inputs are unbounded (both streaming), side-input join cannot be used and Beam SQL throws this UnsupportedOperationException during translation.","triggerScenarios":"BeamSideInputJoinRel.buildPTransform detects leftRelNode.isBounded()==UNBOUNDED && rightRelNode.isBounded()==UNBOUNDED, i.e. a streaming-streaming join planned as a side-input join.","commonSituations":"Joining two streaming sources (e.g. two Pub/Sub topics) in Beam SQL and expecting the side-input optimization to apply; users often think side-input join is the general streaming join, but it is only for one-batched-side joins.","solutions":["Make one side bounded (e.g. read a static reference table from a file/DB) so the side-input join precondition holds.","Use a true streaming join construct instead: window both PCollections and use CoGroupByKey, or rewrite the query so Calcite selects a streaming-capable join strategy.","Window both sides identically and join within windows if the sources are both unbounded.","Check the physical plan and add hints/constraints so the join rule does not pick BeamSideInputJoinRel for two unbounded inputs."],"exampleFix":"-- before\nSELECT * FROM stream1 JOIN stream2 ON stream1.k = stream2.k\n-- after\nSELECT * FROM stream1 JOIN bounded_lookup_table ON stream1.k = bounded_lookup_table.k","handlingStrategy":"validation","validationCode":"if (left.isBounded() == IsBounded.UNBOUNDED && right.isBounded() == IsBounded.UNBOUNDED) { throw new IllegalArgumentException(\"Side-input join requires one bounded table; use CoGroupByKey/windowed join for stream-stream\"); }","typeGuard":"boolean oneBounded = left.isBounded() != right.isBounded();","tryCatchPattern":"try { pipeline.apply(SqlTransform.query(sql)); } catch (UnsupportedOperationException e) { if (e.getMessage().contains(\"one table is bounded\")) { /* switch to windowed CoGroup join */ } else { throw e; } }","preventionTips":["Never join two unbounded sources with a query planned as side-input join","Back one side of lookup-style joins with a bounded reference table","Use CoGroupByKey with matching windows for stream-stream joins","Verify boundedness of both inputs during pipeline assembly"],"tags":["sql","join","streaming","boundedness"],"backgroundTag":"unsupported-operation","analyzedSha":"12126d8942aaf848030c478b4c6a28c6af861c66","analyzedAt":"2026-09-13T01:50:10.254Z","contentChangedAt":"2026-09-13T01:50:10.254Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}