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

silun pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 19ef7f0b47 [CALCITE-7327] Support IS NOT DISTINCT FROM as equi 
condition of hash join
19ef7f0b47 is described below

commit 19ef7f0b47e97137e37c2956ace8319f2de5e5ce
Author: Silun Dong <[email protected]>
AuthorDate: Thu Dec 11 14:42:12 2025 +0800

    [CALCITE-7327] Support IS NOT DISTINCT FROM as equi condition of hash join
---
 .../adapter/enumerable/EnumerableHashJoin.java     |  12 +-
 .../adapter/enumerable/EnumerableMergeJoin.java    |  10 ++
 .../enumerable/EnumerableMergeJoinRule.java        |   5 +-
 .../calcite/adapter/enumerable/PhysType.java       |   9 ++
 .../calcite/adapter/enumerable/PhysTypeImpl.java   |  50 ++++++++
 .../java/org/apache/calcite/plan/RelOptUtil.java   |  21 +++-
 .../java/org/apache/calcite/rel/core/Join.java     |   2 +-
 .../java/org/apache/calcite/rel/core/JoinInfo.java |  36 ++++--
 .../calcite/rel/rules/LoptOptimizeJoinRule.java    |   2 +-
 .../calcite/rel/rules/LoptSemiJoinOptimizer.java   |   2 +-
 .../org/apache/calcite/rel/rules/SemiJoinRule.java |   4 +-
 .../java/org/apache/calcite/runtime/FlatLists.java |  13 ++
 .../org/apache/calcite/util/BuiltInMethod.java     |   1 +
 .../java/org/apache/calcite/test/JdbcTest.java     |   2 +-
 .../test/enumerable/EnumerableHashJoinTest.java    | 131 +++++++++++++++++++++
 core/src/test/resources/sql/blank.iq               |   4 +-
 core/src/test/resources/sql/planner.iq             |  17 ++-
 core/src/test/resources/sql/sub-query.iq           |  32 ++---
 .../apache/calcite/linq4j/EnumerableDefaults.java  |  52 ++++----
 19 files changed, 325 insertions(+), 80 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableHashJoin.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableHashJoin.java
index 8bd9ffbf08..e37173137a 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableHashJoin.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableHashJoin.java
@@ -218,8 +218,10 @@ private Result 
implementHashSemiJoin(EnumerableRelImplementor implementor, Prefe
                 Expressions.list(
                     leftExpression,
                     rightExpression,
-                    
leftResult.physType.generateAccessorWithoutNulls(joinInfo.leftKeys),
-                    
rightResult.physType.generateAccessorWithoutNulls(joinInfo.rightKeys),
+                    leftResult.physType.generateNullAwareAccessor(
+                        joinInfo.leftKeys, joinInfo.nullExclusionFlags),
+                    rightResult.physType.generateNullAwareAccessor(
+                        joinInfo.rightKeys, joinInfo.nullExclusionFlags),
                     Util.first(keyPhysType.comparer(),
                         Expressions.constant(null)),
                     predicate)))
@@ -264,8 +266,10 @@ private Result implementHashJoin(EnumerableRelImplementor 
implementor, Prefer pr
                 BuiltInMethod.HASH_JOIN.method,
                 Expressions.list(
                     rightExpression,
-                    
leftResult.physType.generateAccessorWithoutNulls(joinInfo.leftKeys),
-                    
rightResult.physType.generateAccessorWithoutNulls(joinInfo.rightKeys),
+                    leftResult.physType.generateNullAwareAccessor(
+                        joinInfo.leftKeys, joinInfo.nullExclusionFlags),
+                    rightResult.physType.generateNullAwareAccessor(
+                        joinInfo.rightKeys, joinInfo.nullExclusionFlags),
                     EnumUtils.joinSelector(joinType,
                         physType,
                         ImmutableList.of(
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoin.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoin.java
index 838e37064f..557362e6bc 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoin.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoin.java
@@ -34,6 +34,7 @@
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.CorrelationId;
 import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinInfo;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.metadata.RelMdCollation;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
@@ -71,6 +72,9 @@
  * {@link EnumerableConvention enumerable calling convention} using
  * a merge algorithm. */
 public class EnumerableMergeJoin extends Join implements EnumerableRel {
+  @SuppressWarnings("HidingField")
+  private final JoinInfo joinInfo;
+
   protected EnumerableMergeJoin(
       RelOptCluster cluster,
       RelTraitSet traits,
@@ -80,6 +84,12 @@ protected EnumerableMergeJoin(
       Set<CorrelationId> variablesSet,
       JoinRelType joinType) {
     super(cluster, traits, ImmutableList.of(), left, right, condition, 
variablesSet, joinType);
+    // TODO: support IS NOT DISTINCT FROM condition as join keys of MergeJoin
+    // EnumerableMergeJoin cannot use IS NOT DISTINCT FROM condition as join 
keys
+    // (In the algorithm of MergeJoin in Enumerable convention, it will stop
+    // when leftKey or rightKey is NULL), so we create a new JoinInfo that only
+    // considers EQUALS.
+    this.joinInfo = JoinInfo.createWithStrictEquality(left, right, condition);
     assert getConvention() instanceof EnumerableConvention;
     final List<RelCollation> leftCollations = 
getCollations(left.getTraitSet());
     final List<RelCollation> rightCollations = 
getCollations(right.getTraitSet());
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoinRule.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoinRule.java
index 4137f6b74b..624db0a604 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoinRule.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoinRule.java
@@ -60,7 +60,10 @@ protected EnumerableMergeJoinRule(Config config) {
 
   @Override public @Nullable RelNode convert(RelNode rel) {
     Join join = (Join) rel;
-    final JoinInfo info = join.analyzeCondition();
+    // EnumerableMergeJoin cannot use IS NOT DISTINCT FROM condition as join 
keys. More details
+    // in EnumerableMergeJoin.java.
+    final JoinInfo info =
+        JoinInfo.createWithStrictEquality(join.getLeft(), join.getRight(), 
join.getCondition());
     if (!EnumerableMergeJoin.isMergeJoinSupported(join.getJoinType())) {
       // EnumerableMergeJoin only supports certain join types.
       return null;
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/PhysType.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/PhysType.java
index 8d4eeb176c..c50d6237df 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/PhysType.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/PhysType.java
@@ -134,6 +134,15 @@ Expression fieldReference(Expression expression, int field,
    */
   Expression generateAccessorWithoutNulls(List<Integer> fields);
 
+  /**
+   * Similar to {@link #generateAccessor(List)} and {@link 
#generateAccessorWithoutNulls(List)},
+   * but it's null-aware. It returns a Expression which evaluates to null (if 
one of
+   * field is null and it isn't null-safe) or a list of
+   * fields that may contain null (no field is null, or there are fields with 
null but they are
+   * null-safe) at runtime.
+   */
+  Expression generateNullAwareAccessor(List<Integer> fields, List<Boolean> 
nullExclusionFlags);
+
   /** Generates a selector for the given fields from an expression, with the
    * default row format. */
   Expression generateSelector(
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/PhysTypeImpl.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/PhysTypeImpl.java
index ab51dd3e35..e3fdd4d366 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/PhysTypeImpl.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/PhysTypeImpl.java
@@ -642,6 +642,21 @@ private List<Expression> fieldReferences(
     }
   }
 
+  private static Expression getListExpressionAllowSingleElement(
+      Expressions.FluentList<Expression> list) {
+    assert list.size() > 0;
+
+    if (list.size() == 1) {
+      return Expressions.call(
+          List.class,
+          null,
+          BuiltInMethod.LIST1.method,
+          list);
+    } else {
+      return getListExpression(list);
+    }
+  }
+
   private static Expression 
getListExpression(Expressions.FluentList<Expression> list) {
     assert list.size() >= 2;
 
@@ -713,6 +728,41 @@ private static Expression 
getListExpression(Expressions.FluentList<Expression> l
     return Expressions.lambda(Function1.class, exp, v1);
   }
 
+  @Override public Expression generateNullAwareAccessor(
+      List<Integer> fields,
+      List<Boolean> nullExclusionFlags) {
+    assert fields.size() == nullExclusionFlags.size();
+    ParameterExpression v1 = Expressions.parameter(javaRowClass, "v1");
+    if (fields.isEmpty()) {
+      return Expressions.lambda(
+          Function1.class,
+          Expressions.field(
+              null,
+              BuiltInMethod.COMPARABLE_EMPTY_LIST.field),
+          v1);
+    }
+    Expressions.FluentList<Expression> list = Expressions.list();
+    for (int field : fields) {
+      list.add(fieldReference(v1, field));
+    }
+
+    // in the HashJoin key selector scenario, when there is exactly one join 
key and it is
+    // null-safe, a row whose join key is null must still be correctly 
recognized and extracted.
+    // Therefore, when list.size() == 1, this method returns a list containing 
a single
+    // element (which may be null) rather than returning the element directly.
+    Expression exp = getListExpressionAllowSingleElement(list);
+    for (int i = list.size() - 1; i >= 0; i--) {
+      if (nullExclusionFlags.get(i)) {
+        exp =
+            Expressions.condition(
+                Expressions.equal(list.get(i), Expressions.constant(null)),
+                Expressions.constant(null),
+                exp);
+      }
+    }
+    return Expressions.lambda(Function1.class, exp, v1);
+  }
+
   @Override public Expression fieldReference(
       Expression expression, int field) {
     return fieldReference(expression, field, null);
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java 
b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
index 34724a8adf..274e91f14d 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -1470,11 +1470,24 @@ private static void splitJoinCondition(
     nonEquiList.add(condition);
   }
 
-  /** Builds an equi-join condition from a set of left and right keys. */
+  /** Builds an equi-join condition by conjoining EQUALS operator for each 
corresponding pair of
+   * leftKeys and rightKeys. */
   public static RexNode createEquiJoinCondition(
       final RelNode left, final List<Integer> leftKeys,
       final RelNode right, final List<Integer> rightKeys,
       final RexBuilder rexBuilder) {
+    List<Boolean> filterNulls = Collections.nCopies(leftKeys.size(), 
Boolean.TRUE);
+    return createHashJoinCondition(left, leftKeys, right, rightKeys,
+        filterNulls, rexBuilder);
+  }
+
+  /** Builds an equi-join condition by conjoining operators for each 
corresponding pair of
+   * leftKeys and rightKeys. The operator is EQUALS if filterNulls is true for 
that
+   * position, otherwise IS NOT DISTINCT FROM. */
+  public static RexNode createHashJoinCondition(
+      final RelNode left, final List<Integer> leftKeys,
+      final RelNode right, final List<Integer> rightKeys,
+      final List<Boolean> filterNulls, final RexBuilder rexBuilder) {
     final List<RelDataType> leftTypes =
         RelOptUtil.getFieldTypeList(left.getRowType());
     final List<RelDataType> rightTypes =
@@ -1484,7 +1497,11 @@ public static RexNode createEquiJoinCondition(
           @Override public RexNode get(int index) {
             final int leftKey = leftKeys.get(index);
             final int rightKey = rightKeys.get(index);
-            return rexBuilder.makeCall(SqlStdOperatorTable.EQUALS,
+            final SqlOperator operator =
+                filterNulls.get(index)
+                  ? SqlStdOperatorTable.EQUALS
+                  : SqlStdOperatorTable.IS_NOT_DISTINCT_FROM;
+            return rexBuilder.makeCall(operator,
                 rexBuilder.makeInputRef(leftTypes.get(leftKey), leftKey),
                 rexBuilder.makeInputRef(rightTypes.get(rightKey),
                     leftTypes.size() + rightKey));
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Join.java 
b/core/src/main/java/org/apache/calcite/rel/core/Join.java
index 1a56edbed0..1b81717aed 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Join.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Join.java
@@ -104,7 +104,7 @@ protected Join(
     this.condition = requireNonNull(condition, "condition");
     this.variablesSet = ImmutableSet.copyOf(variablesSet);
     this.joinType = requireNonNull(joinType, "joinType");
-    this.joinInfo = JoinInfo.createWithStrictEquality(left, right, condition);
+    this.joinInfo = JoinInfo.of(left, right, condition);
     this.hints = ImmutableList.copyOf(hints);
   }
 
diff --git a/core/src/main/java/org/apache/calcite/rel/core/JoinInfo.java 
b/core/src/main/java/org/apache/calcite/rel/core/JoinInfo.java
index f092a73ea9..0d470cdde2 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/JoinInfo.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/JoinInfo.java
@@ -29,6 +29,7 @@
 import com.google.common.collect.ImmutableList;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import static java.util.Objects.requireNonNull;
@@ -36,9 +37,10 @@
 /** An analyzed join condition.
  *
  * <p>It is useful for the many algorithms that care whether a join has an
- * equi-join condition.
+ * equi-join condition (contains EQUALS and IS NOT DISTINCT FROM).
  *
- * <p>You can create one using {@link #createWithStrictEquality}, or call
+ * <p>You can create one using {@link #of(RelNode, RelNode, RexNode)},
+ * {@link #createWithStrictEquality}, or call
  * {@link Join#analyzeCondition()}; many kinds of join cache their
  * join info, especially those that are equi-joins.
  *
@@ -46,28 +48,36 @@
 public class JoinInfo {
   public final ImmutableIntList leftKeys;
   public final ImmutableIntList rightKeys;
+  // for each join key, whether it filters out nulls. If TRUE, the join key 
uses EQUALS semantics
+  // (not null-safe); if FALSE, it uses IS NOT DISTINCT FROM semantics 
(null-safe).
+  public final ImmutableList<Boolean> nullExclusionFlags;
+  // non-equi parts of join condition.
+  // after CALCITE-7327, IS NOT DISTINCT FROM can be treated as a hash join 
key and is no longer
+  // part of nonEquiConditions.
   public final ImmutableList<RexNode> nonEquiConditions;
 
   /** Creates a JoinInfo. */
   protected JoinInfo(ImmutableIntList leftKeys, ImmutableIntList rightKeys,
-      ImmutableList<RexNode> nonEquiConditions) {
+      ImmutableList<Boolean> nullExclusionFlags, ImmutableList<RexNode> 
nonEquiConditions) {
     this.leftKeys = requireNonNull(leftKeys, "leftKeys");
     this.rightKeys = requireNonNull(rightKeys, "rightKeys");
+    this.nullExclusionFlags = requireNonNull(nullExclusionFlags, 
"nullExclusionFlags");
     this.nonEquiConditions =
         requireNonNull(nonEquiConditions, "nonEquiConditions");
-    assert leftKeys.size() == rightKeys.size();
+    assert leftKeys.size() == rightKeys.size() && leftKeys.size() == 
nullExclusionFlags.size();
   }
 
   /** Creates a {@code JoinInfo} by analyzing a condition. */
   public static JoinInfo of(RelNode left, RelNode right, RexNode condition) {
     final List<Integer> leftKeys = new ArrayList<>();
     final List<Integer> rightKeys = new ArrayList<>();
-    final List<Boolean> filterNulls = new ArrayList<>();
+    final List<Boolean> nullExclusionFlags = new ArrayList<>();
     final List<RexNode> nonEquiList = new ArrayList<>();
     RelOptUtil.splitJoinCondition(left, right, condition, leftKeys, rightKeys,
-        filterNulls, nonEquiList);
+        nullExclusionFlags, nonEquiList);
     return new JoinInfo(ImmutableIntList.copyOf(leftKeys),
-        ImmutableIntList.copyOf(rightKeys), ImmutableList.copyOf(nonEquiList));
+        ImmutableIntList.copyOf(rightKeys), 
ImmutableList.copyOf(nullExclusionFlags),
+        ImmutableList.copyOf(nonEquiList));
   }
 
   /** Creates a {@code JoinInfo} by analyzing a condition.
@@ -82,14 +92,18 @@ public static JoinInfo createWithStrictEquality(RelNode 
left,
     final List<RexNode> nonEquiList = new ArrayList<>();
     RelOptUtil.splitJoinCondition(left, right, condition, leftKeys, rightKeys,
         null, nonEquiList);
+    List<Boolean> nullExclusionFlags = Collections.nCopies(leftKeys.size(), 
Boolean.TRUE);
     return new JoinInfo(ImmutableIntList.copyOf(leftKeys),
-        ImmutableIntList.copyOf(rightKeys), ImmutableList.copyOf(nonEquiList));
+        ImmutableIntList.copyOf(rightKeys), 
ImmutableList.copyOf(nullExclusionFlags),
+        ImmutableList.copyOf(nonEquiList));
   }
 
-  /** Creates an equi-join. */
+  /** Creates an equi-join (only considers EQUALS operations). */
   public static JoinInfo of(ImmutableIntList leftKeys,
       ImmutableIntList rightKeys) {
-    return new JoinInfo(leftKeys, rightKeys, ImmutableList.of());
+    List<Boolean> nullExclusionFlags = Collections.nCopies(leftKeys.size(), 
Boolean.TRUE);
+    return new JoinInfo(leftKeys, rightKeys,
+        ImmutableList.copyOf(nullExclusionFlags), ImmutableList.of());
   }
 
   /** Returns whether this is an equi-join. */
@@ -117,7 +131,7 @@ public RexNode getRemaining(RexBuilder rexBuilder) {
 
   public RexNode getEquiCondition(RelNode left, RelNode right,
       RexBuilder rexBuilder) {
-    return RelOptUtil.createEquiJoinCondition(left, leftKeys, right, rightKeys,
+    return RelOptUtil.createHashJoinCondition(left, leftKeys, right, 
rightKeys, nullExclusionFlags,
         rexBuilder);
   }
 
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/LoptOptimizeJoinRule.java 
b/core/src/main/java/org/apache/calcite/rel/rules/LoptOptimizeJoinRule.java
index 75056dd981..16cb367ac2 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/LoptOptimizeJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/LoptOptimizeJoinRule.java
@@ -2053,7 +2053,7 @@ public static boolean isRemovableSelfJoin(Join joinRel) {
    */
   private static boolean areSelfJoinKeysUnique(RelMetadataQuery mq,
       RelNode leftRel, RelNode rightRel, RexNode joinFilters) {
-    final JoinInfo joinInfo = JoinInfo.createWithStrictEquality(leftRel, 
rightRel, joinFilters);
+    final JoinInfo joinInfo = JoinInfo.of(leftRel, rightRel, joinFilters);
 
     // Make sure each key on the left maps to the same simple column as the
     // corresponding key on the right
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/LoptSemiJoinOptimizer.java 
b/core/src/main/java/org/apache/calcite/rel/rules/LoptSemiJoinOptimizer.java
index e9d5020b0d..724884641d 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/LoptSemiJoinOptimizer.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/LoptSemiJoinOptimizer.java
@@ -264,7 +264,7 @@ private static int isSuitableFilter(
 
     RelNode factRel = multiJoin.getJoinFactor(factIdx);
     RelNode dimRel = multiJoin.getJoinFactor(dimIdx);
-    final JoinInfo joinInfo = JoinInfo.createWithStrictEquality(factRel, 
dimRel, semiJoinCondition);
+    final JoinInfo joinInfo = JoinInfo.of(factRel, dimRel, semiJoinCondition);
     assert !joinInfo.leftKeys.isEmpty();
 
     // mutable copies
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/SemiJoinRule.java 
b/core/src/main/java/org/apache/calcite/rel/rules/SemiJoinRule.java
index 2a07d7fb95..4a10ec533a 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/SemiJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/SemiJoinRule.java
@@ -108,9 +108,9 @@ protected void perform(RelOptRuleCall call, @Nullable 
Project project,
       final ImmutableIntList newRightKeys = 
ImmutableIntList.copyOf(newRightKeyBuilder);
       relBuilder.push(aggregate.getInput());
       final RexNode newCondition =
-          RelOptUtil.createEquiJoinCondition(relBuilder.peek(2, 0),
+          RelOptUtil.createHashJoinCondition(relBuilder.peek(2, 0),
               joinInfo.leftKeys, relBuilder.peek(2, 1), newRightKeys,
-              rexBuilder);
+              joinInfo.nullExclusionFlags, rexBuilder);
       relBuilder.semiJoin(newCondition).hints(join.getHints());
       break;
 
diff --git a/core/src/main/java/org/apache/calcite/runtime/FlatLists.java 
b/core/src/main/java/org/apache/calcite/runtime/FlatLists.java
index 544a8926f0..872bc76a49 100644
--- a/core/src/main/java/org/apache/calcite/runtime/FlatLists.java
+++ b/core/src/main/java/org/apache/calcite/runtime/FlatLists.java
@@ -58,6 +58,19 @@ public static <T> List<T> of(T t0) {
     return new Flat1List<>(t0);
   }
 
+  /**
+   * Creates a flat list with 1 element. This is different from {@link 
#of(Object)}, because
+   * it prevents overload from {@link #of(List)}. This may be useful when you 
create a flat list
+   * with 1 element of List type.
+   *
+   * @param t0  Element
+   * @param <T> Element type
+   * @return    List containing the given members
+   */
+  public static <T> List<T> ofSingle(T t0) {
+    return new Flat1List<>(t0);
+  }
+
   /** Creates a flat list with 2 elements. */
   public static <T> List<T> of(T t0, T t1) {
     return new Flat2List<>(t0, t1);
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java 
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index 14cb12d3e7..3834409640 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -306,6 +306,7 @@ public enum BuiltInMethod {
       FlatProductInputType[].class),
   FLAT_LIST(SqlFunctions.class, "flatList"),
   LIST_N(FlatLists.class, "copyOf", Comparable[].class),
+  LIST1(FlatLists.class, "ofSingle", Object.class),
   LIST2(FlatLists.class, "of", Object.class, Object.class),
   LIST3(FlatLists.class, "of", Object.class, Object.class, Object.class),
   LIST4(FlatLists.class, "of", Object.class, Object.class, Object.class,
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java 
b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index 6ef217fb0f..e25c6f31cc 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -4155,7 +4155,7 @@ public void checkOrderBy(final boolean desc,
         + "on \"t1\".\"commission\" is not distinct from 
\"t2\".\"commission\"";
     CalciteAssert.hr()
         .query(sql)
-        .explainContains("NestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, 
$1)]")
+        .explainContains("HashJoin(condition=[IS NOT DISTINCT FROM($0, $1)]")
         .returnsUnordered("commission=1000",
             "commission=250",
             "commission=500",
diff --git 
a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableHashJoinTest.java
 
b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableHashJoinTest.java
index 614ed98e03..c589f6eea5 100644
--- 
a/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableHashJoinTest.java
+++ 
b/core/src/test/java/org/apache/calcite/test/enumerable/EnumerableHashJoinTest.java
@@ -320,6 +320,137 @@ class EnumerableHashJoinTest {
             "empid=200");
   }
 
+  @Test void hashJoinWithIsNotDistinctFrom() {
+    String tempTableSql = "WITH t1(id, sal) as ( VALUES (1,10), (2,NULL), 
(3,30), (5, NULL)),"
+        + "t2(id, sal) as ( VALUES (1,10), (2,NULL), (4,40), (5, 50) ) ";
+    //        t1                     t2
+    //        id | sal               id | sal
+    //        1  | 10                1  | 10
+    //        2  | NULL              2  | NULL
+    //        3  | 30                4  | 40
+    //        5  | NULL              5  | 50
+
+    // inner join: t1.sal IS NOT DISTINCT FROM t2.sal
+    tester(false, new HrSchema())
+        .query(
+            tempTableSql
+                + "select t1.id, t1.sal, t2.id, t2.sal from t1 join t2"
+                + " on t1.sal is not distinct from t2.sal")
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner ->
+            planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE))
+        .explainContains(
+            "EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($1, $3)], 
joinType=[inner])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 3, 30 
}, { 5, null }]])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 4, 40 
}, { 5, 50 }]])\n")
+        .returnsUnordered(
+            "id=1; sal=10; id=1; sal=10",
+            "id=2; sal=null; id=2; sal=null",
+            "id=5; sal=null; id=2; sal=null");
+
+    // inner join: t1.sal = t2.sal
+    tester(false, new HrSchema())
+        .query(
+            tempTableSql
+                + "select t1.id, t1.sal, t2.id, t2.sal from t1 join t2"
+                + " on t1.sal = t2.sal")
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner ->
+            planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE))
+        .explainContains("EnumerableHashJoin(condition=[=($1, $3)], 
joinType=[inner])\n"
+            + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 3, 30 }, 
{ 5, null }]])\n"
+            + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 4, 40 }, 
{ 5, 50 }]])\n")
+        .returnsUnordered(
+            "id=1; sal=10; id=1; sal=10");
+
+    // inner join: t1.id = t2.id && t1.sal IS NOT DISTINCT FROM t2.sal
+    tester(false, new HrSchema())
+        .query(
+            tempTableSql
+                + "select t1.id, t1.sal, t2.id, t2.sal from t1 join t2"
+                + " on t1.id = t2.id and t1.sal is not distinct from t2.sal")
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner ->
+            planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE))
+        .explainContains(
+            "EnumerableHashJoin(condition=[AND(=($0, $2), IS NOT DISTINCT 
FROM($1, $3))], joinType=[inner])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 3, 30 
}, { 5, null }]])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 4, 40 
}, { 5, 50 }]])\n")
+        .returnsUnordered(
+            "id=1; sal=10; id=1; sal=10",
+            "id=2; sal=null; id=2; sal=null");
+
+    // semi join: t1.sal IS NOT DISTINCT FROM t2.sal
+    tester(true, new HrSchema())
+        .withRel(builder -> {
+          builder
+              .values(new String[]{"id1", "sal1"}, 1, 10, 2, null, 3, 30, 5, 
null)
+              .values(new String[]{"id2", "sal2"}, 1, 10, 2, null, 4, 40, 5, 
50)
+              .semiJoin(
+                  builder.isNotDistinctFrom(
+                      builder.field(2, 0, "sal1"),
+                      builder.field(2, 1, "sal2")));
+          return builder.build();
+        })
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
+          planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
+        })
+        .explainHookMatches(
+            "EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($1, $3)], 
joinType=[semi])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 3, 30 
}, { 5, null }]])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 4, 40 
}, { 5, 50 }]])\n")
+        .returnsUnordered(
+            "id1=1; sal1=10",
+            "id1=2; sal1=null",
+            "id1=5; sal1=null");
+
+    // semi join: t1.sal = t2.sal
+    tester(true, new HrSchema())
+        .withRel(builder -> {
+          builder
+              .values(new String[]{"id1", "sal1"}, 1, 10, 2, null, 3, 30, 5, 
null)
+              .values(new String[]{"id2", "sal2"}, 1, 10, 2, null, 4, 40, 5, 
50)
+              .semiJoin(
+                  builder.equals(
+                      builder.field(2, 0, "sal1"),
+                      builder.field(2, 1, "sal2")));
+          return builder.build();
+        })
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
+          planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
+        })
+        .explainHookMatches(
+            "EnumerableHashJoin(condition=[=($1, $3)], joinType=[semi])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 3, 30 
}, { 5, null }]])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 4, 40 
}, { 5, 50 }]])\n")
+        .returnsUnordered(
+            "id1=1; sal1=10");
+
+    // semi join: t1.id = t2.id && t1.sal IS NOT DISTINCT FROM t2.sal
+    tester(true, new HrSchema())
+        .withRel(builder -> {
+          builder
+              .values(new String[]{"id1", "sal1"}, 1, 10, 2, null, 3, 30, 5, 
null)
+              .values(new String[]{"id2", "sal2"}, 1, 10, 2, null, 4, 40, 5, 
50)
+              .semiJoin(
+                  builder.and(
+                      builder.equals(
+                          builder.field(2, 0, "id1"),
+                          builder.field(2, 1, "id2")),
+                      builder.isNotDistinctFrom(
+                          builder.field(2, 0, "sal1"),
+                          builder.field(2, 1, "sal2"))));
+          return builder.build();
+        })
+        .withHook(Hook.PLANNER, (Consumer<RelOptPlanner>) planner -> {
+          planner.removeRule(EnumerableRules.ENUMERABLE_MERGE_JOIN_RULE);
+        })
+        .explainHookMatches(
+            "EnumerableHashJoin(condition=[AND(=($0, $2), IS NOT DISTINCT 
FROM($1, $3))], joinType=[semi])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 3, 30 
}, { 5, null }]])\n"
+                + "  EnumerableValues(tuples=[[{ 1, 10 }, { 2, null }, { 4, 40 
}, { 5, 50 }]])\n")
+        .returnsUnordered(
+            "id1=1; sal1=10",
+            "id1=2; sal1=null");
+  }
+
   private CalciteAssert.AssertThat tester(boolean forceDecorrelate,
       Object schema) {
     return CalciteAssert.that()
diff --git a/core/src/test/resources/sql/blank.iq 
b/core/src/test/resources/sql/blank.iq
index 882d430412..9c200caf7e 100644
--- a/core/src/test/resources/sql/blank.iq
+++ b/core/src/test/resources/sql/blank.iq
@@ -92,10 +92,10 @@ select i, j from table1 where table1.j NOT IN (select i 
from table2 where table1
 EnumerableCalc(expr#0..7=[{inputs}], expr#8=[0], expr#9=[=($t3, $t8)], 
expr#10=[IS NULL($t1)], expr#11=[IS NOT NULL($t7)], expr#12=[<($t4, $t3)], 
expr#13=[OR($t10, $t11, $t12)], expr#14=[IS NOT TRUE($t13)], expr#15=[OR($t9, 
$t14)], proj#0..1=[{exprs}], $condition=[$t15])
   EnumerableMergeJoin(condition=[AND(=($0, $6), =($1, $5))], joinType=[left])
     EnumerableSort(sort0=[$0], sort1=[$1], dir0=[ASC], dir1=[ASC])
-      EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $2)], 
joinType=[left])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $2)], 
joinType=[left])
         EnumerableTableScan(table=[[BLANK, TABLE1]])
         EnumerableCalc(expr#0..3=[{inputs}], expr#4=[IS NOT NULL($t2)], 
expr#5=[0], expr#6=[CASE($t4, $t2, $t5)], expr#7=[IS NOT NULL($t3)], 
expr#8=[CASE($t7, $t3, $t5)], J=[$t0], c=[$t6], ck=[$t8])
-          EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+          EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
             EnumerableAggregate(group=[{0}])
               EnumerableTableScan(table=[[BLANK, TABLE1]])
             EnumerableAggregate(group=[{1}], c=[COUNT()], ck=[COUNT($0)])
diff --git a/core/src/test/resources/sql/planner.iq 
b/core/src/test/resources/sql/planner.iq
index a24181cadb..0461cc644b 100644
--- a/core/src/test/resources/sql/planner.iq
+++ b/core/src/test/resources/sql/planner.iq
@@ -54,7 +54,7 @@ select * from t as t2 where t2.i > 0;
 
 !ok
 
-EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[semi])
+EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], joinType=[semi])
   EnumerableValues(tuples=[[{ 0 }, { 1 }]])
   EnumerableCalc(expr#0=[{inputs}], expr#1=[0], expr#2=[>($t0, $t1)], 
EXPR$0=[$t0], $condition=[$t2])
     EnumerableValues(tuples=[[{ 0 }, { 1 }]])
@@ -74,7 +74,7 @@ select * from t as t2 where t2.i > 0;
 
 !ok
 
-EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[semi])
+EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], joinType=[semi])
   EnumerableValues(tuples=[[{ 0 }, { 1 }]])
   EnumerableCalc(expr#0=[{inputs}], expr#1=[0], expr#2=[>($t0, $t1)], 
EXPR$0=[$t0], $condition=[$t2])
     EnumerableValues(tuples=[[{ 0 }, { 1 }]])
@@ -215,14 +215,13 @@ select a from (values (1.0), (4.0), (null)) as t3 (a);
 !ok
 
 EnumerableAggregate(group=[{0}])
-  EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[semi])
+  EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], joinType=[semi])
     EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
A=[$t1])
-      EnumerableAggregate(group=[{0}])
-        EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[semi])
-          EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) 
NOT NULL], A=[$t1])
-            EnumerableValues(tuples=[[{ 1.0 }, { 2.0 }, { 3.0 }, { 4.0 }, { 
5.0 }]])
-          EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) 
NOT NULL], A=[$t1])
-            EnumerableValues(tuples=[[{ 1 }, { 2 }]])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[semi])
+        EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
A=[$t1])
+          EnumerableValues(tuples=[[{ 1.0 }, { 2.0 }, { 3.0 }, { 4.0 }, { 5.0 
}]])
+        EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
A=[$t1])
+          EnumerableValues(tuples=[[{ 1 }, { 2 }]])
     EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
A=[$t1])
       EnumerableValues(tuples=[[{ 1.0 }, { 4.0 }, { null }]])
 !plan
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index a12b087ebb..9f320c7ef0 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -536,7 +536,7 @@ EnumerableCalc(expr#0..9=[{inputs}], expr#10=[0], 
expr#11=[=($t5, $t10)], expr#1
             EnumerableTableScan(table=[[scott, DEPT]])
         EnumerableSort(sort0=[$0], dir0=[ASC])
           EnumerableCalc(expr#0..3=[{inputs}], expr#4=[IS NOT NULL($t2)], 
expr#5=[0], expr#6=[CASE($t4, $t2, $t5)], expr#7=[IS NOT NULL($t3)], 
expr#8=[CASE($t7, $t3, $t5)], DEPTNO0=[$t0], c=[$t6], ck=[$t8])
-            EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+            EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
               EnumerableAggregate(group=[{1}])
                 EnumerableHashJoin(condition=[=($1, $2)], joinType=[semi])
                   EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0], 
DEPTNO=[$t7])
@@ -2666,11 +2666,11 @@ EnumerableAggregate(group=[{}], C=[COUNT()])
     EnumerableMergeJoin(condition=[AND(=($3, $5), =($4, $6))], joinType=[left])
       EnumerableSort(sort0=[$3], sort1=[$4], dir0=[ASC], dir1=[ASC])
         EnumerableCalc(expr#0..6=[{inputs}], expr#7=[100], expr#8=[+($t2, 
$t7)], expr#9=[CAST($t1):VARCHAR(14)], SAL=[$t2], c=[$t4], ck=[$t5], $f5=[$t8], 
ENAME0=[$t9])
-          EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($3, $6)], 
joinType=[left])
+          EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($3, $6)], 
joinType=[left])
             EnumerableCalc(expr#0..7=[{inputs}], 
expr#8=[CAST($t1):VARCHAR(14)], proj#0..1=[{exprs}], SAL=[$t5], ENAME0=[$t8])
               EnumerableTableScan(table=[[scott, EMP]])
             EnumerableCalc(expr#0..2=[{inputs}], expr#3=[IS NOT NULL($t2)], 
expr#4=[0], expr#5=[CASE($t3, $t2, $t4)], c=[$t5], ck=[$t5], DNAME=[$t0])
-              EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, 
$1)], joinType=[left])
+              EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
                 EnumerableAggregate(group=[{0}])
                   EnumerableCalc(expr#0..7=[{inputs}], 
expr#8=[CAST($t1):VARCHAR(14)], ENAME0=[$t8])
                     EnumerableTableScan(table=[[scott, EMP]])
@@ -2686,9 +2686,9 @@ select empno from "scott".emp as e
 where e.empno > ANY(
   select 2 from "scott".dept e2 where e2.deptno = e.deptno) ;
 EnumerableCalc(expr#0..6=[{inputs}], EMPNO=[$t5])
-  EnumerableNestedLoopJoin(condition=[AND(IS NOT DISTINCT FROM($6, $4), 
OR(AND(>($5, $0), IS NOT TRUE(OR(IS NULL($3), =($1, 0)))), AND(>($5, $0), IS 
NOT TRUE(OR(IS NULL($3), =($1, 0))), IS NOT TRUE(>($5, $0)), <=($1, $2))))], 
joinType=[inner])
+  EnumerableHashJoin(condition=[AND(IS NOT DISTINCT FROM($4, $6), OR(AND(>($5, 
$0), IS NOT TRUE(OR(IS NULL($3), =($1, 0)))), AND(>($5, $0), IS NOT TRUE(OR(IS 
NULL($3), =($1, 0))), IS NOT TRUE(>($5, $0)), <=($1, $2))))], joinType=[inner])
     EnumerableCalc(expr#0..4=[{inputs}], expr#5=[IS NOT NULL($t3)], 
expr#6=[0], expr#7=[CASE($t5, $t3, $t6)], m=[$t2], c=[$t7], d=[$t7], 
trueLiteral=[$t4], DEPTNO=[$t0])
-      EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
         EnumerableAggregate(group=[{7}])
           EnumerableTableScan(table=[[scott, EMP]])
         EnumerableCalc(expr#0..2=[{inputs}], expr#3=[2], expr#4=[1:BIGINT], 
expr#5=[true], DEPTNO=[$t0], EXPR$0=[$t3], $f2=[$t4], $f3=[$t5])
@@ -2726,7 +2726,7 @@ EnumerableCalc(expr#0..6=[{inputs}], expr#7=[>($t1, 
$t2)], expr#8=[IS TRUE($t7)]
     EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0], DEPTNO=[$t7])
       EnumerableTableScan(table=[[scott, EMP]])
     EnumerableCalc(expr#0..4=[{inputs}], expr#5=[IS NOT NULL($t3)], 
expr#6=[0], expr#7=[CASE($t5, $t3, $t6)], m=[$t2], c=[$t7], d=[$t7], 
trueLiteral=[$t4], DEPTNO0=[$t0])
-      EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
         EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0])
           EnumerableTableScan(table=[[scott, EMP]])
         EnumerableAggregate(group=[{0}], m=[MIN($1)], c=[COUNT()], 
trueLiteral=[LITERAL_AGG(true)])
@@ -2801,7 +2801,7 @@ EnumerableCalc(expr#0..6=[{inputs}], expr#7=[<>($t2, 
$t1)], expr#8=[1], expr#9=[
     EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0])
       EnumerableTableScan(table=[[scott, EMP]])
     EnumerableCalc(expr#0..5=[{inputs}], expr#6=[IS NOT NULL($t2)], 
expr#7=[0], expr#8=[CASE($t6, $t2, $t7)], expr#9=[IS NOT NULL($t3)], 
expr#10=[CASE($t9, $t3, $t7)], c=[$t8], d=[$t8], dd=[$t10], m=[$t4], 
trueLiteral=[$t5], EMPNO1=[$t0])
-      EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
         EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0])
           EnumerableTableScan(table=[[scott, EMP]])
         EnumerableCalc(expr#0..7=[{inputs}], expr#8=[1:BIGINT], expr#9=[true], 
EMPNO1=[$t0], $f1=[$t8], $f2=[$t8], EMPNO=[$t0], $f4=[$t9])
@@ -2845,10 +2845,10 @@ select *
 from "scott".emp emp1
 where empno <> some (select comm from "scott".emp where deptno = emp1.deptno);
 EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t10, $t9)], expr#15=[1], 
expr#16=[<=($t11, $t15)], expr#17=[AND($t14, $t16)], expr#18=[=($t11, $t15)], 
expr#19=[OR($t17, $t18)], expr#20=[<>($t0, $t12)], expr#21=[IS NULL($t13)], 
expr#22=[0], expr#23=[=($t9, $t22)], expr#24=[OR($t21, $t23)], expr#25=[IS NOT 
TRUE($t24)], expr#26=[AND($t19, $t20, $t25)], expr#27=[IS NOT TRUE($t19)], 
expr#28=[AND($t25, $t27)], expr#29=[OR($t26, $t28)], proj#0..7=[{exprs}], 
$condition=[$t29])
-  EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($7, $8)], 
joinType=[left])
+  EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($7, $8)], joinType=[left])
     EnumerableTableScan(table=[[scott, EMP]])
     EnumerableCalc(expr#0..6=[{inputs}], expr#7=[IS NOT NULL($t2)], 
expr#8=[0], expr#9=[CASE($t7, $t2, $t8)], expr#10=[IS NOT NULL($t3)], 
expr#11=[CASE($t10, $t3, $t8)], expr#12=[IS NOT NULL($t4)], expr#13=[CASE($t12, 
$t4, $t8)], DEPTNO=[$t0], c=[$t9], d=[$t11], dd=[$t13], m=[$t5], 
trueLiteral=[$t6])
-      EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
         EnumerableAggregate(group=[{7}])
           EnumerableTableScan(table=[[scott, EMP]])
         EnumerableCalc(expr#0..5=[{inputs}], expr#6=[CAST($t1):BIGINT NOT 
NULL], expr#7=[CAST($t2):BIGINT NOT NULL], expr#8=[CAST($t5):BOOLEAN NOT NULL], 
DEPTNO=[$t0], c=[$t6], d=[$t7], dd=[$t3], m=[$t4], trueLiteral=[$t8])
@@ -2905,7 +2905,7 @@ EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t9, 
$t8)], expr#15=[1], expr#
   EnumerableHashJoin(condition=[=($0, $13)], joinType=[left])
     EnumerableTableScan(table=[[scott, EMP]])
     EnumerableCalc(expr#0..5=[{inputs}], expr#6=[IS NOT NULL($t2)], 
expr#7=[0], expr#8=[CASE($t6, $t2, $t7)], expr#9=[IS NOT NULL($t3)], 
expr#10=[CASE($t9, $t3, $t7)], c=[$t8], d=[$t8], dd=[$t10], m=[$t4], 
trueLiteral=[$t5], DEPTNO0=[$t0])
-      EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
         EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0])
           EnumerableTableScan(table=[[scott, EMP]])
         EnumerableCalc(expr#0..4=[{inputs}], expr#5=[CAST($t1):BIGINT NOT 
NULL], expr#6=[CAST($t3):INTEGER NOT NULL], expr#7=[CAST($t4):BOOLEAN NOT 
NULL], DEPTNO0=[$t0], c=[$t5], dd=[$t2], m=[$t6], trueLiteral=[$t7])
@@ -2956,7 +2956,7 @@ EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t9, 
$t8)], expr#15=[1], expr#
   EnumerableHashJoin(condition=[=($0, $13)], joinType=[left])
     EnumerableTableScan(table=[[scott, EMP]])
     EnumerableCalc(expr#0..5=[{inputs}], expr#6=[IS NOT NULL($t2)], 
expr#7=[0], expr#8=[CASE($t6, $t2, $t7)], expr#9=[IS NOT NULL($t3)], 
expr#10=[CASE($t9, $t3, $t7)], c=[$t8], d=[$t8], dd=[$t10], m=[$t4], 
trueLiteral=[$t5], DEPTNO0=[$t0])
-      EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
         EnumerableCalc(expr#0..7=[{inputs}], EMPNO=[$t0])
           EnumerableTableScan(table=[[scott, EMP]])
         EnumerableCalc(expr#0..4=[{inputs}], expr#5=[CAST($t1):BIGINT NOT 
NULL], expr#6=[CAST($t3):INTEGER NOT NULL], expr#7=[CAST($t4):BOOLEAN NOT 
NULL], DEPTNO0=[$t0], c=[$t5], dd=[$t2], m=[$t6], trueLiteral=[$t7])
@@ -3004,10 +3004,10 @@ select *
 from "scott".emp emp1
 where emp1.comm <> some (select comm from "scott".emp emp2 where emp2.sal = 
emp1.sal);
 EnumerableCalc(expr#0..13=[{inputs}], expr#14=[<>($t10, $t9)], expr#15=[1], 
expr#16=[<=($t11, $t15)], expr#17=[AND($t14, $t16)], expr#18=[=($t11, $t15)], 
expr#19=[OR($t17, $t18)], expr#20=[<>($t6, $t12)], expr#21=[IS NULL($t13)], 
expr#22=[IS NULL($t6)], expr#23=[0], expr#24=[=($t9, $t23)], expr#25=[OR($t21, 
$t22, $t24)], expr#26=[IS NOT TRUE($t25)], expr#27=[AND($t19, $t20, $t26)], 
expr#28=[IS NOT TRUE($t19)], expr#29=[AND($t26, $t28)], expr#30=[OR($t27, 
$t29)], proj#0..7=[{exprs}], $con [...]
-  EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($5, $8)], 
joinType=[left])
+  EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($5, $8)], joinType=[left])
     EnumerableTableScan(table=[[scott, EMP]])
     EnumerableCalc(expr#0..6=[{inputs}], expr#7=[IS NOT NULL($t2)], 
expr#8=[0], expr#9=[CASE($t7, $t2, $t8)], expr#10=[IS NOT NULL($t3)], 
expr#11=[CASE($t10, $t3, $t8)], expr#12=[IS NOT NULL($t4)], expr#13=[CASE($t12, 
$t4, $t8)], SAL=[$t0], c=[$t9], d=[$t11], dd=[$t13], m=[$t5], trueLiteral=[$t6])
-      EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
         EnumerableAggregate(group=[{5}])
           EnumerableTableScan(table=[[scott, EMP]])
         EnumerableCalc(expr#0..5=[{inputs}], expr#6=[CAST($t1):BIGINT NOT 
NULL], expr#7=[CAST($t2):BIGINT NOT NULL], expr#8=[CAST($t5):BOOLEAN NOT NULL], 
SAL=[$t0], c=[$t6], d=[$t7], dd=[$t3], m=[$t4], trueLiteral=[$t8])
@@ -5388,10 +5388,10 @@ select * from emp where deptno <> (select count(deptno) 
from dept where dept.dep
 
 !ok
 EnumerableCalc(expr#0..4=[{inputs}], expr#5=[IS NULL($t4)], 
expr#6=[CAST($t1):BIGINT], expr#7=[0:BIGINT], expr#8=[<>($t6, $t7)], 
expr#9=[AND($t5, $t8)], expr#10=[<>($t6, $t4)], expr#11=[OR($t9, $t10)], 
proj#0..2=[{exprs}], $condition=[$t11])
-  EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($1, $3)], 
joinType=[left])
+  EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($1, $3)], joinType=[left])
     EnumerableValues(tuples=[[{ 'Jane ', 10, 'F' }, { 'Bob  ', 10, 'M' }, { 
'Eric ', 20, 'M' }, { 'Susan', 30, 'F' }, { 'Alice', 30, 'F' }, { 'Adam ', 50, 
'M' }, { 'Eve  ', 50, 'F' }, { 'Grace', 60, 'F' }, { 'Wilma', null, 'F' }]])
     EnumerableCalc(expr#0..2=[{inputs}], expr#3=[IS NOT NULL($t2)], 
expr#4=[0], expr#5=[CASE($t3, $t2, $t4)], DEPTNO=[$t0], EXPR$0=[$t5])
-      EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
+      EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($0, $1)], 
joinType=[left])
         EnumerableAggregate(group=[{0}])
           EnumerableValues(tuples=[[{ 10 }, { 10 }, { 20 }, { 30 }, { 30 }, { 
50 }, { 50 }, { 60 }, { null }]])
         EnumerableCalc(expr#0..1=[{inputs}], expr#2=[1:BIGINT], DEPTNO=[$t0], 
$f1=[$t2])
@@ -5414,7 +5414,7 @@ select * from emp where deptno <> (select count(deptno) + 
10  from dept where de
 
 !ok
 EnumerableCalc(expr#0..4=[{inputs}], expr#5=[CAST($t1):BIGINT], expr#6=[IS 
NULL($t4)], expr#7=[0:BIGINT], expr#8=[CASE($t6, $t7, $t4)], expr#9=[10], 
expr#10=[+($t8, $t9)], expr#11=[<>($t5, $t10)], proj#0..2=[{exprs}], 
$condition=[$t11])
-  EnumerableNestedLoopJoin(condition=[IS NOT DISTINCT FROM($1, $3)], 
joinType=[left])
+  EnumerableHashJoin(condition=[IS NOT DISTINCT FROM($1, $3)], joinType=[left])
     EnumerableValues(tuples=[[{ 'Jane ', 10, 'F' }, { 'Bob  ', 10, 'M' }, { 
'Eric ', 20, 'M' }, { 'Susan', 30, 'F' }, { 'Alice', 30, 'F' }, { 'Adam ', 50, 
'M' }, { 'Eve  ', 50, 'F' }, { 'Grace', 60, 'F' }, { 'Wilma', null, 'F' }]])
     EnumerableCalc(expr#0=[{inputs}], expr#1=[0], expr#2=[CAST($t1):BIGINT NOT 
NULL], DEPTNO=[$t0], $f1=[$t2])
       EnumerableAggregate(group=[{0}])
diff --git 
a/linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java 
b/linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java
index 335fbc89b2..14309bdfd6 100644
--- a/linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java
+++ b/linq4j/src/main/java/org/apache/calcite/linq4j/EnumerableDefaults.java
@@ -1543,18 +1543,15 @@ private static <TSource, TInner, TKey, TResult> 
Enumerable<TResult> hashEquiJoin
               }
               final TSource outer = outers.current();
               final Enumerable<TInner> innerEnumerable;
-              if (outer == null) {
+              // if the key is null-safe, still extract outerKey and probe 
even if outer is NULL.
+              final TKey outerKey = outerKeySelector.apply(outer);
+              if (outerKey == null) {
                 innerEnumerable = null;
               } else {
-                final TKey outerKey = outerKeySelector.apply(outer);
-                if (outerKey == null) {
-                  innerEnumerable = null;
-                } else {
-                  if (unmatchedKeys != null) {
-                    unmatchedKeys.remove(outerKey);
-                  }
-                  innerEnumerable = innerLookup.get(outerKey);
+                if (unmatchedKeys != null) {
+                  unmatchedKeys.remove(outerKey);
                 }
+                innerEnumerable = innerLookup.get(outerKey);
               }
               if (innerEnumerable == null
                   || !innerEnumerable.any()) {
@@ -1639,30 +1636,27 @@ private static <TSource, TInner, TKey, TResult> 
Enumerable<TResult> hashJoinWith
               }
               final TSource outer = outers.current();
               Enumerable<TInner> innerEnumerable;
-              if (outer == null) {
+              // if the key is null-safe, still extract outerKey and probe 
even if outer is NULL.
+              final TKey outerKey = outerKeySelector.apply(outer);
+              if (outerKey == null) {
                 innerEnumerable = null;
               } else {
-                final TKey outerKey = outerKeySelector.apply(outer);
-                if (outerKey == null) {
-                  innerEnumerable = null;
-                } else {
-                  innerEnumerable = innerLookup.get(outerKey);
-                  // apply predicate to filter per-row
-                  if (innerEnumerable != null) {
-                    final List<TInner> matchedInners = new ArrayList<>();
-                    try (Enumerator<TInner> innerEnumerator =
-                        innerEnumerable.enumerator()) {
-                      while (innerEnumerator.moveNext()) {
-                        final TInner inner = innerEnumerator.current();
-                        if (predicate.apply(outer, inner)) {
-                          matchedInners.add(inner);
-                        }
+                innerEnumerable = innerLookup.get(outerKey);
+                // apply predicate to filter per-row
+                if (innerEnumerable != null) {
+                  final List<TInner> matchedInners = new ArrayList<>();
+                  try (Enumerator<TInner> innerEnumerator =
+                      innerEnumerable.enumerator()) {
+                    while (innerEnumerator.moveNext()) {
+                      final TInner inner = innerEnumerator.current();
+                      if (predicate.apply(outer, inner)) {
+                        matchedInners.add(inner);
                       }
                     }
-                    innerEnumerable = Linq4j.asEnumerable(matchedInners);
-                    if (innersUnmatched != null) {
-                      innersUnmatched.removeAll(matchedInners);
-                    }
+                  }
+                  innerEnumerable = Linq4j.asEnumerable(matchedInners);
+                  if (innersUnmatched != null) {
+                    innersUnmatched.removeAll(matchedInners);
                   }
                 }
               }

Reply via email to