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);
}
/**