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

jooger 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 e2b60531736 IGNITE-27718 Fix ClassCastException in UPDATEs with table 
level hints (#7715)
e2b60531736 is described below

commit e2b605317365eff5863ef3c4fc558fc89f216db8
Author: Ilya Korol <[email protected]>
AuthorDate: Tue Mar 31 13:05:13 2026 +0300

    IGNITE-27718 Fix ClassCastException in UPDATEs with table level hints 
(#7715)
---
 .../datatypes/tests/BaseIndexDataTypeTest.java     | 64 ++++++++++++++++++++++
 .../sql/engine/prepare/IgniteSqlValidator.java     | 53 +++++++++++++++---
 .../internal/sql/engine/util/QueryChecker.java     | 52 +++++++++++++++---
 3 files changed, 152 insertions(+), 17 deletions(-)

diff --git 
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseIndexDataTypeTest.java
 
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseIndexDataTypeTest.java
index 3f0217b2da9..352edd3d9f6 100644
--- 
a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseIndexDataTypeTest.java
+++ 
b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseIndexDataTypeTest.java
@@ -19,6 +19,8 @@ package org.apache.ignite.internal.sql.engine.datatypes.tests;
 
 import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
 import static 
org.apache.ignite.internal.sql.engine.util.QueryChecker.containsIndexScan;
+import static 
org.apache.ignite.internal.sql.engine.util.QueryChecker.containsIndexScanIgnoreBounds;
+import static 
org.apache.ignite.internal.sql.engine.util.QueryChecker.containsIndexScanIgnoreBoundsAtLeastOnce;
 import static 
org.apache.ignite.internal.sql.engine.util.QueryChecker.containsTableScan;
 
 import java.util.stream.Stream;
@@ -234,6 +236,68 @@ public abstract class BaseIndexDataTypeTest<T extends 
Comparable<T>> extends Bas
                 .check();
     }
 
+    /**
+     * Hint in update clause.
+     */
+    @Test
+    public void testUpdateWithHint() {
+        checkQuery("UPDATE t /*+ FORCE_INDEX(t_test_key_idx) */ SET test_key = 
$0")
+                .matches(containsIndexScanIgnoreBounds("PUBLIC", "T", 
"T_TEST_KEY_IDX"))
+                .returnSomething()
+                .check();
+
+        checkQuery("UPDATE t /*+ FORCE_INDEX(t_test_key_idx) */ SET test_key = 
$2 WHERE test_key = $0")
+                .matches(containsIndexScanIgnoreBounds("PUBLIC", "T", 
"T_TEST_KEY_IDX"))
+                .returnSomething()
+                .check();
+
+        checkQuery("UPDATE t /*+ DISABLE_DECORRELATION */ SET test_key = $0")
+                .returnSomething()
+                .check();
+    }
+
+    /**
+     * Hint in delete clause.
+     */
+    @Test
+    public void testDeleteWithHint() {
+        checkQuery("DELETE FROM t /*+ FORCE_INDEX(t_test_key_idx) */")
+                .matches(containsIndexScanIgnoreBounds("PUBLIC", "T", 
"T_TEST_KEY_IDX"))
+                .returnSomething()
+                .check();
+
+        checkQuery("DELETE FROM t /*+ FORCE_INDEX(t_test_key_idx) */ WHERE 
test_key = $0")
+                .matches(containsIndexScanIgnoreBounds("PUBLIC", "T", 
"T_TEST_KEY_IDX"))
+                .returnSomething()
+                .check();
+
+        checkQuery("DELETE FROM t /*+ DISABLE_DECORRELATION */ WHERE test_key 
= $0")
+                .returnSomething()
+                .check();
+    }
+
+    /**
+     * Hint in merge clause.
+     */
+    @Test
+    public void testMergeWithHint() {
+        checkQuery("MERGE INTO t dst USING t /*+ FORCE_INDEX(t_test_key_idx) 
*/ src ON dst.test_key = src.test_key "
+                + "WHEN MATCHED THEN UPDATE SET test_key = $0 "
+                + "WHEN NOT MATCHED THEN INSERT (id, test_key) VALUES (src.id, 
$1)"
+        )
+                .matches(containsIndexScanIgnoreBoundsAtLeastOnce("PUBLIC", 
"T", "T_TEST_KEY_IDX"))
+                .returnSomething()
+                .check();
+
+        checkQuery("MERGE INTO t /*+ FORCE_INDEX(t_test_key_idx) */ dst USING 
t src ON dst.test_key = src.test_key "
+                + "WHEN MATCHED THEN UPDATE SET test_key = $0 "
+                + "WHEN NOT MATCHED THEN INSERT (id, test_key) VALUES (src.id, 
$1)"
+        )
+                .matches(containsIndexScanIgnoreBoundsAtLeastOnce("PUBLIC", 
"T", "T_TEST_KEY_IDX"))
+                .returnSomething()
+                .check();
+    }
+
     public Stream<TestTypeArguments<T>> compoundIndex() {
         return TestTypeArguments.unary(testTypeSpec, dataSamples, 
values.get(0));
     }
diff --git 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java
 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java
index 3d37d43b425..dfd658761d7 100644
--- 
a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java
+++ 
b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteSqlValidator.java
@@ -73,6 +73,7 @@ import org.apache.calcite.sql.SqlNode;
 import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlOperatorTable;
 import org.apache.calcite.sql.SqlSelect;
+import org.apache.calcite.sql.SqlTableRef;
 import org.apache.calcite.sql.SqlTypeNameSpec;
 import org.apache.calcite.sql.SqlUnknownLiteral;
 import org.apache.calcite.sql.SqlUpdate;
@@ -97,6 +98,7 @@ import org.apache.calcite.sql.validate.SqlValidatorScope;
 import org.apache.calcite.sql.validate.SqlValidatorTable;
 import org.apache.calcite.sql.validate.SqlValidatorUtil;
 import org.apache.calcite.util.TimestampString;
+import org.apache.ignite.internal.lang.IgniteBiTuple;
 import org.apache.ignite.internal.sql.engine.exec.exp.IgniteSqlFunctions;
 import org.apache.ignite.internal.sql.engine.schema.IgniteDataSource;
 import org.apache.ignite.internal.sql.engine.schema.IgniteSystemView;
@@ -227,7 +229,7 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
     @Override
     public void validateInsert(SqlInsert insert) {
         SqlValidatorTable table = table(validatedNamespace(insert, 
unknownType));
-        IgniteTable igniteTable = 
getIgniteTableForModification((SqlIdentifier) insert.getTargetTable(), table);
+        IgniteTable igniteTable = 
resolveIgniteTableForModification(insert.getTargetTable(), table);
 
         if (insert.getTargetColumnList() == null) {
             insert.setOperand(3, inferColumnList(igniteTable));
@@ -434,6 +436,15 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
         return getIgniteTableForModification(identifier, table);
     }
 
+    private IgniteTable resolveIgniteTableForModification(SqlNode table, 
SqlValidatorTable validatorTable) {
+        IgniteBiTuple<SqlIdentifier, @Nullable SqlNodeList> resolvedTable = 
resolveTableIdentifierAndHints(table);
+
+        SqlIdentifier targetTable = resolvedTable.get1();
+        assert targetTable != null : "Table should not be null";
+
+        return getIgniteTableForModification(targetTable, validatorTable);
+    }
+
     private IgniteTable getIgniteTableForModification(SqlIdentifier 
identifier, SqlValidatorTable table) {
         IgniteDataSource dataSource = table.unwrap(IgniteDataSource.class);
         assert dataSource != null;
@@ -470,7 +481,7 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
         //
 
         int selectListSize = selectFromUpdate.getSelectList().size();
-        int columnsToUpdateSize = update.getTargetColumnList().size(); 
+        int columnsToUpdateSize = update.getTargetColumnList().size();
         List<SqlNode> sourceExpressionList = 
selectFromUpdate.getSelectList().subList(selectListSize - columnsToUpdateSize, 
selectListSize);
         SqlNodeList selectList = selectFromMerge.getSelectList();
         int sourceExprListSize = sourceExpressionList.size();
@@ -586,8 +597,14 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
     /** {@inheritDoc} */
     @Override
     protected SqlSelect createSourceSelectForUpdate(SqlUpdate call) {
-        final SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
-        final SqlIdentifier targetTable = (SqlIdentifier) 
call.getTargetTable();
+        IgniteBiTuple<SqlIdentifier, @Nullable SqlNodeList> resolvedTable = 
resolveTableIdentifierAndHints(call.getTargetTable());
+
+        SqlIdentifier targetTable = resolvedTable.get1();
+        assert targetTable != null : "Table should not be null";
+
+        SqlNodeList hints = resolvedTable.get2();
+
+        SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
 
         IgniteTable igniteTable = getTableForModification(targetTable);
 
@@ -615,7 +632,19 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
         }
 
         return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
-                call.getCondition(), null, null, null, null, null, null, null, 
null);
+                call.getCondition(), null, null, null, null, null, null, null, 
hints);
+    }
+
+    private static IgniteBiTuple<SqlIdentifier, @Nullable SqlNodeList> 
resolveTableIdentifierAndHints(SqlNode targetTable) {
+        if (targetTable instanceof SqlTableRef) {
+            SqlTableRef tableRef = (SqlTableRef) targetTable;
+            SqlIdentifier tableName = (SqlIdentifier) 
tableRef.getOperandList().get(0);
+            SqlNodeList hints = (SqlNodeList) tableRef.getOperandList().get(1);
+
+            return new IgniteBiTuple<>(tableName, hints);
+        }
+
+        return new IgniteBiTuple<>((SqlIdentifier) targetTable, null);
     }
 
     /** {@inheritDoc} */
@@ -629,8 +658,14 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
     /** {@inheritDoc} */
     @Override
     protected SqlSelect createSourceSelectForDelete(SqlDelete call) {
-        final SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
-        final SqlIdentifier targetTable = (SqlIdentifier) 
call.getTargetTable();
+        IgniteBiTuple<SqlIdentifier, @Nullable SqlNodeList> resolvedTable = 
resolveTableIdentifierAndHints(call.getTargetTable());
+
+        SqlIdentifier targetTable = resolvedTable.get1();
+        assert targetTable != null : "Table should not be null";
+
+        SqlNodeList hints = resolvedTable.get2();
+
+        SqlNodeList selectList = new SqlNodeList(SqlParserPos.ZERO);
 
         IgniteTable igniteTable = getTableForModification(targetTable);
 
@@ -649,7 +684,7 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
         }
 
         return new SqlSelect(SqlParserPos.ZERO, null, selectList, sourceTable,
-                call.getCondition(), null, null, null, null, null, null, null, 
null);
+                call.getCondition(), null, null, null, null, null, null, null, 
hints);
     }
 
     /** {@inheritDoc} */
@@ -1172,7 +1207,7 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
 
         final SqlValidatorNamespace ns = validatedNamespace(call, unknownType);
         final SqlValidatorTable table = table(ns);
-        IgniteTable igniteTable = 
getIgniteTableForModification((SqlIdentifier) call.getTargetTable(), table);
+        IgniteTable igniteTable = 
resolveIgniteTableForModification(call.getTargetTable(), table);
 
         final RelDataType baseType = table.getRowType();
         final RelOptTable relOptTable = relOptTable(ns);
diff --git 
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
 
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
index b762f862dea..bb1f1cc7810 100644
--- 
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
+++ 
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryChecker.java
@@ -164,6 +164,19 @@ public interface QueryChecker {
                 + ".*?index: " + idxName);
     }
 
+    /**
+     * Ignite index scan matcher which ignores search bounds and allows 
multiple occurrences.
+     *
+     * @param schema Schema name.
+     * @param tblName Table name.
+     * @param idxName Index name.
+     * @return Matcher.
+     */
+    static Matcher<String> containsIndexScanIgnoreBoundsAtLeastOnce(String 
schema, String tblName, String idxName) {
+        return occursTimes("IndexScan.*?table: " + QualifiedName.of(schema, 
tblName).toCanonicalForm()
+                + ".*?index: " + idxName, 1, false);
+    }
+
     /**
      * Ignite table|index scan with only one project matcher.
      *
@@ -282,26 +295,49 @@ public interface QueryChecker {
         }
     }
 
-    /** Matches only one occurrence. */
-    static Matcher<String> matchesOnce(String pattern) {
-        return new SubstringMatcher("contains once", false, pattern) {
+    /** Has specified number of occurrence. */
+    static Matcher<String> occursTimes(String pattern, int times, boolean 
noMore) {
+        return new SubstringMatcher(resolveRelation(times, noMore), false, 
pattern) {
             /** {@inheritDoc} */
             @Override
             protected boolean evalSubstringOf(String strIn) {
                 strIn = strIn.replaceAll(System.lineSeparator(), "");
 
-                return containsOnce(strIn, this.substring);
+                return contains(strIn, this.substring, times, noMore);
             }
         };
     }
 
-    /** Check only single matching. */
-    static boolean containsOnce(String s, CharSequence substring) {
+    private static String resolveRelation(int times, boolean noMore) {
+        assert times >= 0;
+
+        switch (times) {
+            case 0:
+                return noMore ? "does not contain" : "can contain";
+            case 1:
+                return noMore ? "contains once" : "contains only once";
+            default:
+                return (noMore ? "contains " : "contains only ") + times + " 
times";
+        }
+    }
+
+    /** Check that {@code s} contains {@code substring} {@code times} times. */
+    static boolean contains(String s, CharSequence substring, int times, 
boolean noMore) {
         Pattern pattern = Pattern.compile(substring.toString());
         java.util.regex.Matcher matcher = pattern.matcher(s);
 
-        // Find first, but no more.
-        return matcher.find() && !matcher.find();
+        for (int i = 0; i < times; i++) {
+            if (!matcher.find()) {
+                return false; // Not enough occurrences
+            }
+        }
+
+        return !noMore || !matcher.find();
+    }
+
+    /** Matches only one occurrence. */
+    static Matcher<String> matchesOnce(String pattern) {
+        return occursTimes(pattern, 1, true);
     }
 
     /**

Reply via email to