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

korlov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 74620272892 IGNITE-27701 Sql. Wrong result is returned for query with 
LEFT JOIN (#7501)
74620272892 is described below

commit 746202728927d18f06dfae2690521c7f61a19fb0
Author: korlov42 <[email protected]>
AuthorDate: Mon Feb 2 14:45:17 2026 +0200

    IGNITE-27701 Sql. Wrong result is returned for query with LEFT JOIN (#7501)
---
 .../ignite/internal/sql/engine/ItJoinTest.java     | 13 ++++++
 .../internal/sql/engine/exec/rel/HashJoinNode.java | 46 +++++++++++++++++++---
 .../engine/exec/rel/AbstractJoinExecutionTest.java |  4 +-
 3 files changed, 56 insertions(+), 7 deletions(-)

diff --git 
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItJoinTest.java
 
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItJoinTest.java
index 5c77a2aa502..b68e87f5d09 100644
--- 
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItJoinTest.java
+++ 
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItJoinTest.java
@@ -1181,4 +1181,17 @@ public class ItJoinTest extends BaseSqlIntegrationTest {
                 + " MATCH_CONDITION t2.t < t1.t\n"
                 + " ON t1.k = t2.k"));
     }
+
+    @ParameterizedTest
+    @EnumSource(mode = Mode.EXCLUDE, names = {"CORRELATED", "MERGE"})
+    void testLeftJoinWithDuplicatesAndAlwaysFalseNonEquiCondition(JoinType 
joinType) {
+        assertQuery("SELECT t1.id FROM t1 LEFT JOIN t1 AS t2 ON t1.c1 = t2.c1 
AND t1.id < 0", joinType)
+                .returns(0)
+                .returns(1)
+                .returns(2)
+                .returns(3)
+                .returns(4)
+                .returns(5)
+                .check();
+    }
 }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashJoinNode.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashJoinNode.java
index 394a63c832a..b3aa1a23b7a 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashJoinNode.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/exec/rel/HashJoinNode.java
@@ -251,6 +251,8 @@ public abstract class HashJoinNode<RowT> extends 
AbstractRightMaterializedJoinNo
         private final RowFactory<RowT> rightRowFactory;
         private final SqlJoinProjection outputProjection;
 
+        private boolean matched;
+
         /**
          * Creates HashJoinNode for LEFT OUTER JOIN operator.
          *
@@ -272,6 +274,14 @@ public abstract class HashJoinNode<RowT> extends 
AbstractRightMaterializedJoinNo
             this.rightRowFactory = rightRowFactory;
         }
 
+        /** {@inheritDoc} */
+        @Override
+        protected void rewindInternal() {
+            matched = false;
+
+            super.rewindInternal();
+        }
+
         /** {@inheritDoc} */
         @Override
         protected void join() throws Exception {
@@ -284,7 +294,22 @@ public abstract class HashJoinNode<RowT> extends 
AbstractRightMaterializedJoinNo
 
                         // Proceed with next left row, if previous was fully 
processed.
                         if (!rightIt.hasNext()) {
+                            // If this is not the first iteration and row 
wasn't previously matched,
+                            // then before advancing to the next one we should 
emit current as non-matched.
+                            if (left != null && !matched) {
+                                --requested;
+                                RowT row = outputProjection.project(context(), 
left, rightRowFactory.create());
+                                downstream().push(row);
+
+                                left = null;
+                            }
+
+                            if (leftInBuf.isEmpty()) {
+                                break;
+                            }
+
                             left = leftInBuf.remove();
+                            matched = false;
 
                             Collection<RowT> rightRows = lookup(left);
 
@@ -310,9 +335,10 @@ public abstract class HashJoinNode<RowT> extends 
AbstractRightMaterializedJoinNo
                                 RowT right = rightIt.next();
 
                                 if (checkNonEquiCondition && 
!nonEquiCondition.test(left, right)) {
-                                    right = rightRowFactory.create();
+                                    continue;
                                 }
 
+                                matched = true;
                                 --requested;
 
                                 RowT row = outputProjection.project(context(), 
left, right);
@@ -320,7 +346,11 @@ public abstract class HashJoinNode<RowT> extends 
AbstractRightMaterializedJoinNo
                             }
                         }
 
-                        if (!rightIt.hasNext()) {
+                        // Postpone nullification of `left` row if no match 
was found.
+                        // Left join should emit any non matched row from left 
side, but 
+                        // by this point we may run out of `requested` quota. 
Therefore we
+                        // handle this case during next iteration.
+                        if (!rightIt.hasNext() && matched) {
                             left = null;
                         }
                     }
@@ -357,7 +387,9 @@ public abstract class HashJoinNode<RowT> extends 
AbstractRightMaterializedJoinNo
         ) {
             super(ctx, joinInfo, nonEquiCondition);
 
-            assert nonEquiCondition == null : "Non equi condition is not 
supported in RIGHT join";
+            if (nonEquiCondition != null) {
+                throw new IllegalStateException("Non equi condition is not 
supported in RIGHT join");
+            }
 
             this.outputProjection = outputProjection;
             this.leftRowFactory = leftRowFactory;
@@ -514,7 +546,9 @@ public abstract class HashJoinNode<RowT> extends 
AbstractRightMaterializedJoinNo
         ) {
             super(ctx, joinInfo, nonEquiCondition);
 
-            assert nonEquiCondition == null : "Non equi condition is not 
supported in FULL OUTER join";
+            if (nonEquiCondition != null) {
+                throw new IllegalStateException("Non equi condition is not 
supported in FULL OUTER join");
+            }
 
             this.outputProjection = outputProjection;
             this.leftRowFactory = leftRowFactory;
@@ -750,7 +784,9 @@ public abstract class HashJoinNode<RowT> extends 
AbstractRightMaterializedJoinNo
         ) {
             super(ctx, joinInfo, nonEquiCondition);
 
-            assert nonEquiCondition == null : "Non equi condition is not 
supported in ANTI join";
+            if (nonEquiCondition != null) {
+                throw new IllegalStateException("Non equi condition is not 
supported in ANTI join");
+            }
         }
 
         /** {@inheritDoc} */
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractJoinExecutionTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractJoinExecutionTest.java
index 93525d533d1..d82e120e6b4 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractJoinExecutionTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/rel/AbstractJoinExecutionTest.java
@@ -518,7 +518,7 @@ public abstract class AbstractJoinExecutionTest extends 
AbstractExecutionTest<Ob
     }
 
     @ParameterizedTest
-    @EnumSource(value = JoinRelType.class, names = {"INNER", "SEMI"})
+    @EnumSource(value = JoinRelType.class, names = {"INNER", "SEMI", "LEFT"})
     void nonEquiJoinWithDifferentBufferSize(JoinRelType joinType) {
         int buffSize = 1;
         validateNonEquiJoin(executionContext(buffSize), joinType, 0, 0);
@@ -620,7 +620,7 @@ public abstract class AbstractJoinExecutionTest extends 
AbstractExecutionTest<Ob
                 (l, r) -> false,
                 () -> IntStream.range(0, leftSize).mapToObj(i -> 
person).iterator(),
                 () -> IntStream.range(0, rightSize).mapToObj(i -> 
department).iterator(),
-                0
+                joinType == LEFT ? leftSize : 0
         );
     }
 

Reply via email to