|
45 | 45 | import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalLookupJoin;
|
46 | 46 | import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalMatch;
|
47 | 47 | import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalMiniBatchAssigner;
|
| 48 | +import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalMultiJoin; |
48 | 49 | import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalOverAggregateBase;
|
49 | 50 | import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalRank;
|
50 | 51 | import org.apache.flink.table.planner.plan.nodes.physical.stream.StreamPhysicalRel;
|
|
77 | 78 |
|
78 | 79 | import org.apache.calcite.rel.RelNode;
|
79 | 80 | import org.apache.calcite.rel.core.AggregateCall;
|
| 81 | +import org.apache.calcite.rel.core.JoinRelType; |
80 | 82 | import org.apache.calcite.rel.type.RelDataType;
|
81 | 83 | import org.apache.calcite.rex.RexNode;
|
82 | 84 | import org.apache.calcite.rex.RexProgram;
|
@@ -180,6 +182,8 @@ public StreamPhysicalRel visit(
|
180 | 182 | return visitExpand((StreamPhysicalExpand) rel, requireDeterminism);
|
181 | 183 | } else if (rel instanceof CommonPhysicalJoin) {
|
182 | 184 | return visitJoin((CommonPhysicalJoin) rel, requireDeterminism);
|
| 185 | + } else if (rel instanceof StreamPhysicalMultiJoin) { |
| 186 | + return visitMultiJoin((StreamPhysicalMultiJoin) rel, requireDeterminism); |
183 | 187 | } else if (rel instanceof StreamPhysicalOverAggregateBase) {
|
184 | 188 | return visitOverAggregate((StreamPhysicalOverAggregateBase) rel, requireDeterminism);
|
185 | 189 | } else if (rel instanceof StreamPhysicalRank) {
|
@@ -621,6 +625,102 @@ private StreamPhysicalRel visitJoin(
|
621 | 625 | join.isSemiJoin());
|
622 | 626 | }
|
623 | 627 |
|
| 628 | + /** |
| 629 | + * Multi-join determinism handling, mirroring the binary join logic: |
| 630 | + * |
| 631 | + * <p>If all inputs are insert-only and every join is INNER, the output is insert-only → no |
| 632 | + * determinism required downstream. |
| 633 | + * |
| 634 | + * <p>Otherwise the combined join condition must be deterministic, and we propagate per-input |
| 635 | + * determinism: |
| 636 | + * |
| 637 | + * <ul> |
| 638 | + * <li>If an input can produce updates, and we cannot guarantee uniqueness, we must require |
| 639 | + * determinism for the entire input row (retract-by-row correctness). |
| 640 | + * <li>If uniqueness is guaranteed, we pass through the part of the requirement that belongs |
| 641 | + * to that input. |
| 642 | + * </ul> |
| 643 | + */ |
| 644 | + private StreamPhysicalRel visitMultiJoin( |
| 645 | + final StreamPhysicalMultiJoin multiJoin, final ImmutableBitSet requireDeterminism) { |
| 646 | + final List<RelNode> inputs = multiJoin.getInputs(); |
| 647 | + final boolean allInputsInsertOnly = |
| 648 | + inputs.stream().allMatch(in -> inputInsertOnly((StreamPhysicalRel) in)); |
| 649 | + final boolean allInner = |
| 650 | + multiJoin.getJoinTypes().stream().allMatch(t -> t == JoinRelType.INNER); |
| 651 | + |
| 652 | + // Fast path: pure insert-only inner join produces insert-only output -> nothing to require. |
| 653 | + if (allInputsInsertOnly && allInner) { |
| 654 | + return transmitDeterminismRequirement(multiJoin, NO_REQUIRED_DETERMINISM); |
| 655 | + } |
| 656 | + |
| 657 | + // Output may carry updates (some input updates or some non-inner join): condition must be |
| 658 | + // deterministic. |
| 659 | + final RexNode multiJoinCondition = multiJoin.getMultiJoinCondition(); |
| 660 | + if (multiJoinCondition != null) { |
| 661 | + final Optional<String> ndCall = |
| 662 | + FlinkRexUtil.getNonDeterministicCallName(multiJoinCondition); |
| 663 | + ndCall.ifPresent( |
| 664 | + s -> throwNonDeterministicConditionError(s, multiJoinCondition, multiJoin)); |
| 665 | + } |
| 666 | + |
| 667 | + // Output may carry updates: we need to propagate determinism requirements to inputs. |
| 668 | + final List<RelNode> newInputs = rewriteMultiJoinInputs(multiJoin, requireDeterminism); |
| 669 | + |
| 670 | + return (StreamPhysicalRel) multiJoin.copy(multiJoin.getTraitSet(), newInputs); |
| 671 | + } |
| 672 | + |
| 673 | + private ImmutableBitSet projectToInput( |
| 674 | + final ImmutableBitSet globalRequired, final int inputStart, final int inputFieldCount) { |
| 675 | + final List<Integer> local = |
| 676 | + globalRequired.toList().stream() |
| 677 | + .filter(idx -> idx >= inputStart && idx < inputStart + inputFieldCount) |
| 678 | + .map(idx -> idx - inputStart) |
| 679 | + .collect(Collectors.toList()); |
| 680 | + return ImmutableBitSet.of(local); |
| 681 | + } |
| 682 | + |
| 683 | + private ImmutableBitSet requiredForUpdatingMultiJoinInput( |
| 684 | + final StreamPhysicalMultiJoin multiJoin, |
| 685 | + final int inputIndex, |
| 686 | + final ImmutableBitSet localRequired, |
| 687 | + final int inputFieldCount) { |
| 688 | + final List<int[]> uniqueKeys = multiJoin.getUniqueKeysForInputs().get(inputIndex); |
| 689 | + final boolean hasUniqueKey = !uniqueKeys.isEmpty(); |
| 690 | + |
| 691 | + if (hasUniqueKey) { |
| 692 | + return localRequired; |
| 693 | + } |
| 694 | + // Without uniqueness guarantees we must retract by entire row for correctness. |
| 695 | + return ImmutableBitSet.range(inputFieldCount); |
| 696 | + } |
| 697 | + |
| 698 | + private List<RelNode> rewriteMultiJoinInputs( |
| 699 | + final StreamPhysicalMultiJoin multiJoin, final ImmutableBitSet requireDeterminism) { |
| 700 | + final List<RelNode> inputs = multiJoin.getInputs(); |
| 701 | + final List<RelNode> newInputs = new ArrayList<>(inputs.size()); |
| 702 | + int fieldStartOffset = 0; |
| 703 | + for (int i = 0; i < inputs.size(); i++) { |
| 704 | + final StreamPhysicalRel input = (StreamPhysicalRel) inputs.get(i); |
| 705 | + final int inputFieldCount = input.getRowType().getFieldCount(); |
| 706 | + |
| 707 | + final ImmutableBitSet localRequired = |
| 708 | + projectToInput(requireDeterminism, fieldStartOffset, inputFieldCount); |
| 709 | + |
| 710 | + final ImmutableBitSet inputRequired = |
| 711 | + inputInsertOnly(input) |
| 712 | + ? NO_REQUIRED_DETERMINISM |
| 713 | + : requiredForUpdatingMultiJoinInput( |
| 714 | + multiJoin, i, localRequired, inputFieldCount); |
| 715 | + |
| 716 | + final ImmutableBitSet finalRequired = |
| 717 | + requireDeterminismExcludeUpsertKey(input, inputRequired); |
| 718 | + newInputs.add(visit(input, finalRequired)); |
| 719 | + fieldStartOffset += inputFieldCount; |
| 720 | + } |
| 721 | + return newInputs; |
| 722 | + } |
| 723 | + |
624 | 724 | private StreamPhysicalRel visitOverAggregate(
|
625 | 725 | final StreamPhysicalOverAggregateBase overAgg,
|
626 | 726 | final ImmutableBitSet requireDeterminism) {
|
|
0 commit comments