This is an automated email from the ASF dual-hosted git repository.
zstan 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 edf625a5b4 IGNITE-20337 Ensure QueryChecker is properly used in tests
(#2539)
edf625a5b4 is described below
commit edf625a5b47fba667287c4b941e13c50407f6b08
Author: Andrew V. Mashenkov <[email protected]>
AuthorDate: Mon Sep 11 13:04:03 2023 +0300
IGNITE-20337 Ensure QueryChecker is properly used in tests (#2539)
---
.../sql/engine/ClusterPerClassIntegrationTest.java | 20 +-
.../ignite/internal/sql/engine/ItDmlTest.java | 2 +-
.../ignite/internal/sql/engine/ItMetadataTest.java | 15 +-
.../engine/datatypes/tests/BaseDataTypeTest.java | 34 +-
.../internal/sql/engine/util/ColumnMatcher.java | 29 ++
.../sql/engine/util/InjectQueryCheckerFactory.java | 35 ++
.../internal/sql/engine/util/MetadataMatcher.java | 5 +-
.../internal/sql/engine/util/QueryChecker.java | 561 +++------------------
.../sql/engine/util/QueryCheckerExtension.java | 122 +++++
.../sql/engine/util/QueryCheckerFactory.java | 41 ++
.../sql/engine/util/QueryCheckerFactoryImpl.java | 113 +++++
.../internal/sql/engine/util/QueryCheckerImpl.java | 418 +++++++++++++++
12 files changed, 873 insertions(+), 522 deletions(-)
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ClusterPerClassIntegrationTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ClusterPerClassIntegrationTest.java
index 8234e04f6d..aadaa1b6d1 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ClusterPerClassIntegrationTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ClusterPerClassIntegrationTest.java
@@ -61,7 +61,10 @@ import
org.apache.ignite.internal.schema.configuration.index.TableIndexConfigura
import org.apache.ignite.internal.schema.configuration.index.TableIndexView;
import org.apache.ignite.internal.sql.engine.property.PropertiesHelper;
import org.apache.ignite.internal.sql.engine.session.SessionId;
+import org.apache.ignite.internal.sql.engine.util.InjectQueryCheckerFactory;
import org.apache.ignite.internal.sql.engine.util.QueryChecker;
+import org.apache.ignite.internal.sql.engine.util.QueryCheckerExtension;
+import org.apache.ignite.internal.sql.engine.util.QueryCheckerFactory;
import org.apache.ignite.internal.sql.engine.util.TestQueryProcessor;
import org.apache.ignite.internal.storage.index.IndexStorage;
import org.apache.ignite.internal.storage.index.StorageIndexDescriptor;
@@ -85,10 +88,12 @@ import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.ExtendWith;
/**
* Abstract basic integration test that starts a cluster once for all the
tests it runs.
*/
+@ExtendWith(QueryCheckerExtension.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public abstract class ClusterPerClassIntegrationTest extends
IgniteIntegrationTest {
private static final IgniteLogger LOG =
Loggers.forClass(ClusterPerClassIntegrationTest.class);
@@ -139,6 +144,9 @@ public abstract class ClusterPerClassIntegrationTest
extends IgniteIntegrationTe
LOG.info("End beforeAll()");
}
+ @InjectQueryCheckerFactory
+ protected static QueryCheckerFactory queryCheckerFactory;
+
/**
* Starts and initializes a test cluster.
*/
@@ -258,17 +266,9 @@ public abstract class ClusterPerClassIntegrationTest
extends IgniteIntegrationTe
* @return Instance of QueryChecker.
*/
protected static QueryChecker assertQuery(Transaction tx, String qry) {
- return new QueryChecker(tx, qry) {
- @Override
- protected QueryProcessor getEngine() {
- return ((IgniteImpl) CLUSTER_NODES.get(0)).queryEngine();
- }
+ IgniteImpl node = (IgniteImpl) CLUSTER_NODES.get(0);
- @Override
- protected IgniteTransactions transactions() {
- return CLUSTER_NODES.get(0).transactions();
- }
- };
+ return queryCheckerFactory.create(node.queryEngine(),
node.transactions(), tx, qry);
}
/**
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDmlTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDmlTest.java
index df7c2dc76b..0a464df2b7 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDmlTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDmlTest.java
@@ -669,7 +669,7 @@ public class ItDmlTest extends
ClusterPerClassIntegrationTest {
assertQuery("SELECT b FROM test").returns("4").check();
sql("DELETE FROM test WHERE a = 0");
- assertQuery("SELECT d FROM test").returnNothing();
+ assertQuery("SELECT d FROM test").returnNothing().check();
}
private static void checkDuplicatePk(IgniteException ex) {
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java
index ef6bfcf166..c036dfa244 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItMetadataTest.java
@@ -21,8 +21,6 @@ import static java.util.stream.Collectors.joining;
import static java.util.stream.Stream.generate;
import static org.apache.ignite.sql.ColumnMetadata.UNDEFINED_SCALE;
-import java.time.Duration;
-import java.time.Period;
import org.apache.ignite.internal.sql.engine.util.MetadataMatcher;
import org.apache.ignite.sql.ColumnType;
import org.junit.jupiter.api.BeforeAll;
@@ -92,9 +90,14 @@ public class ItMetadataTest extends
ClusterPerClassIntegrationTest {
public void infixTypeCast() {
assertQuery("select id, id::tinyint as tid, id::smallint as sid,
id::varchar as vid, id::interval hour, "
+ "id::interval year from person")
- .columnNames("ID", "TID", "SID", "VID", "ID :: INTERVAL
INTERVAL_HOUR", "ID :: INTERVAL INTERVAL_YEAR")
- .columnTypes(Integer.class, Byte.class, Short.class,
String.class, Duration.class, Period.class)
- .check();
+ .columnMetadata(
+ new
MetadataMatcher().name("ID").type(ColumnType.INT32),
+ new
MetadataMatcher().name("TID").type(ColumnType.INT8),
+ new
MetadataMatcher().name("SID").type(ColumnType.INT16),
+ new
MetadataMatcher().name("VID").type(ColumnType.STRING),
+ new MetadataMatcher().name("ID :: INTERVAL
INTERVAL_HOUR").type(ColumnType.DURATION),
+ new MetadataMatcher().name("ID :: INTERVAL
INTERVAL_YEAR").type(ColumnType.PERIOD)
+ ).check();
}
@Test
@@ -109,7 +112,7 @@ public class ItMetadataTest extends
ClusterPerClassIntegrationTest {
@Test
public void metadata() {
sql("CREATE TABLE METADATA_TABLE (" + "ID INT PRIMARY KEY, "
- + "BOOLEAN_C BOOLEAN, "
+ + "BOOLEAN_C BOOLEAN, "
// Exact numeric types
+ "TINY_C TINYINT, " // TINYINT is not a part of any SQL
standard.
diff --git
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseDataTypeTest.java
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseDataTypeTest.java
index bceb796de4..585bdbbc00 100644
---
a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseDataTypeTest.java
+++
b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/sql/engine/datatypes/tests/BaseDataTypeTest.java
@@ -23,22 +23,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.List;
import java.util.NavigableSet;
import java.util.Objects;
-import java.util.Optional;
import java.util.stream.Stream;
import org.apache.calcite.sql.SqlKind;
import org.apache.ignite.internal.app.IgniteImpl;
-import org.apache.ignite.internal.sql.engine.AsyncSqlCursor;
import org.apache.ignite.internal.sql.engine.ClusterPerClassIntegrationTest;
-import org.apache.ignite.internal.sql.engine.QueryProcessor;
import org.apache.ignite.internal.sql.engine.type.IgniteCustomTypeSpec;
import org.apache.ignite.internal.sql.engine.util.Commons;
import org.apache.ignite.internal.sql.engine.util.NativeTypeWrapper;
import org.apache.ignite.internal.sql.engine.util.QueryChecker;
import org.apache.ignite.internal.sql.engine.util.QueryChecker.QueryTemplate;
import org.apache.ignite.internal.sql.engine.util.TestQueryProcessor;
-import org.apache.ignite.sql.ColumnMetadata;
import org.apache.ignite.sql.ColumnType;
-import org.apache.ignite.tx.IgniteTransactions;
+import org.apache.ignite.sql.ResultSetMetadata;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.provider.Arguments;
@@ -120,33 +116,23 @@ public abstract class BaseDataTypeTest<T extends
Comparable<T>> extends ClusterP
protected final QueryChecker checkQuery(String query) {
QueryTemplate queryTemplate = createQueryTemplate(query);
- return new QueryChecker(null, queryTemplate) {
- @Override
- protected QueryProcessor getEngine() {
- return ((IgniteImpl) CLUSTER_NODES.get(0)).queryEngine();
- }
-
- @Override
- protected IgniteTransactions transactions() {
- return igniteTx();
- }
+ IgniteImpl node = (IgniteImpl) CLUSTER_NODES.get(0);
- @Override
- protected void checkMetadata(AsyncSqlCursor<?> cursor) {
- Optional<ColumnMetadata> testKey = cursor.metadata().columns()
- .stream()
- .filter(c -> "test_key".equalsIgnoreCase(c.name()))
- .findAny();
+ return queryCheckerFactory.create(node.queryEngine(),
node.transactions(), this::validateMetadata, queryTemplate);
+ }
- testKey.ifPresent((c) -> {
+ private void validateMetadata(ResultSetMetadata metadata) {
+ metadata.columns()
+ .stream()
+ .filter(c -> "test_key".equalsIgnoreCase(c.name()))
+ .findAny()
+ .ifPresent((c) -> {
ColumnType columnType = testTypeSpec.columnType();
String error = format(
"test_key should have type {}. This can happen if
a query returned a column ", columnType
);
assertEquals(c.type(), columnType, error);
});
- }
- };
}
/**
diff --git
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/ColumnMatcher.java
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/ColumnMatcher.java
new file mode 100644
index 0000000000..02eb537870
--- /dev/null
+++
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/ColumnMatcher.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.util;
+
+import org.apache.ignite.sql.ColumnMetadata;
+
+/**
+ * Column metadata matcher interface.
+ */
+@FunctionalInterface
+public interface ColumnMatcher {
+ /** Validates column metadata. */
+ void check(ColumnMetadata columnMetadata);
+}
diff --git
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/InjectQueryCheckerFactory.java
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/InjectQueryCheckerFactory.java
new file mode 100644
index 0000000000..4c0073ef60
--- /dev/null
+++
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/InjectQueryCheckerFactory.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for injecting query checker factory instances into tests.
+ *
+ * <p>This annotation should be used on either fields or method parameters of
the {@link QueryCheckerFactory} type.
+ *
+ * @see QueryCheckerExtension
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD, ElementType.PARAMETER})
+public @interface InjectQueryCheckerFactory {
+}
diff --git
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/MetadataMatcher.java
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/MetadataMatcher.java
index f65d2d0fea..bc78b1ff0f 100644
---
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/MetadataMatcher.java
+++
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/MetadataMatcher.java
@@ -34,7 +34,7 @@ import org.junit.jupiter.api.function.Executable;
/**
* Column metadata checker.
*/
-public class MetadataMatcher {
+public class MetadataMatcher implements ColumnMatcher {
/** Marker object. */
private static final Object NO_CHECK = new Object() {
@Override
@@ -132,7 +132,8 @@ public class MetadataMatcher {
*
* @param actualMeta Metadata to check.
*/
- void check(ColumnMetadata actualMeta) {
+ @Override
+ public void check(ColumnMetadata actualMeta) {
List<Executable> matchers = new ArrayList<>();
if (name != NO_CHECK) {
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 a676e3cdc2..44adbece25 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
@@ -17,52 +17,37 @@
package org.apache.ignite.internal.sql.engine.util;
-import static
org.apache.ignite.internal.sql.engine.util.CursorUtils.getAllFromCursor;
-import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
-import static org.apache.ignite.internal.util.ArrayUtils.OBJECT_EMPTY_ARRAY;
import static org.apache.ignite.internal.util.ArrayUtils.nullOrEmpty;
-import static org.hamcrest.CoreMatchers.equalTo;
-import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.fail;
import java.lang.reflect.Array;
import java.lang.reflect.Type;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
-import java.util.concurrent.CompletableFuture;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
-import org.apache.ignite.internal.sql.engine.AsyncSqlCursor;
-import org.apache.ignite.internal.sql.engine.QueryContext;
-import org.apache.ignite.internal.sql.engine.QueryProcessor;
-import org.apache.ignite.internal.sql.engine.SqlQueryType;
-import org.apache.ignite.internal.sql.engine.hint.IgniteHint;
-import org.apache.ignite.internal.sql.engine.property.PropertiesHelper;
-import org.apache.ignite.internal.sql.engine.session.SessionId;
-import org.apache.ignite.internal.util.ArrayUtils;
-import org.apache.ignite.internal.util.CollectionUtils;
-import org.apache.ignite.sql.ColumnMetadata;
-import org.apache.ignite.sql.ColumnType;
-import org.apache.ignite.tx.IgniteTransactions;
-import org.apache.ignite.tx.Transaction;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Matcher;
import org.hamcrest.core.SubstringMatcher;
-/**
- * Query checker.
- */
-public abstract class QueryChecker {
- private static final Object[] NULL_AS_VARARG = {null};
+/** Query checker interface. */
+public interface QueryChecker {
+ Object[] NULL_AS_VARARG = {null};
+ List<List<?>> EMPTY_RES = List.of(List.of());
- private static final List<List<?>> EMPTY_RES = List.of(List.of());
+ /** Creates a matcher that matches if the examined string contains the
specified string anywhere. */
+ static Matcher<String> containsUnion(boolean all) {
+ return CoreMatchers.containsString("IgniteUnionAll(all=[" + all +
"])");
+ }
+
+ /** Creates a matcher that matches if the examined string contains the
specified string anywhere. */
+ static Matcher<String> containsUnion() {
+ return CoreMatchers.containsString("IgniteUnionAll(all=");
+ }
/**
* Ignite table scan matcher.
@@ -71,7 +56,7 @@ public abstract class QueryChecker {
* @param tblName Table name.
* @return Matcher.
*/
- public static Matcher<String> containsTableScan(String schema, String
tblName) {
+ static Matcher<String> containsTableScan(String schema, String tblName) {
return containsSubPlan("IgniteTableScan(table=[[" + schema + ", " +
tblName + "]]");
}
@@ -82,7 +67,7 @@ public abstract class QueryChecker {
* @param tblName Table name.
* @return Matcher.
*/
- public static Matcher<String> containsIndexScan(String schema, String
tblName) {
+ static Matcher<String> containsIndexScan(String schema, String tblName) {
return matchesOnce(".*IgniteIndexScan\\(table=\\[\\[" + schema + ", "
+ tblName + "\\]\\],"
+ " tableId=\\[.*\\].*\\)");
}
@@ -95,7 +80,7 @@ public abstract class QueryChecker {
* @param idxName Index name.
* @return Matcher.
*/
- public static Matcher<String> containsIndexScan(String schema, String
tblName, String idxName) {
+ static Matcher<String> containsIndexScan(String schema, String tblName,
String idxName) {
return matchesOnce(".*IgniteIndexScan\\(table=\\[\\[" + schema + ", "
+ tblName + "\\]\\],"
+ " tableId=\\[.*\\], index=\\[" + idxName + "\\].*\\)");
}
@@ -107,7 +92,7 @@ public abstract class QueryChecker {
* @param tblName Table name.
* @return Matcher.
*/
- public static Matcher<String> notContainsProject(String schema, String
tblName) {
+ static Matcher<String> notContainsProject(String schema, String tblName) {
return CoreMatchers.not(containsSubPlan("Scan(table=[[" + schema + ", "
+ tblName + "]], " + "requiredColumns="));
}
@@ -115,7 +100,7 @@ public abstract class QueryChecker {
/**
* {@link #containsProject(String, String, int...)} reverter.
*/
- public static Matcher<String> notContainsProject(String schema, String
tblName, int... requiredColumns) {
+ static Matcher<String> notContainsProject(String schema, String tblName,
int... requiredColumns) {
return CoreMatchers.not(containsProject(schema, tblName,
requiredColumns));
}
@@ -127,7 +112,7 @@ public abstract class QueryChecker {
* @param requiredColumns columns in projection.
* @return Matcher.
*/
- public static Matcher<String> containsProject(String schema, String
tblName, int... requiredColumns) {
+ static Matcher<String> containsProject(String schema, String tblName,
int... requiredColumns) {
return matches(".*Ignite(Table|Index)Scan\\(table=\\[\\[" + schema +
", "
+ tblName + "\\]\\], " + ".*requiredColumns=\\[\\{"
+ Arrays.toString(requiredColumns)
@@ -143,7 +128,7 @@ public abstract class QueryChecker {
* @param requiredColumns columns in projection.
* @return Matcher.
*/
- public static Matcher<String> containsOneProject(String schema, String
tblName, int... requiredColumns) {
+ static Matcher<String> containsOneProject(String schema, String tblName,
int... requiredColumns) {
return matchesOnce(".*Ignite(Table|Index)Scan\\(table=\\[\\[" + schema
+ ", "
+ tblName + "\\]\\], " + ".*requiredColumns=\\[\\{"
+ Arrays.toString(requiredColumns)
@@ -158,7 +143,7 @@ public abstract class QueryChecker {
* @param tblName Table name.
* @return Matcher.
*/
- public static Matcher<String> containsAnyProject(String schema, String
tblName) {
+ static Matcher<String> containsAnyProject(String schema, String tblName) {
return matchesOnce(".*Ignite(Table|Index)Scan\\(table=\\[\\[" + schema
+ ", "
+ tblName +
"\\]\\],.*requiredColumns=\\[\\{(\\d|\\W|,)+\\}\\].*");
}
@@ -169,7 +154,7 @@ public abstract class QueryChecker {
* @param subPlan Subplan.
* @return Matcher.
*/
- public static Matcher<String> containsSubPlan(String subPlan) {
+ static Matcher<String> containsSubPlan(String subPlan) {
return CoreMatchers.containsString(subPlan);
}
@@ -179,388 +164,20 @@ public abstract class QueryChecker {
* @param substring Substring.
* @return Matcher.
*/
- public static Matcher<String> matches(final String substring) {
+ static Matcher<String> matches(String substring) {
return new SubstringMatcher("contains", false, substring) {
/** {@inheritDoc} */
@Override
protected boolean evalSubstringOf(String strIn) {
strIn = strIn.replaceAll(System.lineSeparator(), "");
- return strIn.matches(substring);
+ return strIn.matches(this.substring);
}
};
}
- /**
- * Adds plan matchers.
- */
- @SafeVarargs
- public final QueryChecker matches(Matcher<String>... planMatcher) {
- Collections.addAll(planMatchers, planMatcher);
-
- return this;
- }
-
- /** Matches only one occurrence. */
- public static Matcher<String> matchesOnce(final String substring) {
- return new SubstringMatcher("contains once", false, substring) {
- /** {@inheritDoc} */
- @Override
- protected boolean evalSubstringOf(String strIn) {
- strIn = strIn.replaceAll(System.lineSeparator(), "");
-
- return containsOnce(strIn, substring);
- }
- };
- }
-
- /** Check only single matching. */
- public static boolean containsOnce(final String s, final CharSequence
substring) {
- Pattern pattern = Pattern.compile(substring.toString());
- java.util.regex.Matcher matcher = pattern.matcher(s);
-
- if (matcher.find()) {
- return !matcher.find();
- }
-
- return false;
- }
-
- /**
- * Ignite any index can matcher.
- *
- * @param schema Schema name.
- * @param tblName Table name.
- * @param idxNames Index names.
- * @return Matcher.
- */
- public static Matcher<String> containsAnyScan(final String schema, final
String tblName, String... idxNames) {
- if (nullOrEmpty(idxNames)) {
- return matchesOnce(".*Ignite(Table|Index)Scan\\(table=\\[\\[" +
schema + ", " + tblName + "\\]\\].*");
- }
-
- return CoreMatchers.anyOf(
- Arrays.stream(idxNames).map(idx -> containsIndexScan(schema,
tblName, idx)).collect(Collectors.toList())
- );
- }
-
- /**
- * Allows to parameterize an SQL query string.
- */
- public interface QueryTemplate {
-
- /** Template that always returns original query. **/
- static QueryTemplate returnOriginalQuery(String query) {
- return new QueryTemplate() {
- @Override
- public String originalQueryString() {
- return query;
- }
-
- @Override
- public String createQuery() {
- return query;
- }
- };
- }
-
- /** Returns the original query string. **/
- String originalQueryString();
-
- /**
- * Produces an SQL query from the original query string.
- */
- String createQuery();
- }
-
- private final QueryTemplate queryTemplate;
-
- private final ArrayList<Matcher<String>> planMatchers = new ArrayList<>();
-
- private final ArrayList<String> disabledRules = new ArrayList<>();
-
- private List<List<?>> expectedResult;
-
- private List<String> expectedColumnNames;
-
- private List<Type> expectedColumnTypes;
-
- private List<MetadataMatcher> metadataMatchers;
-
- private boolean ordered;
-
- private Object[] params = OBJECT_EMPTY_ARRAY;
-
- private String exactPlan;
-
- private Transaction tx;
-
- /**
- * Constructor.
- *
- * @param tx Transaction.
- * @param qry Query.
- */
- public QueryChecker(Transaction tx, String qry) {
- this(tx, QueryTemplate.returnOriginalQuery(qry));
- }
-
- /**
- * Constructor.
- *
- * @param tx Transaction.
- * @param queryTemplate A query template.
- */
- public QueryChecker(Transaction tx, QueryTemplate queryTemplate) {
- this.tx = tx;
- this.queryTemplate = new AddDisabledRulesTemplate(queryTemplate,
disabledRules);
- }
-
- /**
- * Sets ordered.
- *
- * @return This.
- */
- public QueryChecker ordered() {
- ordered = true;
-
- return this;
- }
-
- /**
- * Sets params.
- *
- * @return This.
- */
- public QueryChecker withParams(Object... params) {
- // let's interpret null array as simple single null.
- if (params == null) {
- params = NULL_AS_VARARG;
- }
-
- this.params =
Arrays.stream(params).map(NativeTypeWrapper::unwrap).toArray();
-
- return this;
- }
-
- /**
- * Set a single param.
- * Useful for specifying array parameters w/o triggering IDE-inspection
warnings about confusing varargs/array params.
- *
- * @return This.
- */
- public QueryChecker withParam(Object param) {
- return this.withParams(param);
- }
-
- /**
- * Disables rules.
- *
- * @param rules Rules to disable.
- * @return This.
- */
- public QueryChecker disableRules(String... rules) {
- if (rules != null) {
-
Arrays.stream(rules).filter(Objects::nonNull).forEach(disabledRules::add);
- }
-
- return this;
- }
-
- /**
- * This method add the given row to the list of expected, the order of
enumeration does not matter unless {@link #ordered()} is set.
- *
- * @param res Array with values one returning tuple. {@code null} array
will be interpreted as single-column-null row.
- * @return This.
- */
- public QueryChecker returns(Object... res) {
- assert expectedResult != EMPTY_RES : "Erroneous awaiting results
mixing, impossible to simultaneously wait something and nothing";
-
- if (expectedResult == null) {
- expectedResult = new ArrayList<>();
- }
-
- // let's interpret null array as simple single null.
- if (res == null) {
- res = NULL_AS_VARARG;
- }
-
- expectedResult.add(Arrays.asList(res));
-
- return this;
- }
-
- /**
- * Check that return empty result.
- *
- * @return This.
- */
- public QueryChecker returnNothing() {
- assert expectedResult == null : "Erroneous awaiting results mixing,
impossible to simultaneously wait nothing and something";
-
- expectedResult = EMPTY_RES;
-
- return this;
- }
-
- /** Creates a matcher that matches if the examined string contains the
specified string anywhere. */
- public static Matcher<String> containsUnion(boolean all) {
- return CoreMatchers.containsString("IgniteUnionAll(all=[" + all +
"])");
- }
-
- /** Creates a matcher that matches if the examined string contains the
specified string anywhere. */
- public static Matcher<String> containsUnion() {
- return CoreMatchers.containsString("IgniteUnionAll(all=");
- }
-
- /**
- * Sets columns names.
- *
- * @return This.
- */
- public QueryChecker columnNames(String... columns) {
- expectedColumnNames = Arrays.asList(columns);
-
- return this;
- }
-
- /**
- * Sets columns types.
- *
- * @return This.
- */
- public QueryChecker columnTypes(Type... columns) {
- expectedColumnTypes = Arrays.asList(columns);
-
- return this;
- }
-
- /**
- * Sets columns metadata.
- *
- * @return This.
- */
- public QueryChecker columnMetadata(MetadataMatcher... matchers) {
- metadataMatchers = Arrays.asList(matchers);
-
- return this;
- }
-
- /**
- * Sets plan.
- *
- * @return This.
- */
- public QueryChecker planEquals(String plan) {
- exactPlan = plan;
-
- return this;
- }
-
- /**
- * Run checks.
- */
- public void check() {
- // Check plan.
- QueryProcessor qryProc = getEngine();
-
- SessionId sessionId =
qryProc.createSession(PropertiesHelper.emptyHolder());
-
- QueryContext context = QueryContext.create(SqlQueryType.ALL, tx);
-
- String qry = queryTemplate.createQuery();
-
- try {
-
- if (!CollectionUtils.nullOrEmpty(planMatchers) || exactPlan !=
null) {
-
- CompletableFuture<AsyncSqlCursor<List<Object>>> explainCursors
= qryProc.querySingleAsync(sessionId,
- context, transactions(), "EXPLAIN PLAN FOR " + qry,
params);
- AsyncSqlCursor<List<Object>> explainCursor =
await(explainCursors);
- List<List<Object>> explainRes =
getAllFromCursor(explainCursor);
-
- String actualPlan = (String) explainRes.get(0).get(0);
-
- if (!CollectionUtils.nullOrEmpty(planMatchers)) {
- for (Matcher<String> matcher : planMatchers) {
- assertThat("Invalid plan:\n" + actualPlan, actualPlan,
matcher);
- }
- }
-
- if (exactPlan != null) {
- assertEquals(exactPlan, actualPlan);
- }
- }
- // Check result.
- CompletableFuture<AsyncSqlCursor<List<Object>>> cursors =
- qryProc.querySingleAsync(sessionId, context,
transactions(), qry, params);
-
- AsyncSqlCursor<List<Object>> cur = await(cursors);
-
- checkMetadata(cur);
-
- if (expectedColumnNames != null) {
- List<String> colNames = cur.metadata().columns().stream()
- .map(ColumnMetadata::name)
- .collect(Collectors.toList());
-
- assertThat("Column names don't match", colNames,
equalTo(expectedColumnNames));
- }
-
- if (expectedColumnTypes != null) {
- List<Type> colTypes = cur.metadata().columns().stream()
- .map(ColumnMetadata::type)
- .map(ColumnType::columnTypeToClass)
- .collect(Collectors.toList());
-
- assertThat("Column types don't match", colTypes,
equalTo(expectedColumnTypes));
- }
-
- if (metadataMatchers != null) {
- List<ColumnMetadata> columnMetadata = cur.metadata().columns();
-
- Iterator<ColumnMetadata> valueIterator =
columnMetadata.iterator();
- Iterator<MetadataMatcher> matcherIterator =
metadataMatchers.iterator();
-
- while (matcherIterator.hasNext() && valueIterator.hasNext()) {
- MetadataMatcher matcher = matcherIterator.next();
- ColumnMetadata actualElement = valueIterator.next();
-
- matcher.check(actualElement);
- }
-
- assertEquals(metadataMatchers.size(), columnMetadata.size(),
"Column metadata doesn't match");
- }
-
- var res = getAllFromCursor(cur);
-
- if (expectedResult != null) {
- if (Objects.equals(expectedResult, EMPTY_RES)) {
- assertEquals(0, res.size(), "Empty result expected");
-
- return;
- }
-
- if (!ordered) {
- // Avoid arbitrary order.
- res.sort(new ListComparator());
- expectedResult.sort(new ListComparator());
- }
-
- assertEqualsCollections(expectedResult, res);
- }
- } finally {
- await(qryProc.closeSession(sessionId));
- }
- }
-
- protected abstract QueryProcessor getEngine();
-
- protected abstract IgniteTransactions transactions();
-
- protected void checkMetadata(AsyncSqlCursor<?> cursor) {
-
- }
+ @SuppressWarnings("unchecked")
+ QueryChecker matches(Matcher<String>... planMatcher);
/**
* Check collections equals (ordered).
@@ -568,7 +185,7 @@ public abstract class QueryChecker {
* @param exp Expected collection.
* @param act Actual collection.
*/
- private void assertEqualsCollections(Collection<?> exp, Collection<?> act)
{
+ static void assertEqualsCollections(Collection<?> exp, Collection<?> act) {
assertEquals(exp.size(), act.size(), "Collections sizes are not
equal:\nExpected: " + exp + "\nActual: " + act);
Iterator<?> it1 = exp.iterator();
@@ -596,7 +213,7 @@ public abstract class QueryChecker {
/**
* Converts the given value to a test-output friendly representation that
includes type information.
*/
- private static String displayValue(Object value, boolean includeType) {
+ static String displayValue(Object value, boolean includeType) {
if (value == null) {
return "<null>";
} else if (value.getClass().isArray()) {
@@ -630,90 +247,76 @@ public abstract class QueryChecker {
}
}
- /**
- * List comparator.
- */
- private static class ListComparator implements Comparator<List<?>> {
- /** {@inheritDoc} */
- @SuppressWarnings({"rawtypes", "unchecked"})
- @Override
- public int compare(List<?> o1, List<?> o2) {
- if (o1.size() != o2.size()) {
- fail("Collections are not equal:\nExpected:\t" + o1 +
"\nActual:\t" + o2);
- }
-
- Iterator<?> it1 = o1.iterator();
- Iterator<?> it2 = o2.iterator();
-
- while (it1.hasNext()) {
- Object item1 = it1.next();
- Object item2 = it2.next();
+ /** Matches only one occurrence. */
+ static Matcher<String> matchesOnce(String substring) {
+ return new SubstringMatcher("contains once", false, substring) {
+ /** {@inheritDoc} */
+ @Override
+ protected boolean evalSubstringOf(String strIn) {
+ strIn = strIn.replaceAll(System.lineSeparator(), "");
- if (Objects.deepEquals(item1, item2)) {
- continue;
- }
+ return containsOnce(strIn, this.substring);
+ }
+ };
+ }
- if (item1 == null) {
- return 1;
- }
+ /** Check only single matching. */
+ static boolean containsOnce(String s, CharSequence substring) {
+ Pattern pattern = Pattern.compile(substring.toString());
+ java.util.regex.Matcher matcher = pattern.matcher(s);
- if (item2 == null) {
- return -1;
- }
+ // Find first, but no more.
+ return matcher.find() && !matcher.find();
+ }
- if (!(item1 instanceof Comparable) && !(item2 instanceof
Comparable)) {
- continue;
- }
+ /**
+ * Ignite any index can matcher.
+ *
+ * @param schema Schema name.
+ * @param tblName Table name.
+ * @param idxNames Index names.
+ * @return Matcher.
+ */
+ static Matcher<String> containsAnyScan(String schema, String tblName,
String... idxNames) {
+ if (nullOrEmpty(idxNames)) {
+ return matchesOnce(".*Ignite(Table|Index)Scan\\(table=\\[\\[" +
schema + ", " + tblName + "\\]\\].*");
+ }
- Comparable c1 = (Comparable) item1;
- Comparable c2 = (Comparable) item2;
+ return CoreMatchers.anyOf(
+ Arrays.stream(idxNames).map(idx -> containsIndexScan(schema,
tblName, idx)).collect(Collectors.toList())
+ );
+ }
- int c = c1.compareTo(c2);
+ QueryChecker ordered();
- if (c != 0) {
- return c;
- }
- }
+ QueryChecker withParams(Object... params);
- return 0;
- }
- }
+ QueryChecker withParam(Object param);
- /**
- * Updates an SQL query string to include hints for the optimizer to
disable certain rules.
- */
- private static final class AddDisabledRulesTemplate implements
QueryTemplate {
- private static final Pattern SELECT_REGEXP =
Pattern.compile("(?i)^select");
- private static final Pattern SELECT_QRY_CHECK =
Pattern.compile("(?i)^select .*");
+ QueryChecker disableRules(String... rules);
- private final QueryTemplate input;
+ QueryChecker returns(Object... res);
- private final List<String> disabledRules;
+ QueryChecker returnNothing();
- private AddDisabledRulesTemplate(QueryTemplate input, List<String>
disabledRules) {
- this.input = input;
- this.disabledRules = disabledRules;
- }
+ QueryChecker columnNames(String... columns);
- @Override
- public String originalQueryString() {
- return input.originalQueryString();
- }
+ QueryChecker columnTypes(Type... columns);
- @Override
- public String createQuery() {
- String qry = input.createQuery();
+ QueryChecker columnMetadata(MetadataMatcher... matchers);
- if (!disabledRules.isEmpty()) {
- String originalQuery = input.originalQueryString();
+ void check();
- assert SELECT_QRY_CHECK.matcher(qry).matches() : "SELECT query
was expected: " + originalQuery + ". Updated: " + qry;
+ /**
+ * Allows to parameterize an SQL query string.
+ */
+ interface QueryTemplate {
+ /** Returns the original query string. **/
+ String originalQueryString();
- return SELECT_REGEXP.matcher(qry).replaceAll("select "
- + HintUtils.toHint(IgniteHint.DISABLE_RULE,
disabledRules.toArray(ArrayUtils.STRING_EMPTY_ARRAY)));
- } else {
- return qry;
- }
- }
+ /**
+ * Produces an SQL query from the original query string.
+ */
+ String createQuery();
}
}
diff --git
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerExtension.java
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerExtension.java
new file mode 100644
index 0000000000..7c07e88093
--- /dev/null
+++
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerExtension.java
@@ -0,0 +1,122 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.util;
+
+import static java.lang.reflect.Modifier.isStatic;
+import static org.apache.ignite.lang.IgniteStringFormatter.format;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.lang.reflect.Field;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.junit.platform.commons.support.HierarchyTraversalMode;
+
+/**
+ * JUnit extension to inject {@link QueryCheckerFactory} instance into test
classes, and ensure the {@link QueryChecker#check()} method is
+ * called for each {@link QueryChecker} instance, which was created via the
factory.
+ *
+ * @see InjectQueryCheckerFactory
+ */
+public class QueryCheckerExtension implements BeforeEachCallback,
BeforeAllCallback, AfterEachCallback {
+ /** QueryCheckers instances that are managed by this extension. */
+ private final Set<QueryChecker> queryCheckers = new HashSet<>();
+
+ private final QueryCheckerFactory factory = new QueryCheckerFactoryImpl(
+ this::register,
+ this::unregister
+ );
+
+ /** {@inheritDoc} */
+ @Override
+ public void beforeAll(ExtensionContext context) throws Exception {
+ injectFields(context, true);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void beforeEach(ExtensionContext context) throws Exception {
+ injectFields(context, false);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void afterEach(ExtensionContext context) throws Exception {
+ ensureNoUnusedChecker();
+ }
+
+ private void injectFields(ExtensionContext context, boolean forStatic)
throws Exception {
+ Class<?> testClass = context.getRequiredTestClass();
+ Object testInstance = context.getTestInstance().orElse(null);
+
+ assert forStatic || testInstance != null;
+
+ List<Field> annotatedFields = AnnotationSupport.findAnnotatedFields(
+ testClass,
+ InjectQueryCheckerFactory.class,
+ field ->
field.getType().isAssignableFrom(QueryCheckerFactory.class) &&
(isStatic(field.getModifiers()) == forStatic),
+ HierarchyTraversalMode.TOP_DOWN
+ );
+
+ for (Field field : annotatedFields) {
+ field.setAccessible(true);
+
+ field.set(forStatic ? null : testInstance, factory);
+ }
+ }
+
+ private void register(QueryChecker newChecker) {
+ queryCheckers.add(newChecker);
+ }
+
+ private void unregister(QueryChecker queryChecker) {
+ boolean remove = queryCheckers.remove(queryChecker);
+
+ if (!remove) {
+ throw new IllegalStateException(format("Unknown QueryChecker
instance for SQL query: {}", queryChecker));
+ }
+ }
+
+ /**
+ * Validates that {@link QueryChecker#check()} was called for each
QueryChecker instance, which was created via the factory.
+ *
+ * @throws AssertionError If found any registered QueryChecker.
+ * @see QueryCheckerExtension
+ */
+ private void ensureNoUnusedChecker() {
+ if (queryCheckers.isEmpty()) {
+ return;
+ }
+
+ String failureDetails = queryCheckers.stream()
+ .map(Object::toString)
+ .collect(Collectors.joining("\n", "Found unused QueryCheckers
for queries: ", ""));
+
+ // Clear collection to allow passing next tests in suite.
+ queryCheckers.clear();
+
+ fail(failureDetails);
+ }
+
+}
diff --git
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerFactory.java
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerFactory.java
new file mode 100644
index 0000000000..a0d56fbe8b
--- /dev/null
+++
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerFactory.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.util;
+
+import java.util.function.Consumer;
+import org.apache.ignite.internal.sql.engine.QueryProcessor;
+import org.apache.ignite.internal.sql.engine.util.QueryChecker.QueryTemplate;
+import org.apache.ignite.sql.ResultSetMetadata;
+import org.apache.ignite.tx.IgniteTransactions;
+import org.apache.ignite.tx.Transaction;
+
+/**
+ * Interface for {@link QueryChecker} factory.
+ */
+public interface QueryCheckerFactory {
+ /** Creates query checker instance. */
+ QueryChecker create(QueryProcessor queryProcessor, IgniteTransactions
transactions, Transaction tx, String query);
+
+ /** Creates query checker with custom metadata validator. */
+ QueryChecker create(
+ QueryProcessor queryProcessor,
+ IgniteTransactions transactions,
+ Consumer<ResultSetMetadata> metadataValidator,
+ QueryTemplate queryTemplate
+ );
+}
diff --git
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerFactoryImpl.java
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerFactoryImpl.java
new file mode 100644
index 0000000000..460d3b6b58
--- /dev/null
+++
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerFactoryImpl.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.util;
+
+import java.util.function.Consumer;
+import org.apache.ignite.internal.sql.engine.QueryProcessor;
+import org.apache.ignite.internal.sql.engine.util.QueryChecker.QueryTemplate;
+import org.apache.ignite.sql.ResultSetMetadata;
+import org.apache.ignite.tx.IgniteTransactions;
+import org.apache.ignite.tx.Transaction;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Factory class for {@link QueryCheckerImpl}.
+ *
+ * @see QueryCheckerExtension
+ */
+class QueryCheckerFactoryImpl implements QueryCheckerFactory {
+ private final Consumer<QueryChecker> onCreatedCallback;
+ private final Consumer<QueryChecker> onUsedCallback;
+
+ /**
+ * Creates factory instance.
+ *
+ * @param onCreatedCallback Callback function that is called when a new
QueryChecker is created.
+ * @param onUsedCallback Callback function that is called when previously
created QueryChecker is used.
+ */
+ QueryCheckerFactoryImpl(Consumer<QueryChecker> onCreatedCallback,
Consumer<QueryChecker> onUsedCallback) {
+ this.onCreatedCallback = onCreatedCallback;
+ this.onUsedCallback = onUsedCallback;
+ }
+
+ @Override
+ public QueryChecker create(QueryProcessor queryProcessor,
IgniteTransactions transactions, Transaction tx, String query) {
+ return create(queryProcessor, transactions, (ignore) -> {}, tx,
returnOriginalQuery(query));
+ }
+
+ @Override
+ public QueryChecker create(
+ QueryProcessor queryProcessor,
+ IgniteTransactions transactions,
+ Consumer<ResultSetMetadata> metadataValidator,
+ QueryTemplate queryTemplate
+ ) {
+ return create(queryProcessor, transactions, (ignore) -> {}, null,
queryTemplate);
+ }
+
+ private QueryChecker create(
+ QueryProcessor queryProcessor,
+ IgniteTransactions transactions,
+ Consumer<ResultSetMetadata> metadataValidator,
+ @Nullable Transaction tx,
+ QueryTemplate queryTemplate
+ ) {
+ QueryCheckerImpl queryChecker = new QueryCheckerImpl(tx,
queryTemplate) {
+ @Override
+ protected QueryProcessor getEngine() {
+ return queryProcessor;
+ }
+
+ @Override
+ protected IgniteTransactions transactions() {
+ return transactions;
+ }
+
+ @Override
+ protected void checkMetadata(ResultSetMetadata metadata) {
+ metadataValidator.accept(metadata);
+ }
+
+ @Override
+ public void check() {
+ onUsedCallback.accept(this);
+
+ super.check();
+ }
+ };
+
+ onCreatedCallback.accept(queryChecker);
+
+ return queryChecker;
+ }
+
+ /** Template that always returns original query. **/
+ private static QueryTemplate returnOriginalQuery(String query) {
+ return new QueryTemplate() {
+ @Override
+ public String originalQueryString() {
+ return query;
+ }
+
+ @Override
+ public String createQuery() {
+ return query;
+ }
+ };
+ }
+}
diff --git
a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java
new file mode 100644
index 0000000000..57f3211555
--- /dev/null
+++
b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerImpl.java
@@ -0,0 +1,418 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.sql.engine.util;
+
+import static
org.apache.ignite.internal.sql.engine.util.CursorUtils.getAllFromCursor;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
+import static org.apache.ignite.internal.util.ArrayUtils.OBJECT_EMPTY_ARRAY;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.sql.engine.AsyncSqlCursor;
+import org.apache.ignite.internal.sql.engine.QueryContext;
+import org.apache.ignite.internal.sql.engine.QueryProcessor;
+import org.apache.ignite.internal.sql.engine.SqlQueryType;
+import org.apache.ignite.internal.sql.engine.hint.IgniteHint;
+import org.apache.ignite.internal.sql.engine.property.PropertiesHelper;
+import org.apache.ignite.internal.sql.engine.session.SessionId;
+import org.apache.ignite.internal.util.ArrayUtils;
+import org.apache.ignite.internal.util.CollectionUtils;
+import org.apache.ignite.sql.ColumnMetadata;
+import org.apache.ignite.sql.ColumnType;
+import org.apache.ignite.sql.ResultSetMetadata;
+import org.apache.ignite.tx.IgniteTransactions;
+import org.apache.ignite.tx.Transaction;
+import org.hamcrest.Matcher;
+
+/**
+ * Query checker base class.
+ */
+abstract class QueryCheckerImpl implements QueryChecker {
+
+ final QueryTemplate queryTemplate;
+
+ private final ArrayList<Matcher<String>> planMatchers = new ArrayList<>();
+
+ private final ArrayList<String> disabledRules = new ArrayList<>();
+
+ private List<List<?>> expectedResult;
+
+ private List<ColumnMatcher> metadataMatchers;
+
+ private boolean ordered;
+
+ private Object[] params = OBJECT_EMPTY_ARRAY;
+
+ private final Transaction tx;
+
+ /**
+ * Constructor.
+ *
+ * @param tx Transaction.
+ * @param queryTemplate A query template.
+ */
+ QueryCheckerImpl(Transaction tx, QueryTemplate queryTemplate) {
+ this.tx = tx;
+ this.queryTemplate = new
AddDisabledRulesTemplate(Objects.requireNonNull(queryTemplate), disabledRules);
+ }
+
+ /**
+ * Sets ordered.
+ *
+ * @return This.
+ */
+ @Override
+ public QueryChecker ordered() {
+ ordered = true;
+
+ return this;
+ }
+
+ /**
+ * Sets params.
+ *
+ * @return This.
+ */
+ @Override
+ public QueryChecker withParams(Object... params) {
+ // let's interpret null array as simple single null.
+ if (params == null) {
+ params = QueryChecker.NULL_AS_VARARG;
+ }
+
+ this.params =
Arrays.stream(params).map(NativeTypeWrapper::unwrap).toArray();
+
+ return this;
+ }
+
+ /**
+ * Set a single param. Useful for specifying array parameters w/o
triggering IDE-inspection warnings about confusing varargs/array
+ * params.
+ *
+ * @return This.
+ */
+ @Override
+ public QueryChecker withParam(Object param) {
+ return this.withParams(param);
+ }
+
+ /**
+ * Disables rules.
+ *
+ * @param rules Rules to disable.
+ * @return This.
+ */
+ @Override
+ public QueryChecker disableRules(String... rules) {
+ if (rules != null) {
+
Arrays.stream(rules).filter(Objects::nonNull).forEach(disabledRules::add);
+ }
+
+ return this;
+ }
+
+ /**
+ * This method add the given row to the list of expected, the order of
enumeration does not matter unless {@link #ordered()} is set.
+ *
+ * @param res Array with values one returning tuple. {@code null} array
will be interpreted as single-column-null row.
+ * @return This.
+ */
+ @Override
+ public QueryChecker returns(Object... res) {
+ assert expectedResult != QueryChecker.EMPTY_RES
+ : "Erroneous awaiting results mixing, impossible to
simultaneously wait something and nothing";
+
+ if (expectedResult == null) {
+ expectedResult = new ArrayList<>();
+ }
+
+ // let's interpret null array as simple single null.
+ if (res == null) {
+ res = QueryChecker.NULL_AS_VARARG;
+ }
+
+ expectedResult.add(Arrays.asList(res));
+
+ return this;
+ }
+
+ /**
+ * Check that return empty result.
+ *
+ * @return This.
+ */
+ @Override
+ public QueryChecker returnNothing() {
+ assert expectedResult == null : "Erroneous awaiting results mixing,
impossible to simultaneously wait nothing and something";
+
+ expectedResult = QueryChecker.EMPTY_RES;
+
+ return this;
+ }
+
+ /**
+ * Sets columns names.
+ *
+ * @return This.
+ */
+ @Override
+ public QueryChecker columnNames(String... columns) {
+ assert metadataMatchers == null;
+
+ metadataMatchers = Arrays.stream(columns)
+ .map(name -> new MetadataMatcher().name(name))
+ .collect(Collectors.toList());
+
+ return this;
+ }
+
+ /**
+ * Sets columns types.
+ *
+ * @return This.
+ */
+ @Override
+ public QueryChecker columnTypes(Type... columns) {
+ assert metadataMatchers == null;
+
+ metadataMatchers = Arrays.stream(columns)
+ .map(t -> (ColumnMatcher) columnMetadata -> {
+ Class<?> type =
ColumnType.columnTypeToClass(columnMetadata.type());
+
+ assertThat("Column type don't match", type, equalTo(t));
+ })
+ .collect(Collectors.toList());
+
+ return this;
+ }
+
+ /**
+ * Sets columns metadata.
+ *
+ * @return This.
+ */
+ @Override
+ public QueryChecker columnMetadata(MetadataMatcher... matchers) {
+ assert metadataMatchers == null;
+
+ metadataMatchers = Arrays.asList(matchers);
+
+ return this;
+ }
+
+ /**
+ * Adds plan matchers.
+ */
+ @Override
+ @SafeVarargs
+ public final QueryChecker matches(Matcher<String>... planMatcher) {
+ Collections.addAll(planMatchers, planMatcher);
+
+ return this;
+ }
+
+ /**
+ * Run checks.
+ */
+ @Override
+ public void check() {
+ // Check plan.
+ QueryProcessor qryProc = getEngine();
+
+ SessionId sessionId =
qryProc.createSession(PropertiesHelper.emptyHolder());
+
+ QueryContext context = QueryContext.create(SqlQueryType.ALL, tx);
+
+ String qry = queryTemplate.createQuery();
+
+ try {
+
+ if (!CollectionUtils.nullOrEmpty(planMatchers)) {
+
+ CompletableFuture<AsyncSqlCursor<List<Object>>> explainCursors
= qryProc.querySingleAsync(sessionId,
+ context, transactions(), "EXPLAIN PLAN FOR " + qry,
params);
+ AsyncSqlCursor<List<Object>> explainCursor =
await(explainCursors);
+ List<List<Object>> explainRes =
getAllFromCursor(explainCursor);
+
+ String actualPlan = (String) explainRes.get(0).get(0);
+
+ if (!CollectionUtils.nullOrEmpty(planMatchers)) {
+ for (Matcher<String> matcher : planMatchers) {
+ assertThat("Invalid plan:\n" + actualPlan, actualPlan,
matcher);
+ }
+ }
+ }
+ // Check result.
+ CompletableFuture<AsyncSqlCursor<List<Object>>> cursors =
+ qryProc.querySingleAsync(sessionId, context,
transactions(), qry, params);
+
+ AsyncSqlCursor<List<Object>> cur = await(cursors);
+
+ checkMetadata(cur.metadata());
+
+ if (metadataMatchers != null) {
+ List<ColumnMetadata> columnMetadata = cur.metadata().columns();
+
+ Iterator<ColumnMetadata> valueIterator =
columnMetadata.iterator();
+ Iterator<ColumnMatcher> matcherIterator =
metadataMatchers.iterator();
+
+ while (matcherIterator.hasNext() && valueIterator.hasNext()) {
+ ColumnMatcher matcher = matcherIterator.next();
+ ColumnMetadata actualElement = valueIterator.next();
+
+ matcher.check(actualElement);
+ }
+
+ assertEquals(metadataMatchers.size(), columnMetadata.size(),
"Column metadata doesn't match");
+ }
+
+ var res = getAllFromCursor(cur);
+
+ if (expectedResult != null) {
+ if (Objects.equals(expectedResult, QueryChecker.EMPTY_RES)) {
+ assertEquals(0, res.size(), "Empty result expected");
+
+ return;
+ }
+
+ if (!ordered) {
+ // Avoid arbitrary order.
+ res.sort(new ListComparator());
+ expectedResult.sort(new ListComparator());
+ }
+
+ QueryChecker.assertEqualsCollections(expectedResult, res);
+ }
+ } finally {
+ await(qryProc.closeSession(sessionId));
+ }
+ }
+
+ @Override
+ public String toString() {
+ return QueryCheckerImpl.class.getSimpleName() + "[sql=" +
queryTemplate.originalQueryString() + "]";
+ }
+
+ protected abstract QueryProcessor getEngine();
+
+ protected abstract IgniteTransactions transactions();
+
+ protected void checkMetadata(ResultSetMetadata metadata) {
+ // No-op.
+ }
+
+ /**
+ * List comparator.
+ */
+ private static class ListComparator implements Comparator<List<?>> {
+ /** {@inheritDoc} */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ @Override
+ public int compare(List<?> o1, List<?> o2) {
+ if (o1.size() != o2.size()) {
+ fail("Collections are not equal:\nExpected:\t" + o1 +
"\nActual:\t" + o2);
+ }
+
+ Iterator<?> it1 = o1.iterator();
+ Iterator<?> it2 = o2.iterator();
+
+ while (it1.hasNext()) {
+ Object item1 = it1.next();
+ Object item2 = it2.next();
+
+ if (Objects.deepEquals(item1, item2)) {
+ continue;
+ }
+
+ if (item1 == null) {
+ return 1;
+ }
+
+ if (item2 == null) {
+ return -1;
+ }
+
+ if (!(item1 instanceof Comparable) && !(item2 instanceof
Comparable)) {
+ continue;
+ }
+
+ Comparable c1 = (Comparable) item1;
+ Comparable c2 = (Comparable) item2;
+
+ int c = c1.compareTo(c2);
+
+ if (c != 0) {
+ return c;
+ }
+ }
+
+ return 0;
+ }
+ }
+
+ /**
+ * Updates an SQL query string to include hints for the optimizer to
disable certain rules.
+ */
+ private static final class AddDisabledRulesTemplate implements
QueryTemplate {
+ private static final Pattern SELECT_REGEXP =
Pattern.compile("(?i)^select");
+ private static final Pattern SELECT_QRY_CHECK =
Pattern.compile("(?i)^select .*");
+
+ private final QueryTemplate input;
+
+ private final List<String> disabledRules;
+
+ private AddDisabledRulesTemplate(QueryTemplate input, List<String>
disabledRules) {
+ this.input = input;
+ this.disabledRules = disabledRules;
+ }
+
+ @Override
+ public String originalQueryString() {
+ return input.originalQueryString();
+ }
+
+ @Override
+ public String createQuery() {
+ String qry = input.createQuery();
+
+ if (!disabledRules.isEmpty()) {
+ String originalQuery = input.originalQueryString();
+
+ assert SELECT_QRY_CHECK.matcher(qry).matches() : "SELECT query
was expected: " + originalQuery + ". Updated: " + qry;
+
+ return SELECT_REGEXP.matcher(qry).replaceAll("select "
+ + HintUtils.toHint(IgniteHint.DISABLE_RULE,
disabledRules.toArray(ArrayUtils.STRING_EMPTY_ARRAY)));
+ } else {
+ return qry;
+ }
+ }
+ }
+}