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

amashenkov 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 97bb0b78dea IGNITE-26014 Sql. ArrayIndexOutOfBoundsException when 
converting MERGE with join (#6335)
97bb0b78dea is described below

commit 97bb0b78dea9794ae7b6fe02539ed09d15f91618
Author: Andrew V. Mashenkov <[email protected]>
AuthorDate: Fri Aug 1 14:47:50 2025 +0300

    IGNITE-26014 Sql. ArrayIndexOutOfBoundsException when converting MERGE with 
join (#6335)
---
 .../engine/prepare/IgniteSqlToRelConvertor.java    | 42 +++++++++-
 .../sql/engine/planner/DmlPlannerTest.java         | 94 +++++++++++++++++++++-
 2 files changed, 134 insertions(+), 2 deletions(-)

diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlToRelConvertor.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlToRelConvertor.java
index 0220e52aa28..adb6012cf4b 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlToRelConvertor.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlToRelConvertor.java
@@ -233,6 +233,7 @@ public class IgniteSqlToRelConvertor extends 
SqlToRelConverter implements Initia
         int numLevel1Exprs = 0;
         List<RexNode> level1InsertExprs = null;
         List<RexNode> level2InsertExprs = null;
+        boolean needRepairProject = false;
         if (insertCall != null) {
             RelNode insertRel = convertInsert(insertCall);
 
@@ -264,11 +265,19 @@ public class IgniteSqlToRelConvertor extends 
SqlToRelConverter implements Initia
             if (!input.getInputs().isEmpty() && input.getInput(0) instanceof 
LogicalProject) {
                 level2InsertExprs = ((LogicalProject) 
input.getInput(0)).getProjects();
             }
+
+            // If source rel contains project, then we expect at least 3 
nested projects,
+            // otherwise it means source rel project was merged unexpectedly 
and project must be repaired.
+            needRepairProject = ((LogicalJoin) 
mergeSourceRel.getInput(0)).getLeft() instanceof LogicalProject
+                    && (input.getInputs().isEmpty()
+                    || !(input.getInput(0) instanceof LogicalProject)
+                    || input.getInput(0).getInputs().isEmpty()
+                    || !(input.getInput(0).getInput(0) instanceof 
LogicalProject));
         }
 
         LogicalJoin join = (LogicalJoin) mergeSourceRel.getInput(0);
 
-        final List<RexNode> projects = new ArrayList<>();
+        List<RexNode> projects = new ArrayList<>();
 
         for (int level1Idx = 0; level1Idx < numLevel1Exprs; level1Idx++) {
             requireNonNull(level1InsertExprs, "level1InsertExprs");
@@ -281,6 +290,14 @@ public class IgniteSqlToRelConvertor extends 
SqlToRelConverter implements Initia
                 projects.add(level1InsertExprs.get(level1Idx));
             }
         }
+
+        // It is possible the method `convertInsert` merge projections (e.g. 
due to RelBuilder.Config.withBloat)
+        // In that case, we should recover project on top of source project 
(mergeSourceRel left branch) to get correct input refs.
+        // Most likely, we should disable bloat, but the `relBuilder` is out 
of our control, due parent private field visibility.
+        if (needRepairProject) {
+            projects = repairProject(join, projects);
+        }
+
         if (updateCall != null) {
             final LogicalProject project = (LogicalProject) mergeSourceRel;
             projects.addAll(project.getProjects());
@@ -301,6 +318,29 @@ public class IgniteSqlToRelConvertor extends 
SqlToRelConverter implements Initia
                 targetColumnNameList, null, false);
     }
 
+    /**
+     * This is a dirty hack to fix merged InsertCall projection.
+     */
+    private List<RexNode> repairProject(LogicalJoin join, List<RexNode> 
actual) {
+        if (!(join.getLeft() instanceof LogicalProject)) {
+            return actual;
+        }
+
+        List<RexNode> original = ((LogicalProject) 
join.getLeft()).getProjects();
+
+        ArrayList<RexNode> recovered = new ArrayList<>(actual.size());
+        for (RexNode rexNode : actual) {
+            int index = original.indexOf(rexNode);
+            if (index == -1) {
+                recovered.add(rexNode);
+            } else {
+                recovered.add(rexBuilder.makeInputRef(rexNode.getType(), 
index));
+            }
+        }
+
+        return recovered;
+    }
+
     @Override
     public RelOptTable getTargetTable(SqlNode call) {
         return super.getTargetTable(call);
diff --git 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/DmlPlannerTest.java
 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/DmlPlannerTest.java
index 0e4f16c5b32..8aded72edbe 100644
--- 
a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/DmlPlannerTest.java
+++ 
b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/DmlPlannerTest.java
@@ -18,17 +18,21 @@
 package org.apache.ignite.internal.sql.engine.planner;
 
 import static org.apache.ignite.internal.sql.engine.util.Commons.cast;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import java.util.List;
 import java.util.UUID;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.sql.validate.SqlValidatorException;
 import org.apache.ignite.internal.sql.engine.framework.TestBuilders;
 import org.apache.ignite.internal.sql.engine.rel.IgniteExchange;
 import org.apache.ignite.internal.sql.engine.rel.IgniteKeyValueModify;
+import org.apache.ignite.internal.sql.engine.rel.IgniteMergeJoin;
 import org.apache.ignite.internal.sql.engine.rel.IgniteProject;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTableModify;
 import org.apache.ignite.internal.sql.engine.rel.IgniteTableScan;
@@ -263,7 +267,7 @@ public class DmlPlannerTest extends AbstractPlannerTest {
         IgniteSchema schema = createSchema(test);
 
         IgniteTestUtils.assertThrowsWithCause(
-                () ->  physicalPlan(query, schema),
+                () -> physicalPlan(query, schema),
                 SqlValidatorException.class,
                 "Primary key columns are not modifiable"
         );
@@ -316,6 +320,94 @@ public class DmlPlannerTest extends AbstractPlannerTest {
         );
     }
 
+    @Test
+    public void testMergeWithSubquery() throws Exception {
+        IgniteTable test1 = TestBuilders.table()
+                .name("T1")
+                .addKeyColumn("ID", NativeTypes.INT32)
+                .addColumn("VAL1", NativeTypes.INT32)
+                .addColumn("VAL2", NativeTypes.INT32)
+                .distribution(IgniteDistributions.single())
+                .build();
+
+        IgniteTable test2 = TestBuilders.table()
+                .name("T2")
+                .addKeyColumn("ID", NativeTypes.INT32)
+                .addColumn("VAL1", NativeTypes.INT32)
+                .addColumn("VAL2", NativeTypes.INT32)
+                .addColumn("VAL3", NativeTypes.INT32)
+                .addColumn("VAL4", NativeTypes.INT32)
+                .addColumn("VAL5", NativeTypes.INT32)
+                .distribution(IgniteDistributions.single())
+                .build();
+
+        IgniteSchema schema = createSchema(test1, test2);
+
+        assertPlan(
+                "MERGE INTO t1 dst\n"
+                        + " USING (\n"
+                        + "    SELECT t1.id, t2.val5\n"
+                        + "      FROM t1 LEFT JOIN t2 ON t1.id = t2.id\n"
+                        + " ) src\n"
+                        + "   ON src.id = dst.id\n"
+                        + " WHEN MATCHED THEN UPDATE SET val1 = src.val5\n"
+                        + " WHEN NOT MATCHED THEN INSERT (id, val1) VALUES 
(src.id, src.val5)",
+                schema,
+                isInstanceOf(IgniteTableModify.class)
+                        
.and(hasChildThat(isInstanceOf(IgniteProject.class).and(
+                                expectedProject(
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 3,
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 4,
+                                        p -> p instanceof RexLiteral && 
((RexLiteral) p).isNull(),
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 0,
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 1,
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 2,
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 4
+                                ))))
+                        .and(hasChildThat(isInstanceOf(IgniteMergeJoin.class)
+                                .and(m -> 
m.getRowType().getFieldNames().equals(List.of("ID", "VAL1", "VAL2", "ID$0", 
"VAL5")))
+                        )));
+
+        assertPlan(
+                "MERGE INTO t1 dst\n"
+                        + " USING (\n"
+                        + "    SELECT t1.id, t2.val5\n"
+                        + "      FROM t1 LEFT JOIN t2 ON t1.id = t2.id\n"
+                        + " ) src\n"
+                        + "   ON src.id = dst.id\n"
+                        + " WHEN MATCHED THEN UPDATE SET val1 = src.val5\n"
+                        + " WHEN NOT MATCHED THEN INSERT (id, val2) VALUES 
(src.id, src.val5)",
+                schema,
+                isInstanceOf(IgniteTableModify.class)
+                        
.and(hasChildThat(isInstanceOf(IgniteProject.class).and(
+                                expectedProject(
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 3,
+                                        p -> p instanceof RexLiteral && 
((RexLiteral) p).isNull(),
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 4,
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 0,
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 1,
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 2,
+                                        p -> p instanceof RexInputRef && 
((RexInputRef) p).getIndex() == 4
+                                ))))
+                        .and(hasChildThat(isInstanceOf(IgniteMergeJoin.class)
+                                .and(m -> 
m.getRowType().getFieldNames().equals(List.of("ID", "VAL1", "VAL2", "ID$0", 
"VAL5")))
+                        )));
+    }
+
+    @SafeVarargs
+    private static Predicate<IgniteProject> 
expectedProject(Predicate<RexNode>... predicates) {
+        return p -> {
+            int i = 0;
+            assertEquals(p.getProjects().size(), predicates.length);
+            for (RexNode project : p.getProjects()) {
+                if (!predicates[i++].test(project)) {
+                    return false;
+                }
+            }
+            return true;
+        };
+    }
+
     private static Stream<String> updatePrimaryKey() {
         return Stream.of(
                 "UPDATE TEST SET ID = ID + 1",

Reply via email to