This is an automated email from the ASF dual-hosted git repository.

alexpl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 7ae89601007 IGNITE-27661 SQL Calcite: Add non-equi condition support 
for LEFT/RIGHT/FULL/ANTI HASH JOIN - Fixes #12669.
7ae89601007 is described below

commit 7ae8960100725d1fd006c108248279702260909e
Author: Aleksey Plekhanov <[email protected]>
AuthorDate: Mon Feb 2 09:39:56 2026 +0300

    IGNITE-27661 SQL Calcite: Add non-equi condition support for 
LEFT/RIGHT/FULL/ANTI HASH JOIN - Fixes #12669.
    
    Signed-off-by: Aleksey Plekhanov <[email protected]>
---
 .../query/calcite/exec/rel/HashJoinNode.java       | 998 ++++++++++++---------
 .../query/calcite/rule/HashJoinConverterRule.java  |  12 +-
 .../calcite/exec/rel/HashJoinExecutionTest.java    | 330 ++++++-
 .../calcite/integration/JoinIntegrationTest.java   |  27 +-
 .../query/calcite/planner/HashJoinPlannerTest.java |  34 +-
 5 files changed, 914 insertions(+), 487 deletions(-)

diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinNode.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinNode.java
index fbdec244838..f845fb21d95 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinNode.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinNode.java
@@ -18,11 +18,12 @@
 package org.apache.ignite.internal.processors.query.calcite.exec.rel;
 
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
+import java.util.BitSet;
 import java.util.HashMap;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
+import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.type.RelDataType;
@@ -33,78 +34,21 @@ import 
org.apache.ignite.internal.processors.query.calcite.exec.RowHandler;
 import 
org.apache.ignite.internal.processors.query.calcite.exec.exp.agg.GroupKey;
 import org.apache.ignite.internal.processors.query.calcite.rel.IgniteJoinInfo;
 import 
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
-import org.apache.ignite.internal.util.typedef.F;
 import org.jetbrains.annotations.Nullable;
 
 /** Hash join implementor. */
 public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNode<Row> {
-    /** */
-    private static final int INITIAL_CAPACITY = 128;
-
-    /** */
-    private final RowHandler<Row> leftRowHnd;
-
-    /** */
-    private final RowHandler<Row> rightRowHnd;
-
-    /** */
-    private final boolean keepRowsWithNull;
-
-    /** */
-    private final ImmutableBitSet allowNulls;
-
-    /** Output row handler. */
-    protected final RowHandler<Row> outRowHnd;
-
-    /** Right rows storage. */
-    protected Map<GroupKey<Row>, TouchedArrayList<Row>> hashStore = new 
HashMap<>(INITIAL_CAPACITY);
-
-    /** */
-    protected Iterator<Row> rightIt = Collections.emptyIterator();
-
-    /** */
-    @Nullable protected final BiPredicate<Row, Row> nonEqCond;
-
     /**
      * Creates hash join node.
      *
      * @param ctx Execution context.
      * @param rowType Out row type.
-     * @param info Join info.
-     * @param outRowHnd Output row handler.
-     * @param keepRowsWithNull {@code True} if we need to store the row from 
right shoulder even if it contains NULL in
-     *                         any of join key position. This is required for 
joins which emit unmatched part
-     *                         of the right shoulder, such as RIGHT JOIN and 
FULL OUTER JOIN.
-     * @param nonEqCond If provided, only rows matching the predicate will be 
emitted as matched rows.
      */
     protected HashJoinNode(
         ExecutionContext<Row> ctx,
-        RelDataType rowType,
-        IgniteJoinInfo info,
-        RowHandler<Row> outRowHnd,
-        boolean keepRowsWithNull,
-        @Nullable BiPredicate<Row, Row> nonEqCond
+        RelDataType rowType
     ) {
         super(ctx, rowType);
-
-        allowNulls = info.allowNulls();
-        this.keepRowsWithNull = keepRowsWithNull;
-
-        leftRowHnd = new MappingRowHandler<>(ctx.rowHandler(), 
info.leftKeys.toIntArray());
-        rightRowHnd = new MappingRowHandler<>(ctx.rowHandler(), 
info.rightKeys.toIntArray());
-
-        this.outRowHnd = outRowHnd;
-
-        this.nonEqCond = nonEqCond;
-    }
-
-    /** {@inheritDoc} */
-    @Override protected void rewindInternal() {
-        super.rewindInternal();
-
-        rightIt = Collections.emptyIterator();
-
-        hashStore.clear();
     }
 
     /** Creates certain join node. */
@@ -117,8 +61,7 @@ public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNod
         IgniteJoinInfo info,
         @Nullable BiPredicate<RowT, RowT> nonEqCond
     ) {
-        assert !info.pairs().isEmpty() && (info.isEqui() || type == 
JoinRelType.INNER || type == JoinRelType.SEMI);
-        assert nonEqCond == null || type == JoinRelType.INNER || type == 
JoinRelType.SEMI;
+        assert !info.pairs().isEmpty();
 
         IgniteTypeFactory typeFactory = ctx.getTypeFactory();
         RowHandler<RowT> rowHnd = ctx.rowHandler();
@@ -128,21 +71,34 @@ public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNod
                 return new InnerHashJoin<>(ctx, rowType, info, rowHnd, 
nonEqCond);
 
             case LEFT:
-                return new LeftHashJoin<>(ctx, rowType, info, rowHnd, 
rowHnd.factory(typeFactory, rightRowType));
+                return new LeftHashJoin<>(ctx, rowType, info, rowHnd, 
rowHnd.factory(typeFactory, rightRowType),
+                    nonEqCond);
 
             case RIGHT:
-                return new RightHashJoin<>(ctx, rowType, info, rowHnd, 
rowHnd.factory(typeFactory, leftRowType));
+                if (nonEqCond == null) {
+                    return new RightHashJoin<>(ctx, rowType, info, rowHnd,
+                        rowHnd.factory(typeFactory, leftRowType), nonEqCond);
+                }
+                else {
+                    return new RightRowTouchingHashJoin<>(ctx, rowType, info, 
rowHnd,
+                        rowHnd.factory(typeFactory, leftRowType), nonEqCond);
+                }
 
-            case FULL: {
-                return new FullOuterHashJoin<>(ctx, rowType, info, rowHnd, 
rowHnd.factory(typeFactory, leftRowType),
-                    rowHnd.factory(typeFactory, rightRowType), nonEqCond);
-            }
+            case FULL:
+                if (nonEqCond == null) {
+                    return new FullOuterHashJoin<>(ctx, rowType, info, rowHnd,
+                        rowHnd.factory(typeFactory, leftRowType), 
rowHnd.factory(typeFactory, rightRowType), nonEqCond);
+                }
+                else {
+                    return new FullOuterRowTouchingHashJoin<>(ctx, rowType, 
info, rowHnd,
+                        rowHnd.factory(typeFactory, leftRowType), 
rowHnd.factory(typeFactory, rightRowType), nonEqCond);
+                }
 
             case SEMI:
-                return new SemiHashJoin<>(ctx, rowType, info, rowHnd, 
nonEqCond);
+                return new SemiHashJoin<>(ctx, rowType, info, nonEqCond);
 
             case ANTI:
-                return new AntiHashJoin<>(ctx, rowType, info, rowHnd, 
nonEqCond);
+                return new AntiHashJoin<>(ctx, rowType, info, nonEqCond);
 
             default:
                 throw new IllegalArgumentException("Join of type '" + type + 
"' isn't supported.");
@@ -150,251 +106,210 @@ public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNod
     }
 
     /** */
-    protected Collection<Row> lookup(Row row) {
-        GroupKey<Row> key = GroupKey.of(row, leftRowHnd, allowNulls);
-
-        if (key == null)
-            return Collections.emptyList();
-
-        TouchedArrayList<Row> res = hashStore.get(key);
-
-        if (res == null)
-            return Collections.emptyList();
-
-        res.touched = true;
-
-        return res;
-    }
-
-    /** */
-    protected Iterator<Row> untouched() {
-        return F.flat(F.iterator(hashStore.values(), c0 -> c0, true, c1 -> 
!c1.touched));
-    }
-
-    /** {@inheritDoc} */
-    @Override protected void pushRight(Row row) throws Exception {
-        assert downstream() != null;
-        assert waitingRight > 0;
-
-        waitingRight--;
-
-        GroupKey<Row> key = keepRowsWithNull ? GroupKey.of(row, rightRowHnd) : 
GroupKey.of(row, rightRowHnd, allowNulls);
-
-        if (key != null) {
-            nodeMemoryTracker.onRowAdded(row);
-
-            hashStore.computeIfAbsent(key, k -> new 
TouchedArrayList<>()).add(row);
-        }
+    private abstract static class AbstractStoringHashJoin<Row, RowList extends 
List<Row>> extends HashJoinNode<Row> {
+        /** */
+        private static final int INITIAL_CAPACITY = 128;
 
-        if (waitingRight == 0)
-            rightSource().request(waitingRight = IN_BUFFER_SIZE);
-    }
+        /** */
+        private final RowHandler<Row> leftRowHnd;
 
-    /** */
-    protected boolean leftFinished() {
-        return waitingLeft == NOT_WAITING && left == null && 
leftInBuf.isEmpty();
-    }
+        /** */
+        private final RowHandler<Row> rightRowHnd;
 
-    /** */
-    protected boolean rightFinished() {
-        return waitingRight == NOT_WAITING && !rightIt.hasNext();
-    }
+        /** */
+        private final boolean keepRowsWithNull;
 
-    /** */
-    protected boolean checkJoinFinished() throws Exception {
-        if (requested > 0 && leftFinished() && rightFinished()) {
-            requested = 0;
+        /** */
+        private final ImmutableBitSet allowNulls;
 
-            hashStore.clear();
+        /** */
+        protected @Nullable RowList rightRows;
 
-            downstream().end();
+        /** */
+        protected int rightIdx;
 
-            return true;
-        }
+        /** */
+        @Nullable protected final BiPredicate<Row, Row> nonEqCond;
 
-        return false;
-    }
+        /** Right rows storage. */
+        protected Map<GroupKey<Row>, RowList> hashStore = new 
HashMap<>(INITIAL_CAPACITY);
 
-    /** */
-    private static final class InnerHashJoin<RowT> extends HashJoinNode<RowT> {
         /**
-         * Creates node for INNER JOIN.
+         * Constructor.
          *
          * @param ctx Execution context.
          * @param rowType Out row type.
          * @param info Join info.
-         * @param outRowHnd Output row handler.
+         * @param keepRowsWithNull {@code True} if we need to store the row 
from right hand even if it contains NULL in
+         *                         any of join key position. This is required 
for joins which emit unmatched part
+         *                         of the right hand, such as RIGHT JOIN and 
FULL OUTER JOIN.
          * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
          */
-        private InnerHashJoin(ExecutionContext<RowT> ctx,
+        protected AbstractStoringHashJoin(
+            ExecutionContext<Row> ctx,
             RelDataType rowType,
             IgniteJoinInfo info,
-            RowHandler<RowT> outRowHnd,
-            @Nullable BiPredicate<RowT, RowT> nonEqCond
+            boolean keepRowsWithNull,
+            @Nullable BiPredicate<Row, Row> nonEqCond
         ) {
-            super(ctx, rowType, info, outRowHnd, false, nonEqCond);
+            super(ctx, rowType);
+
+            allowNulls = info.allowNulls();
+            this.keepRowsWithNull = keepRowsWithNull;
+
+            leftRowHnd = new MappingRowHandler<>(ctx.rowHandler(), 
info.leftKeys.toIntArray());
+            rightRowHnd = new MappingRowHandler<>(ctx.rowHandler(), 
info.rightKeys.toIntArray());
+
+            this.nonEqCond = nonEqCond;
         }
 
         /** {@inheritDoc} */
-        @Override protected void join() throws Exception {
-            if (waitingRight == NOT_WAITING) {
-                inLoop = true;
+        @Override protected void rewindInternal() {
+            super.rewindInternal();
 
-                try {
-                    while (requested > 0 && (left != null || 
!leftInBuf.isEmpty())) {
-                        // Proceed with next left row, if previous was fully 
processed.
-                        if (left == null) {
-                            left = leftInBuf.remove();
+            rightRows = null;
+            rightIdx = 0;
 
-                            rightIt = lookup(left).iterator();
-                        }
+            hashStore.clear();
+        }
 
-                        if (rightIt.hasNext()) {
-                            // Emits matched rows.
-                            while (requested > 0 && rightIt.hasNext()) {
-                                if (rescheduleJoin())
-                                    return;
+        /** */
+        protected @Nullable RowList lookup(Row row) {
+            GroupKey<Row> key = GroupKey.of(row, leftRowHnd, allowNulls);
 
-                                RowT right = rightIt.next();
+            if (key == null)
+                return null;
 
-                                if (nonEqCond != null && !nonEqCond.test(left, 
right))
-                                    continue;
+            return hashStore.get(key);
+        }
 
-                                --requested;
+        /** {@inheritDoc} */
+        @Override protected void pushRight(Row row) throws Exception {
+            assert downstream() != null;
+            assert waitingRight > 0;
 
-                                downstream().push(outRowHnd.concat(left, 
right));
-                            }
+            waitingRight--;
 
-                            if (!rightIt.hasNext())
-                                left = null;
-                        }
-                        else
-                            left = null;
-                    }
-                }
-                finally {
-                    inLoop = false;
-                }
-            }
+            GroupKey<Row> key = keepRowsWithNull ? GroupKey.of(row, 
rightRowHnd) : GroupKey.of(row, rightRowHnd, allowNulls);
 
-            if (checkJoinFinished())
-                return;
-
-            tryToRequestInputs();
-        }
-    }
+            if (key != null) {
+                nodeMemoryTracker.onRowAdded(row);
 
-    /** */
-    private static final class LeftHashJoin<RowT> extends HashJoinNode<RowT> {
-        /** Right row factory. */
-        private final RowHandler.RowFactory<RowT> rightRowFactory;
+                hashStore.computeIfAbsent(key, k -> createRowList()).add(row);
+            }
 
-        /**
-         * Creates node for LEFT OUTER JOIN.
-         *
-         * @param ctx Execution context.
-         * @param info Join info.
-         * @param rowType Out row type.
-         * @param outRowHnd Output row handler.
-         * @param rightRowFactory Right row factory.
-         */
-        private LeftHashJoin(
-            ExecutionContext<RowT> ctx,
-            RelDataType rowType,
-            IgniteJoinInfo info,
-            RowHandler<RowT> outRowHnd,
-            RowHandler.RowFactory<RowT> rightRowFactory
-        ) {
-            super(ctx, rowType, info, outRowHnd, false, null);
+            if (waitingRight == 0)
+                rightSource().request(waitingRight = IN_BUFFER_SIZE);
+        }
 
-            assert nonEqCond == null : "Non equi condition is not supported in 
LEFT join";
+        /** */
+        protected abstract RowList createRowList();
 
-            this.rightRowFactory = rightRowFactory;
+        /** */
+        protected boolean hasNextRight() {
+            return rightRows != null && rightIdx < rightRows.size();
         }
 
-        /** {@inheritDoc} */
-        @Override protected void join() throws Exception {
-            if (waitingRight == NOT_WAITING) {
-                inLoop = true;
+        /** */
+        protected Row nextRight() {
+            return rightRows.get(rightIdx++);
+        }
 
-                try {
-                    while (requested > 0 && (left != null || 
!leftInBuf.isEmpty())) {
-                        // Proceed with next left row, if previous was fully 
processed.
-                        if (left == null) {
-                            left = leftInBuf.remove();
+        /** */
+        protected boolean leftFinished() {
+            return waitingLeft == NOT_WAITING && left == null && 
leftInBuf.isEmpty();
+        }
 
-                            Collection<RowT> rightRows = lookup(left);
+        /** */
+        protected boolean rightFinished() {
+            return waitingRight == NOT_WAITING && !hasNextRight();
+        }
 
-                            // Emit unmatched left row.
-                            if (rightRows.isEmpty()) {
-                                requested--;
+        /** */
+        protected boolean checkNextLeftRowStarted() {
+            // Proceed with next left row, if previous was fully processed.
+            if (left == null) {
+                left = leftInBuf.remove();
 
-                                downstream().push(outRowHnd.concat(left, 
rightRowFactory.create()));
-                            }
+                rightRows = lookup(left);
+                rightIdx = 0;
 
-                            rightIt = rightRows.iterator();
-                        }
+                return true;
+            }
 
-                        if (rightIt.hasNext()) {
-                            while (requested > 0 && rightIt.hasNext()) {
-                                if (rescheduleJoin())
-                                    return;
+            return false;
+        }
 
-                                RowT right = rightIt.next();
+        /** */
+        protected boolean checkJoinFinished() throws Exception {
+            if (requested > 0 && leftFinished() && rightFinished()) {
+                requested = 0;
 
-                                --requested;
+                hashStore.clear();
 
-                                downstream().push(outRowHnd.concat(left, 
right));
-                            }
+                downstream().end();
 
-                            if (!rightIt.hasNext())
-                                left = null;
-                        }
-                        else
-                            left = null;
-                    }
-                }
-                finally {
-                    inLoop = false;
-                }
+                return true;
             }
 
-            if (checkJoinFinished())
-                return;
-
-            tryToRequestInputs();
+            return false;
         }
     }
 
     /** */
-    private static final class RightHashJoin<RowT> extends HashJoinNode<RowT> {
-        /** Left row factory. */
-        private final RowHandler.RowFactory<RowT> leftRowFactory;
+    private abstract static class AbstractMatchingHashJoin<Row, RowList 
extends List<Row>>
+        extends AbstractStoringHashJoin<Row, RowList> {
+        /** Output row factory. */
+        private final BiFunction<Row, Row, Row> outRowFactory;
+
+        /** Empty right row. */
+        private final @Nullable Row emptyRightRow;
+
+        /** Empty left row. */
+        private final @Nullable Row emptyLeftRow;
+
+        /** Whether current left row was matched. */
+        protected boolean leftMatched;
 
         /** */
-        private boolean drainMaterialization;
+        protected boolean drainMaterialization;
+
+        /** */
+        protected Iterator<RowList> materializedIt;
 
         /**
-         * Creates node for RIGHT OUTER JOIN.
+         * Constructor.
          *
          * @param ctx Execution context.
-         * @param rowType Out row type.
+         * @param rowType Row type.
          * @param info Join info.
-         * @param outRowHnd Output row handler.
-         * @param leftRowFactory Left row factory.
+         * @param outRowHnd Out row handler.
+         * @param leftRowFactory Left row factory (or {@code null} if join 
don't produce rows for unmatched right side).
+         * @param rightRowFactory Right row factory (or {@code null} if join 
don't produce rows for unmatched left side).
          */
-        private RightHashJoin(
-            ExecutionContext<RowT> ctx,
+        private AbstractMatchingHashJoin(
+            ExecutionContext<Row> ctx,
             RelDataType rowType,
             IgniteJoinInfo info,
-            RowHandler<RowT> outRowHnd,
-            RowHandler.RowFactory<RowT> leftRowFactory
+            RowHandler<Row> outRowHnd,
+            @Nullable RowHandler.RowFactory<Row> leftRowFactory,
+            @Nullable RowHandler.RowFactory<Row> rightRowFactory,
+            @Nullable BiPredicate<Row, Row> nonEqCond
         ) {
-            super(ctx, rowType, info, outRowHnd, true, null);
+            super(ctx, rowType, info, leftRowFactory != null, nonEqCond);
+
+            outRowFactory = outRowHnd::concat;
+            emptyLeftRow = leftRowFactory == null ? null : 
leftRowFactory.create();
+            emptyRightRow = rightRowFactory == null ? null : 
rightRowFactory.create();
+        }
 
-            assert nonEqCond == null : "Non equi condition is not supported in 
RIGHT join";
+        /** {@inheritDoc} */
+        @Override protected void rewindInternal() {
+            super.rewindInternal();
 
-            this.leftRowFactory = leftRowFactory;
+            drainMaterialization = false;
+
+            leftMatched = false;
         }
 
         /** {@inheritDoc} */
@@ -404,63 +319,61 @@ public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNod
 
                 try {
                     while (requested > 0 && (left != null || 
!leftInBuf.isEmpty())) {
-                        // Proceed with next left row, if previous was fully 
processed.
-                        if (left == null) {
-                            left = leftInBuf.remove();
+                        if (checkNextLeftRowStarted())
+                            leftMatched = false;
 
-                            rightIt = lookup(left).iterator();
-                        }
+                        while (hasNextRight()) {
+                            if (rescheduleJoin())
+                                return;
 
-                        if (rightIt.hasNext()) {
-                            // Emits matched rows.
-                            while (requested > 0 && rightIt.hasNext()) {
-                                if (rescheduleJoin())
-                                    return;
+                            Row right = nextRight();
 
-                                RowT right = rightIt.next();
+                            if (nonEqCond != null && !nonEqCond.test(left, 
right))
+                                continue;
 
-                                --requested;
+                            leftMatched = true;
 
-                                downstream().push(outRowHnd.concat(left, 
right));
-                            }
+                            // Emit matched row.
+                            downstreamPush(left, right);
+
+                            touchRight();
 
-                            if (!rightIt.hasNext())
-                                left = null;
+                            if (requested == 0)
+                                return;
                         }
-                        else
-                            left = null;
-                    }
-                }
-                finally {
-                    inLoop = false;
-                }
-            }
 
-            // Emit unmatched right rows.
-            if (leftFinished() && waitingRight == NOT_WAITING && requested > 
0) {
-                inLoop = true;
+                        assert requested > 0;
 
-                try {
-                    if (!rightIt.hasNext() && !drainMaterialization) {
-                        // Prevent scanning store more than once.
-                        drainMaterialization = true;
+                        // For LEFT/FULL join.
+                        if (emptyRightRow != null && !leftMatched) {
+                            // Emit unmatched left row.
+                            downstreamPush(left, emptyRightRow);
+                        }
 
-                        rightIt = untouched();
+                        left = null;
                     }
 
-                    RowT emptyLeft = leftRowFactory.create();
+                    // For RIGHT/FULL join.
+                    if (emptyLeftRow != null && leftFinished() && requested > 
0) {
+                        // Emit unmatched right rows.
+                        if (!hasNextRight() && !drainMaterialization) {
+                            // Prevent scanning store more than once.
+                            drainMaterialization = true;
 
-                    while (requested > 0 && rightIt.hasNext()) {
-                        if (rescheduleJoin())
-                            return;
+                            materializedIt = hashStore.values().iterator();
+                        }
 
-                        RowT right = rightIt.next();
+                        while (requested > 0 && hasNextRight()) {
+                            if (rescheduleJoin())
+                                return;
 
-                        RowT row = outRowHnd.concat(emptyLeft, right);
+                            Row right = nextRight();
 
-                        --requested;
+                            if (touchedRight())
+                                continue;
 
-                        downstream().push(row);
+                            downstreamPush(emptyLeftRow, right);
+                        }
                     }
                 }
                 finally {
@@ -474,27 +387,42 @@ public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNod
             tryToRequestInputs();
         }
 
-        /** {@inheritDoc} */
-        @Override protected void rewindInternal() {
-            drainMaterialization = false;
+        /** */
+        private void downstreamPush(Row left, Row right) throws Exception {
+            requested--;
 
-            super.rewindInternal();
+            downstream().push(outRowFactory.apply(left, right));;
         }
-    }
 
-    /** */
-    private static final class FullOuterHashJoin<RowT> extends 
HashJoinNode<RowT> {
-        /** Left row factory. */
-        private final RowHandler.RowFactory<RowT> leftRowFactory;
+        /** {@inheritDoc} */
+        @Override protected boolean hasNextRight() {
+            boolean res = super.hasNextRight();
+
+            if (drainMaterialization && !res && materializedIt.hasNext()) {
+                rightRows = materializedIt.next();
+                rightIdx = 0;
+                return true; // Every key in hashStore contains at least one 
row.
+            }
+
+            return res;
+        }
 
-        /** Right row factory. */
-        private final RowHandler.RowFactory<RowT> rightRowFactory;
+        /** */
+        protected void touchRight() {
+            // No-op.
+        }
 
         /** */
-        private boolean drainMaterialization;
+        protected boolean touchedRight() {
+            return false;
+        }
+    }
 
+    /** */
+    private abstract static class AbstractNoTouchingHashJoin<RowT>
+        extends AbstractMatchingHashJoin<RowT, ArrayList<RowT>> {
         /**
-         * Creates node for FULL OUTER JOIN.
+         * Constructor.
          *
          * @param ctx Execution context.
          * @param rowType Row type.
@@ -502,135 +430,306 @@ public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNod
          * @param outRowHnd Out row handler.
          * @param leftRowFactory Left row factory.
          * @param rightRowFactory Right row factory.
+         * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
          */
-        private FullOuterHashJoin(
+        private AbstractNoTouchingHashJoin(
             ExecutionContext<RowT> ctx,
             RelDataType rowType,
             IgniteJoinInfo info,
             RowHandler<RowT> outRowHnd,
-            RowHandler.RowFactory<RowT> leftRowFactory,
-            RowHandler.RowFactory<RowT> rightRowFactory,
+            @Nullable RowHandler.RowFactory<RowT> leftRowFactory,
+            @Nullable RowHandler.RowFactory<RowT> rightRowFactory,
             @Nullable BiPredicate<RowT, RowT> nonEqCond
         ) {
-            super(ctx, rowType, info, outRowHnd, true, null);
-
-            assert nonEqCond == null : "Non equi condition is not supported in 
FULL OUTER join";
-
-            this.leftRowFactory = leftRowFactory;
-            this.rightRowFactory = rightRowFactory;
+            super(ctx, rowType, info, outRowHnd, leftRowFactory, 
rightRowFactory, nonEqCond);
         }
 
         /** {@inheritDoc} */
-        @Override protected void join() throws Exception {
-            if (waitingRight == NOT_WAITING) {
-                inLoop = true;
-
-                try {
-                    while (requested > 0 && (left != null || 
!leftInBuf.isEmpty())) {
-                        // Proceed with next left row, if previous was fully 
processed.
-                        if (left == null) {
-                            left = leftInBuf.remove();
-
-                            Collection<RowT> rightRows = lookup(left);
+        @Override protected ArrayList<RowT> createRowList() {
+            return new ArrayList<>();
+        }
+    }
 
-                            if (rightRows.isEmpty()) {
-                                // Emit empty right row for unmatched left row.
-                                rightIt = 
Collections.singletonList(rightRowFactory.create()).iterator();
-                            }
-                            else
-                                rightIt = rightRows.iterator();
-                        }
+    /** */
+    private static final class InnerHashJoin<RowT> extends 
AbstractNoTouchingHashJoin<RowT> {
+        /**
+         * Creates node for INNER JOIN.
+         *
+         * @param ctx Execution context.
+         * @param rowType Out row type.
+         * @param info Join info.
+         * @param outRowHnd Output row handler.
+         * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
+         */
+        private InnerHashJoin(
+            ExecutionContext<RowT> ctx,
+            RelDataType rowType,
+            IgniteJoinInfo info,
+            RowHandler<RowT> outRowHnd,
+            @Nullable BiPredicate<RowT, RowT> nonEqCond
+        ) {
+            super(ctx, rowType, info, outRowHnd, null, null, nonEqCond);
+        }
+    }
 
-                        if (rightIt.hasNext()) {
-                            // Emits matched rows.
-                            while (requested > 0 && rightIt.hasNext()) {
-                                if (rescheduleJoin())
-                                    return;
+    /** */
+    private static final class LeftHashJoin<RowT> extends 
AbstractNoTouchingHashJoin<RowT> {
+        /**
+         * Creates node for LEFT OUTER JOIN.
+         *
+         * @param ctx Execution context.
+         * @param info Join info.
+         * @param rowType Out row type.
+         * @param outRowHnd Output row handler.
+         * @param rightRowFactory Right row factory.
+         * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
+         */
+        private LeftHashJoin(
+            ExecutionContext<RowT> ctx,
+            RelDataType rowType,
+            IgniteJoinInfo info,
+            RowHandler<RowT> outRowHnd,
+            RowHandler.RowFactory<RowT> rightRowFactory,
+            @Nullable BiPredicate<RowT, RowT> nonEqCond
+        ) {
+            super(ctx, rowType, info, outRowHnd, null, rightRowFactory, 
nonEqCond);
+        }
+    }
 
-                                RowT right = rightIt.next();
+    /** */
+    private abstract static class AbstractListTouchingHashJoin<RowT>
+        extends AbstractMatchingHashJoin<RowT, TouchableList<RowT>> {
+        /**
+         * Constructor.
+         *
+         * @param ctx Execution context.
+         * @param rowType Row type.
+         * @param info Join info.
+         * @param outRowHnd Out row handler.
+         * @param leftRowFactory Left row factory.
+         * @param rightRowFactory Right row factory.
+         * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
+         */
+        private AbstractListTouchingHashJoin(
+            ExecutionContext<RowT> ctx,
+            RelDataType rowType,
+            IgniteJoinInfo info,
+            RowHandler<RowT> outRowHnd,
+            @Nullable RowHandler.RowFactory<RowT> leftRowFactory,
+            @Nullable RowHandler.RowFactory<RowT> rightRowFactory,
+            @Nullable BiPredicate<RowT, RowT> nonEqCond
+        ) {
+            super(ctx, rowType, info, outRowHnd, leftRowFactory, 
rightRowFactory, nonEqCond);
 
-                                --requested;
+            assert nonEqCond == null;
+        }
 
-                                downstream().push(outRowHnd.concat(left, 
right));
-                            }
+        /** {@inheritDoc} */
+        @Override protected TouchableList<RowT> createRowList() {
+            return new TouchableList<>();
+        }
 
-                            if (!rightIt.hasNext())
-                                left = null;
-                        }
-                        else
-                            left = null;
-                    }
-                }
-                finally {
-                    inLoop = false;
-                }
-            }
+        /** {@inheritDoc} */
+        @Override protected @Nullable TouchableList<RowT> lookup(RowT t) {
+            TouchableList<RowT> res = super.lookup(t);
 
-            // Emit unmatched right rows.
-            if (leftFinished() && waitingRight == NOT_WAITING && requested > 
0) {
-                inLoop = true;
+            if (res != null)
+                res.touch();
 
-                try {
-                    if (!rightIt.hasNext() && !drainMaterialization) {
-                        // Prevent scanning store more than once.
-                        drainMaterialization = true;
+            return res;
+        }
 
-                        rightIt = untouched();
-                    }
+        /** {@inheritDoc} */
+        @Override protected boolean touchedRight() {
+            return rightRows.touched();
+        }
 
-                    RowT emptyLeft = leftRowFactory.create();
+        /** {@inheritDoc} */
+        @Override protected boolean hasNextRight() {
+            boolean res = super.hasNextRight();
+
+            if (drainMaterialization && res && rightIdx > 0 && 
rightRows.touched()) {
+                // Emit one row even for touched list, to correctly reschedule 
after some rows have been processed
+                // and allow others to do their job.
+                if (materializedIt.hasNext()) {
+                    rightRows = materializedIt.next();
+                    rightIdx = 0;
+                    return true; // Every key in hashStore contains at least 
one row.
+                }
 
-                    while (requested > 0 && rightIt.hasNext()) {
-                        if (rescheduleJoin())
-                            return;
+                return false;
+            }
 
-                        RowT right = rightIt.next();
+            return res;
+        }
+    }
 
-                        RowT row = outRowHnd.concat(emptyLeft, right);
+    /** */
+    private static class RightHashJoin<RowT> extends 
AbstractListTouchingHashJoin<RowT> {
+        /**
+         * Creates node for RIGHT OUTER JOIN.
+         *
+         * @param ctx Execution context.
+         * @param rowType Out row type.
+         * @param info Join info.
+         * @param outRowHnd Output row handler.
+         * @param leftRowFactory Left row factory.
+         * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
+         */
+        private RightHashJoin(
+            ExecutionContext<RowT> ctx,
+            RelDataType rowType,
+            IgniteJoinInfo info,
+            RowHandler<RowT> outRowHnd,
+            RowHandler.RowFactory<RowT> leftRowFactory,
+            @Nullable BiPredicate<RowT, RowT> nonEqCond
+        ) {
+            super(ctx, rowType, info, outRowHnd, leftRowFactory, null, 
nonEqCond);
+        }
+    }
 
-                        --requested;
+    /** */
+    private static final class FullOuterHashJoin<RowT> extends 
AbstractListTouchingHashJoin<RowT> {
+        /**
+         * Creates node for FULL OUTER JOIN.
+         *
+         * @param ctx Execution context.
+         * @param rowType Row type.
+         * @param info Join info.
+         * @param outRowHnd Out row handler.
+         * @param leftRowFactory Left row factory.
+         * @param rightRowFactory Right row factory.
+         * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
+         */
+        private FullOuterHashJoin(
+            ExecutionContext<RowT> ctx,
+            RelDataType rowType,
+            IgniteJoinInfo info,
+            RowHandler<RowT> outRowHnd,
+            RowHandler.RowFactory<RowT> leftRowFactory,
+            RowHandler.RowFactory<RowT> rightRowFactory,
+            @Nullable BiPredicate<RowT, RowT> nonEqCond
+        ) {
+            super(ctx, rowType, info, outRowHnd, leftRowFactory, 
rightRowFactory, nonEqCond);
+        }
+    }
 
-                        downstream().push(row);
-                    }
-                }
-                finally {
-                    inLoop = false;
-                }
-            }
+    /** */
+    private abstract static class AbstractRowTouchingHashJoin<RowT>
+        extends AbstractMatchingHashJoin<RowT, RowTouchableList<RowT>> {
+        /**
+         * Constructor.
+         *
+         * @param ctx Execution context.
+         * @param rowType Row type.
+         * @param info Join info.
+         * @param outRowHnd Out row handler.
+         * @param leftRowFactory Left row factory.
+         * @param rightRowFactory Right row factory.
+         * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
+         */
+        private AbstractRowTouchingHashJoin(
+            ExecutionContext<RowT> ctx,
+            RelDataType rowType,
+            IgniteJoinInfo info,
+            RowHandler<RowT> outRowHnd,
+            @Nullable RowHandler.RowFactory<RowT> leftRowFactory,
+            @Nullable RowHandler.RowFactory<RowT> rightRowFactory,
+            @Nullable BiPredicate<RowT, RowT> nonEqCond
+        ) {
+            super(ctx, rowType, info, outRowHnd, leftRowFactory, 
rightRowFactory, nonEqCond);
 
-            if (checkJoinFinished())
-                return;
+            assert nonEqCond != null;
+        }
 
-            tryToRequestInputs();
+        /** {@inheritDoc} */
+        @Override protected RowTouchableList<RowT> createRowList() {
+            return new RowTouchableList<>();
         }
 
         /** {@inheritDoc} */
-        @Override protected void rewindInternal() {
-            drainMaterialization = false;
+        @Override protected void touchRight() {
+            // Index was already moved by nextRight call, so use previous 
index.
+            rightRows.touch(rightIdx - 1);
+        }
 
-            super.rewindInternal();
+        /** {@inheritDoc} */
+        @Override protected boolean touchedRight() {
+            // Index was already moved by nextRight call, so use previous 
index.
+            return rightRows.touched(rightIdx - 1);
         }
     }
 
     /** */
-    private static final class SemiHashJoin<RowT> extends HashJoinNode<RowT> {
+    private static class RightRowTouchingHashJoin<RowT> extends 
AbstractRowTouchingHashJoin<RowT> {
         /**
-         * Creates node for SEMI JOIN operator.
+         * Creates node for RIGHT OUTER JOIN.
          *
          * @param ctx Execution context.
          * @param rowType Out row type.
          * @param info Join info.
          * @param outRowHnd Output row handler.
+         * @param leftRowFactory Left row factory.
          * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
          */
-        private SemiHashJoin(
+        private RightRowTouchingHashJoin(
             ExecutionContext<RowT> ctx,
             RelDataType rowType,
             IgniteJoinInfo info,
             RowHandler<RowT> outRowHnd,
+            RowHandler.RowFactory<RowT> leftRowFactory,
             @Nullable BiPredicate<RowT, RowT> nonEqCond
         ) {
-            super(ctx, rowType, info, outRowHnd, false, nonEqCond);
+            super(ctx, rowType, info, outRowHnd, leftRowFactory, null, 
nonEqCond);
+        }
+    }
+
+    /** */
+    private static final class FullOuterRowTouchingHashJoin<RowT> extends 
AbstractRowTouchingHashJoin<RowT> {
+        /**
+         * Creates node for FULL OUTER JOIN.
+         *
+         * @param ctx Execution context.
+         * @param rowType Row type.
+         * @param info Join info.
+         * @param outRowHnd Out row handler.
+         * @param leftRowFactory Left row factory.
+         * @param rightRowFactory Right row factory.
+         * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
+         */
+        private FullOuterRowTouchingHashJoin(
+            ExecutionContext<RowT> ctx,
+            RelDataType rowType,
+            IgniteJoinInfo info,
+            RowHandler<RowT> outRowHnd,
+            RowHandler.RowFactory<RowT> leftRowFactory,
+            RowHandler.RowFactory<RowT> rightRowFactory,
+            @Nullable BiPredicate<RowT, RowT> nonEqCond
+        ) {
+            super(ctx, rowType, info, outRowHnd, leftRowFactory, 
rightRowFactory, nonEqCond);
+        }
+    }
+
+    /**
+     * Abstract base class for filtering rows of the left table based on 
matching or non-matching conditions with the
+     * right table.
+     */
+    private abstract static class AbstractFilteringHashJoin<RowT> extends 
AbstractStoringHashJoin<RowT, ArrayList<RowT>> {
+        /**
+         * Constructor.
+         *
+         * @param ctx Execution context.
+         * @param rowType Out row type.
+         * @param info Join info.
+         * @param nonEqCond Non-equi conditions.
+         */
+        private AbstractFilteringHashJoin(
+            ExecutionContext<RowT> ctx,
+            RelDataType rowType,
+            IgniteJoinInfo info,
+            @Nullable BiPredicate<RowT, RowT> nonEqCond
+        ) {
+            super(ctx, rowType, info, false, nonEqCond);
         }
 
         /** {@inheritDoc} */
@@ -640,19 +739,14 @@ public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNod
 
                 try {
                     while (requested > 0 && (left != null || 
!leftInBuf.isEmpty())) {
-                        // Proceed with next left row, if previous was fully 
processed.
-                        if (left == null) {
-                            left = leftInBuf.remove();
+                        checkNextLeftRowStarted();
 
-                            rightIt = lookup(left).iterator();
-                        }
-
-                        boolean anyMatched = rightIt.hasNext() && nonEqCond == 
null;
+                        boolean anyMatched = hasNextRight() && nonEqCond == 
null;
 
                         if (!anyMatched) {
-                            // Find any matched row.
-                            while (rightIt.hasNext()) {
-                                RowT right = rightIt.next();
+                            // Find any matched right row.
+                            while (hasNextRight()) {
+                                RowT right = nextRight();
 
                                 if (nonEqCond.test(left, right)) {
                                     anyMatched = true;
@@ -666,15 +760,14 @@ public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNod
                         }
 
                         if (anyMatched) {
-                            requested--;
-
-                            downstream().push(left);
+                            onMatchesFound();
 
-                            rightIt = Collections.emptyIterator();
+                            rightRows = null;
                         }
+                        else
+                            onMatchesNotFound();
 
-                        if (!rightIt.hasNext())
-                            left = null;
+                        left = null;
                     }
                 }
                 finally {
@@ -687,67 +780,112 @@ public abstract class HashJoinNode<Row> extends 
AbstractRightMaterializedJoinNod
 
             tryToRequestInputs();
         }
+
+        /** {@inheritDoc} */
+        @Override protected ArrayList<RowT> createRowList() {
+            return new ArrayList<>();
+        }
+
+        /** Triggered when matches found for current left row. */
+        protected void onMatchesFound() throws Exception {
+            // No-op.
+        }
+
+        /** Triggered when matches not found for current left row. */
+        protected void onMatchesNotFound() throws Exception {
+            // No-op.
+        }
+    }
+
+    /** */
+    private static final class SemiHashJoin<RowT> extends 
AbstractFilteringHashJoin<RowT> {
+        /**
+         * Creates node for SEMI JOIN operator.
+         *
+         * @param ctx Execution context.
+         * @param rowType Out row type.
+         * @param info Join info.
+         * @param nonEqCond If provided, only rows matching the predicate will 
be emitted as matched rows.
+         */
+        private SemiHashJoin(
+            ExecutionContext<RowT> ctx,
+            RelDataType rowType,
+            IgniteJoinInfo info,
+            @Nullable BiPredicate<RowT, RowT> nonEqCond
+        ) {
+            super(ctx, rowType, info, nonEqCond);
+        }
+
+        /** {@inheritDoc} */
+        @Override protected void onMatchesFound() throws Exception {
+            requested--;
+
+            downstream().push(left);
+        }
     }
 
     /** */
-    private static final class AntiHashJoin<RowT> extends HashJoinNode<RowT> {
+    private static final class AntiHashJoin<RowT> extends 
AbstractFilteringHashJoin<RowT> {
         /**
          * Creates node for ANTI JOIN.
          *
          * @param ctx Execution context.
          * @param rowType Out row type.
          * @param info Join info.
-         * @param outRowHnd Output row handler.
          * @param nonEqCond Non-equi conditions.
          */
         private AntiHashJoin(
             ExecutionContext<RowT> ctx,
             RelDataType rowType,
             IgniteJoinInfo info,
-            RowHandler<RowT> outRowHnd,
             @Nullable BiPredicate<RowT, RowT> nonEqCond
         ) {
-            super(ctx, rowType, info, outRowHnd, false, nonEqCond);
+            super(ctx, rowType, info, nonEqCond);
         }
 
         /** {@inheritDoc} */
-        @Override protected void join() throws Exception {
-            if (waitingRight == NOT_WAITING) {
-                inLoop = true;
+        @Override protected void onMatchesNotFound() throws Exception {
+            requested--;
 
-                try {
-                    while (requested > 0 && (left != null || 
!leftInBuf.isEmpty())) {
-                        if (rescheduleJoin())
-                            return;
-
-                        left = leftInBuf.remove();
+            downstream().push(left);
+        }
+    }
 
-                        Collection<RowT> rightRows = lookup(left);
+    /** */
+    private static final class TouchableList<T> extends ArrayList<T> {
+        /** */
+        private boolean touched;
 
-                        if (rightRows.isEmpty()) {
-                            requested--;
+        /** */
+        public void touch() {
+            touched = true;
+        }
 
-                            downstream().push(left);
-                        }
+        /** */
+        public boolean touched() {
+            return touched;
+        }
+    }
 
-                        left = null;
-                    }
-                }
-                finally {
-                    inLoop = false;
-                }
-            }
+    /** */
+    private static final class RowTouchableList<T> extends ArrayList<T> {
+        /** */
+        private @Nullable BitSet touched;
 
-            if (checkJoinFinished())
-                return;
+        /** */
+        public void touch(int idx) {
+            if (touched == null)
+                touched = new BitSet(size());
 
-            tryToRequestInputs();
+            touched.set(idx);
         }
-    }
 
-    /** */
-    private static final class TouchedArrayList<T> extends ArrayList<T> {
         /** */
-        private boolean touched;
+        public boolean touched(int idx) {
+            if (touched == null)
+                return false;
+
+            return touched.get(idx);
+        }
     }
 }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/HashJoinConverterRule.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/HashJoinConverterRule.java
index de1016d3c51..1a3cc988085 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/HashJoinConverterRule.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/HashJoinConverterRule.java
@@ -17,7 +17,6 @@
 
 package org.apache.ignite.internal.processors.query.calcite.rule;
 
-import java.util.EnumSet;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptRule;
@@ -25,7 +24,6 @@ import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.PhysicalNode;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.logical.LogicalJoin;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
@@ -38,9 +36,6 @@ public class HashJoinConverterRule extends 
AbstractIgniteJoinConverterRule {
     /** */
     public static final RelOptRule INSTANCE = new HashJoinConverterRule();
 
-    /** */
-    private static final EnumSet<JoinRelType> NON_EQ_CONDITIONS_SUPPORT = 
EnumSet.of(JoinRelType.INNER, JoinRelType.SEMI);
-
     /** Ctor. */
     private HashJoinConverterRule() {
         super("HashJoinConverter", HintDefinition.HASH_JOIN);
@@ -52,12 +47,7 @@ public class HashJoinConverterRule extends 
AbstractIgniteJoinConverterRule {
 
         IgniteJoinInfo joinInfo = IgniteJoinInfo.of(join);
 
-        if (joinInfo.pairs().isEmpty())
-            return false;
-
-        // Current limitation: unmatched products on left or right part 
requires special handling of non-equi condition
-        // on execution level.
-        return joinInfo.isEqui() || 
NON_EQ_CONDITIONS_SUPPORT.contains(join.getJoinType());
+        return !joinInfo.pairs().isEmpty();
     }
 
     /** {@inheritDoc} */
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinExecutionTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinExecutionTest.java
index 25f1023d0b7..56d650f254c 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinExecutionTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/exec/rel/HashJoinExecutionTest.java
@@ -19,7 +19,6 @@ package 
org.apache.ignite.internal.processors.query.calcite.exec.rel;
 
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Comparator;
 import java.util.function.BiPredicate;
 import java.util.stream.IntStream;
 import java.util.stream.Stream;
@@ -92,9 +91,9 @@ public class HashJoinExecutionTest extends 
AbstractExecutionTest {
 
         Object[][] expected = {
             {1, 1, "Igor"},
-            {1, 1, "Alexey"},
             {2, 2, "Roman"},
             {5, null, "Ivan"},
+            {1, 1, "Alexey"},
         };
 
         checkResults(expected, rows);
@@ -194,32 +193,331 @@ public class HashJoinExecutionTest extends 
AbstractExecutionTest {
     }
 
     /** */
-    @Test public void testInnerJoinWithPostFiltration() {
-        doTestJoinWithPostFiltration(INNER, new Object[][] {{3, "Alexey", 1, 
1, "Core"}});
+    @Test
+    public void testInnerJoinWithPostFiltration() {
+        Object[][] expected = {
+            // No rows for cases 0, 1, 2, 10 - empty left side, no rows for 
cases, 0, 3, 6, 9 - empty right side.
+            {1, "Roman4", 4, 4, "SQL4"},
+            {1, "Roman5", 5, 5, "SQL5"},
+            {1, "Roman5", 5, 5, "QA5"},
+            {1, "Roman7", 7, 7, "SQL7"},
+            {2, "Ivan7", 7, 7, "SQL7"},
+            {1, "Roman8", 8, 8, "SQL8"},
+            {1, "Roman8", 8, 8, "QA8"},
+            {2, "Ivan8", 8, 8, "SQL8"},
+            {2, "Ivan8", 8, 8, "QA8"},
+        };
+
+        doTestJoinWithPostFiltration(INNER, expected);
+    }
+
+    /** */
+    @Test
+    public void testLeftJoinWithPostFiltration() {
+        Object[][] expected = {
+            // Case 0.
+            {0, "Igor0", 0, null, null},
+
+            // Case 1.
+            {0, "Igor1", 1, null, null},
+
+            // Case 2.
+            {0, "Igor2", 2, null, null},
+
+            // Case 3.
+            {0, "Igor3", 3, null, null},
+            {1, "Roman3", 3, null, null},
+
+            // Case 4.
+            {0, "Igor4", 4, null, null},
+            {1, "Roman4", 4, 4, "SQL4"},
+
+            // Case 5.
+            {0, "Igor5", 5, null, null},
+            {1, "Roman5", 5, 5, "SQL5"},
+            {1, "Roman5", 5, 5, "QA5"},
+
+            // Case 6.
+            {0, "Igor6", 6, null, null},
+            {1, "Roman6", 6, null, null},
+            {2, "Ivan6", 6, null, null},
+
+            // Case 7.
+            {0, "Igor7", 7, null, null},
+            {1, "Roman7", 7, 7, "SQL7"},
+            {2, "Ivan7", 7, 7, "SQL7"},
+
+            // Case 8.
+            {0, "Igor8", 8, null, null},
+            {1, "Roman8", 8, 8, "SQL8"},
+            {1, "Roman8", 8, 8, "QA8"},
+            {2, "Ivan8", 8, 8, "SQL8"},
+            {2, "Ivan8", 8, 8, "QA8"},
+
+            // Case 9.
+            {0, "Igor9", 9, null, null},
+            {1, "Roman9", 9, null, null},
+            {2, "Ivan9", 9, null, null},
+        };
+
+        doTestJoinWithPostFiltration(LEFT, expected);
+    }
+
+    /** */
+    @Test
+    public void testRightJoinWithPostFiltration() {
+        Object[][] expected = {
+            // Case 0.
+            {null, null, null, 0, "Core0"},
+
+            // Case 1.
+            {null, null, null, 1, "Core1"},
+            {null, null, null, 1, "SQL1"},
+
+            // Case 2.
+            {null, null, null, 2, "Core2"},
+            {null, null, null, 2, "SQL2"},
+            {null, null, null, 2, "QA2"},
+
+            // Case 3.
+            {null, null, null, 3, "Core3"},
+
+            // Case 4.
+            {1, "Roman4", 4, 4, "SQL4"},
+            {null, null, null, 4, "Core4"},
+
+            // Case 5.
+            {1, "Roman5", 5, 5, "SQL5"},
+            {1, "Roman5", 5, 5, "QA5"},
+            {null, null, null, 5, "Core5"},
+
+            // Case 6.
+            {null, null, null, 6, "Core6"},
+
+            // Case 7.
+            {1, "Roman7", 7, 7, "SQL7"},
+            {2, "Ivan7", 7, 7, "SQL7"},
+            {null, null, null, 7, "Core7"},
+
+            // Case 8.
+            {1, "Roman8", 8, 8, "SQL8"},
+            {1, "Roman8", 8, 8, "QA8"},
+            {2, "Ivan8", 8, 8, "SQL8"},
+            {2, "Ivan8", 8, 8, "QA8"},
+            {null, null, null, 8, "Core8"},
+
+            // Case 10.
+            {null, null, null, 10, "Core10"},
+            {null, null, null, 10, "SQL10"},
+            {null, null, null, 10, "QA10"},
+        };
+
+        doTestJoinWithPostFiltration(RIGHT, expected);
+    }
+
+    /** */
+    @Test
+    public void testFullJoinWithPostFiltration() {
+        Object[][] expected = {
+            // Case 0.
+            {0, "Igor0", 0, null, null},
+            {null, null, null, 0, "Core0"},
+
+            // Case 1.
+            {0, "Igor1", 1, null, null},
+            {null, null, null, 1, "Core1"},
+            {null, null, null, 1, "SQL1"},
+
+            // Case 2.
+            {0, "Igor2", 2, null, null},
+            {null, null, null, 2, "Core2"},
+            {null, null, null, 2, "SQL2"},
+            {null, null, null, 2, "QA2"},
+
+            // Case 3.
+            {0, "Igor3", 3, null, null},
+            {1, "Roman3", 3, null, null},
+            {null, null, null, 3, "Core3"},
+
+            // Case 4.
+            {0, "Igor4", 4, null, null},
+            {1, "Roman4", 4, 4, "SQL4"},
+            {null, null, null, 4, "Core4"},
+
+            // Case 5.
+            {0, "Igor5", 5, null, null},
+            {1, "Roman5", 5, 5, "SQL5"},
+            {1, "Roman5", 5, 5, "QA5"},
+            {null, null, null, 5, "Core5"},
+
+            // Case 6.
+            {0, "Igor6", 6, null, null},
+            {1, "Roman6", 6, null, null},
+            {2, "Ivan6", 6, null, null},
+            {null, null, null, 6, "Core6"},
+
+            // Case 7.
+            {0, "Igor7", 7, null, null},
+            {1, "Roman7", 7, 7, "SQL7"},
+            {2, "Ivan7", 7, 7, "SQL7"},
+            {null, null, null, 7, "Core7"},
+
+            // Case 8.
+            {0, "Igor8", 8, null, null},
+            {1, "Roman8", 8, 8, "SQL8"},
+            {1, "Roman8", 8, 8, "QA8"},
+            {2, "Ivan8", 8, 8, "SQL8"},
+            {2, "Ivan8", 8, 8, "QA8"},
+            {null, null, null, 8, "Core8"},
+
+            // Case 9.
+            {0, "Igor9", 9, null, null},
+            {1, "Roman9", 9, null, null},
+            {2, "Ivan9", 9, null, null},
+
+            // Case 10.
+            {null, null, null, 10, "Core10"},
+            {null, null, null, 10, "SQL10"},
+            {null, null, null, 10, "QA10"},
+        };
+
+        doTestJoinWithPostFiltration(FULL, expected);
     }
 
     /** */
     @Test
     public void testSemiJoinWithPostFiltration() {
-        doTestJoinWithPostFiltration(SEMI, new Object[][] {{3, "Alexey", 1}});
+        Object[][] expected = {
+            // No rows for cases 0, 1, 2, 10 - empty left side, no rows for 
cases, 0, 3, 6, 9 - empty right side.
+            {1, "Roman4", 4},
+            {1, "Roman5", 5},
+            {1, "Roman7", 7},
+            {2, "Ivan7", 7},
+            {1, "Roman8", 8},
+            {2, "Ivan8", 8},
+        };
+
+        doTestJoinWithPostFiltration(SEMI, expected);
+    }
+
+    /** */
+    @Test
+    public void testAntiJoinWithPostFiltration() {
+        Object[][] expected = {
+            {0, "Igor0", 0},
+            {0, "Igor1", 1},
+            {0, "Igor2", 2},
+            {0, "Igor3", 3},
+            {1, "Roman3", 3},
+            {0, "Igor4", 4},
+            {0, "Igor5", 5},
+            {0, "Igor6", 6},
+            {1, "Roman6", 6},
+            {2, "Ivan6", 6},
+            {0, "Igor7", 7},
+            {0, "Igor8", 8},
+            {0, "Igor9", 9},
+            {1, "Roman9", 9},
+            {2, "Ivan9", 9},
+        };
+
+        doTestJoinWithPostFiltration(ANTI, expected);
     }
 
     /** */
     private void doTestJoinWithPostFiltration(JoinRelType joinType, Object[][] 
expected) {
         Object[][] persons = {
-            new Object[] {0, "Igor", 1},
-            new Object[] {1, "Roman", 2},
-            new Object[] {2, "Ivan", 5},
-            new Object[] {3, "Alexey", 1}
+            // Case 0: 1 rows on left (-1 filtered) to 1 rows on right (-1 
filtered).
+            {0, "Igor0", 0},
+
+            // Case 1: 1 rows on left (-1 filtered) to 2 rows on right (-1 
filtered).
+            {0, "Igor1", 1},
+
+            // Case 2: 1 rows on left (-1 filtered) to 3 rows on right (-1 
filtered).
+            {0, "Igor2", 2},
+
+            // Case 3: 2 rows on left (-1 filtered) to 1 rows on right (-1 
filtered).
+            {0, "Igor3", 3},
+            {1, "Roman3", 3},
+
+            // Case 4: 2 rows on left (-1 filtered) to 2 rows on right (-1 
filtered).
+            {0, "Igor4", 4},
+            {1, "Roman4", 4},
+
+            // Case 5: 2 rows on left (-1 filtered) to 3 rows on right (-1 
filtered).
+            {0, "Igor5", 5},
+            {1, "Roman5", 5},
+
+            // Case 6: 3 rows on left (-1 filtered) to 1 rows on right (-1 
filtered).
+            {0, "Igor6", 6},
+            {1, "Roman6", 6},
+            {2, "Ivan6", 6},
+
+            // Case 7: 3 rows on left (-1 filtered) to 2 rows on right (-1 
filtered).
+            {0, "Igor7", 7},
+            {1, "Roman7", 7},
+            {2, "Ivan7", 7},
+
+            // Case 8: 3 rows on left (-1 filtered) to 3 rows on right (-1 
filtered).
+            {0, "Igor8", 8},
+            {1, "Roman8", 8},
+            {2, "Ivan8", 8},
+
+            // Case 9: 3 rows on left (-1 filtered) to 0 rows on right.
+            {0, "Igor9", 9},
+            {1, "Roman9", 9},
+            {2, "Ivan9", 9},
+
+            // Case 10: 0 rows on left to 3 rows on right (-1 filtered).
         };
 
         Object[][] deps = {
-            new Object[] {1, "Core"},
-            new Object[] {2, "SQL"},
-            new Object[] {3, "QA"}
+            // Case 0: 1 rows on left (-1 filtered) to 1 rows on right (-1 
filtered).
+            {0, "Core0"},
+
+            // Case 1: 1 rows on left (-1 filtered) to 2 rows on right (-1 
filtered).
+            {1, "Core1"},
+            {1, "SQL1"},
+
+            // Case 2: 1 rows on left (-1 filtered) to 3 rows on right (-1 
filtered).
+            {2, "Core2"},
+            {2, "SQL2"},
+            {2, "QA2"},
+
+            // Case 3: 2 rows on left (-1 filtered) to 1 rows on right (-1 
filtered).
+            {3, "Core3"},
+
+            // Case 4: 2 rows on left (-1 filtered) to 2 rows on right (-1 
filtered).
+            {4, "Core4"},
+            {4, "SQL4"},
+
+            // Case 5: 2 rows on left (-1 filtered) to 3 rows on right (-1 
filtered).
+            {5, "Core5"},
+            {5, "SQL5"},
+            {5, "QA5"},
+
+            // Case 6: 3 rows on left (-1 filtered) to 1 rows on right (-1 
filtered).
+            {6, "Core6"},
+
+            // Case 7: 3 rows on left (-1 filtered) to 2 rows on right (-1 
filtered).
+            {7, "Core7"},
+            {7, "SQL7"},
+
+            // Case 8: 3 rows on left (-1 filtered) to 3 rows on right (-1 
filtered).
+            {8, "Core8"},
+            {8, "SQL8"},
+            {8, "QA8"},
+
+            // Case 9: 3 rows on left (-1 filtered) to 0 rows on right.
+
+            // Case 10: 0 rows on left to 3 rows on right (-1 filtered).
+            {10, "Core10"},
+            {10, "SQL10"},
+            {10, "QA10"},
         };
 
-        BiPredicate<Object[], Object[]> condition = (l, r) -> 
((CharSequence)r[1]).length() > 3 && ((CharSequence)l[1]).length() > 4;
+        BiPredicate<Object[], Object[]> condition =
+            (l, r) -> !((String)l[1]).startsWith("Igor") && 
!((String)r[1]).startsWith("Core");
 
         validate(joinType, Stream.of(persons)::iterator, 
Stream.of(deps)::iterator, expected, -1, condition);
     }
@@ -395,7 +693,8 @@ public class HashJoinExecutionTest extends 
AbstractExecutionTest {
     private static void checkResults(Object[][] expected, ArrayList<Object[]> 
actual) {
         assertEquals(expected.length, actual.size());
 
-        actual.sort(Comparator.comparing(r -> (int)r[0]));
+        actual.sort(F::compareArrays);
+        Arrays.sort(expected, F::compareArrays);
 
         int length = expected.length;
 
@@ -403,8 +702,7 @@ public class HashJoinExecutionTest extends 
AbstractExecutionTest {
             Object[] exp = expected[i];
             Object[] act = actual.get(i);
 
-            assertEquals(exp.length, act.length);
-            assertEquals(0, F.compareArrays(exp, act));
+            assertEqualsArraysAware(exp, act);
         }
     }
 }
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/JoinIntegrationTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/JoinIntegrationTest.java
index 1343afe37b3..f115ae3ea15 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/JoinIntegrationTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/JoinIntegrationTest.java
@@ -342,8 +342,7 @@ public class JoinIntegrationTest extends 
AbstractBasicIntegrationTransactionalTe
             .returns(null, 2, 2, null)
             .check();
 
-        // HASH JOIN doesn't support: completely non-equi conditions, 
additional non-equi conditions
-        // except INNER and SEMI joins.
+        // HASH JOIN doesn't support: completely non-equi conditions.
         // MERGE JOIN doesn't support: non-equi conditions.
         if (joinType == JoinType.HASH || joinType == JoinType.MERGE)
             return;
@@ -622,10 +621,8 @@ public class JoinIntegrationTest extends 
AbstractBasicIntegrationTransactionalTe
             .returns(null, 2, 2, null)
             .check();
 
-        // HASH JOIN doesn't support: completely non-equi conditions, 
additional non-equi conditions
-        // except INNER and SEMI joins.
         // MERGE JOIN doesn't support: non-equi conditions.
-        if (joinType == JoinType.MERGE || joinType == JoinType.HASH)
+        if (joinType == JoinType.MERGE)
             return;
 
         assertQuery("select t1.c2, t1.c3, t2.c3 from t1 left join t2 on t1.c2 
is not distinct from t2.c3 and t1.c3 > 3" +
@@ -648,6 +645,10 @@ public class JoinIntegrationTest extends 
AbstractBasicIntegrationTransactionalTe
             .returns(null, 2, 5, null)
             .check();
 
+        // HASH JOIN doesn't support: completely non-equi conditions.
+        if (joinType == JoinType.HASH)
+            return;
+
         assertQuery("select t1.c1, t2.c1 from t1 left join t2 on t1.c1=4 order 
by t1.c1, t2.c1")
             .returns(1, null)
             .returns(2, null)
@@ -968,10 +969,8 @@ public class JoinIntegrationTest extends 
AbstractBasicIntegrationTransactionalTe
             .returns(null, 2, 2, null)
             .check();
 
-        // HASH JOIN doesn't support: completely non-equi conditions, 
additional non-equi conditions
-        // except INNER and SEMI joins.
         // MERGE JOIN doesn't support: non-equi conditions.
-        if (joinType == JoinType.MERGE || joinType == JoinType.HASH)
+        if (joinType == JoinType.MERGE)
             return;
 
         assertQuery("select t1.c2, t1.c3, t2.c3 from t1 right join t2 on t1.c2 
is not distinct from t2.c3 and t1.c3 > 3" +
@@ -994,6 +993,10 @@ public class JoinIntegrationTest extends 
AbstractBasicIntegrationTransactionalTe
             .returns(null, null, null, 3)
             .check();
 
+        // HASH JOIN doesn't support: completely non-equi conditions.
+        if (joinType == JoinType.HASH)
+            return;
+
         assertQuery("select t1.c1, t2.c1 from t1 right join t2 on t1.c1=4 
order by t1.c1, t2.c1")
             .returns(4, 1)
             .returns(4, 2)
@@ -1086,10 +1089,8 @@ public class JoinIntegrationTest extends 
AbstractBasicIntegrationTransactionalTe
             .returns(null, 2, 2, null)
             .check();
 
-        // HASH JOIN doesn't support: completely non-equi conditions, 
additional non-equi conditions
-        // except INNER and SEMI joins.
         // MERGE JOIN doesn't support: non-equi conditions.
-        if (joinType == JoinType.MERGE || joinType == JoinType.HASH)
+        if (joinType == JoinType.MERGE)
             return;
 
         assertQuery("select t1.c2, t1.c3, t2.c3 from t1 full join t2 on t1.c2 
is not distinct from t2.c3 and t1.c3 > 3" +
@@ -1118,6 +1119,10 @@ public class JoinIntegrationTest extends 
AbstractBasicIntegrationTransactionalTe
             .returns(null, null, null, 3)
             .check();
 
+        // HASH JOIN doesn't support: completely non-equi conditions.
+        if (joinType == JoinType.HASH)
+            return;
+
         assertQuery("select t1.c1, t2.c1 from t1 full join t2 on t1.c1=4 order 
by t1.c1, t2.c1")
             .returns(1, null)
             .returns(2, null)
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashJoinPlannerTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashJoinPlannerTest.java
index 88a95c20fce..97a8689535e 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashJoinPlannerTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/HashJoinPlannerTest.java
@@ -112,28 +112,27 @@ public class HashJoinPlannerTest extends 
AbstractPlannerTest {
     public void testHashJoinApplied() throws Exception {
         // Parms: request, can be planned, only INNER or SEMI join.
         List<List<Object>> testParams = F.asList(
-            F.asList("select t1.c1 from t1 %s join t2 on t1.id = t2.id", true, 
false),
-            F.asList("select t1.c1 from t1 %s join t2 on t1.id = t2.id and 
t1.c1=t2.c1", true, false),
-            F.asList("select t1.c1 from t1 %s join t2 using(c1)", true, false),
-            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 1", false, 
false),
-            F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct 
from t2.c1", true, false),
-            F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct 
from t2.c1 and t1.c1 is not distinct from  t2.id",
-                true, false),
-            F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct 
from t2.c1 and t1.c1 = t2.id", true, false),
-            F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct 
from t2.c1 and t1.c1 > t2.id", true, true),
-            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = ?", false, 
false),
-            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 
OCTET_LENGTH('TEST')", false, false),
-            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 
LOG10(t1.c1)", false, false),
-            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = t2.c1 and 
t1.ID > t2.ID", true, true),
-            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 1 and t2.c1 = 
1", false, false)
+            F.asList("select t1.c1 from t1 %s join t2 on t1.id = t2.id", true),
+            F.asList("select t1.c1 from t1 %s join t2 on t1.id = t2.id and 
t1.c1=t2.c1", true),
+            F.asList("select t1.c1 from t1 %s join t2 using(c1)", true),
+            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 1", false),
+            F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct 
from t2.c1", true),
+            F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct 
from t2.c1 and t1.c1 is not distinct from t2.id",
+                true),
+            F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct 
from t2.c1 and t1.c1 = t2.id", true),
+            F.asList("select t1.c1 from t1 %s join t2 ON t1.id is not distinct 
from t2.c1 and t1.c1 > t2.id", true),
+            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = ?", false),
+            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 
OCTET_LENGTH('TEST')", false),
+            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 
LOG10(t1.c1)", false),
+            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = t2.c1 and 
t1.ID > t2.ID", true),
+            F.asList("select t1.c1 from t1 %s join t2 on t1.c1 = 1 and t2.c1 = 
1", false)
         );
 
         for (List<Object> paramSet : testParams) {
-            assert paramSet != null && paramSet.size() == 3;
+            assert paramSet != null && paramSet.size() == 2;
 
             String sql = (String)paramSet.get(0);
             boolean canBePlanned = (Boolean)paramSet.get(1);
-            boolean onlyInnerOrSemi = (Boolean)paramSet.get(2);
 
             TestTable tbl1 = createTable("T1", IgniteDistributions.single(), 
"ID", Integer.class, "C1", Integer.class);
             TestTable tbl2 = createTable("T2", IgniteDistributions.single(), 
"ID", Integer.class, "C1", Integer.class);
@@ -141,9 +140,6 @@ public class HashJoinPlannerTest extends 
AbstractPlannerTest {
             IgniteSchema schema = createSchema(tbl1, tbl2);
 
             for (String joinType : JOIN_TYPES) {
-                if (onlyInnerOrSemi && !joinType.equals("INNER") && 
!joinType.equals("SEMI"))
-                    continue;
-
                 String sql0 = String.format(sql, joinType);
 
                 if (log.isInfoEnabled())

Reply via email to