You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by am...@apache.org on 2023/12/13 11:21:16 UTC
(ignite-3) branch main updated: IGNITE-20778: Provide internal API to get a list of parameters for non-executed query (#2857)
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 32c7178459 IGNITE-20778: Provide internal API to get a list of parameters for non-executed query (#2857)
32c7178459 is described below
commit 32c7178459d59f91e6476e29430b465ab46f7098
Author: Max Zhuravkov <sh...@gmail.com>
AuthorDate: Wed Dec 13 13:21:11 2023 +0200
IGNITE-20778: Provide internal API to get a list of parameters for non-executed query (#2857)
---
.../client/fakes/FakeIgniteQueryProcessor.java | 7 +
.../internal/sql/engine/ItDataTypesTest.java | 2 +-
.../sql/engine/ItDynamicParameterTest.java | 167 ++++++
.../ignite/internal/sql/engine/QueryProcessor.java | 18 +
.../internal/sql/engine/SqlQueryProcessor.java | 88 ++-
.../internal/sql/engine/prepare/CacheKey.java | 7 +-
.../internal/sql/engine/prepare/DdlPlan.java | 10 +
.../internal/sql/engine/prepare/ExplainPlan.java | 6 +
.../internal/sql/engine/prepare/IgnitePlanner.java | 18 +-
.../sql/engine/prepare/IgniteSqlValidator.java | 426 ++++++++++++---
.../sql/engine/prepare/IgniteTypeCoercion.java | 156 +++++-
.../internal/sql/engine/prepare/MultiStepPlan.java | 11 +-
.../{QueryPlan.java => ParameterMetadata.java} | 36 +-
.../internal/sql/engine/prepare/ParameterType.java | 96 ++++
.../sql/engine/prepare/PlanningContext.java | 26 +-
.../sql/engine/prepare/PrepareService.java | 1 +
.../sql/engine/prepare/PrepareServiceImpl.java | 227 +++++++-
.../internal/sql/engine/prepare/QueryPlan.java | 5 +
.../ignite/internal/sql/engine/util/Commons.java | 48 +-
.../internal/sql/engine/util/IgniteResource.java | 75 +++
.../ignite/internal/sql/engine/util/TypeUtils.java | 2 +-
.../sql/engine/exec/ExecutionServiceImplTest.java | 2 +
.../sql/engine/exec/mapping/MappingTestRunner.java | 5 +-
.../sql/engine/planner/AbstractPlannerTest.java | 29 +-
.../sql/engine/planner/DynamicParametersTest.java | 587 ++++++++++++++++++++-
.../sql/engine/prepare/PrepareServiceImplTest.java | 166 +++++-
.../internal/sql/engine/util/CommonsTest.java | 13 +
.../internal/sql/engine/util/QueryCheckerTest.java | 7 +
.../internal/sql/engine/util/StatementChecker.java | 66 ++-
.../sql/engine/util/StatementCheckerTest.java | 8 +-
.../sql/metrics/PlanningCacheMetricsTest.java | 22 +-
.../internal/sql/engine/util/SqlTestUtils.java | 33 ++
32 files changed, 2148 insertions(+), 222 deletions(-)
diff --git a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgniteQueryProcessor.java b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgniteQueryProcessor.java
index 0e2aade3dd..e6bb5b0d77 100644
--- a/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgniteQueryProcessor.java
+++ b/modules/client/src/test/java/org/apache/ignite/client/fakes/FakeIgniteQueryProcessor.java
@@ -21,6 +21,7 @@ import java.util.concurrent.CompletableFuture;
import org.apache.ignite.internal.sql.engine.AsyncSqlCursor;
import org.apache.ignite.internal.sql.engine.InternalSqlRow;
import org.apache.ignite.internal.sql.engine.QueryProcessor;
+import org.apache.ignite.internal.sql.engine.prepare.ParameterMetadata;
import org.apache.ignite.internal.sql.engine.property.SqlProperties;
import org.apache.ignite.internal.tx.InternalTransaction;
import org.apache.ignite.tx.IgniteTransactions;
@@ -30,6 +31,12 @@ import org.jetbrains.annotations.Nullable;
* Fake {@link QueryProcessor}.
*/
public class FakeIgniteQueryProcessor implements QueryProcessor {
+ @Override
+ public CompletableFuture<ParameterMetadata> prepareSingleAsync(SqlProperties properties,
+ @Nullable InternalTransaction transaction, String qry, Object... params) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public CompletableFuture<AsyncSqlCursor<InternalSqlRow>> querySingleAsync(
SqlProperties properties,
diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
index 85a23500a7..3161184a8f 100644
--- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
+++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDataTypesTest.java
@@ -410,7 +410,7 @@ public class ItDataTypesTest extends BaseSqlIntegrationTest {
expectResult(checker, result);
}
- @ParameterizedTest(name = "{1}")
+ @ParameterizedTest(name = "{1} {2}")
@MethodSource("decimalOverflows")
public void testCalcOpOverflow(SqlTypeName type, String expr, Object param) {
if (param == EMPTY_PARAM) {
diff --git a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
index 517b100b52..22ebd676f5 100644
--- a/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
+++ b/modules/sql-engine/src/integrationTest/java/org/apache/ignite/internal/sql/engine/ItDynamicParameterTest.java
@@ -19,20 +19,32 @@ package org.apache.ignite.internal.sql.engine;
import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
import static org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import java.sql.Date;
import java.time.LocalDate;
+import java.util.Arrays;
import java.util.List;
+import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.ignite.internal.sql.BaseSqlIntegrationTest;
+import org.apache.ignite.internal.sql.engine.prepare.ParameterMetadata;
+import org.apache.ignite.internal.sql.engine.prepare.ParameterType;
+import org.apache.ignite.internal.sql.engine.property.SqlProperties;
+import org.apache.ignite.internal.sql.engine.property.SqlPropertiesHelper;
import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
import org.apache.ignite.internal.sql.engine.util.Commons;
import org.apache.ignite.internal.sql.engine.util.MetadataMatcher;
import org.apache.ignite.internal.sql.engine.util.SqlTestUtils;
+import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.apache.ignite.internal.type.NativeType;
+import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.util.Pair;
import org.apache.ignite.lang.ErrorGroups.Sql;
import org.apache.ignite.sql.ColumnType;
import org.junit.jupiter.api.AfterEach;
@@ -279,6 +291,155 @@ public class ItDynamicParameterTest extends BaseSqlIntegrationTest {
);
}
+ @ParameterizedTest
+ @MethodSource("statementsWithParameters")
+ public void testGetParameterTypesSimple(String stmt, List<ColumnType> expectedTypes, Object[] params) {
+ ParameterMetadata parameterTypes = getParameterMetadata(stmt, params);
+
+ List<ColumnType> columnTypes = parameterTypes.parameterTypes()
+ .stream()
+ .map(ParameterType::columnType)
+ .collect(Collectors.toList());
+
+ assertEquals(expectedTypes, columnTypes, stmt);
+ }
+
+ private static Stream<Arguments> statementsWithParameters() {
+ return Stream.of(
+ arguments("SELECT CAST(? AS BIGINT)", List.of(ColumnType.INT64), new Object[0]),
+ arguments("SELECT CAST(? AS BIGINT)", List.of(ColumnType.INT32), new Object[]{1}),
+ arguments("SELECT CAST(? AS BIGINT)", List.of(ColumnType.STRING), new Object[]{"1"}),
+
+ arguments("INSERT INTO t1 VALUES(1, ?, ?)", List.of(ColumnType.INT32, ColumnType.INT32), new Object[0]),
+ arguments("UPDATE t1 SET val1 = ? WHERE id = ?", List.of(ColumnType.INT32, ColumnType.INT32), new Object[0]),
+ arguments("SELECT val1 + ? FROM t1 WHERE id = ?", List.of(ColumnType.INT32, ColumnType.INT32), new Object[0])
+ );
+ }
+
+ @Test
+ public void testRejectPrepareWithMoreParameters() {
+ assertThrowsSqlException(
+ Sql.STMT_VALIDATION_ERR,
+ "Unexpected number of query parameters",
+ () -> getParameterMetadata("SELECT ? + ?", 1, 2, 3));
+ }
+
+ @Test
+ public void testGetUnspecifiedParameterTypesInPredicate() {
+ List<Pair<String, NativeType>> dataTypes = columnNameAndType();
+
+ sql(createTableFoColumns("params0", dataTypes));
+
+ StringBuilder stmt = new StringBuilder("SELECT * FROM params0 WHERE ");
+ for (int i = 0; i < dataTypes.size(); i++) {
+ if (i > 0) {
+ stmt.append(" AND ");
+ }
+ stmt.append(dataTypes.get(i).getFirst());
+ stmt.append(" = ?");
+ }
+
+ log.info("SELECT from column names: {}", stmt);
+
+ ParameterMetadata parameterTypes = getParameterMetadata(stmt.toString());
+
+ List<NativeType> actualTypes = parameterTypes.parameterTypes().stream()
+ .map(p -> TypeUtils.columnType2NativeType(p.columnType(), p.precision(), p.scale(), p.precision()))
+ .collect(Collectors.toList());
+
+ assertEquals(actualTypes, dataTypes.stream().map(Pair::getSecond).collect(Collectors.toList()), "parameter types");
+ }
+
+ @Test
+ public void testGetUnspecifiedParameterTypesInInsert() {
+ List<Pair<String, NativeType>> dataTypes = columnNameAndType();
+
+ sql(createTableFoColumns("params1", dataTypes));
+
+ StringBuilder stmt = new StringBuilder("INSERT INTO params1 VALUES(1");
+ stmt.append(", ?".repeat(dataTypes.size()));
+ stmt.append(")");
+
+ log.info("INSERT from column names: {}", stmt);
+
+ ParameterMetadata parameterTypes = getParameterMetadata(stmt.toString());
+
+ List<NativeType> actualTypes = parameterTypes.parameterTypes().stream()
+ .map(p -> TypeUtils.columnType2NativeType(p.columnType(), p.precision(), p.scale(), p.precision()))
+ .collect(Collectors.toList());
+
+ assertEquals(actualTypes, dataTypes.stream().map(Pair::getSecond).collect(Collectors.toList()), "parameter types");
+ }
+
+ @Test
+ public void testGetUnspecifiedParameterTypesInUpdate() {
+ List<Pair<String, NativeType>> dataTypes = columnNameAndType();
+
+ sql(createTableFoColumns("params2", dataTypes));
+
+ StringBuilder stmt = new StringBuilder("UPDATE params2 SET ");
+ for (int i = 0; i < dataTypes.size(); i++) {
+ if (i > 0) {
+ stmt.append(", ");
+ }
+ String colName = dataTypes.get(i).getFirst();
+ stmt.append(colName);
+ stmt.append("=?");
+ }
+
+ log.info("UPDATE from column names: {}", stmt);
+
+ ParameterMetadata parameterTypes = getParameterMetadata(stmt.toString());
+
+ List<NativeType> actualTypes = parameterTypes.parameterTypes().stream()
+ .map(p -> TypeUtils.columnType2NativeType(p.columnType(), p.precision(), p.scale(), p.precision()))
+ .collect(Collectors.toList());
+
+ assertEquals(actualTypes, dataTypes.stream().map(Pair::getSecond).collect(Collectors.toList()), "parameter types");
+ }
+
+ private static List<Pair<String, NativeType>> columnNameAndType() {
+ return Arrays.asList(
+ new Pair<>("bool_col", NativeTypes.BOOLEAN),
+ new Pair<>("int8_col", NativeTypes.INT8),
+ new Pair<>("int16_col", NativeTypes.INT16),
+ new Pair<>("int32_col", NativeTypes.INT32),
+ new Pair<>("int64_col", NativeTypes.INT64),
+ new Pair<>("float_col", NativeTypes.FLOAT),
+ new Pair<>("double_col", NativeTypes.DOUBLE),
+ new Pair<>("dec_col", NativeTypes.decimalOf(5, 2)),
+ new Pair<>("date_col", NativeTypes.DATE),
+ new Pair<>("datetime_col", NativeTypes.datetime(2)),
+ new Pair<>("time_col", NativeTypes.time(2)),
+ new Pair<>("string_col", NativeTypes.stringOf(10)),
+ new Pair<>("bytes_col", NativeTypes.blobOf(10)),
+ new Pair<>("uuid_col", NativeTypes.UUID)
+ );
+ }
+
+ private String createTableFoColumns(String name, List<Pair<String, NativeType>> dataTypes) {
+ IgniteTypeFactory tf = Commons.typeFactory();
+
+ StringBuilder ddl = new StringBuilder("CREATE TABLE ");
+ ddl.append(name);
+ ddl.append("(id INTEGER PRIMARY KEY");
+
+ for (Pair<String, NativeType> entry : dataTypes) {
+ RelDataType relDataType = TypeUtils.native2relationalType(tf, entry.getSecond(), true);
+
+ ddl.append(", ");
+ ddl.append(entry.getFirst());
+ ddl.append(" ");
+ ddl.append(relDataType);
+ }
+
+ ddl.append(")");
+
+ log.info("CREATE TABLE from column names: {}", ddl);
+
+ return ddl.toString();
+ }
+
@Override
protected int initialNodes() {
return 1;
@@ -290,4 +451,10 @@ public class ItDynamicParameterTest extends BaseSqlIntegrationTest {
"Unexpected number of query parameters",
() -> assertQuery(query).withParams(params).check());
}
+
+ private ParameterMetadata getParameterMetadata(String query, Object... params) {
+ QueryProcessor qryProc = queryProcessor();
+ SqlProperties properties = SqlPropertiesHelper.emptyProperties();
+ return await(qryProc.prepareSingleAsync(properties, null, query, params));
+ }
}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java
index b64acc5103..258651d37e 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/QueryProcessor.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.sql.engine;
import java.util.concurrent.CompletableFuture;
import org.apache.ignite.internal.manager.IgniteComponent;
+import org.apache.ignite.internal.sql.engine.prepare.ParameterMetadata;
import org.apache.ignite.internal.sql.engine.property.SqlProperties;
import org.apache.ignite.internal.tx.InternalTransaction;
import org.apache.ignite.lang.IgniteException;
@@ -29,6 +30,23 @@ import org.jetbrains.annotations.Nullable;
* QueryProcessor interface.
*/
public interface QueryProcessor extends IgniteComponent {
+
+ /**
+ * Returns parameter metadata for the given statement. This method uses optional array of parameters to assist with type inference.
+ *
+ * @param properties User query properties. See {@link QueryProperty} for available properties.
+ * @param transaction A transaction to use to resolve a schema.
+ * @param qry Single statement SQL query.
+ * @param params Query parameters.
+ * @return Parameter metadata.
+ *
+ * @throws IgniteException in case of an error.
+ * @see QueryProperty
+ */
+ CompletableFuture<ParameterMetadata> prepareSingleAsync(SqlProperties properties,
+ @Nullable InternalTransaction transaction,
+ String qry, Object... params);
+
/**
* Execute the single statement query with given schema name and parameters.
*
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
index bae9c77f99..dcc00d9aa6 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/SqlQueryProcessor.java
@@ -86,6 +86,7 @@ import org.apache.ignite.internal.sql.engine.exec.mapping.ExecutionTargetFactory
import org.apache.ignite.internal.sql.engine.exec.mapping.ExecutionTargetProvider;
import org.apache.ignite.internal.sql.engine.exec.mapping.MappingServiceImpl;
import org.apache.ignite.internal.sql.engine.message.MessageServiceImpl;
+import org.apache.ignite.internal.sql.engine.prepare.ParameterMetadata;
import org.apache.ignite.internal.sql.engine.prepare.PrepareService;
import org.apache.ignite.internal.sql.engine.prepare.PrepareServiceImpl;
import org.apache.ignite.internal.sql.engine.prepare.QueryPlan;
@@ -416,6 +417,23 @@ public class SqlQueryProcessor implements QueryProcessor {
IgniteUtils.closeAll(services.stream().map(s -> s::stop));
}
+ /** {@inheritDoc} */
+ @Override
+ public CompletableFuture<ParameterMetadata> prepareSingleAsync(SqlProperties properties,
+ @Nullable InternalTransaction transaction,
+ String qry, Object... params) {
+
+ if (!busyLock.enterBusy()) {
+ throw new IgniteInternalException(NODE_STOPPING_ERR, new NodeStoppingException());
+ }
+
+ try {
+ return prepareSingleAsync0(properties, transaction, qry, params);
+ } finally {
+ busyLock.leaveBusy();
+ }
+ }
+
/** {@inheritDoc} */
@Override
public CompletableFuture<AsyncSqlCursor<InternalSqlRow>> querySingleAsync(
@@ -462,6 +480,43 @@ public class SqlQueryProcessor implements QueryProcessor {
return service;
}
+ private CompletableFuture<ParameterMetadata> prepareSingleAsync0(
+ SqlProperties properties,
+ @Nullable InternalTransaction explicitTransaction,
+ String sql,
+ Object... params
+ ) {
+ SqlProperties properties0 = SqlPropertiesHelper.chain(properties, DEFAULT_PROPERTIES);
+ String schemaName = properties0.get(QueryProperty.DEFAULT_SCHEMA);
+
+ QueryCancel queryCancel = new QueryCancel();
+
+ CompletableFuture<ParameterMetadata> start = new CompletableFuture<>();
+
+ CompletableFuture<ParameterMetadata> stage = start.thenCompose(ignored -> {
+ ParsedResult result = parserService.parse(sql);
+
+ validateParsedStatement(properties0, result);
+ validateDynamicParameters(result.dynamicParamsCount(), params, false);
+
+ HybridTimestamp timestamp = explicitTransaction != null ? explicitTransaction.startTimestamp() : clock.now();
+
+ return prepareParsedStatement(schemaName, result, timestamp, queryCancel, params)
+ .thenApply(QueryPlan::parameterMetadata);
+ });
+
+ // TODO IGNITE-20078 Improve (or remove) CancellationException handling.
+ stage.whenComplete((cur, ex) -> {
+ if (ex instanceof CancellationException) {
+ queryCancel.cancel();
+ }
+ });
+
+ start.completeAsync(() -> null, taskExecutor);
+
+ return stage;
+ }
+
private CompletableFuture<AsyncSqlCursor<InternalSqlRow>> querySingle0(
SqlProperties properties,
QueryTransactionContext txCtx,
@@ -478,7 +533,8 @@ public class SqlQueryProcessor implements QueryProcessor {
CompletableFuture<AsyncSqlCursor<InternalSqlRow>> stage = start.thenCompose(ignored -> {
ParsedResult result = parserService.parse(sql);
- validateParsedStatement(properties0, result, params);
+ validateParsedStatement(properties0, result);
+ validateDynamicParameters(result.dynamicParamsCount(), params, true);
QueryTransactionWrapper txWrapper = txCtx.getOrStartImplicit(result.queryType());
@@ -522,6 +578,25 @@ public class SqlQueryProcessor implements QueryProcessor {
return parseFut;
}
+ private CompletableFuture<QueryPlan> prepareParsedStatement(String schemaName,
+ ParsedResult parsedResult,
+ HybridTimestamp timestamp,
+ QueryCancel queryCancel,
+ Object[] params) {
+
+ return waitForActualSchema(schemaName, timestamp)
+ .thenCompose(schema -> {
+ BaseQueryContext ctx = BaseQueryContext.builder()
+ .frameworkConfig(Frameworks.newConfigBuilder(FRAMEWORK_CONFIG).defaultSchema(schema).build())
+ .queryId(UUID.randomUUID())
+ .cancel(queryCancel)
+ .parameters(params)
+ .build();
+
+ return prepareSvc.prepareAsync(parsedResult, ctx);
+ });
+ }
+
private CompletableFuture<AsyncSqlCursor<InternalSqlRow>> executeParsedStatement(
String schemaName,
ParsedResult parsedResult,
@@ -624,8 +699,7 @@ public class SqlQueryProcessor implements QueryProcessor {
/** Performs additional validation of a parsed statement. **/
private static void validateParsedStatement(
SqlProperties properties,
- ParsedResult parsedResult,
- Object[] params
+ ParsedResult parsedResult
) {
Set<SqlQueryType> allowedTypes = properties.get(QueryProperty.ALLOWED_QUERY_TYPES);
SqlQueryType queryType = parsedResult.queryType();
@@ -641,12 +715,10 @@ public class SqlQueryProcessor implements QueryProcessor {
throw new SqlException(STMT_VALIDATION_ERR, message);
}
-
- validateDynamicParameters(parsedResult.dynamicParamsCount(), params);
}
- private static void validateDynamicParameters(int expectedParamsCount, Object[] params) throws SqlException {
- if (expectedParamsCount != params.length) {
+ private static void validateDynamicParameters(int expectedParamsCount, Object[] params, boolean exactMatch) throws SqlException {
+ if (exactMatch && expectedParamsCount != params.length || params.length > expectedParamsCount) {
String message = format(
"Unexpected number of query parameters. Provided {} but there is only {} dynamic parameter(s).",
params.length, expectedParamsCount
@@ -695,7 +767,7 @@ public class SqlQueryProcessor implements QueryProcessor {
int paramsCount = parsedResults.stream().mapToInt(ParsedResult::dynamicParamsCount).sum();
- validateDynamicParameters(paramsCount, params);
+ validateDynamicParameters(paramsCount, params, true);
ScriptStatement[] results = new ScriptStatement[parsedResults.size()];
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/CacheKey.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/CacheKey.java
index eb82b0d456..6a58ce4321 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/CacheKey.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/CacheKey.java
@@ -19,6 +19,7 @@ package org.apache.ignite.internal.sql.engine.prepare;
import java.util.Arrays;
import java.util.Objects;
+import org.apache.ignite.sql.ColumnType;
/**
* CacheKey.
@@ -26,7 +27,7 @@ import java.util.Objects;
* context could be schema name, dynamic parameters, and so on...
*/
public class CacheKey {
- static final Class<?>[] EMPTY_CLASS_ARRAY = {};
+ static final ColumnType[] EMPTY_CLASS_ARRAY = {};
private final int catalogVersion;
@@ -36,7 +37,7 @@ public class CacheKey {
private final Object contextKey;
- private final Class<?>[] paramTypes;
+ private final Object[] paramTypes;
/**
* Constructor.
@@ -48,7 +49,7 @@ public class CacheKey {
* LOCAL flag)
* @param paramTypes Types of all dynamic parameters, no any type can be {@code null}.
*/
- public CacheKey(int catalogVersion, String schemaName, String query, Object contextKey, Class<?>[] paramTypes) {
+ public CacheKey(int catalogVersion, String schemaName, String query, Object contextKey, ColumnType[] paramTypes) {
this.catalogVersion = catalogVersion;
this.schemaName = schemaName;
this.query = query;
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/DdlPlan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/DdlPlan.java
index 3b27d68b85..87d845f38c 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/DdlPlan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/DdlPlan.java
@@ -17,6 +17,7 @@
package org.apache.ignite.internal.sql.engine.prepare;
+import java.util.Collections;
import java.util.List;
import org.apache.ignite.internal.sql.api.ColumnMetadataImpl;
import org.apache.ignite.internal.sql.api.ResultSetMetadataImpl;
@@ -35,6 +36,9 @@ public class DdlPlan implements QueryPlan {
private static final ResultSetMetadata DDL_METADATA = new ResultSetMetadataImpl(List.of(
new ColumnMetadataImpl("APPLIED", ColumnType.BOOLEAN, 1, ColumnMetadata.UNDEFINED_SCALE, false, null)));
+ /** DDL has no parameters. */
+ private static final ParameterMetadata EMPTY_PARAMETERS = new ParameterMetadata(Collections.emptyList());
+
private final PlanId id;
private final DdlCommand cmd;
@@ -65,6 +69,12 @@ public class DdlPlan implements QueryPlan {
return DDL_METADATA;
}
+ /** {@inheritDoc} */
+ @Override
+ public ParameterMetadata parameterMetadata() {
+ return EMPTY_PARAMETERS;
+ }
+
/** {@inheritDoc} */
@Override
public String toString() {
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ExplainPlan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ExplainPlan.java
index 4cb3522629..f5b9403d6f 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ExplainPlan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ExplainPlan.java
@@ -60,6 +60,12 @@ public class ExplainPlan implements QueryPlan {
return EXPLAIN_METADATA;
}
+ /** {@inheritDoc} */
+ @Override
+ public ParameterMetadata parameterMetadata() {
+ return plan.parameterMetadata();
+ }
+
public MultiStepPlan plan() {
return plan;
}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgnitePlanner.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgnitePlanner.java
index 2870c97cca..d7f9f652d0 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgnitePlanner.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgnitePlanner.java
@@ -18,10 +18,8 @@
package org.apache.ignite.internal.sql.engine.prepare;
import static java.util.Objects.requireNonNull;
-import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
import static org.apache.ignite.internal.sql.engine.util.Commons.shortRuleName;
import static org.apache.ignite.lang.ErrorGroups.Sql.STMT_PARSE_ERR;
-import static org.apache.ignite.lang.ErrorGroups.Sql.STMT_VALIDATION_ERR;
import java.io.PrintWriter;
import java.io.Reader;
@@ -182,18 +180,8 @@ public class IgnitePlanner implements Planner, RelOptTable.ViewExpander {
/** {@inheritDoc} */
@Override
public SqlNode parse(Reader reader) throws SqlParseException {
+ // This method is only used in tests.
StatementParseResult parseResult = IgniteSqlParser.parse(reader, StatementParseResult.MODE);
- Object[] parameters = ctx.parameters();
-
- // Parse method is only used in tests.
- if (parameters.length != parseResult.dynamicParamsCount()) {
- String message = format(
- "Unexpected number of query parameters. Provided {} but there is only {} dynamic parameter(s).",
- parameters.length, parseResult.dynamicParamsCount()
- );
-
- throw new SqlException(STMT_VALIDATION_ERR, message);
- }
return parseResult.statement();
}
@@ -210,9 +198,11 @@ public class IgnitePlanner implements Planner, RelOptTable.ViewExpander {
public Pair<SqlNode, RelDataType> validateAndGetType(SqlNode sqlNode) {
SqlNode validatedNode = validator().validate(sqlNode);
RelDataType type = validator().getValidatedNodeType(validatedNode);
+ this.validatedSqlNode = validatedNode;
return Pair.of(validatedNode, type);
}
+ /** {@inheritDoc} */
@Override
public RelDataType getParameterRowType() {
return requireNonNull(validator, "validator")
@@ -316,6 +306,8 @@ public class IgnitePlanner implements Planner, RelOptTable.ViewExpander {
}
}
+ this.validatedSqlNode = validatedNode;
+
return new ValidationResult(validatedNode, type, origins, derived == null ? List.of() : derived);
}
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 a3a4a40a1d..c6d2ce411b 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
@@ -18,17 +18,24 @@
package org.apache.ignite.internal.sql.engine.prepare;
import static java.util.Objects.requireNonNull;
+import static org.apache.calcite.sql.type.SqlTypeName.INTEGER;
import static org.apache.calcite.sql.type.SqlTypeName.INT_TYPES;
-import static org.apache.calcite.sql.type.SqlTypeUtil.equalSansNullability;
import static org.apache.calcite.sql.type.SqlTypeUtil.isNull;
import static org.apache.calcite.util.Static.RESOURCE;
import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
import static org.apache.ignite.lang.ErrorGroups.Sql.STMT_PARSE_ERR;
+import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.IntArraySet;
+import it.unimi.dsi.fastutil.ints.IntSet;
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.util.AbstractList;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -40,6 +47,7 @@ import org.apache.calcite.prepare.CalciteCatalogReader;
import org.apache.calcite.prepare.Prepare;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeField;
+import org.apache.calcite.schema.impl.ModifiableViewTable;
import org.apache.calcite.sql.JoinConditionType;
import org.apache.calcite.sql.SqlAggFunction;
import org.apache.calcite.sql.SqlBasicCall;
@@ -67,6 +75,7 @@ import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.type.SqlTypeName.Limit;
import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.sql.util.SqlBasicVisitor;
+import org.apache.calcite.sql.util.SqlShuttle;
import org.apache.calcite.sql.validate.SelectScope;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorImpl;
@@ -119,14 +128,18 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
HUMAN_READABLE_ALIASES_FOR = Collections.unmodifiableSet(kinds);
}
- /** Dynamic parameter values. */
- private final Object[] dynamicParamValues;
+ /** Dynamic parameters state. */
+ private final Int2ObjectMap<DynamicParamState> dynamicParameters;
- /** Dynamic parameters SQL AST nodes for invariant checks - see {@link #validateInferredDynamicParameters()}. */
- private final SqlDynamicParam[] dynamicParamNodes;
+ /**
+ * The same dynamic parameter can be used in the same SQL tree multiple types after a rewrite.
+ * (E.g. COALESCE(?0, ?1) is rewritten into CASE WHEN ?0 IS NOT NULL THEN ?0 ELSE ?1 END)
+ * We store them to check that every i-th parameter has the same type.
+ */
+ private final IdentityHashMap<SqlDynamicParam, SqlDynamicParam> dynamicParamNodes = new IdentityHashMap<>();
/** Literal processing. */
- private LiteralExtractor litExtractor;
+ private final LiteralExtractor litExtractor = new LiteralExtractor();
/**
* Creates a validator.
@@ -138,11 +151,14 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
* @param parameters Dynamic parameters
*/
public IgniteSqlValidator(SqlOperatorTable opTab, CalciteCatalogReader catalogReader,
- IgniteTypeFactory typeFactory, SqlValidator.Config config, Object[] parameters) {
+ IgniteTypeFactory typeFactory, SqlValidator.Config config, Int2ObjectMap<Object> parameters) {
super(opTab, catalogReader, typeFactory, config);
- this.dynamicParamValues = parameters;
- this.dynamicParamNodes = new SqlDynamicParam[parameters.length];
+ this.dynamicParameters = new Int2ObjectArrayMap<>(parameters.size());
+ for (Map.Entry<Integer, Object> param : parameters.int2ObjectEntrySet()) {
+ Object value = param.getValue();
+ dynamicParameters.put(param.getKey().intValue(), new DynamicParamState(value));
+ }
}
/** {@inheritDoc} */
@@ -234,7 +250,7 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
}
if (!coerced) {
- super.checkTypeAssignment(sourceScope, table, sourceRowType, targetRowType, query);
+ doCheckTypeAssignment(sourceScope, table, sourceRowType, targetRowType, query);
}
}
@@ -276,6 +292,80 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
return (IgniteTable) dataSource;
}
+ /**
+ * The copy of {@link SqlValidatorImpl#checkTypeAssignment(SqlValidatorScope, SqlValidatorTable, RelDataType, RelDataType, SqlNode)}
+ * with a fixed condition to skip dynamic parameters + this method does not try to find a location of a type error.
+ */
+ private void doCheckTypeAssignment(
+ @Nullable SqlValidatorScope sourceScope,
+ SqlValidatorTable table,
+ RelDataType sourceRowType,
+ RelDataType targetRowType,
+ final SqlNode query) {
+ // NOTE jvs 23-Feb-2006: subclasses may allow for extra targets
+ // representing system-maintained columns, so stop after all sources
+ // matched
+ boolean isUpdateModifiableViewTable = false;
+ if (query instanceof SqlUpdate) {
+ final SqlNodeList targetColumnList =
+ requireNonNull(((SqlUpdate) query).getTargetColumnList());
+ final int targetColumnCount = targetColumnList.size();
+ targetRowType =
+ SqlTypeUtil.extractLastNFields(typeFactory, targetRowType,
+ targetColumnCount);
+ sourceRowType =
+ SqlTypeUtil.extractLastNFields(typeFactory, sourceRowType,
+ targetColumnCount);
+ isUpdateModifiableViewTable =
+ table.unwrap(ModifiableViewTable.class) != null;
+ }
+ if (SqlTypeUtil.equalAsStructSansNullability(typeFactory,
+ sourceRowType, targetRowType, null)) {
+ // Returns early if source and target row type equals sans nullability.
+ return;
+ }
+ if (config().typeCoercionEnabled() && !isUpdateModifiableViewTable) {
+ // Try type coercion first if implicit type coercion is allowed.
+ boolean coerced =
+ getTypeCoercion().querySourceCoercion(sourceScope, sourceRowType,
+ targetRowType, query);
+ if (coerced) {
+ return;
+ }
+ }
+
+ // Fall back to default behavior: compare the type families.
+ List<RelDataTypeField> sourceFields = sourceRowType.getFieldList();
+ List<RelDataTypeField> targetFields = targetRowType.getFieldList();
+ final int sourceCount = sourceFields.size();
+ for (int i = 0; i < sourceCount; ++i) {
+ RelDataType sourceType = sourceFields.get(i).getType();
+ RelDataType targetType = targetFields.get(i).getType();
+ if (!SqlTypeUtil.canAssignFrom(targetType, sourceType)) {
+ // A correct condition for skipping dynamic parameters.
+ if (sourceType == unknownType) {
+ continue;
+ }
+ String targetTypeString;
+ String sourceTypeString;
+ if (SqlTypeUtil.areCharacterSetsMismatched(
+ sourceType,
+ targetType)) {
+ sourceTypeString = sourceType.getFullTypeString();
+ targetTypeString = targetType.getFullTypeString();
+ } else {
+ sourceTypeString = sourceType.toString();
+ targetTypeString = targetType.toString();
+ }
+ // Always use a query as an error source.
+ throw newValidationError(query,
+ RESOURCE.typeNotAssignable(
+ targetFields.get(i).getName(), targetTypeString,
+ sourceFields.get(i).getName(), sourceTypeString));
+ }
+ }
+ }
+
private static void syncSelectList(SqlSelect select, SqlUpdate update) {
//
// If a table has N columns and update::SourceExpressionList has size = M
@@ -401,13 +491,28 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
throw newValidationError(n, IgniteResource.INSTANCE.correctIntegerLimit(nodeName));
}
} else if (n instanceof SqlDynamicParam) {
- Object param = getDynamicParamValue((SqlDynamicParam) n);
+ SqlDynamicParam dynamicParam = (SqlDynamicParam) n;
+ RelDataType intType = typeFactory.createSqlType(INTEGER);
+
+ // Validate value, if present.
+ if (!isUnspecified(dynamicParam)) {
+ Object param = getDynamicParamValue(dynamicParam);
- if (param instanceof Integer) {
- if ((Integer) param < 0) {
- throw newValidationError(n, IgniteResource.INSTANCE.correctIntegerLimit(nodeName));
+ if (param instanceof Integer) {
+ if ((Integer) param < 0) {
+ throw newValidationError(n, IgniteResource.INSTANCE.correctIntegerLimit(nodeName));
+ }
+ } else {
+ String actualType = deriveDynamicParamType(dynamicParam).toString();
+ String expectedType = intType.toString();
+
+ var err = IgniteResource.INSTANCE.incorrectDynamicParameterType(expectedType, actualType);
+ throw newValidationError(n, err);
}
}
+
+ // Dynamic parameters are nullable.
+ setDynamicParamType(dynamicParam, typeFactory.createTypeWithNullability(intType, true));
}
}
@@ -440,22 +545,14 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
/** {@inheritDoc} */
@Override
public RelDataType deriveType(SqlValidatorScope scope, SqlNode expr) {
+ if (expr instanceof SqlDynamicParam) {
+ return deriveDynamicParamType((SqlDynamicParam) expr);
+ }
+
checkTypesInteroperability(scope, expr);
RelDataType dataType = super.deriveType(scope, expr);
- // If type of dynamic parameter has not been inferred, use a type of its value.
- RelDataType paramType = expr instanceof SqlDynamicParam
- ? getDynamicParamType((SqlDynamicParam) expr) : null;
-
- if (dataType.equals(unknownType) && expr instanceof SqlDynamicParam) {
- // If paramType is unknown setValidatedNodeType is a no-op.
- setValidatedNodeType(expr, paramType);
- return paramType;
- } else if (!(expr instanceof SqlCall)) {
- return dataType;
- }
-
SqlKind sqlKind = expr.getKind();
// See the comments below.
if (!SqlKind.BINARY_COMPARISON.contains(sqlKind)) {
@@ -485,6 +582,37 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
return dataType;
}
+ /** {@inheritDoc} */
+ @Override public RelDataType getParameterRowType(SqlNode sqlQuery) {
+ // We do not use calcite' version since it is contains a bug,
+ // alreadyVisited visited uses object identity, but rewrites of NULLIF, COALESCE
+ // into dynamic parameters may place the same parameter into multiple positions
+ // in SQL tree.
+ List<RelDataType> types = new ArrayList<>();
+ IntSet alreadyVisited = new IntArraySet(dynamicParameters.size());
+ sqlQuery.accept(
+ new SqlShuttle() {
+ @Override public SqlNode visit(SqlDynamicParam param) {
+ if (alreadyVisited.add(param.getIndex())) {
+ RelDataType type = getValidatedNodeType(param);
+ types.add(type);
+ }
+ return param;
+ }
+ });
+ return typeFactory.createStructType(
+ types,
+ new AbstractList<String>() {
+ @Override public String get(int index) {
+ return "?" + index;
+ }
+
+ @Override public int size() {
+ return types.size();
+ }
+ });
+ }
+
/** Check appropriate type cast availability. */
private void checkTypesInteroperability(SqlValidatorScope scope, SqlNode expr) {
boolean castOp = expr.getKind() == SqlKind.CAST;
@@ -498,7 +626,8 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
RelDataType returnType = super.deriveType(scope, ret);
if (first instanceof SqlDynamicParam) {
- firstType = getDynamicParamType((SqlDynamicParam) first);
+ SqlDynamicParam dynamicParam = (SqlDynamicParam) first;
+ firstType = deriveDynamicParamType(dynamicParam);
} else {
firstType = super.deriveType(scope, first);
}
@@ -518,7 +647,7 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
if (fromCustomType != null && returnCustomType != null) {
// it`s not allowed to convert between different custom types for now.
- check = equalSansNullability(typeFactory, firstType, returnType);
+ check = SqlTypeUtil.equalSansNullability(typeFactory, firstType, returnType);
} else if (fromCustomType != null) {
check = coercionRules.needToCast(returnType, (IgniteCustomType) fromCustomType);
} else if (returnCustomType != null) {
@@ -549,7 +678,7 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
/** Check literal can fit to declared exact numeric type, work only for single literal. */
private void literalCanFitType(SqlNode expr, RelDataType toType) {
if (INT_TYPES.contains(toType.getSqlTypeName())) {
- SqlLiteral literal = Objects.requireNonNullElseGet(litExtractor, LiteralExtractor::new).getLiteral(expr);
+ SqlLiteral literal = litExtractor.getLiteral(expr);
if (literal == null || literal.toValue() == null) {
return;
@@ -581,9 +710,10 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
}
private static class LiteralExtractor extends SqlBasicVisitor<SqlNode> {
- private @Nullable SqlLiteral extracted = null;
+ private @Nullable SqlLiteral extracted;
private @Nullable SqlLiteral getLiteral(SqlNode expr) {
+ extracted = null;
try {
expr.accept(this);
} catch (Util.FoundOne e) {
@@ -772,47 +902,93 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
/** {@inheritDoc} */
@Override
protected void inferUnknownTypes(RelDataType inferredType, SqlValidatorScope scope, SqlNode node) {
- if (node instanceof SqlDynamicParam) {
- RelDataType result = inferDynamicParamType(inferredType, (SqlDynamicParam) node);
- this.setValidatedNodeType(node, result);
+ // See SqlStdOperatorTable::IS_NULL and SqlStdOperatorTable::IS_NOT_NULL
+ // Operators use VARCHAR_1024 for argument type inference, so we need to manually fix this.
+ if (node.getKind() == SqlKind.IS_NULL || node.getKind() == SqlKind.IS_NOT_NULL) {
+ SqlCall call = (SqlCall) node;
+
+ if (isUnspecifiedDynamicParam(call.operand(0))) {
+ SqlCallBinding binding = new SqlCallBinding(this, scope, call);
+ String signature = IgniteResource.makeSignature(binding, List.of(unknownType));
+
+ throw binding.newValidationError(IgniteResource.INSTANCE.ambiguousOperator1(signature));
+ }
+ } else if (node.getKind() == SqlKind.IN) {
+ // TypeInference for IN operator fails with
+ // java.lang.UnsupportedOperationException: class org.apache.calcite.sql.SqlNodeList: 1
+ // if the first operand has unknown type.
+ SqlCall call = (SqlCall) node;
+
+ if (isUnspecifiedDynamicParam(call.operand(0))) {
+ SqlDynamicParam dynamicParam = call.operand(0);
+ throw newValidationError(dynamicParam, IgniteResource.INSTANCE.unableToResolveDynamicParameterType());
+ }
+ }
+
+ if (node instanceof SqlDynamicParam) {
+ SqlDynamicParam dynamicParam = (SqlDynamicParam) node;
+ inferDynamicParamType(inferredType, dynamicParam);
} else {
super.inferUnknownTypes(inferredType, scope, node);
}
}
- private RelDataType inferDynamicParamType(RelDataType inferredType, SqlDynamicParam dynamicParam) {
- RelDataType type = getDynamicParamType(dynamicParam);
- RelDataType paramTypeToUse;
+ private void inferDynamicParamType(RelDataType inferredType, SqlDynamicParam dynamicParam) {
+ RelDataType type = deriveDynamicParamType(dynamicParam);
/*
- * If inferredType is unknown - use a type of dynamic parameter since there is no other source of type information.
+ * If inferredType type is unknown and parameter is not specified, then use unknown type.
+ * Otherwise use parameter type if it set, or inferredType provided by operator's
+ * SqlOperandTypeInference and SqlOperandTypeCheckers.
*
- * If parameter's type and the inferredType do not match - use parameter's type.
- * This makes CAST operations to work correctly. Otherwise cast's operand is going to have
- * the same type as a target type which is not correct as it
- * makes every CAST operation eligible to redundant type conversion elimination
- * at later stages:
- * E.g: CAST(? AS INTEGER) where ?='hello' operand is going to be inferred as INTEGER
- * although it is a string.
- *
- * In other cases use the inferredType and we rely on type inference rules provided by
- * operator's SqlOperandTypeInference and SqlOperandTypeCheckers.
+ * If dynamic parameter is an operand of a CAST expression, its type is set to
+ * the type equal to the target type. Although CAST(p:T AS T) is a no-op and later removed at the sql-to-rel conversion phase,
+ * the runtime _should_ perform type conversion of unspecified dynamic parameter values to complete type checking.
*/
- if (inferredType.equals(unknownType) || (!equalSansNullability(type, inferredType))) {
- paramTypeToUse = type;
+ if (inferredType == unknownType && type == unknownType) {
+ setDynamicParamType(dynamicParam, unknownType);
+ } else if (type != unknownType) {
+ setDynamicParamType(dynamicParam, type);
} else {
- paramTypeToUse = inferredType;
+ // Make sure to set nullability to true, so types are nullable in all cases.
+ RelDataType nullableType = typeFactory.createTypeWithNullability(inferredType, true);
+ setDynamicParamType(dynamicParam, nullableType);
}
+ }
+
+ /** Derives the type of the given dynamic parameter. */
+ private RelDataType deriveDynamicParamType(SqlDynamicParam dynamicParam) {
+ dynamicParamNodes.put(dynamicParam, dynamicParam);
+
+ if (isUnspecified(dynamicParam)) {
+ RelDataType validatedNodeType = getValidatedNodeTypeIfKnown(dynamicParam);
+
+ if (validatedNodeType == null) {
+ setDynamicParamType(dynamicParam, unknownType);
+ return unknownType;
+ } else {
+ setDynamicParamType(dynamicParam, validatedNodeType);
+ return validatedNodeType;
+ }
+ } else {
+ Object value = getDynamicParamValue(dynamicParam);
+ RelDataType parameterType = deriveTypeFromDynamicParamValue(value);
- return typeFactory.createTypeWithNullability(paramTypeToUse, true);
+ // Dynamic parameters are always nullable.
+ // Otherwise it seem to cause "Conversion to relational algebra failed to preserve datatypes" errors
+ // in some cases.
+ RelDataType nullableType = typeFactory.createTypeWithNullability(parameterType, true);
+
+ setDynamicParamType(dynamicParam, nullableType);
+
+ return nullableType;
+ }
}
- /** Returns type of the given dynamic parameter. */
- RelDataType getDynamicParamType(SqlDynamicParam dynamicParam) {
- IgniteTypeFactory typeFactory = (IgniteTypeFactory) this.getTypeFactory();
- Object value = getDynamicParamValue(dynamicParam);
+ private RelDataType deriveTypeFromDynamicParamValue(@Nullable Object value) {
+ IgniteTypeFactory typeFactory = typeFactory();
RelDataType parameterType;
// IgniteCustomType: first we must check whether dynamic parameter is a custom data type.
@@ -825,38 +1001,142 @@ public class IgniteSqlValidator extends SqlValidatorImpl {
parameterType = typeFactory.toSql(typeFactory.createType(value.getClass()));
}
- // Dynamic parameters are always nullable.
- // Otherwise it seem to cause "Conversion to relational algebra failed to preserve datatypes" errors
- // in some cases.
- return typeFactory.createTypeWithNullability(parameterType, true);
+ return parameterType;
}
+ /** if dynamic parameter is not specified, set its type to the provided type, otherwise return the type of its value. */
+ RelDataType resolveDynamicParameterType(SqlDynamicParam dynamicParam, RelDataType contextType) {
+ if (isUnspecified(dynamicParam)) {
+ RelDataType nullableContextType = typeFactory.createTypeWithNullability(contextType, true);
+
+ setDynamicParamType(dynamicParam, nullableContextType);
+
+ return nullableContextType;
+ } else {
+ return deriveDynamicParamType(dynamicParam);
+ }
+ }
+
+ private void setDynamicParamType(SqlDynamicParam dynamicParam, RelDataType dataType) {
+ setValidatedNodeType(dynamicParam, dataType);
+
+ setDynamicParamResolvedType(dynamicParam, dataType);
+ }
+
+ /**
+ * Returns the value of the given dynamic parameter. If the value is not specified,
+ * this method throws {@link IllegalArgumentException}.
+ */
+ @Nullable
private Object getDynamicParamValue(SqlDynamicParam dynamicParam) {
- Object value = dynamicParamValues[dynamicParam.getIndex()];
- // save dynamic parameter for later validation.
- dynamicParamNodes[dynamicParam.getIndex()] = dynamicParam;
- return value;
+ int index = dynamicParam.getIndex();
+ DynamicParamState paramState = dynamicParameters.computeIfAbsent(index, (i) -> new DynamicParamState());
+ Object value = paramState.value;
+ if (!paramState.hasValue) {
+ throw new IllegalArgumentException(format("Value of dynamic parameter#{} is not specified", index));
+ } else {
+ return value;
+ }
}
private void validateInferredDynamicParameters() {
- // Derived types of dynamic parameters should not change (current type inference behavior).
+ for (int i = 0; i < dynamicParameters.size(); i++) {
+ DynamicParamState paramState = dynamicParameters.get(i);
- for (int i = 0; i < dynamicParamValues.length; i++) {
- SqlDynamicParam param = dynamicParamNodes[i];
- assert param != null : format("Dynamic parameter#{} has not been checked", i);
+ if (paramState == null || paramState.resolvedType == null || paramState.node == null) {
+ throw new AssertionError("Dynamic parameter has not been validated: " + i);
+ } else if (paramState.resolvedType == unknownType) {
+ throw newValidationError(paramState.node, IgniteResource.INSTANCE.unableToResolveDynamicParameterType());
+ }
+ }
+
+ // Ensure that all nodes for i-th parameter have the same type.
+ for (SqlDynamicParam node : dynamicParamNodes.keySet()) {
+ int i = node.getIndex();
+ DynamicParamState state = dynamicParameters.get(i);
+
+ if (!state.hasValue) {
+ continue;
+ }
- RelDataType paramType = getDynamicParamType(param);
- RelDataType derivedType = getValidatedNodeType(param);
+ Object value = state.value;
+ RelDataType valueType = deriveTypeFromDynamicParamValue(value);
+ RelDataType derivedType = getValidatedNodeTypeIfKnown(node);
+ RelDataType paramType = state.resolvedType;
- // We can check for nullability, but it was set to true.
- if (!equalSansNullability(derivedType, paramType)) {
+ // Ensure that derived type matches parameter's value.
+ if (!SqlTypeUtil.equalSansNullability(derivedType, valueType)) {
String message = format(
- "Type of dynamic parameter#{} does not match. Expected: {} derived: {}", i, paramType.getFullTypeString(),
- derivedType.getFullTypeString()
+ "Type of dynamic parameter#{} value type does not match. Expected: {} derived: {}",
+ i, valueType.getFullTypeString(), derivedType.getFullTypeString()
);
throw new AssertionError(message);
}
+
+ if (!Objects.equals(paramType, derivedType)) {
+ String message = format(
+ "Type of dynamic parameter node#{} does not match. Expected: {} derived: {}", i, paramType.getFullTypeString(),
+ derivedType != null ? derivedType.getFullTypeString() : null
+ );
+
+ throw new AssertionError(message);
+ }
+ }
+ }
+
+ private void setDynamicParamResolvedType(SqlDynamicParam param, RelDataType type) {
+ int index = param.getIndex();
+ DynamicParamState state = dynamicParameters.computeIfAbsent(index, (i) -> new DynamicParamState());
+
+ state.node = param;
+ state.resolvedType = type;
+ }
+
+ /** Returns {@code true} if the given dynamic parameter has no value set. */
+ public boolean isUnspecified(SqlDynamicParam param) {
+ int index = param.getIndex();
+ DynamicParamState state = dynamicParameters.computeIfAbsent(index, (i) -> new DynamicParamState());
+
+ return !state.hasValue;
+ }
+
+ /** Returns {@code true} if the given node is dynamic parameter that has no value set. */
+ public boolean isUnspecifiedDynamicParam(SqlNode node) {
+ if (node.getKind() != SqlKind.DYNAMIC_PARAM) {
+ return false;
+ } else {
+ return isUnspecified((SqlDynamicParam) node);
+ }
+ }
+
+ private static final class DynamicParamState {
+
+ final Object value;
+
+ final boolean hasValue;
+
+ SqlDynamicParam node;
+
+ /**
+ * Resolved type of a dynamic parameter.
+ *
+ * <ul>
+ * <li>{@code null} - parameter has not been checked - this is a bug.</li>
+ * <li>{@code unknownType} means the type of this parameter has not been resolved due to ambiguity.</li>
+ * <li>Otherwise contains a type of a parameter.</li>
+ * </ul>
+ */
+ RelDataType resolvedType;
+
+ private DynamicParamState(@Nullable Object value) {
+ this.value = value;
+ this.hasValue = true;
+ }
+
+ private DynamicParamState() {
+ this.value = null;
+ this.hasValue = false;
}
}
}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java
index 5ddb4efce7..c5931dab69 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/IgniteTypeCoercion.java
@@ -23,6 +23,7 @@ import static org.apache.calcite.util.Static.RESOURCE;
import java.nio.charset.Charset;
import java.util.ArrayDeque;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -38,10 +39,13 @@ import org.apache.calcite.sql.SqlDataTypeSpec;
import org.apache.calcite.sql.SqlDynamicParam;
import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlInsert;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlOperator;
+import org.apache.calcite.sql.SqlSelect;
+import org.apache.calcite.sql.SqlUpdate;
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParserPos;
@@ -93,8 +97,8 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
}
SqlValidatorScope scope = binding.getScope();
- RelDataType leftType = validator.deriveType(scope, call.operand(0));
- RelDataType rightType = validator.deriveType(scope, call.operand(1));
+ RelDataType leftType = binding.getOperandType(0);
+ RelDataType rightType = binding.getOperandType(1);
//
// binaryComparisonCoercion that makes '1' > 1 work, may introduce some inconsistent results
@@ -151,7 +155,16 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
public boolean binaryArithmeticCoercion(SqlCallBinding binding) {
ContextStack ctxStack = contextStack.get();
Context ctx = ctxStack.push(ContextType.UNSPECIFIED);
+
try {
+ // Called from CompositeOperandTypeChecker that does not care whether this is a binary arithmetic or not.
+ if (binding.getOperandCount() == 2 && SqlKind.BINARY_ARITHMETIC.contains(binding.getCall().getKind())) {
+ RelDataType leftType = binding.getOperandType(0);
+ RelDataType rightType = binding.getOperandType(1);
+
+ validateBinaryOperation(binding, leftType, rightType);
+ }
+
return super.binaryArithmeticCoercion(binding);
} finally {
ctxStack.pop(ctx);
@@ -164,6 +177,8 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
ContextStack ctxStack = contextStack.get();
Context ctx = ctxStack.push(ContextType.UNSPECIFIED);
try {
+ validateFunctionOperands(binding, operandTypes, expectedFamilies);
+
return super.builtinFunctionCoercion(binding, operandTypes, expectedFamilies);
} finally {
ctxStack.pop(ctx);
@@ -238,6 +253,8 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
ContextStack ctxStack = contextStack.get();
Context ctx = ctxStack.push(ContextType.MODIFY);
try {
+ validateDynamicParametersInModify(scope, targetRowType, query);
+
return super.querySourceCoercion(scope, sourceRowType, targetRowType, query);
} finally {
ctxStack.pop(ctx);
@@ -529,6 +546,103 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
return SqlStdOperatorTable.CAST.createCall(SqlParserPos.ZERO, node, targetDataType);
}
+ /**
+ * Validate dynamic parameters in binary operator.
+ */
+ private void validateBinaryOperation(SqlCallBinding binding, RelDataType type1, RelDataType type2) {
+ SqlValidator validator = binding.getValidator();
+ boolean lhsUnknown = validator.getUnknownType() == type1;
+ boolean rhsUnknown = validator.getUnknownType() == type2;
+
+ if (lhsUnknown && rhsUnknown) {
+ String signature = IgniteResource.makeSignature(binding, type1, type2);
+ throw binding.newValidationError(IgniteResource.INSTANCE.ambiguousOperator1(signature));
+ } else if (lhsUnknown) {
+ RelDataType nullableType = typeFactory.createTypeWithNullability(type2, true);
+ validator.setValidatedNodeType(binding.operand(0), nullableType);
+ } else if (rhsUnknown) {
+ RelDataType nullableType = typeFactory.createTypeWithNullability(type1, true);
+ validator.setValidatedNodeType(binding.operand(1), nullableType);
+ }
+ }
+
+ /**
+ * Validates dynamic parameters passed as function arguments.
+ */
+ private void validateFunctionOperands(SqlCallBinding binding, List<RelDataType> operandTypes, List<SqlTypeFamily> expectedFamilies) {
+ // This method is also called from SqlBinaryOperator's CompositeOperandTypeChecker
+ // in case fo binary operators.
+ IgniteSqlValidator validator = (IgniteSqlValidator) binding.getValidator();
+
+ for (int i = 0; i < binding.getOperandCount(); i++) {
+ SqlNode operand = binding.getCall().operand(i);
+
+ if (validator.isUnspecifiedDynamicParam(operand)) {
+ SqlTypeFamily expectedTypeFamily = expectedFamilies.get(i);
+
+ if (expectedTypeFamily.getTypeNames().size() > 1) {
+ String signature = IgniteResource.makeSignature(binding, operandTypes);
+ String allowedSignatures = binding.getOperator().getAllowedSignatures();
+
+ throw binding.newValidationError(IgniteResource.INSTANCE.ambiguousOperator2(signature, allowedSignatures));
+ }
+ }
+ }
+ }
+
+ /**
+ * Validates parameters in INSERT/UPDATE.
+ */
+ private void validateDynamicParametersInModify(@Nullable SqlValidatorScope scope, RelDataType targetRowType, SqlNode query) {
+ ContextType contextType;
+ List<List<SqlNode>> sourceLists;
+
+ if (query instanceof SqlInsert) {
+ SqlInsert insert = (SqlInsert) query;
+
+ if (insert.getSource() instanceof SqlSelect) {
+ // NOT MATCHED THEN arm of a MERGE statement.
+ SqlSelect select = (SqlSelect) insert.getSource();
+ sourceLists = List.of(select.getSelectList());
+ } else {
+ // Basic INSERT INTO ... VALUES (...).
+ SqlCall values = (SqlCall) insert.getSource();
+ assert values.getKind() == SqlKind.VALUES : "Unexpected source node for INSERT " + values;
+ List<List<SqlNode>> rows = new ArrayList<>(values.getOperandList().size());
+
+ for (SqlNode rowNode : values.getOperandList()) {
+ SqlCall row = (SqlCall) rowNode;
+ rows.add(row.getOperandList());
+ }
+
+ sourceLists = rows;
+ }
+
+ contextType = ContextType.INSERT;
+ } else if (query instanceof SqlUpdate) {
+ SqlUpdate update = (SqlUpdate) query;
+ sourceLists = List.of(update.getSourceExpressionList());
+
+ contextType = ContextType.MODIFY;
+ } else {
+ throw new AssertionError("Encountered unexpected SQL node during dynamic parameter validation: " + query);
+ }
+
+ for (List<SqlNode> sourceList : sourceLists) {
+ for (int i = 0; i < sourceList.size(); i++) {
+ SqlNode node = sourceList.get(i);
+
+ if (node.getKind() == SqlKind.DYNAMIC_PARAM) {
+ SqlDynamicParam dynamicParam = (SqlDynamicParam) node;
+ RelDataType targetType = targetRowType.getFieldList().get(i).getType();
+ IgniteSqlValidator validator1 = (IgniteSqlValidator) scope.getValidator();
+
+ validateAssignment(dynamicParam, targetType, contextType, validator1);
+ }
+ }
+ }
+ }
+
/**
* Validates dynamic parameters in binary comparison operation.
*/
@@ -538,12 +652,30 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
SqlNode lhs = binding.operand(0);
SqlNode rhs = binding.operand(1);
+ boolean lhsUnknown = validator.isUnspecifiedDynamicParam(lhs);
+ boolean rhsUnknown = validator.isUnspecifiedDynamicParam(rhs);
+
+ if (lhsUnknown && rhsUnknown) {
+ String signature = IgniteResource.makeSignature(binding, leftType, rightType);
+ throw binding.newValidationError(IgniteResource.INSTANCE.ambiguousOperator1(signature));
+ }
+
if (lhs instanceof SqlDynamicParam) {
- validateOperand((SqlDynamicParam) lhs, rightType, binding.getOperator(), validator);
+ if (rhsUnknown) {
+ RelDataType nullableType = typeFactory.createTypeWithNullability(rightType, true);
+ validator.setValidatedNodeType(binding.operand(0), nullableType);
+ } else {
+ validateOperand((SqlDynamicParam) lhs, rightType, binding.getOperator(), validator);
+ }
}
if (rhs instanceof SqlDynamicParam) {
- validateOperand((SqlDynamicParam) rhs, leftType, binding.getOperator(), validator);
+ if (lhsUnknown) {
+ RelDataType nullableType = typeFactory.createTypeWithNullability(leftType, true);
+ validator.setValidatedNodeType(binding.operand(1), nullableType);
+ } else {
+ validateOperand((SqlDynamicParam) rhs, leftType, binding.getOperator(), validator);
+ }
}
}
@@ -580,7 +712,7 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
// (and we have no such information here).
if (ctxType == ContextType.SET_OP) {
- RelDataType paramType = validator.getDynamicParamType(dynamicParam);
+ RelDataType paramType = validator.resolveDynamicParameterType(dynamicParam, targetType);
return TypeUtils.typeFamiliesAreCompatible(typeFactory, targetType, paramType);
} else {
validateAssignment(dynamicParam, targetType, ctxType, validator);
@@ -590,8 +722,7 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
// TODO: https://issues.apache.org/jira/browse/IGNITE-19721 - move this check to SqlValidator (if possible).
private void validateAssignment(SqlDynamicParam node, RelDataType targetType, ContextType ctxType, IgniteSqlValidator validator) {
-
- RelDataType paramType = validator.getDynamicParamType(node);
+ RelDataType paramType = validator.resolveDynamicParameterType(node, targetType);
boolean compatible = TypeUtils.typeFamiliesAreCompatible(typeFactory, targetType, paramType);
if (compatible) {
return;
@@ -619,14 +750,19 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
// TODO: https://issues.apache.org/jira/browse/IGNITE-19721 - move this check to SqlValidator (if possible).
private void validateOperand(SqlDynamicParam node, RelDataType targetType, SqlOperator operator, IgniteSqlValidator validator) {
- RelDataType paramType = validator.getDynamicParamType(node);
+ RelDataType paramType = validator.resolveDynamicParameterType(node, targetType);
boolean compatible = TypeUtils.typeFamiliesAreCompatible(typeFactory, targetType, paramType);
if (compatible) {
return;
}
- var ex = IgniteResource.INSTANCE.operationRequiresExplicitCast(operator.getName());
- throw SqlUtil.newContextException(node.getParserPosition(), ex);
+ if (validator.isUnspecified(node)) {
+ var ex = IgniteResource.INSTANCE.ambiguousOperator1(operator.getName());
+ throw SqlUtil.newContextException(node.getParserPosition(), ex);
+ } else {
+ var ex = IgniteResource.INSTANCE.operationRequiresExplicitCast(operator.getName());
+ throw SqlUtil.newContextException(node.getParserPosition(), ex);
+ }
}
/**
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/MultiStepPlan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/MultiStepPlan.java
index e1978e34e2..0ed45ad47a 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/MultiStepPlan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/MultiStepPlan.java
@@ -35,12 +35,15 @@ public class MultiStepPlan implements QueryPlan {
private final IgniteRel root;
+ private final ParameterMetadata parameterMetadata;
+
/** Constructor. */
- public MultiStepPlan(PlanId id, SqlQueryType type, IgniteRel root, ResultSetMetadata meta) {
+ public MultiStepPlan(PlanId id, SqlQueryType type, IgniteRel root, ResultSetMetadata meta, ParameterMetadata parameterMetadata) {
this.id = id;
this.type = type;
this.root = root;
this.meta = meta;
+ this.parameterMetadata = parameterMetadata;
}
/** {@inheritDoc} */
@@ -55,6 +58,12 @@ public class MultiStepPlan implements QueryPlan {
return meta;
}
+ /** {@inheritDoc} */
+ @Override
+ public ParameterMetadata parameterMetadata() {
+ return parameterMetadata;
+ }
+
/** {@inheritDoc} */
@Override
@Nullable
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryPlan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterMetadata.java
similarity index 58%
copy from modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryPlan.java
copy to modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterMetadata.java
index 95d752d1b2..2fe60c909d 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryPlan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterMetadata.java
@@ -17,26 +17,28 @@
package org.apache.ignite.internal.sql.engine.prepare;
-import org.apache.ignite.internal.sql.engine.SqlQueryType;
-import org.apache.ignite.sql.ResultSetMetadata;
+import java.util.List;
+import org.apache.ignite.internal.tostring.S;
/**
- * QueryPlan interface.
- * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
+ * Metadata for dynamic parameters.
*/
-public interface QueryPlan {
- /**
- * Get a unique identifier of a plan.
- */
- PlanId id();
+public final class ParameterMetadata {
- /**
- * Get query type, or {@code null} if this is a fragment.
- */
- SqlQueryType type();
+ private final List<ParameterType> parameterTypes;
- /**
- * Get fields metadata.
- */
- ResultSetMetadata metadata();
+ /** Constructor. */
+ public ParameterMetadata(List<ParameterType> parameterTypes) {
+ this.parameterTypes = parameterTypes;
+ }
+
+ /** Return metadata for each parameter. */
+ public List<ParameterType> parameterTypes() {
+ return parameterTypes;
+ }
+
+ @Override
+ public String toString() {
+ return S.toString(ParameterMetadata.class, this, "parametersTypes", parameterTypes);
+ }
}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterType.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterType.java
new file mode 100644
index 0000000000..8afff2cf53
--- /dev/null
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/ParameterType.java
@@ -0,0 +1,96 @@
+/*
+ * 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.prepare;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.apache.ignite.internal.tostring.S;
+import org.apache.ignite.sql.ColumnMetadata;
+import org.apache.ignite.sql.ColumnType;
+
+/**
+ * Dynamic parameter metadata.
+ */
+public final class ParameterType {
+
+ private final ColumnType columnType;
+
+ private final int precision;
+
+ private final int scale;
+
+ private final boolean nullable;
+
+ /** Constructor. */
+ public ParameterType(ColumnType columnType, int precision, int scale, boolean nullable) {
+ this.columnType = columnType;
+ this.precision = precision;
+ this.scale = scale;
+ this.nullable = nullable;
+ }
+
+ /**
+ * {@link ColumnType} of this parameter.
+ */
+ public ColumnType columnType() {
+ return columnType;
+ }
+
+ /**
+ * Returns precision of parameter type, if applicable.
+ *
+ * @see ColumnMetadata#precision()
+ */
+ public int precision() {
+ return precision;
+ }
+
+ /**
+ * Returns scale of this parameter type, if applicable.
+ *
+ * @see ColumnMetadata#scale()
+ */
+ public int scale() {
+ return scale;
+ }
+
+ /** Returns {@code true} if parameter is nullable and {@code false} otherwise. */
+ public boolean nullable() {
+ return nullable;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public String toString() {
+ return S.toString(this);
+ }
+
+ /** Creates parameter metadata from the given logical type. */
+ static ParameterType fromRelDataType(RelDataType type) {
+ ColumnType columnType = TypeUtils.columnType(type);
+ assert columnType != null : "No column type for " + type;
+
+ int precision = columnType.lengthAllowed() || columnType.precisionAllowed()
+ ? type.getPrecision()
+ : ColumnMetadata.UNDEFINED_PRECISION;
+
+ int scale = columnType.scaleAllowed() ? type.getScale() : ColumnMetadata.UNDEFINED_SCALE;
+
+ return new ParameterType(columnType, precision, scale, type.isNullable());
+ }
+}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlanningContext.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlanningContext.java
index c74d75191e..275bb580ec 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlanningContext.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PlanningContext.java
@@ -17,6 +17,8 @@
package org.apache.ignite.internal.sql.engine.prepare;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import org.apache.calcite.plan.Context;
@@ -58,17 +60,21 @@ public final class PlanningContext implements Context {
/** Flag indicated if planning has been canceled due to timeout. */
private boolean timeouted = false;
+ private final Int2ObjectMap<Object> parameters;
+
/** Private constructor, used by a builder. */
private PlanningContext(
Context parentCtx,
String qry,
- long plannerTimeout
+ long plannerTimeout,
+ Int2ObjectMap<Object> parameters
) {
this.qry = qry;
this.parentCtx = parentCtx;
startTs = FastTimestamps.coarseCurrentTimeMillis();
this.plannerTimeout = plannerTimeout;
+ this.parameters = parameters;
}
/** Get framework config. */
@@ -82,8 +88,8 @@ public final class PlanningContext implements Context {
}
/** Get query parameters. */
- public Object[] parameters() {
- return unwrap(BaseQueryContext.class).parameters();
+ public Int2ObjectMap<Object> parameters() {
+ return parameters;
}
// Helper methods
@@ -185,34 +191,46 @@ public final class PlanningContext implements Context {
* Planner context builder.
*/
public static class Builder {
+
private Context parentCtx = Contexts.empty();
private String qry;
private long plannerTimeout;
+ private Int2ObjectMap<Object> parameters = Int2ObjectMaps.emptyMap();
+
+ /** Parent context. */
public Builder parentContext(Context parentCtx) {
this.parentCtx = parentCtx;
return this;
}
+ /** SQL statement. */
public Builder query(String qry) {
this.qry = qry;
return this;
}
+ /** Planner timeout. */
public Builder plannerTimeout(long plannerTimeout) {
this.plannerTimeout = plannerTimeout;
return this;
}
+ /** Values of dynamic parameters to assist with type inference. */
+ public Builder parameters(Int2ObjectMap<Object> parameters) {
+ this.parameters = parameters;
+ return this;
+ }
+
/**
* Builds planner context.
*
* @return Planner context.
*/
public PlanningContext build() {
- return new PlanningContext(parentCtx, qry, plannerTimeout);
+ return new PlanningContext(parentCtx, qry, plannerTimeout, parameters);
}
}
}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareService.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareService.java
index 8c0de1a4b7..769bedfc20 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareService.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareService.java
@@ -26,6 +26,7 @@ import org.apache.ignite.internal.sql.engine.util.BaseQueryContext;
* Preparation service that accepts an AST of the query and returns a prepared query plan.
*/
public interface PrepareService extends LifecycleAware {
+
/**
* Prepare query plan.
*
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java
index beff874de2..d828777231 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImpl.java
@@ -23,7 +23,7 @@ import static org.apache.ignite.internal.sql.engine.trait.TraitUtils.distributio
import static org.apache.ignite.lang.ErrorGroups.Sql.PLANNING_TIMEOUT_ERR;
import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -60,6 +60,7 @@ import org.apache.ignite.internal.sql.engine.util.cache.CacheFactory;
import org.apache.ignite.internal.sql.metrics.SqlPlanCacheMetricSource;
import org.apache.ignite.internal.storage.DataStorageManager;
import org.apache.ignite.internal.thread.NamedThreadFactory;
+import org.apache.ignite.internal.type.NativeTypeSpec;
import org.apache.ignite.internal.util.ExceptionUtils;
import org.apache.ignite.lang.ErrorGroups.Sql;
import org.apache.ignite.sql.ColumnMetadata;
@@ -79,6 +80,10 @@ public class PrepareServiceImpl implements PrepareService {
new ColumnMetadataImpl("ROWCOUNT", ColumnType.INT64,
ColumnMetadata.UNDEFINED_PRECISION, ColumnMetadata.UNDEFINED_SCALE, false, null)));
+ /** Parameter metadata. */
+ private static final ParameterMetadata EMPTY_PARAMETER_METADATA =
+ new ParameterMetadata(Collections.emptyList());
+
/** Default planner timeout, in ms. */
public static final long DEFAULT_PLANNER_TIMEOUT = 15000L;
@@ -192,6 +197,7 @@ public class PrepareServiceImpl implements PrepareService {
.parentContext(ctx)
.query(parsedResult.originalQuery())
.plannerTimeout(plannerTimeout)
+ .parameters(Commons.arrayToMap(ctx.parameters()))
.build();
result = prepareAsync0(parsedResult, planningContext);
@@ -280,9 +286,15 @@ public class PrepareServiceImpl implements PrepareService {
}
private CompletableFuture<QueryPlan> prepareQuery(ParsedResult parsedResult, PlanningContext ctx) {
- CacheKey key = createCacheKey(parsedResult, ctx);
+ CompletableFuture<QueryPlan> f = getPlanIfParameterHaveValues(parsedResult, ctx);
+
+ if (f != null) {
+ return f;
+ }
+
+ // First validate statement
- CompletableFuture<QueryPlan> planFut = cache.get(key, k -> CompletableFuture.supplyAsync(() -> {
+ CompletableFuture<ValidStatement<ValidationResult>> validFut = CompletableFuture.supplyAsync(() -> {
IgnitePlanner planner = ctx.planner();
SqlNode sqlNode = parsedResult.parsedTree();
@@ -292,20 +304,38 @@ public class PrepareServiceImpl implements PrepareService {
// Validate
ValidationResult validated = planner.validateAndGetTypeMetadata(sqlNode);
- SqlNode validatedNode = validated.sqlNode();
+ // Get parameter metadata.
+ RelDataType parameterRowType = planner.getParameterRowType();
+ ParameterMetadata parameterMetadata = createParameterMetadata(parameterRowType);
+
+ return new ValidStatement<>(parsedResult, validated, parameterMetadata);
+ }, planningPool);
+
+ return validFut.thenCompose(stmt -> {
+ // Use parameter metadata to compute a cache key.
+ CacheKey key = createCacheKeyFromParameterMetadata(stmt.parsedResult, ctx, stmt.parameterMetadata);
+
+ CompletableFuture<QueryPlan> planFut = cache.get(key, k -> CompletableFuture.supplyAsync(() -> {
+ IgnitePlanner planner = ctx.planner();
+
+ ValidationResult validated = stmt.value;
+ ParameterMetadata parameterMetadata = stmt.parameterMetadata;
- IgniteRel igniteRel = optimize(validatedNode, planner);
+ SqlNode validatedNode = validated.sqlNode();
- // cluster keeps a lot of cached stuff that won't be used anymore.
- // In order let GC collect that, let's reattach tree to an empty cluster
- // before storing tree in plan cache
- IgniteRel clonedTree = Cloner.clone(igniteRel, Commons.emptyCluster());
+ IgniteRel igniteRel = optimize(validatedNode, planner);
- return new MultiStepPlan(nextPlanId(), SqlQueryType.QUERY, clonedTree,
- resultSetMetadata(validated.dataType(), validated.origins(), validated.aliases()));
- }, planningPool));
+ // cluster keeps a lot of cached stuff that won't be used anymore.
+ // In order let GC collect that, let's reattach tree to an empty cluster
+ // before storing tree in plan cache
+ IgniteRel clonedTree = Cloner.clone(igniteRel, Commons.emptyCluster());
- return planFut.thenApply(Function.identity());
+ ResultSetMetadata resultSetMetadata = resultSetMetadata(validated.dataType(), validated.origins(), validated.aliases());
+ return new MultiStepPlan(nextPlanId(), SqlQueryType.QUERY, clonedTree, resultSetMetadata, parameterMetadata);
+ }, planningPool));
+
+ return planFut.thenApply(Function.identity());
+ });
}
private PlanId nextPlanId() {
@@ -313,9 +343,13 @@ public class PrepareServiceImpl implements PrepareService {
}
private CompletableFuture<QueryPlan> prepareDml(ParsedResult parsedResult, PlanningContext ctx) {
- var key = createCacheKey(parsedResult, ctx);
+ // If a caller passes all the parameters, then get parameter types and check to see whether a plan future already exists.
+ CompletableFuture<QueryPlan> f = getPlanIfParameterHaveValues(parsedResult, ctx);
+ if (f != null) {
+ return f;
+ }
- CompletableFuture<QueryPlan> planFut = cache.get(key, k -> CompletableFuture.supplyAsync(() -> {
+ CompletableFuture<ValidStatement<SqlNode>> validFut = CompletableFuture.supplyAsync(() -> {
IgnitePlanner planner = ctx.planner();
SqlNode sqlNode = parsedResult.parsedTree();
@@ -325,27 +359,135 @@ public class PrepareServiceImpl implements PrepareService {
// Validate
SqlNode validatedNode = planner.validate(sqlNode);
- // Convert to Relational operators graph
- IgniteRel igniteRel = optimize(validatedNode, planner);
+ // Get parameter metadata.
+ RelDataType parameterRowType = planner.getParameterRowType();
+ ParameterMetadata parameterMetadata = createParameterMetadata(parameterRowType);
+
+ return new ValidStatement<>(parsedResult, validatedNode, parameterMetadata);
+ }, planningPool);
+
+ // Optimize
+
+ return validFut.thenCompose(stmt -> {
+ // Use parameter metadata to compute a cache key.
+ CacheKey key = createCacheKeyFromParameterMetadata(stmt.parsedResult, ctx, stmt.parameterMetadata);
+
+ CompletableFuture<QueryPlan> planFut = cache.get(key, k -> CompletableFuture.supplyAsync(() -> {
+ IgnitePlanner planner = ctx.planner();
+
+ SqlNode validatedNode = stmt.value;
+ ParameterMetadata parameterMetadata = stmt.parameterMetadata;
+
+ // Convert to Relational operators graph
+ IgniteRel igniteRel = optimize(validatedNode, planner);
+
+ // cluster keeps a lot of cached stuff that won't be used anymore.
+ // In order let GC collect that, let's reattach tree to an empty cluster
+ // before storing tree in plan cache
+ IgniteRel clonedTree = Cloner.clone(igniteRel, Commons.emptyCluster());
+
+ return new MultiStepPlan(nextPlanId(), SqlQueryType.DML, clonedTree, DML_METADATA, parameterMetadata);
+ }, planningPool));
+
+ return planFut.thenApply(Function.identity());
+ });
+ }
+
+ /**
+ * Tries to find a prepared plan if all parameters are set.
+ *
+ * <p>This method relies on the fact that if parameter is specified, it's type does not change during the validation.
+ * Given the following query: SELECT * FROM t WHERE int_key = ?0, the validator assigns type {@code INTEGER} to ?0,
+ * regardless whether prepare is called with parameter values (type hints) or not:
+ * <ul>
+ * <li>If parameter value (type hint) is int, then the validator returns the same plan</li>
+ * <li>if type hint is not an int, then the validator return different plan with different parameter metadata.</li>
+ * </ul>
+ *
+ * <p>Because of that we can optimistically create a cache key, if all parameters are set.
+ *
+ * <p>If some parameters are not, always returns {@code null}.
+ */
+ @Nullable
+ private CompletableFuture<QueryPlan> getPlanIfParameterHaveValues(ParsedResult parsedResult, PlanningContext ctx) {
+ // If a caller passes all the parameters, then get parameter types and check to see whether a plan future already exists.
+
+ CacheKey cacheKey = tryCreateCacheKeyFromParameterValues(parsedResult, ctx);
+ if (cacheKey != null) {
+ CompletableFuture<QueryPlan> f = cache.get(cacheKey);
+ if (f != null) {
+ return f;
+ }
+ }
+ return null;
+ }
+
+ @Nullable
+ private static CacheKey tryCreateCacheKeyFromParameterValues(ParsedResult parsedResult, PlanningContext ctx) {
+
+ Map<Integer, Object> parameters = ctx.parameters();
+
+ int maxParamNum = 0;
+ for (Integer key : parameters.keySet()) {
+ maxParamNum = Math.max(maxParamNum, key);
+ }
- // cluster keeps a lot of cached stuff that won't be used anymore.
- // In order let GC collect that, let's reattach tree to an empty cluster
- // before storing tree in plan cache
- IgniteRel clonedTree = Cloner.clone(igniteRel, Commons.emptyCluster());
+ for (int i = 0; i < maxParamNum; i++) {
+ if (!parameters.containsKey(i)) {
+ // Some parameters are not specified,
+ // we do not known the their type and we can not create a cache key.
+ return null;
+ }
+ }
- return new MultiStepPlan(nextPlanId(), SqlQueryType.DML, clonedTree, DML_METADATA);
- }, planningPool));
+ // If parameters type are known, they do not change and we can create a cache key.
+ // See IgniteSqlValidator::validateInferredDynamicParameters
- return planFut.thenApply(Function.identity());
+ boolean distributed = distributionPresent(ctx.config().getTraitDefs());
+ int catalogVersion = ctx.unwrap(BaseQueryContext.class).schemaVersion();
+ ColumnType[] paramTypes;
+
+ if (parameters.isEmpty()) {
+ paramTypes = new ColumnType[0];
+ } else {
+ ColumnType[] result = new ColumnType[parameters.size()];
+
+ for (Map.Entry<Integer, Object> entry : parameters.entrySet()) {
+ Object value = entry.getValue();
+ ColumnType columnType;
+ if (value != null) {
+ columnType = NativeTypeSpec.fromObject(value).asColumnType();
+ } else {
+ columnType = ColumnType.NULL;
+ }
+ result[entry.getKey()] = columnType;
+ }
+
+ paramTypes = result;
+ }
+
+ return new CacheKey(catalogVersion, ctx.schemaName(), parsedResult.normalizedQuery(), distributed, paramTypes);
}
- private static CacheKey createCacheKey(ParsedResult parsedResult, PlanningContext ctx) {
+ private static CacheKey createCacheKeyFromParameterMetadata(ParsedResult parsedResult, PlanningContext ctx,
+ ParameterMetadata parameterMetadata) {
+
boolean distributed = distributionPresent(ctx.config().getTraitDefs());
int catalogVersion = ctx.unwrap(BaseQueryContext.class).schemaVersion();
+ ColumnType[] paramTypes;
+
+ List<ParameterType> parameterTypes = parameterMetadata.parameterTypes();
+ if (parameterTypes.isEmpty()) {
+ paramTypes = EMPTY_CLASS_ARRAY;
+ } else {
+ ColumnType[] result = new ColumnType[parameterTypes.size()];
- Class[] paramTypes = ctx.parameters().length == 0
- ? EMPTY_CLASS_ARRAY :
- Arrays.stream(ctx.parameters()).map(p -> (p != null) ? p.getClass() : Void.class).toArray(Class[]::new);
+ for (int i = 0; i < parameterTypes.size(); i++) {
+ result[i] = parameterTypes.get(i).columnType();
+ }
+
+ paramTypes = result;
+ }
return new CacheKey(catalogVersion, ctx.schemaName(), parsedResult.normalizedQuery(), distributed, paramTypes);
}
@@ -380,6 +522,23 @@ public class PrepareServiceImpl implements PrepareService {
);
}
+ private static ParameterMetadata createParameterMetadata(RelDataType parameterRowType) {
+ if (parameterRowType.getFieldCount() == 0) {
+ return EMPTY_PARAMETER_METADATA;
+ }
+
+ List<ParameterType> parameterTypes = new ArrayList<>(parameterRowType.getFieldCount());
+
+ for (int i = 0; i < parameterRowType.getFieldCount(); i++) {
+ RelDataTypeField field = parameterRowType.getFieldList().get(i);
+ ParameterType parameterType = ParameterType.fromRelDataType(field.getType());
+
+ parameterTypes.add(parameterType);
+ }
+
+ return new ParameterMetadata(parameterTypes);
+ }
+
private static class ParsedResultImpl implements ParsedResult {
private final SqlQueryType queryType;
private final String originalQuery;
@@ -431,4 +590,16 @@ public class PrepareServiceImpl implements PrepareService {
return parsedTree;
}
}
+
+ private static class ValidStatement<T> {
+ final ParsedResult parsedResult;
+ final T value;
+ final ParameterMetadata parameterMetadata;
+
+ private ValidStatement(ParsedResult parsedResult, T value, ParameterMetadata parameterMetadata) {
+ this.parsedResult = parsedResult;
+ this.value = value;
+ this.parameterMetadata = parameterMetadata;
+ }
+ }
}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryPlan.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryPlan.java
index 95d752d1b2..4fb88e9c5a 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryPlan.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/prepare/QueryPlan.java
@@ -39,4 +39,9 @@ public interface QueryPlan {
* Get fields metadata.
*/
ResultSetMetadata metadata();
+
+ /**
+ * Returns parameters metadata.
+ */
+ ParameterMetadata parameterMetadata();
}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/Commons.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/Commons.java
index 85bab1af85..a3494146cf 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/Commons.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/Commons.java
@@ -23,6 +23,9 @@ import static org.apache.ignite.lang.ErrorGroups.Common.INTERNAL_ERR;
import static org.apache.ignite.sql.ColumnMetadata.UNDEFINED_PRECISION;
import static org.apache.ignite.sql.ColumnMetadata.UNDEFINED_SCALE;
+import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
@@ -307,16 +310,43 @@ public final class Commons {
}
/**
- * ParametersMap.
- * TODO Documentation https://issues.apache.org/jira/browse/IGNITE-15859
+ * Creates a map of parameters values from the given array of dynamic parameter values.
+ * All dynamic parameter should have their values specified.
*
* @param params Parameters.
* @return Parameters map.
*/
public static Map<String, Object> parametersMap(@Nullable Object[] params) {
- HashMap<String, Object> res = new HashMap<>();
+ if (ArrayUtils.nullOrEmpty(params)) {
+ return Collections.emptyMap();
+ } else {
+ HashMap<String, Object> res = new HashMap<>();
+
+ populateParameters(res, params);
+
+ return res;
+ }
+ }
+
+ /**
+ * Creates an array from the given map in which array indices become map keys. e.g: [1, null, "3"] -> {0: 1, 1: null, 2: "3"}.
+ * If the given array is null, this method returns an empty map.
+ *
+ * @param params Array of values.
+ * @return Map of values.
+ */
+ public static Int2ObjectMap<Object> arrayToMap(@Nullable Object[] params) {
+ if (ArrayUtils.nullOrEmpty(params)) {
+ return Int2ObjectMaps.emptyMap();
+ } else {
+ Int2ObjectMap<Object> res = new Int2ObjectArrayMap<>(params.length);
+
+ for (int i = 0; i < params.length; i++) {
+ res.put(i, params[i]);
+ }
- return params != null ? populateParameters(res, params) : res;
+ return res;
+ }
}
/**
@@ -324,15 +354,11 @@ public final class Commons {
*
* @param dst Map to populate.
* @param params Parameters.
- * @return Parameters map.
*/
- public static Map<String, Object> populateParameters(Map<String, Object> dst, @Nullable Object[] params) {
- if (!ArrayUtils.nullOrEmpty(params)) {
- for (int i = 0; i < params.length; i++) {
- dst.put("?" + i, params[i]);
- }
+ private static void populateParameters(Map<String, Object> dst, Object[] params) {
+ for (int i = 0; i < params.length; i++) {
+ dst.put("?" + i, params[i]);
}
- return dst;
}
/**
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteResource.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteResource.java
index 91421978da..c4b7974061 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteResource.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/IgniteResource.java
@@ -17,9 +17,20 @@
package org.apache.ignite.internal.sql.engine.util;
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.runtime.Resources;
import org.apache.calcite.runtime.Resources.ExInst;
+import org.apache.calcite.sql.SqlCallBinding;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.validate.SqlValidatorException;
+import org.apache.ignite.internal.sql.engine.prepare.IgniteSqlValidator;
/**
* IgniteResource interface.
@@ -51,4 +62,68 @@ public interface IgniteResource {
@Resources.BaseMessage("System view {0} is not modifiable")
ExInst<SqlValidatorException> systemViewIsNotModifiable(String systemViewName);
+
+ @Resources.BaseMessage("Ambiguous operator {0}. Dynamic parameter requires adding explicit type cast")
+ Resources.ExInst<SqlValidatorException> ambiguousOperator1(String signature);
+
+ @Resources.BaseMessage("Ambiguous operator {0}. Dynamic parameter requires adding explicit type cast. Supported form(s): \n{1}")
+ Resources.ExInst<SqlValidatorException> ambiguousOperator2(String signature, String allowedSignatures);
+
+ @Resources.BaseMessage("Unable to determine type of a dynamic parameter. Dynamic parameter requires adding explicit type cast")
+ Resources.ExInst<SqlValidatorException> unableToResolveDynamicParameterType();
+
+ @Resources.BaseMessage("Incorrect type of a dynamic parameter. Expected <{0}> but got <{1}>")
+ Resources.ExInst<SqlValidatorException> incorrectDynamicParameterType(String expected, String actual);
+
+ /** Constructs a signature string to use in error messages. */
+ static String makeSignature(SqlCallBinding binding, RelDataType... operandTypes) {
+ return makeSignature(binding, Arrays.asList(operandTypes));
+ }
+
+ /** Constructs a signature string to use in error messages. */
+ static String makeSignature(SqlCallBinding binding, List<RelDataType> operandTypes) {
+ IgniteSqlValidator validator = (IgniteSqlValidator) binding.getValidator();
+ List<SqlNode> operandList = binding.getCall().getOperandList();
+
+ return makeSignature(binding.getOperator(), validator, operandList, operandTypes);
+ }
+
+ /** Constructs a signature string to use in error messages. */
+ static String makeSignature(SqlOperator operator, IgniteSqlValidator validator,
+ List<SqlNode> operands, List<RelDataType> originalOperandTypes) {
+
+ List<String> operandTypeNames = new ArrayList<>(originalOperandTypes.size());
+
+ // Replace types for unspecified parameters with UNKNOWN type.
+
+ for (int i = 0; i < operands.size(); i++) {
+ SqlNode node = operands.get(i);
+ RelDataType dataType;
+
+ if (validator.isUnspecifiedDynamicParam(node)) {
+ dataType = validator.getUnknownType();
+ } else {
+ dataType = originalOperandTypes.get(i);
+ }
+
+ operandTypeNames.add(format("<{}>", dataType));
+ }
+
+ SqlKind kind = operator.getKind();
+ if (SqlKind.BINARY_ARITHMETIC.contains(kind) || SqlKind.BINARY_COMPARISON.contains(kind)) {
+ return format("{} {} {}", operandTypeNames.get(0), operator.getName(), operandTypeNames.get(1));
+ } else if (operator.getName().startsWith("IS ")) {
+ // IS NULL, IS NOT NULL, etc.
+ return format("{} {}", operandTypeNames.get(0), operator.getName());
+ } else if (kind == SqlKind.BETWEEN) {
+ return format("{} {} AND {}", operator.getName(), operandTypeNames.get(0), operandTypeNames.get(1));
+ } else if (kind == SqlKind.MINUS_PREFIX) {
+ // -
+ return format("{}{}", operator.getName(), operandTypeNames.get(0));
+ } else {
+ // Other operators
+ String operandStr = String.join(", ", operandTypeNames);
+ return format("{}({})", operator.getName(), operandStr);
+ }
+ }
}
diff --git a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
index 20141b207c..c7c006d836 100644
--- a/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
+++ b/modules/sql-engine/src/main/java/org/apache/ignite/internal/sql/engine/util/TypeUtils.java
@@ -121,7 +121,7 @@ public class TypeUtils {
}
/** Return {@code true} if supplied object is suitable as dynamic parameter. */
- public static boolean supportParamInstance(Object param) {
+ public static boolean supportParamInstance(@Nullable Object param) {
return param == null || supportedParamClasses().contains(param.getClass());
}
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
index 66bb31fa48..554883f4bf 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/ExecutionServiceImplTest.java
@@ -139,6 +139,7 @@ import org.apache.ignite.sql.ColumnType;
import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
/**
@@ -663,6 +664,7 @@ public class ExecutionServiceImplTest extends BaseIgniteAbstractTest {
* Test ensures that an exception during data prefetching is propagated to the callback.
*/
@Test
+ @Disabled("https://issues.apache.org/jira/browse/IGNITE-21052")
public void testErrorIsPropagatedToPrefetchCallback() {
ExecutionService execService = executionServices.get(0);
CompletableFuture<Void> prefetchFut = new CompletableFuture<>();
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/mapping/MappingTestRunner.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/mapping/MappingTestRunner.java
index 5c3bde8b95..bf5c148c1d 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/mapping/MappingTestRunner.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/exec/mapping/MappingTestRunner.java
@@ -41,6 +41,7 @@ import org.apache.ignite.internal.lang.IgniteSystemProperties;
import org.apache.ignite.internal.sql.api.ResultSetMetadataImpl;
import org.apache.ignite.internal.sql.engine.SqlQueryType;
import org.apache.ignite.internal.sql.engine.prepare.MultiStepPlan;
+import org.apache.ignite.internal.sql.engine.prepare.ParameterMetadata;
import org.apache.ignite.internal.sql.engine.prepare.PlanId;
import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
import org.apache.ignite.internal.sql.engine.rel.IgniteTableModify;
@@ -164,7 +165,9 @@ final class MappingTestRunner {
sqlQueryType = SqlQueryType.QUERY;
}
ResultSetMetadataImpl resultSetMetadata = new ResultSetMetadataImpl(Collections.emptyList());
- MultiStepPlan multiStepPlan = new MultiStepPlan(new PlanId(UUID.randomUUID(), 1), sqlQueryType, rel, resultSetMetadata);
+ ParameterMetadata parameterMetadata = new ParameterMetadata(Collections.emptyList());
+ MultiStepPlan multiStepPlan = new MultiStepPlan(new PlanId(UUID.randomUUID(), 1), sqlQueryType, rel,
+ resultSetMetadata, parameterMetadata);
String actualText = produceMapping(testDef.nodeName, targetProvider, snapshot, multiStepPlan);
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
index 02f968fc96..b0cb5d75f2 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/AbstractPlannerTest.java
@@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -105,6 +106,7 @@ import org.apache.ignite.internal.sql.engine.util.StatementChecker;
import org.apache.ignite.internal.testframework.IgniteAbstractTest;
import org.apache.ignite.internal.testframework.IgniteTestUtils;
import org.apache.ignite.internal.type.NativeType;
+import org.apache.ignite.internal.util.Pair;
import org.jetbrains.annotations.Nullable;
/**
@@ -232,9 +234,19 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
List<Object> params,
String... disabledRules
) {
+
+ Int2ObjectArrayMap<Object> paramsMap = new Int2ObjectArrayMap<>();
+ for (int i = 0; i < params.size(); i++) {
+ Object value = params.get(i);
+ if (value != Unspecified.UNKNOWN) {
+ paramsMap.put(i, value);
+ }
+ }
+
PlanningContext ctx = PlanningContext.builder()
.parentContext(baseQueryContext(schemas, hintStrategies, params.toArray()))
.query(sql)
+ .parameters(paramsMap)
.build();
IgnitePlanner planner = ctx.planner();
@@ -268,6 +280,7 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
relConvCfg = relConvCfg.withHintStrategyTable(hintStrategies);
}
+
return BaseQueryContext.builder()
.queryId(UUID.randomUUID())
.frameworkConfig(
@@ -971,14 +984,17 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
PlanChecker() {
super((schema, sql, params) -> {
- return physicalPlan(sql, List.of(schema), HintStrategyTable.EMPTY,
- params, new NoopRelOptListener());
+ PlanningContext planningContext = plannerCtx(sql, List.of(schema), HintStrategyTable.EMPTY, params);
+ IgnitePlanner planner = planningContext.planner();
+ IgniteRel igniteRel = physicalPlan(planner, sql);
+
+ return new Pair<>(igniteRel, planner);
});
}
/** {@inheritDoc} */
@Override
- protected void checkRel(IgniteRel igniteRel, IgniteSchema schema) {
+ protected void checkRel(IgniteRel igniteRel, IgnitePlanner planner, IgniteSchema schema) {
checkSplitAndSerialization(igniteRel, schema);
}
}
@@ -1022,4 +1038,11 @@ public abstract class AbstractPlannerTest extends IgniteAbstractTest {
static Function<TableBuilder, TableBuilder> setSize(int size) {
return t -> t.size(size);
}
+
+
+ /** Unspecified dynamic parameter. */
+ public enum Unspecified {
+ /** Placeholder for unspecified dynamic parameter. */
+ UNKNOWN
+ }
}
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/DynamicParametersTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/DynamicParametersTest.java
index 7ec2f77943..bda29a8abb 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/DynamicParametersTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/planner/DynamicParametersTest.java
@@ -20,9 +20,17 @@ package org.apache.ignite.internal.sql.engine.planner;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.stream.Stream;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.ignite.internal.sql.engine.prepare.IgniteSqlValidator;
+import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
+import org.apache.ignite.internal.sql.engine.util.Commons;
import org.apache.ignite.internal.sql.engine.util.StatementChecker;
+import org.apache.ignite.internal.sql.engine.util.TypeUtils;
+import org.apache.ignite.internal.type.NativeType;
import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.internal.type.VarlenNativeType;
+import org.jetbrains.annotations.Nullable;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
@@ -48,15 +56,22 @@ public class DynamicParametersTest extends AbstractPlannerTest {
@TestFactory
public Stream<DynamicTest> testArithExprs() {
return Stream.of(
- sql("SELECT 1 + ?", 1).ok(),
- sql("SELECT NULL + ?", 1).project("null:INTEGER"),
- sql("SELECT ? + NULL", 1).project("null:INTEGER"),
+ sql("SELECT 1 + ?", 1).parameterTypes(nullable(NativeTypes.INT32)).ok(),
+ sql("SELECT NULL + ?", 1).parameterTypes(nullable(NativeTypes.INT32)).project("null:INTEGER"),
+ sql("SELECT ? + NULL", 1).parameterTypes(nullable(NativeTypes.INT32)).project("null:INTEGER"),
sql("SELECT 1 + ?", "1").fails("Values passed to + operator must have compatible types"),
sql("SELECT ? + 1", "1").fails("Values passed to + operator must have compatible types"),
- // NULL is allowed in arithmetic expressions
- sql("SELECT ? * 2", new Object[]{null}).ok()
+ // NULL is allowed in arithmetic expressions, if another operand is present.
+ sql("SELECT ? * 2", new Object[]{null})
+ .parameterTypes(nullable(null))
+ .ok(),
+
+ sql("SELECT 1 + ?", Unspecified.UNKNOWN).parameterTypes(nullable(NativeTypes.INT32)).ok(),
+ sql("SELECT ? + 1", Unspecified.UNKNOWN).parameterTypes(nullable(NativeTypes.INT32)).ok(),
+ sql("SELECT ? + ?", Unspecified.UNKNOWN, Unspecified.UNKNOWN).fails("Ambiguous operator <UNKNOWN> + <UNKNOWN>"),
+ sql("SELECT NULL + ?", Unspecified.UNKNOWN).fails("Ambiguous operator <NULL> + <UNKNOWN>")
);
}
@@ -65,14 +80,20 @@ public class DynamicParametersTest extends AbstractPlannerTest {
public Stream<DynamicTest> testCmpExprs() {
return Stream.of(
// comparison
- sql("SELECT ? > 1", 1).ok(),
+ sql("SELECT ? > 1", 1).parameterTypes(nullable(NativeTypes.INT32)).ok(),
+ sql("SELECT ? > 1", Unspecified.UNKNOWN).parameterTypes(nullable(NativeTypes.INT32)).ok(),
+ sql("SELECT 1 > ?", Unspecified.UNKNOWN).parameterTypes(nullable(NativeTypes.INT32)).ok(),
+ sql("SELECT ? > ?", Unspecified.UNKNOWN, Unspecified.UNKNOWN).fails("Ambiguous operator <UNKNOWN> > <UNKNOWN>"),
+
+ sql("SELECT ? > 1", "1")
+ .fails("Values passed to > operator must have compatible types"),
- sql("SELECT ? > 1", "1").fails("Values passed to > operator must have compatible types"),
- sql("SELECT 1 > ?", "1").fails("Values passed to > operator must have compatible types"),
+ sql("SELECT 1 > ?", "1")
+ .fails("Values passed to > operator must have compatible types"),
- sql("SELECT ? > NULL", 1).project("null:BOOLEAN"),
- sql("SELECT NULL = ?", 1).project("null:BOOLEAN"),
- sql("SELECT ? = NULL", 1).project("null:BOOLEAN"),
+ sql("SELECT ? > NULL", 1).parameterTypes(nullable(NativeTypes.INT32)).project("null:BOOLEAN"),
+ sql("SELECT NULL = ?", 1).parameterTypes(nullable(NativeTypes.INT32)).project("null:BOOLEAN"),
+ sql("SELECT ? = NULL", 1).parameterTypes(nullable(NativeTypes.INT32)).project("null:BOOLEAN"),
// NULL is allowed in comparison
sql("SELECT ? = ?", null, null).project("=(?0, ?1)")
@@ -84,28 +105,60 @@ public class DynamicParametersTest extends AbstractPlannerTest {
*/
@TestFactory
public Stream<DynamicTest> testInExpression() {
- String error =
+ String requireExplicitCast =
"Values passed to IN operator must have compatible types. Dynamic parameter requires adding explicit type cast";
+
return Stream.of(
- sql("SELECT ? IN ('1', '2')", 1).project("OR(=(?0, 1), =(?0, 2))"),
- sql("SELECT ? IN (1, 2)", "1").fails(error),
- sql("SELECT ? IN ('1', 2)", 2).project("OR(=(?0, 1), =(?0, 2))"),
+ sql("SELECT ? IN ('1', '2')", 1).parameterTypes(nullable(NativeTypes.INT32)).project("OR(=(?0, 1), =(?0, 2))"),
+ sql("SELECT ? IN (1, 2)", "1").fails(requireExplicitCast),
+ sql("SELECT ? IN (1, 2)", 1).parameterTypes(nullable(NativeTypes.INT32)).project("OR(=(?0, 1), =(?0, 2))"),
+
+ sql("SELECT ? IN (1)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ sql("SELECT ? IN (?, 1)", Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ sql("SELECT ? IN (?, ?)", Unspecified.UNKNOWN, Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ sql("SELECT 1 IN (?, ?)", Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .project("OR(=(1, ?0), =(1, ?1))"),
+
+ sql("SELECT ? IN ('1')", 2)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .project("=(?0, 1)"),
- sql("SELECT (?,?) IN ((1,2))", 1, 2).project("AND(=(?0, 1), =(?1, 2))"),
+ sql("SELECT ? IN ('1', 2)", 2)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .project("OR(=(?0, 1), =(?0, 2))"),
- sql("SELECT (?,?) IN ((1,2))", "1", "2").fails(error),
- sql("SELECT (?,?) IN (('1', 2))", 1, "2").fails(error),
- sql("SELECT (?,?) IN ((1, '2'))", "1", "2").fails(error)
+ sql("SELECT (?,?) IN ((1,2))", 1, 2)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
+ .project("AND(=(?0, 1), =(?1, 2))"),
+
+ sql("SELECT (?,?) IN ((1,2))", "1", "2").fails(requireExplicitCast),
+ sql("SELECT (?,?) IN (('1', 2))", 1, "2").fails(requireExplicitCast),
+ sql("SELECT (?,?) IN ((1, '2'))", "1", "2").fails(requireExplicitCast)
);
}
/** CASE expression. */
@TestFactory
public Stream<DynamicTest> testCaseWhenExpression() {
+ IgniteTypeFactory tf = Commons.typeFactory();
+ // String parameter is inferred as VARCHAR with default attributes, but
+ // the createSqlType(VARCHAR, DEFAULT_PRECISION) != createSqlType(VARCHAR)
+ // have to create rel data type directly instead of building one from a native type.
+ RelDataType nullableVarchar = tf.createTypeWithNullability(tf.createSqlType(SqlTypeName.VARCHAR), true);
+
return Stream.of(
- sql("SELECT CASE ? = ? WHEN true THEN 1 ELSE 2 END", 1, 1).ok(),
+ sql("SELECT CASE ? = ? WHEN true THEN 1 ELSE 2 END", 1, 1)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
+ .ok(),
sql("SELECT CASE WHEN ? = '1' THEN ? ELSE ? END", "1", 2, 2.5)
+ .parameterTypes(nullableVarchar, nullable(NativeTypes.INT32), nullable(NativeTypes.DOUBLE))
.project("CASE(=(?0, _UTF-8'1'), CAST(?1):DOUBLE, ?2)"),
sql("SELECT CASE ? = ? WHEN true THEN 1 ELSE 2 END", 1, "1")
@@ -114,7 +167,49 @@ public class DynamicParametersTest extends AbstractPlannerTest {
sql("SELECT CASE WHEN ? = '1' THEN ? ELSE ? END", "1", "2", 2.5)
.fails("Illegal mixing of types in CASE or COALESCE statement"),
- sql("SELECT CASE WHEN ? THEN 1 WHEN ? THEN 2 ELSE 3 END", 1, 2).fails("Expected a boolean type")
+ sql("SELECT CASE WHEN ? THEN 1 WHEN ? THEN 2 ELSE 3 END", 1, 2)
+ .fails("Expected a boolean type")
+ );
+ }
+
+ @TestFactory
+ public Stream<DynamicTest> testCase() {
+ //CREATE TABLE TBL1(ID INT PRIMARY KEY, VAL VARCHAR, NUM INT)
+ //select case when (VAL = ?) then 0 else (case when (NUM IS NULL) then ? else ? end) end
+
+ IgniteTypeFactory tf = Commons.typeFactory();
+ RelDataType nullableStr = tf.createTypeWithNullability(tf.createSqlType(SqlTypeName.VARCHAR), true);
+ RelDataType nullType = tf.createSqlType(SqlTypeName.NULL);
+ RelDataType nullableInt = tf.createTypeWithNullability(tf.createSqlType(SqlTypeName.INTEGER), true);
+
+ return Stream.of(
+ checkStatement()
+ .table("TBL1", "ID", NativeTypes.INT32, "VAL", NativeTypes.STRING, "NUM", NativeTypes.INT32)
+ .sql("select case when (VAL = ?) then 0 else (case when (NUM IS NULL) then ? else ? end) end FROM TBL1",
+ "diff", null, 1)
+ .parameterTypes(nullableStr, nullType, nullableInt)
+ .project("CASE(=(CAST($t0):VARCHAR CHARACTER SET \"UTF-8\", ?0), 0, CASE(IS NULL($t1), CAST(?1):INTEGER, ?2))"),
+
+ checkStatement()
+ .table("TBL1", "ID", NativeTypes.INT32, "VAL", NativeTypes.STRING, "NUM", NativeTypes.INT32)
+ .sql("select case when (VAL = ?) then 0 else (case when (NUM IS NULL) then ? else ? end) end FROM TBL1",
+ Unspecified.UNKNOWN, Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .parameterTypes(nullableStr, nullType, nullableInt)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .table("TBL1", "ID", NativeTypes.INT32, "VAL", NativeTypes.STRING, "NUM", NativeTypes.INT32)
+ .sql("select case when (VAL = ?) then 0 else (case when (NUM IS NULL) then ? else ? end) end FROM TBL1",
+ "diff", 1, null)
+ .parameterTypes(nullableStr, nullableInt, nullType)
+ .project("CASE(=(CAST($t0):VARCHAR CHARACTER SET \"UTF-8\", ?0), 0, CASE(IS NULL($t1), ?1, CAST(?2):INTEGER))"),
+
+ checkStatement()
+ .table("TBL1", "ID", NativeTypes.INT32, "VAL", NativeTypes.STRING, "NUM", NativeTypes.INT32)
+ .sql("select case when (VAL = ?) then 0 else (case when (NUM IS NULL) then ? else ? end) end FROM TBL1",
+ Unspecified.UNKNOWN, Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .parameterTypes(nullableStr, nullType, nullableInt)
+ .fails("Unable to determine type of a dynamic parameter")
);
}
@@ -125,6 +220,12 @@ public class DynamicParametersTest extends AbstractPlannerTest {
@TestFactory
public Stream<DynamicTest> testWherePredicate() {
return Stream.of(
+ checkStatement()
+ .table("t1", "int_col", NativeTypes.INT32)
+ .sql("SELECT * FROM t1 WHERE int_col = ?", Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
checkStatement()
.table("t1", "int_col", NativeTypes.INT32)
.sql("SELECT * FROM t1 WHERE int_col = ?", "1").fails("Values passed to = operator must have compatible types"),
@@ -135,6 +236,247 @@ public class DynamicParametersTest extends AbstractPlannerTest {
);
}
+ /** Dynamic parameters in independent contexts. */
+ @TestFactory
+ public Stream<DynamicTest> testStandalone() {
+ return Stream.of(
+ checkStatement()
+ .sql("SELECT ?", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ checkStatement()
+ .sql("SELECT ?", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .sql("SELECT CAST(? AS INTEGER)", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ // We are going to cast at runtime.
+ .project("?0"),
+
+ checkStatement()
+ .sql("SELECT CAST(? AS INTEGER)", "1")
+ .parameterTypes(nullable(NativeTypes.STRING))
+ // We are going to cast at runtime.
+ .project("CAST(?0):INTEGER"),
+
+ checkStatement()
+ .sql("SELECT CAST(? AS INTEGER)", Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ // We are going to cast at runtime.
+ .project("?0"),
+
+ checkStatement()
+ .sql("SELECT -?", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ checkStatement()
+ .sql("SELECT -?", Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .fails("Ambiguous operator -<UNKNOWN>"),
+
+ checkStatement()
+ .sql("SELECT NOT ?", Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.BOOLEAN))
+ .project("NOT(?0)"),
+
+ checkStatement()
+ .sql("SELECT ? IS NULL", Unspecified.UNKNOWN)
+ .fails("Ambiguous operator <UNKNOWN> IS NULL"),
+
+ checkStatement()
+ .sql("SELECT ? IS NOT NULL", Unspecified.UNKNOWN)
+ .fails("Ambiguous operator <UNKNOWN> IS NOT NULL")
+ );
+ }
+
+ /** Subqueries in various contexts. */
+ @TestFactory
+ public Stream<DynamicTest> testSubqueries() {
+ return Stream.of(
+ checkStatement()
+ .sql("SELECT (SELECT ?)", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ checkStatement()
+ .sql("SELECT (SELECT ?)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ // Predicates
+
+ checkStatement()
+ .table("t1", "int_col", NativeTypes.INT32)
+ .sql("SELECT * FROM t1 WHERE int_col = SOME(SELECT ?)", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+
+ checkStatement()
+ .table("t1", "int_col", NativeTypes.INT32)
+ .sql("SELECT * FROM t1 WHERE int_col = SOME(SELECT ?)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .table("t1", "int_col", NativeTypes.INT32)
+ .sql("SELECT * FROM t1 WHERE int_col = ANY(SELECT ?)", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ checkStatement()
+ .table("t1", "int_col", NativeTypes.INT32)
+ .sql("SELECT * FROM t1 WHERE int_col = ANY(SELECT ?)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+
+ // DML
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT32)
+ .sql("INSERT INTO t1 VALUES (1), ((SELECT ?))", 1)
+ .ok(),
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT32)
+ .sql("INSERT INTO t1 VALUES (1), ((SELECT ?))", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT32, "c2", NativeTypes.INT64)
+ .sql("UPDATE t1 SET c1 = (SELECT ?)", 1)
+ .ok()
+
+ // TODO https://issues.apache.org/jira/browse/IGNITE-20835
+ // Assertion is caused by incorrect subquery handling
+ // checkStatement()
+ // .table("t1", "c1", NativeTypes.INT32, "c2", NativeTypes.INT64)
+ // .sql("UPDATE t1 SET c1 = (SELECT ?)", Unspecified.UNKNOWN)
+ // .fails("Unable to determine type of a dynamic parameter")
+ );
+ }
+
+ /** BETWEEN operator. */
+ @TestFactory
+ public Stream<DynamicTest> testBetween() {
+ return Stream.of(
+ sql("SELECT 1 BETWEEN ? AND ?", 1, 10)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
+ .ok(),
+
+ sql("SELECT 1 BETWEEN ? AND ?", 1, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
+ .ok(),
+
+ sql("SELECT 1 BETWEEN ? AND ?", Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
+ .ok(),
+
+ sql("SELECT ? BETWEEN ? AND ?", 1, Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
+ .ok(),
+
+ sql("SELECT 1 BETWEEN ? AND ?", Unspecified.UNKNOWN, 10L)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT64))
+ .ok(),
+
+ sql("SELECT ? BETWEEN ? AND ?", Unspecified.UNKNOWN, 1, 10)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
+ .ok(),
+
+ sql("SELECT ? BETWEEN ? AND ?", Unspecified.UNKNOWN, 1, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
+ .ok(),
+
+ sql("SELECT ? BETWEEN ? AND ?", Unspecified.UNKNOWN, Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter")
+ );
+ }
+
+ /** Aggregate functions. */
+ @TestFactory
+ public Stream<DynamicTest> testAggregateFunctions() {
+ return Stream.of(
+ checkStatement()
+ .sql("SELECT MAX(?)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .sql("SELECT MIN(?)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .sql("SELECT AVG(?)", Unspecified.UNKNOWN)
+ .fails("Ambiguous operator AVG(<UNKNOWN>)"),
+
+ checkStatement()
+ .sql("SELECT COUNT(?)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter")
+ );
+ }
+
+ /** NULLIF function. */
+ @TestFactory
+ public Stream<DynamicTest> testNullIf() {
+ // NULLIF($1, $2) is rewritten as case CASE WHEN $1 = $2 THEN NULL ELSE $1 END
+ return Stream.of(
+ checkStatement()
+ .sql("SELECT NULLIF(?, ?)", Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .fails("Ambiguous operator <UNKNOWN> = <UNKNOWN>"),
+
+ checkStatement()
+ .sql("SELECT NULLIF(?, ?)", 1, "1")
+ .fails("Values passed to = operator must have compatible types"),
+
+ checkStatement()
+ .sql("SELECT NULLIF(?, 1)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .sql("SELECT NULLIF(CAST(? AS INTEGER), 1)", Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .project("CASE(=(?0, 1), null:INTEGER, ?0)")
+ );
+ }
+
+ /** COALESCE function. */
+ @TestFactory
+ public Stream<DynamicTest> testCoalesce() {
+ // COALESCE($1, $2) is rewritten into: CASE WHEN $1 IS NOT NULL THEN $1 ELSE $2 END
+ return Stream.of(
+ checkStatement()
+ .sql("SELECT COALESCE(?)", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .project("?0"),
+
+ checkStatement()
+ .sql("SELECT COALESCE(?)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .sql("SELECT COALESCE(?)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .sql("SELECT COALESCE(?, ?)", null, null)
+ .parameterTypes(nullable(null), nullable(null))
+ .project("CASE(IS NOT NULL(?0), ?0, ?1)"),
+
+ checkStatement()
+ .sql("SELECT COALESCE(?, 1)", Unspecified.UNKNOWN)
+ .fails("Ambiguous operator <UNKNOWN> IS NOT NULL"),
+
+ checkStatement()
+ .sql("SELECT COALESCE(1, ?)", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ checkStatement()
+ .sql("SELECT COALESCE(CAST(? AS INTEGER), 1)", 2)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .project("CASE(IS NOT NULL(?0), CAST(?0):INTEGER NOT NULL, 1)")
+ );
+ }
/** Dynamic params in INSERT statement. */
@TestFactory
@@ -143,6 +485,37 @@ public class DynamicParametersTest extends AbstractPlannerTest {
checkStatement()
.table("t1", "c1", NativeTypes.INT32)
.sql("INSERT INTO t1 VALUES (?)", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .project("?0"),
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT64)
+ .sql("INSERT INTO t1 VALUES (?)", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .project("CAST(?0):BIGINT"),
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT64)
+ .sql("INSERT INTO t1 VALUES (?)", Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT64))
+ .project("?0"),
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT64)
+ .sql("INSERT INTO t1 VALUES (?)", new Object[]{null})
+ .parameterTypes(new NativeType[]{null})
+ .project("CAST(?0):BIGINT"),
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT32)
+ .sql("INSERT INTO t1 VALUES (?), (2), (?)", 1, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
+ .project("?0"),
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT32)
+ .sql("INSERT INTO t1 VALUES (?), (2), (?)", Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT32))
.project("?0"),
// compatible type
@@ -150,6 +523,7 @@ public class DynamicParametersTest extends AbstractPlannerTest {
checkStatement()
.table("t1", "c1", NativeTypes.INT64)
.sql("INSERT INTO t1 VALUES (?)", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
.project("CAST(?0):BIGINT"),
@@ -177,18 +551,27 @@ public class DynamicParametersTest extends AbstractPlannerTest {
checkStatement()
.table("t1", "c1", NativeTypes.INT32)
.sql("UPDATE t1 SET c1 = ?", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
.project("$t0", "?0"),
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT32, "c2", NativeTypes.INT64)
+ .sql("UPDATE t1 SET c1 = ?, c2 = ?", 1, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT64))
+ .project("$t0", "$t1", "?0", "?1"),
+
// compatible type
checkStatement()
.table("t1", "c1", NativeTypes.INT64)
.sql("UPDATE t1 SET c1 = ?", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
.project("$t0", "CAST(?0):BIGINT"),
// null
checkStatement()
.table("t1", "c1", NativeTypes.INT32)
.sql("UPDATE t1 SET c1 = ?", new Object[]{null})
+ .parameterTypes(new NativeType[]{null})
.project("$t0", "CAST(?0):INTEGER"),
// Incompatible types in dynamic params
@@ -214,6 +597,18 @@ public class DynamicParametersTest extends AbstractPlannerTest {
+ "WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES (src.c1, src.c2, ?)";
return sql;
}, 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .project("$0", "$1", "?0"),
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT32, "c2", NativeTypes.INT32, "c3", NativeTypes.INT32)
+ .table("t2", "c1", NativeTypes.INT32, "c2", NativeTypes.INT32, "c3", NativeTypes.INT32)
+ .sql(() -> {
+ String sql = "MERGE INTO T2 dst USING t1 src ON dst.c1 = src.c1 "
+ + "WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES (src.c1, src.c2, ?)";
+ return sql;
+ }, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32))
.project("$0", "$1", "?0"),
// null
@@ -226,6 +621,7 @@ public class DynamicParametersTest extends AbstractPlannerTest {
+ "WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES (src.c1, src.c2, ?)";
return sql;
}, new Object[]{null})
+ .parameterTypes(new NativeType[]{null})
.project("$0", "$1", "CAST(?0):INTEGER"),
// incompatible types
@@ -248,13 +644,26 @@ public class DynamicParametersTest extends AbstractPlannerTest {
return Stream.of(
checkStatement()
.table("t1", "c1", NativeTypes.INT32, "c2", NativeTypes.INT32, "c3", NativeTypes.INT32)
- .table("t2", "c1", NativeTypes.INT32, "c2", NativeTypes.INT32, "c3", NativeTypes.INT32)
+ .table("t2", "c1", NativeTypes.INT32, "c2", NativeTypes.INT32, "c3", NativeTypes.INT64)
+ .sql(() -> {
+ String sql = "MERGE INTO T2 dst USING t1 src ON dst.c1 = src.c1 "
+ + "WHEN MATCHED THEN UPDATE SET c2 = ? "
+ + "WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES (src.c1, src.c2, ?)";
+ return sql;
+ }, 1, 2L)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT64))
+ .project("$3", "$4", "?1", "$0", "$1", "$2", "?0"),
+
+ checkStatement()
+ .table("t1", "c1", NativeTypes.INT32, "c2", NativeTypes.INT32, "c3", NativeTypes.INT32)
+ .table("t2", "c1", NativeTypes.INT32, "c2", NativeTypes.INT32, "c3", NativeTypes.INT64)
.sql(() -> {
String sql = "MERGE INTO T2 dst USING t1 src ON dst.c1 = src.c1 "
+ "WHEN MATCHED THEN UPDATE SET c2 = ? "
+ "WHEN NOT MATCHED THEN INSERT (c1, c2, c3) VALUES (src.c1, src.c2, ?)";
return sql;
- }, 1, 2)
+ }, 1, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32), nullable(NativeTypes.INT64))
.project("$3", "$4", "?1", "$0", "$1", "$2", "?0"),
// Incompatible types
@@ -283,12 +692,65 @@ public class DynamicParametersTest extends AbstractPlannerTest {
);
}
+ /**
+ * Dynamic parameters in LIMIT / OFFSET.
+ */
+ @TestFactory
+ public Stream<DynamicTest> testLimitOffset() {
+ Consumer<StatementChecker> setup = (checker) -> {
+ checker.table("t1", "c1", NativeTypes.INT32);
+ };
+
+ return Stream.of(
+ checkStatement(setup)
+ .sql("SELECT * FROM t1 LIMIT ?", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ checkStatement(setup)
+ .sql("SELECT * FROM t1 LIMIT ?", Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ checkStatement(setup)
+ .sql("SELECT * FROM t1 LIMIT ?", "a")
+ .fails("Incorrect type of a dynamic parameter. Expected <INTEGER> but got <VARCHAR"),
+
+ checkStatement(setup)
+ .sql("SELECT * FROM t1 OFFSET ?", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ checkStatement(setup)
+ .sql("SELECT * FROM t1 OFFSET ?", Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ checkStatement(setup)
+ .sql("SELECT * FROM t1 OFFSET ?", "a")
+ .fails("Incorrect type of a dynamic parameter. Expected <INTEGER> but got <VARCHAR")
+ );
+ }
+
/**
* Function calls.
*/
@TestFactory
public Stream<DynamicTest> testFunction() {
return Stream.of(
+ checkStatement()
+ .sql("SELECT SUBSTRING(?, 1)", "abc")
+ .parameterTypes(nullable(NativeTypes.STRING))
+ .ok(),
+
+ checkStatement()
+ .sql("SELECT SUBSTRING(?, 1)", Unspecified.UNKNOWN)
+ .fails("Ambiguous operator SUBSTRING(<UNKNOWN>, <INTEGER>)"),
+
+ checkStatement()
+ .sql("SELECT SUBSTRING('aaaa', ?)", Unspecified.UNKNOWN)
+ .fails("Ambiguous operator SUBSTRING(<CHAR(4)>, <UNKNOWN>)"),
+
// nested function call - OK
checkStatement()
.sql("SELECT SUBSTRING(SUBSTRING(?, 1), 2)", "123456")
@@ -301,6 +763,33 @@ public class DynamicParametersTest extends AbstractPlannerTest {
);
}
+ /** Set operations. */
+ @TestFactory
+ public Stream<DynamicTest> testSetOps() {
+ return Stream.of(
+ sql("SELECT 1 UNION SELECT ?", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ sql("SELECT 1 UNION SELECT ?", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ sql("SELECT ? UNION SELECT 1", 1)
+ .parameterTypes(nullable(NativeTypes.INT32))
+ .ok(),
+
+ sql("SELECT ? UNION SELECT ?", 1L, 2)
+ .parameterTypes(nullable(NativeTypes.INT64), nullable(NativeTypes.INT32))
+ .ok(),
+
+ sql("SELECT ? UNION SELECT 1", Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter"),
+
+ sql("SELECT ? UNION SELECT ?", 1, Unspecified.UNKNOWN)
+ .fails("Unable to determine type of a dynamic parameter")
+ );
+ }
+
/**
* Custom data types.
*/
@@ -332,6 +821,16 @@ public class DynamicParametersTest extends AbstractPlannerTest {
.sql("SELECT str_col IN ('a'::UUID) FROM t1")
.project("=(CAST($t0):UUID, CAST(_UTF-8'a'):UUID NOT NULL)"),
+ checkStatement(setup)
+ .sql("SELECT uuid_col IN (?) FROM t1", UUID.randomUUID())
+ .parameterTypes(nullable(NativeTypes.UUID))
+ .project("=($t0, ?0)"),
+
+ checkStatement(setup)
+ .sql("SELECT uuid_col IN (?) FROM t1", Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.UUID))
+ .project("=($t0, ?0)"),
+
// CASE
checkStatement(setup)
@@ -342,6 +841,31 @@ public class DynamicParametersTest extends AbstractPlannerTest {
.sql("SELECT CASE RAND_UUID() WHEN ? THEN 1 WHEN ? THEN 2 ELSE 3 END", new UUID(0, 1), 2)
.fails("Values passed to = operator must have compatible types"),
+ checkStatement(setup)
+ .sql("SELECT CASE RAND_UUID() WHEN ? THEN 1 WHEN ? THEN 2 ELSE 3 END", new UUID(0, 1), new UUID(0, 2))
+ .parameterTypes(nullable(NativeTypes.UUID), nullable(NativeTypes.UUID))
+ .ok(),
+
+ checkStatement(setup)
+ .sql("SELECT CASE RAND_UUID() WHEN ? THEN 1 WHEN ? THEN 2 ELSE 3 END", new UUID(0, 1), Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.UUID), nullable(NativeTypes.UUID))
+ .ok(),
+
+ checkStatement(setup)
+ .sql("SELECT CASE RAND_UUID() WHEN ? THEN 1 WHEN ? THEN 2 ELSE 3 END", Unspecified.UNKNOWN, Unspecified.UNKNOWN)
+ .parameterTypes(nullable(NativeTypes.UUID), nullable(NativeTypes.UUID))
+ .ok(),
+
+ checkStatement()
+ .sql("SELECT COALESCE(?, 'UUID'::UUID)", new UUID(0, 0))
+ .parameterTypes(nullable(NativeTypes.UUID))
+ .project("CASE(IS NOT NULL(?0), ?0, CAST(_UTF-8'UUID'):UUID NOT NULL)"),
+
+ checkStatement()
+ .sql("SELECT NULLIF(?, 'UUID'::UUID)", new UUID(0, 0))
+ .parameterTypes(nullable(NativeTypes.UUID))
+ .project("CASE(=(?0, CAST(_UTF-8'UUID'):UUID NOT NULL), null:NULL, ?0)"),
+
// Set operations
checkStatement(setup)
@@ -357,4 +881,21 @@ public class DynamicParametersTest extends AbstractPlannerTest {
.fails("Type mismatch in column 1 of UNION")
);
}
+
+ private static RelDataType nullable(@Nullable NativeType type) {
+ IgniteTypeFactory tf = Commons.typeFactory();
+ if (type == null) {
+ return tf.createTypeWithNullability(tf.createSqlType(SqlTypeName.NULL), true);
+ }
+
+ RelDataType relDataType = TypeUtils.native2relationalType(tf, type, true);
+
+ // For var length types inferred without precision.
+ if (type instanceof VarlenNativeType) {
+ SqlTypeName typeName = relDataType.getSqlTypeName();
+ return tf.createTypeWithNullability(tf.createSqlType(typeName), true);
+ }
+
+ return relDataType;
+ }
}
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java
index d0057bbec4..42c13dc6f4 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/prepare/PrepareServiceImplTest.java
@@ -18,15 +18,22 @@
package org.apache.ignite.internal.sql.engine.prepare;
import static org.apache.ignite.internal.sql.engine.util.Commons.FRAMEWORK_CONFIG;
+import static org.apache.ignite.internal.sql.engine.util.SqlTestUtils.assertThrowsSqlException;
import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.tools.Frameworks;
import org.apache.ignite.internal.metrics.MetricManager;
@@ -39,21 +46,29 @@ import org.apache.ignite.internal.sql.engine.sql.ParserServiceImpl;
import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
import org.apache.ignite.internal.sql.engine.util.BaseQueryContext;
import org.apache.ignite.internal.sql.engine.util.EmptyCacheFactory;
+import org.apache.ignite.internal.sql.engine.util.SqlTestUtils;
import org.apache.ignite.internal.sql.engine.util.cache.CaffeineCacheFactory;
import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.internal.type.NativeType;
import org.apache.ignite.internal.type.NativeTypes;
+import org.apache.ignite.lang.ErrorGroups.Sql;
+import org.apache.ignite.sql.ColumnMetadata;
+import org.apache.ignite.sql.ColumnType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
/**
* Tests to verify {@link PrepareServiceImpl}.
*/
@SuppressWarnings("DataFlowIssue")
-class PrepareServiceImplTest extends BaseIgniteAbstractTest {
+public class PrepareServiceImplTest extends BaseIgniteAbstractTest {
private static final List<PrepareService> createdServices = new ArrayList<>();
@AfterEach
- void stopServices() throws Exception {
+ public void stopServices() throws Exception {
for (PrepareService createdService : createdServices) {
createdService.stop();
}
@@ -62,7 +77,7 @@ class PrepareServiceImplTest extends BaseIgniteAbstractTest {
}
@Test
- void prepareServiceReturnsExistingPlanForExplain() {
+ public void prepareServiceReturnsExistingPlanForExplain() {
PrepareService service = createPlannerService();
QueryPlan queryPlan = await(service.prepareAsync(
@@ -83,7 +98,7 @@ class PrepareServiceImplTest extends BaseIgniteAbstractTest {
}
@Test
- void prepareServiceCachesPlanCreatedForExplain() {
+ public void prepareServiceCachesPlanCreatedForExplain() {
PrepareService service = createPlannerService();
QueryPlan explainPlan = await(service.prepareAsync(
@@ -103,11 +118,138 @@ class PrepareServiceImplTest extends BaseIgniteAbstractTest {
assertThat(plan.plan(), sameInstance(queryPlan));
}
+ @Test
+ public void prepareReturnsQueryPlanThatDependsOnParameterTypeMatchInferred() {
+ PrepareService service = createPlannerService();
+
+ QueryPlan queryPlan1 = await(service.prepareAsync(
+ parse("SELECT * FROM t WHERE a = ? and c = ?"),
+ createContext()
+ ));
+
+ List<ColumnType> parameterTypes = queryPlan1.parameterMetadata().parameterTypes()
+ .stream()
+ .map(ParameterType::columnType)
+ .collect(Collectors.toList());
+
+ assertEquals(List.of(ColumnType.INT64, ColumnType.INT32), parameterTypes);
+
+ // Parameter types match, we should return plan1.
+ QueryPlan queryPlan2 = await(service.prepareAsync(
+ parse("SELECT * FROM t WHERE a = ? and c = ?"),
+ createContext(1L, 1))
+ );
+ assertSame(queryPlan1, queryPlan2);
+
+ // Parameter types do not match
+ QueryPlan queryPlan3 = await(service.prepareAsync(
+ parse("SELECT * FROM t WHERE a = ? and c = ?"),
+ createContext(1, 1L)
+ ));
+ assertNotSame(queryPlan1, queryPlan3);
+ }
+
+ @Test
+ public void prepareReturnsDmlPlanThatDependsOnParameterTypeMatchInferred() {
+ PrepareService service = createPlannerService();
+
+ QueryPlan queryPlan1 = await(service.prepareAsync(
+ parse("UPDATE t SET a = ? WHERE c = ?"),
+ createContext()
+ ));
+
+ List<ColumnType> parameterTypes = queryPlan1.parameterMetadata().parameterTypes()
+ .stream()
+ .map(ParameterType::columnType)
+ .collect(Collectors.toList());
+
+ assertEquals(List.of(ColumnType.INT64, ColumnType.INT32), parameterTypes);
+
+ // Parameter types match, we should return plan1.
+ QueryPlan queryPlan2 = await(service.prepareAsync(
+ parse("UPDATE t SET a = ? WHERE c = ?"),
+ createContext(1L, 1)
+ ));
+ assertSame(queryPlan1, queryPlan2);
+
+ // Parameter types do not match
+ QueryPlan queryPlan3 = await(service.prepareAsync(
+ parse("UPDATE t SET a = ? WHERE c = ?"),
+ createContext(1, 1L)
+ ));
+ assertNotSame(queryPlan1, queryPlan3);
+ }
+
+ @Test
+ public void preparePropagatesValidationError() {
+ PrepareService service = createPlannerService();
+
+ assertThrowsSqlException(Sql.STMT_VALIDATION_ERR,
+ "Ambiguous operator <UNKNOWN> + <UNKNOWN>. Dynamic parameter requires adding explicit type cast",
+ () -> {
+ ParsedResult parsedResult = parse("SELECT ? + ?");
+ BaseQueryContext context = createContext();
+ await(service.prepareAsync(parsedResult, context));
+ }
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("parameterTypes")
+ public void prepareParamInPredicateAllTypes(NativeType nativeType, int precision, int scale) {
+ IgniteTable table = TestBuilders.table()
+ .name("T")
+ .addColumn("C", nativeType)
+ .distribution(IgniteDistributions.single())
+ .build();
+
+ PrepareService service = createPlannerService();
+
+ IgniteSchema schema = new IgniteSchema("PUBLIC", 0, List.of(table));
+ Object paramValue = SqlTestUtils.generateValueByType(nativeType.spec().asColumnType());
+
+ QueryPlan queryPlan = await(service.prepareAsync(
+ parse("SELECT * FROM t WHERE c = ?"),
+ createContext(schema, paramValue)
+ ));
+
+ ParameterType parameterType = queryPlan.parameterMetadata().parameterTypes().get(0);
+
+ ColumnType columnType = nativeType.spec().asColumnType();
+ assertEquals(columnType, parameterType.columnType(), "Column type does not match: " + parameterType);
+ assertEquals(precision, parameterType.precision(), "Precision does not match: " + parameterType);
+ assertEquals(scale, parameterType.scale(), "Scale does not match: " + parameterType);
+ assertTrue(parameterType.nullable(), "Nullabilty does not match: " + parameterType);
+ }
+
+ private static Stream<Arguments> parameterTypes() {
+ int noScale = ColumnMetadata.UNDEFINED_SCALE;
+ int noPrecision = ColumnMetadata.UNDEFINED_PRECISION;
+
+ return Stream.of(
+ Arguments.of(NativeTypes.BOOLEAN, noPrecision, noScale),
+ Arguments.of(NativeTypes.INT8, noPrecision, noScale),
+ Arguments.of(NativeTypes.INT16, noPrecision, noScale),
+ Arguments.of(NativeTypes.INT32, noPrecision, noScale),
+ Arguments.of(NativeTypes.INT64, noPrecision, noScale),
+ Arguments.of(NativeTypes.FLOAT, noPrecision, noScale),
+ Arguments.of(NativeTypes.DOUBLE, noPrecision, noScale),
+ Arguments.of(NativeTypes.decimalOf(10, 2), Short.MAX_VALUE, 0),
+ Arguments.of(NativeTypes.stringOf(42), -1, noScale),
+ Arguments.of(NativeTypes.blobOf(42), -1, noScale),
+ Arguments.of(NativeTypes.UUID, noPrecision, noScale),
+ Arguments.of(NativeTypes.DATE, noPrecision, noScale),
+ Arguments.of(NativeTypes.time(2), 0, noScale),
+ Arguments.of(NativeTypes.datetime(2), 6, noScale),
+ Arguments.of(NativeTypes.timestamp(2), 6, noScale)
+ );
+ }
+
private static ParsedResult parse(String query) {
return new ParserServiceImpl(0, EmptyCacheFactory.INSTANCE).parse(query);
}
- private static BaseQueryContext createContext() {
+ private static BaseQueryContext createContext(Object... params) {
return BaseQueryContext.builder()
.queryId(UUID.randomUUID())
.frameworkConfig(
@@ -115,6 +257,19 @@ class PrepareServiceImplTest extends BaseIgniteAbstractTest {
.defaultSchema(wrap(createSchema()))
.build()
)
+ .parameters(params)
+ .build();
+ }
+
+ private static BaseQueryContext createContext(IgniteSchema schema, Object... params) {
+ return BaseQueryContext.builder()
+ .queryId(UUID.randomUUID())
+ .frameworkConfig(
+ Frameworks.newConfigBuilder(FRAMEWORK_CONFIG)
+ .defaultSchema(wrap(schema))
+ .build()
+ )
+ .parameters(params)
.build();
}
@@ -129,6 +284,7 @@ class PrepareServiceImplTest extends BaseIgniteAbstractTest {
private static IgniteSchema createSchema() {
IgniteTable table = TestBuilders.table()
.name("T")
+ .addColumn("A", NativeTypes.INT64)
.addColumn("C", NativeTypes.INT32)
.distribution(IgniteDistributions.single())
.build();
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/CommonsTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/CommonsTest.java
index 344e94e021..95acc299f5 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/CommonsTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/CommonsTest.java
@@ -19,6 +19,8 @@ package org.apache.ignite.internal.sql.engine.util;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import java.util.HashMap;
+import java.util.Map;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.mapping.Mapping;
import org.apache.calcite.util.mapping.Mappings;
@@ -39,6 +41,17 @@ public class CommonsTest {
expectMapped(mapping, ImmutableBitSet.of(1, 4, 3, 7), ImmutableBitSet.of(0, 1, 2, 3));
}
+ @Test
+ public void testArrayToMap() {
+ assertEquals(Map.of(), Commons.arrayToMap(null));
+
+ HashMap<Integer, Object> vals = new HashMap<>();
+ vals.put(0, 1);
+ vals.put(1, null);
+ vals.put(2, "3");
+ assertEquals(vals, Commons.arrayToMap(new Object[]{1, null, "3"}));
+ }
+
private static void expectMapped(Mapping mapping, ImmutableBitSet bitSet, ImmutableBitSet expected) {
assertEquals(expected, Mappings.apply(mapping, bitSet), "direct mapping");
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerTest.java
index e80e4e7b2b..9d1fe05381 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/QueryCheckerTest.java
@@ -32,6 +32,7 @@ import org.apache.ignite.internal.sql.engine.framework.NoOpTransaction;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders;
import org.apache.ignite.internal.sql.engine.framework.TestCluster;
import org.apache.ignite.internal.sql.engine.framework.TestNode;
+import org.apache.ignite.internal.sql.engine.prepare.ParameterMetadata;
import org.apache.ignite.internal.sql.engine.prepare.QueryPlan;
import org.apache.ignite.internal.sql.engine.property.SqlProperties;
import org.apache.ignite.internal.sql.engine.tx.QueryTransactionWrapperImpl;
@@ -266,6 +267,12 @@ public class QueryCheckerTest extends BaseIgniteAbstractTest {
this.node = node;
}
+ @Override
+ public CompletableFuture<ParameterMetadata> prepareSingleAsync(SqlProperties properties,
+ @Nullable InternalTransaction transaction, String qry, Object... params) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public CompletableFuture<AsyncSqlCursor<InternalSqlRow>> querySingleAsync(
SqlProperties properties,
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/StatementChecker.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/StatementChecker.java
index b49b003475..e2d2ef252d 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/StatementChecker.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/StatementChecker.java
@@ -31,14 +31,19 @@ import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
+import java.util.stream.Collectors;
import org.apache.calcite.plan.RelOptUtil;
import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeField;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlExplainFormat;
import org.apache.calcite.sql.SqlExplainLevel;
+import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders;
import org.apache.ignite.internal.sql.engine.framework.TestBuilders.TableBuilder;
import org.apache.ignite.internal.sql.engine.framework.TestTable;
+import org.apache.ignite.internal.sql.engine.prepare.IgnitePlanner;
import org.apache.ignite.internal.sql.engine.rel.IgniteIndexScan;
import org.apache.ignite.internal.sql.engine.rel.IgniteProject;
import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
@@ -49,6 +54,7 @@ import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
import org.apache.ignite.internal.sql.engine.schema.IgniteTable;
import org.apache.ignite.internal.sql.engine.trait.IgniteDistributions;
import org.apache.ignite.internal.type.NativeType;
+import org.apache.ignite.internal.util.Pair;
import org.hamcrest.CoreMatchers;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
@@ -120,6 +126,8 @@ public class StatementChecker {
private Consumer<StatementChecker> setup = (checker) -> {};
+ private List<RelDataType> expectedParameterTypes;
+
private BiFunction<String, RelNode, String> planToString = (header, plan) -> {
return RelOptUtil.dumpPlan(
header, plan,
@@ -141,7 +149,7 @@ public class StatementChecker {
* @param sql An SQL statement.
* @param params A list of dynamic parameters.
*/
- RelNode prepare(IgniteSchema schema, String sql, List<Object> params) throws Exception;
+ Pair<IgniteRel, IgnitePlanner> prepare(IgniteSchema schema, String sql, List<Object> params) throws Exception;
}
/** Sets a function that is going to be called prior to test run. */
@@ -226,6 +234,24 @@ public class StatementChecker {
return this;
}
+ /** Specifies expected types of dynamic parameters. Use {@code null} to represent {@link SqlTypeName#NULL}. */
+ public StatementChecker parameterTypes(NativeType... types) {
+ this.expectedParameterTypes = Arrays.stream(types)
+ .map(t -> {
+ if (t == null) {
+ return Commons.typeFactory().createSqlType(SqlTypeName.NULL);
+ } else {
+ return TypeUtils.native2relationalType(Commons.typeFactory(), t);
+ }
+ }).collect(Collectors.toList());
+ return this;
+ }
+
+ public StatementChecker parameterTypes(RelDataType... types) {
+ this.expectedParameterTypes = Arrays.asList(types);
+ return this;
+ }
+
/** Expect that validation succeeds. */
public DynamicTest ok() {
return ok((node) -> {}, true);
@@ -288,6 +314,9 @@ public class StatementChecker {
return shouldFail(name, exception, new TypeSafeMatcher<Throwable>() {
@Override
protected boolean matchesSafely(Throwable t) {
+ if (t.getMessage() == null) {
+ return false;
+ }
return t.getMessage().contains(errorMessage);
}
@@ -365,7 +394,7 @@ public class StatementChecker {
}
/** Executes default checks. Runs before a check provide by {@link StatementChecker#ok(Consumer)}. */
- protected void checkRel(IgniteRel igniteRel, IgniteSchema schema) {
+ protected void checkRel(IgniteRel igniteRel, IgnitePlanner planner, IgniteSchema schema) {
}
@@ -387,11 +416,15 @@ public class StatementChecker {
return DynamicTest.dynamicTest(name, () -> {
IgniteSchema schema = initSchema(exception);
IgniteRel root;
+ IgnitePlanner planner;
try {
- root = (IgniteRel) sqlPrepare.prepare(schema, sqlStatement, dynamicParams);
+ Pair<IgniteRel, IgnitePlanner> result = sqlPrepare.prepare(schema, sqlStatement, dynamicParams);
+ root = result.getFirst();
+ planner = result.getSecond();
+
if (relCheck) {
- checkRel(root, schema);
+ checkRel(root, planner, schema);
}
} catch (Throwable e) {
String message = format("Failed to validate:\n{}\n", formatSqlStatementForErrorMessage());
@@ -405,6 +438,8 @@ public class StatementChecker {
try {
check.accept(root);
+
+ checkParameterTypes(planner);
} catch (Throwable e) {
String planDump = buildPlanInfo(root);
String mismatchDescription = format("Plan does not match:\n\n{}", planDump);
@@ -443,7 +478,8 @@ public class StatementChecker {
Throwable err = null;
IgniteRel unexpectedPlan = null;
try {
- unexpectedPlan = (IgniteRel) sqlPrepare.prepare(schema, sqlStatement, dynamicParams);
+ Pair<IgniteRel, ?> unexpected = sqlPrepare.prepare(schema, sqlStatement, dynamicParams);
+ unexpectedPlan = unexpected.getFirst();
} catch (Throwable t) {
err = t;
}
@@ -451,7 +487,7 @@ public class StatementChecker {
if (err != null) {
if (!matcher.matches(err)) {
StringDescription desc = new StringDescription();
- matcher.describeTo(desc);
+ matcher.describeMismatch(err, desc);
AssertionFailedError error = AssertionFailureBuilder.assertionFailure()
.reason("Error does not match")
@@ -520,5 +556,23 @@ public class StatementChecker {
assertEquals(expected, projectionString, "Projection list does not match");
}
+
+ private void checkParameterTypes(IgnitePlanner planner) {
+ if (expectedParameterTypes != null) {
+ RelDataType parameterRowType = planner.getParameterRowType();
+ // Compare string representation because that representation includes nullability attribute.
+
+ List<String> actualTypes = parameterRowType.getFieldList().stream()
+ .map(RelDataTypeField::getType)
+ .map(RelDataType::getFullTypeString)
+ .collect(Collectors.toList());
+
+ List<String> expectedTypes = expectedParameterTypes.stream()
+ .map(RelDataType::getFullTypeString)
+ .collect(Collectors.toList());
+
+ assertEquals(expectedTypes, actualTypes, "Parameters do not match");
+ }
+ }
}
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/StatementCheckerTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/StatementCheckerTest.java
index 6cc74a1ae7..ebddaef6c2 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/StatementCheckerTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/engine/util/StatementCheckerTest.java
@@ -31,17 +31,18 @@ import java.io.StringWriter;
import java.util.List;
import java.util.Map;
import org.apache.calcite.plan.RelTraitSet;
-import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.ignite.internal.sql.engine.prepare.IgnitePlanner;
import org.apache.ignite.internal.sql.engine.rel.IgniteRel;
import org.apache.ignite.internal.sql.engine.rel.IgniteValues;
import org.apache.ignite.internal.sql.engine.schema.IgniteSchema;
import org.apache.ignite.internal.sql.engine.type.IgniteTypeFactory;
import org.apache.ignite.internal.sql.engine.util.StatementChecker.SqlPrepare;
import org.apache.ignite.internal.testframework.BaseIgniteAbstractTest;
+import org.apache.ignite.internal.util.Pair;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DynamicTest;
@@ -54,7 +55,7 @@ import org.opentest4j.AssertionFailedError;
*/
public class StatementCheckerTest extends BaseIgniteAbstractTest {
- private final RelNode dummyNode = Mockito.mock(IgniteRel.class);
+ private final Pair<IgniteRel, IgnitePlanner> dummyNode = new Pair<>(Mockito.mock(IgniteRel.class), Mockito.mock(IgnitePlanner.class));
private final SqlPrepare sqlPrepare = Mockito.mock(SqlPrepare.class);
@@ -144,8 +145,9 @@ public class StatementCheckerTest extends BaseIgniteAbstractTest {
.add(ImmutableList.of(lit))
.add(ImmutableList.of(lit)).build(), RelTraitSet.createEmpty());
+ Pair<IgniteValues, IgnitePlanner> result = new Pair<>(values, Mockito.mock(IgnitePlanner.class));
when(sqlPrepare.prepare(any(IgniteSchema.class), any(String.class), any(List.class)))
- .thenReturn(values);
+ .thenReturn(result);
DynamicTest test = newChecker().sql("SELECT 1")
.planToString((header, node) -> {
diff --git a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/metrics/PlanningCacheMetricsTest.java b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/metrics/PlanningCacheMetricsTest.java
index 163ed1a531..df43751fe4 100644
--- a/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/metrics/PlanningCacheMetricsTest.java
+++ b/modules/sql-engine/src/test/java/org/apache/ignite/internal/sql/metrics/PlanningCacheMetricsTest.java
@@ -56,20 +56,20 @@ public class PlanningCacheMetricsTest extends AbstractPlannerTest {
MetricSet metricSet = metricManager.enable(SqlPlanCacheMetricSource.NAME);
try {
- checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 0, 1);
- checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 1, 1);
+ checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 0, 2);
+ checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 1, 2);
- checkCachePlanStatistics("SELECT * FROM T t1, T t2", prepareService, metricSet, 1, 2);
- checkCachePlanStatistics("SELECT * FROM T t1, T t2", prepareService, metricSet, 2, 2);
- checkCachePlanStatistics("SELECT * FROM T t1, T t2", prepareService, metricSet, 3, 2);
+ checkCachePlanStatistics("SELECT * FROM T t1, T t2", prepareService, metricSet, 1, 4);
+ checkCachePlanStatistics("SELECT * FROM T t1, T t2", prepareService, metricSet, 2, 4);
+ checkCachePlanStatistics("SELECT * FROM T t1, T t2", prepareService, metricSet, 3, 4);
- checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 4, 2);
+ checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 4, 4);
- checkCachePlanStatistics("SELECT * FROM T t1, T t2, T t3", prepareService, metricSet, 4, 3);
+ checkCachePlanStatistics("SELECT * FROM T t1, T t2, T t3", prepareService, metricSet, 4, 6);
// Here, the very first plan has been evicted from cache.
- checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 4, 4);
- checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 5, 4);
+ checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 4, 8);
+ checkCachePlanStatistics("SELECT * FROM T", prepareService, metricSet, 5, 8);
} finally {
prepareService.stop();
}
@@ -92,7 +92,7 @@ public class PlanningCacheMetricsTest extends AbstractPlannerTest {
await(prepareService.prepareAsync(parsedResult, ctx));
assertEquals("sql.plan.cache", metricSet.name());
- assertEquals(String.valueOf(hits), metricSet.get("Hits").getValueAsString());
- assertEquals(String.valueOf(misses), metricSet.get("Misses").getValueAsString());
+ assertEquals(String.valueOf(hits), metricSet.get("Hits").getValueAsString(), "Hits");
+ assertEquals(String.valueOf(misses), metricSet.get("Misses").getValueAsString(), "Misses");
}
}
diff --git a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/SqlTestUtils.java b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/SqlTestUtils.java
index e3fbcb01fd..2ce4b1c59d 100644
--- a/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/SqlTestUtils.java
+++ b/modules/sql-engine/src/testFixtures/java/org/apache/ignite/internal/sql/engine/util/SqlTestUtils.java
@@ -22,7 +22,10 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.fail;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Duration;
@@ -49,6 +52,8 @@ import org.apache.ignite.lang.ErrorGroup;
import org.apache.ignite.lang.ErrorGroups;
import org.apache.ignite.sql.ColumnType;
import org.apache.ignite.sql.SqlException;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.StringDescription;
import org.junit.jupiter.api.function.Executable;
/**
@@ -92,6 +97,34 @@ public class SqlTestUtils {
String expectedError = format("{}-{}", expectedErrorGroup.name(), expectedErrorCode);
String actualError = format("{}-{}", ex.groupName(), ex.errorCode());
+ boolean errorMatches = expectedError.equals(actualError);
+ boolean errorMessageMatches = CoreMatchers.containsString(expectedMessage).matches(ex.getMessage());
+
+ if (!errorMatches || !errorMessageMatches) {
+ StringWriter sw = new StringWriter();
+
+ try (PrintWriter pw = new PrintWriter(sw)) {
+ StringDescription description = new StringDescription();
+
+ if (errorMatches) {
+ description.appendText("Error code does not match. Expected: ");
+ description.appendValue(expectedError);
+ description.appendText(" actual: ");
+ description.appendValue(actualError);
+ } else {
+ description.appendText("Error message does not match. Expected to include: ");
+ description.appendValue(expectedMessage);
+ description.appendText(" actual: ");
+ description.appendValue(ex.getMessage());
+ }
+
+ pw.println(description);
+ ex.printStackTrace(pw);
+ }
+
+ fail(sw.toString());
+ }
+
assertEquals(expectedError, actualError, "Error does not match. " + ex);
assertThat("Error message", ex.getMessage(), containsString(expectedMessage));