This is an automated email from the ASF dual-hosted git repository.
alexpl pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git
The following commit(s) were added to refs/heads/master by this push:
new 5b6a433b17d IGNITE-23308 SQL Calcite: Fix wrong numeric type coercion
with set-op operations - Fixes #11557.
5b6a433b17d is described below
commit 5b6a433b17db89b0ba69b9c16aa63ac2cd24c46d
Author: Vladimir Steshin <[email protected]>
AuthorDate: Fri Oct 4 12:13:46 2024 +0300
IGNITE-23308 SQL Calcite: Fix wrong numeric type coercion with set-op
operations - Fixes #11557.
Signed-off-by: Aleksey Plekhanov <[email protected]>
---
.../query/calcite/rule/SetOpConverterRule.java | 5 +
.../query/calcite/rule/UnionConverterRule.java | 4 +-
.../query/calcite/type/IgniteTypeFactory.java | 3 +-
.../processors/query/calcite/util/Commons.java | 62 +++++++++++
.../calcite/integration/SetOpIntegrationTest.java | 69 ++++++++++++
.../calcite/planner/DataTypesPlannerTest.java | 123 +++++++++++++++++++++
.../apache/ignite/testsuites/PlannerTestSuite.java | 2 +
7 files changed, 265 insertions(+), 3 deletions(-)
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/SetOpConverterRule.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/SetOpConverterRule.java
index c6782cab398..9e4269a5d97 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/SetOpConverterRule.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/SetOpConverterRule.java
@@ -38,6 +38,7 @@ import
org.apache.ignite.internal.processors.query.calcite.rel.set.IgniteMapMinu
import
org.apache.ignite.internal.processors.query.calcite.rel.set.IgniteReduceIntersect;
import
org.apache.ignite.internal.processors.query.calcite.rel.set.IgniteReduceMinus;
import
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
/**
* Set op (MINUS, INTERSECT) converter rule.
@@ -77,6 +78,8 @@ public class SetOpConverterRule {
RelTraitSet outTrait =
cluster.traitSetOf(IgniteConvention.INSTANCE).replace(IgniteDistributions.single());
List<RelNode> inputs = Util.transform(setOp.getInputs(), rel ->
convert(rel, inTrait));
+ inputs = Commons.castToLeastRestrictiveIfRequired(inputs, cluster,
inTrait);
+
return createNode(cluster, outTrait, inputs, setOp.all);
}
}
@@ -131,6 +134,8 @@ public class SetOpConverterRule {
RelTraitSet outTrait =
cluster.traitSetOf(IgniteConvention.INSTANCE);
List<RelNode> inputs = Util.transform(setOp.getInputs(), rel ->
convert(rel, inTrait));
+ inputs = Commons.castToLeastRestrictiveIfRequired(inputs, cluster,
inTrait);
+
RelNode map = createMapNode(cluster, outTrait, inputs, setOp.all);
return createReduceNode(
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/UnionConverterRule.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/UnionConverterRule.java
index df7944cb006..6b106a49f15 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/UnionConverterRule.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/rule/UnionConverterRule.java
@@ -47,12 +47,14 @@ public class UnionConverterRule extends
RelRule<UnionConverterRule.Config> {
/** {@inheritDoc} */
@Override public void onMatch(RelOptRuleCall call) {
- final LogicalUnion union = call.rel(0);
+ LogicalUnion union = call.rel(0);
RelOptCluster cluster = union.getCluster();
RelTraitSet traits = cluster.traitSetOf(IgniteConvention.INSTANCE);
List<RelNode> inputs = Commons.transform(union.getInputs(), input ->
convert(input, traits));
+ inputs = Commons.castToLeastRestrictiveIfRequired(inputs, cluster,
traits);
+
RelNode res = new IgniteUnionAll(cluster, traits, inputs);
if (!union.all) {
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/type/IgniteTypeFactory.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/type/IgniteTypeFactory.java
index ab5f6f2a30c..954b9e64ddf 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/type/IgniteTypeFactory.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/type/IgniteTypeFactory.java
@@ -145,9 +145,8 @@ public class IgniteTypeFactory extends JavaTypeFactoryImpl {
return Enum.class;
case ANY:
case OTHER:
- return Object.class;
case NULL:
- return Void.class;
+ return Object.class;
default:
break;
}
diff --git
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/Commons.java
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/Commons.java
index a88d52c8865..468c4f53d7b 100644
---
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/Commons.java
+++
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/Commons.java
@@ -38,10 +38,16 @@ import org.apache.calcite.linq4j.Ord;
import org.apache.calcite.plan.Context;
import org.apache.calcite.plan.Contexts;
import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelTraitSet;
import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
+import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.calcite.util.ImmutableBitSet;
import org.apache.calcite.util.ImmutableIntList;
import org.apache.calcite.util.SourceStringReader;
@@ -61,6 +67,7 @@ import
org.apache.ignite.internal.processors.query.calcite.exec.RowHandler;
import
org.apache.ignite.internal.processors.query.calcite.exec.exp.ExpressionFactoryImpl;
import
org.apache.ignite.internal.processors.query.calcite.prepare.BaseQueryContext;
import
org.apache.ignite.internal.processors.query.calcite.prepare.MappingQueryContext;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
import
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
import org.apache.ignite.internal.util.typedef.F;
import org.apache.ignite.internal.util.typedef.internal.A;
@@ -140,6 +147,61 @@ public final class Commons {
.collect(Collectors.toList());
}
+ /**
+ * Finds the least restrictive type of the inputs and adds a cast
projection if required.
+ *
+ * @param inputs Inputs to try to cast.
+ * @param cluster Cluster.
+ * @param traits Traits.
+ * @return Converted inputs.
+ */
+ public static List<RelNode> castToLeastRestrictiveIfRequired(List<RelNode>
inputs, RelOptCluster cluster, RelTraitSet traits) {
+ List<RelDataType> inputRowTypes =
inputs.stream().map(RelNode::getRowType).collect(Collectors.toList());
+
+ // Output type of a set operator is equal to
leastRestrictive(inputTypes) (see SetOp::deriveRowType).
+ RelDataTypeFactory typeFactory = cluster.getTypeFactory();
+
+ RelDataType leastRestrictive =
typeFactory.leastRestrictive(inputRowTypes);
+
+ if (leastRestrictive == null)
+ throw new IllegalStateException("Cannot find least restrictive
type for arguments to set op: " + inputRowTypes);
+
+ // If input's type does not match the result type, then add a cast
projection for non-matching fields.
+ RexBuilder rexBuilder = cluster.getRexBuilder();
+ List<RelNode> newInputs = new ArrayList<>(inputs.size());
+
+ for (RelNode input : inputs) {
+ RelDataType inputRowType = input.getRowType();
+
+ // It is always safe to convert from [T1 nullable, T2 not
nullable] to [T1 nullable, T2 nullable] and
+ // the least restrictive type does exactly that.
+ if (SqlTypeUtil.equalAsStructSansNullability(typeFactory,
leastRestrictive, inputRowType, null)) {
+ newInputs.add(input);
+
+ continue;
+ }
+
+ List<RexNode> expressions = new
ArrayList<>(inputRowType.getFieldCount());
+
+ for (int i = 0; i < leastRestrictive.getFieldCount(); i++) {
+ RelDataType fieldType =
inputRowType.getFieldList().get(i).getType();
+
+ RelDataType outFieldType =
leastRestrictive.getFieldList().get(i).getType();
+
+ RexNode ref = rexBuilder.makeInputRef(input, i);
+
+ if (fieldType.equals(outFieldType))
+ expressions.add(ref);
+ else
+ expressions.add(rexBuilder.makeCast(outFieldType, ref,
true, false));
+ }
+
+ newInputs.add(new IgniteProject(cluster, traits, input,
expressions, leastRestrictive));
+ }
+
+ return newInputs;
+ }
+
/**
* Returns a given list as a typed list.
*/
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/SetOpIntegrationTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/SetOpIntegrationTest.java
index bc87bbb8e59..59d0620eed2 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/SetOpIntegrationTest.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/SetOpIntegrationTest.java
@@ -17,6 +17,7 @@
package org.apache.ignite.internal.processors.query.calcite.integration;
+import java.util.Arrays;
import java.util.List;
import javax.cache.Cache;
@@ -466,4 +467,72 @@ public class SetOpIntegrationTest extends
AbstractBasicIntegrationTest {
.returns(2)
.check();
}
+
+ /** */
+ @Test
+ public void testNumbersCastInUnion() throws Exception {
+ doTestNumbersCastInSetOp("UNION", 10, 20, 30, 33, 40, 44, 50, null);
+
+ doTestNumbersCastInSetOp("UNION ALL", 10, 20, 20, 30, 30, 33, 40, 44,
50, 50, 50, 50, null, null);
+ }
+
+ /** */
+ @Test
+ public void testNumbersCastInIntersect() throws Exception {
+ doTestNumbersCastInSetOp("INTERSECT", 20, 50, null);
+
+ doTestNumbersCastInSetOp("INTERSECT ALL", 20, 50, 50, null);
+ }
+
+ /** */
+ @Test
+ public void testNumbersCastInExcept() throws Exception {
+ doTestNumbersCastInSetOp("EXCEPT", 30, 40);
+
+ doTestNumbersCastInSetOp("EXCEPT ALL", 30, 30, 40);
+ }
+
+ /**
+ * Tests 'SELECT TBL1.val SetOp TBL2.val' where TBL1 has `INT val` and
TBL2 has 'val' of different numeric type.
+ * TBL1: 30, 20, 30, 40, 50, 50, null
+ * TBL2: 10, 20, 33, 44, 50, 50, null
+ *
+ * @param op Operation like 'UNION' or 'INTERSECT'
+ * @param expected Expected result as integers.
+ */
+ private void doTestNumbersCastInSetOp(String op, Integer... expected)
throws InterruptedException {
+ List<String> types = F.asList("TINYINT", "SMALLINT", "INTEGER",
"REAL", "FLOAT", "BIGINT", "DOUBLE", "DECIMAL");
+
+ sql(client, "CREATE TABLE t0(id INT PRIMARY KEY, val INTEGER) WITH
\"affinity_key=id\"");
+
+ try {
+ sql(client, "INSERT INTO t0 VALUES (1, 30), (2, 20), (3, 30), (4,
40), (5, 50), (6, 50), (7, null)");
+
+ for (String tblOpts : Arrays.asList("", " WITH
\"template=replicated\"", " WITH \"affinity_key=aff\"")) {
+ for (String t2 : types) {
+ sql(client, "CREATE TABLE t1(id INT, aff INT, val " + t2 +
", PRIMARY KEY(id, aff))" + tblOpts);
+
+ sql(client, "INSERT INTO t1 VALUES (1, 1, 10), (2, 1, 20),
(3, 1, 33), (4, 2, 44), (5, 2, 50), " +
+ "(6, 3, 50), (7, 3, null)");
+
+ List<List<?>> res = sql(client, "SELECT val from t0 " + op
+ " select val from t1 ORDER BY 1 NULLS LAST");
+
+ sql(client, "DROP TABLE t1");
+
+ assertEquals(expected.length, res.size());
+
+ for (int i = 0; i < expected.length; ++i) {
+ assertEquals(1, res.get(i).size());
+
+ assertEquals(expected[i], res.get(i).get(0) == null ?
null : ((Number)res.get(i).get(0)).intValue());
+ }
+ }
+ }
+ }
+ finally {
+ sql(client, "DROP TABLE t0");
+
+ awaitPartitionMapExchange();
+ }
+ }
}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/DataTypesPlannerTest.java
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/DataTypesPlannerTest.java
new file mode 100644
index 00000000000..c2edafb8bf3
--- /dev/null
+++
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/planner/DataTypesPlannerTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.processors.query.calcite.planner;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Predicate;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.SetOp;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.ignite.internal.processors.query.calcite.rel.IgniteProject;
+import org.apache.ignite.internal.processors.query.calcite.schema.IgniteSchema;
+import
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistribution;
+import
org.apache.ignite.internal.processors.query.calcite.trait.IgniteDistributions;
+import
org.apache.ignite.internal.processors.query.calcite.type.IgniteTypeFactory;
+import org.apache.ignite.internal.processors.query.calcite.util.Commons;
+import org.junit.Test;
+
+/**
+ * Planner test various types, casts and coercions.
+ */
+public class DataTypesPlannerTest extends AbstractPlannerTest {
+ /** Tests casts of numeric types in SetOps (UNION, EXCEPT, INTERSECT,
etc.). */
+ @Test
+ public void testSetOpNumbersCast() throws Exception {
+ List<IgniteDistribution> distrs =
Arrays.asList(IgniteDistributions.single(), IgniteDistributions.random(),
+ IgniteDistributions.affinity(0, 1001, 0));
+
+ for (IgniteDistribution d1 : distrs) {
+ for (IgniteDistribution d2 : distrs) {
+ doTestSetOpNumbersCast(d1, d2, true, true);
+
+ doTestSetOpNumbersCast(d1, d2, false, true);
+
+ doTestSetOpNumbersCast(d1, d2, false, false);
+ }
+ }
+ }
+
+ /** */
+ private void doTestSetOpNumbersCast(
+ IgniteDistribution distr1,
+ IgniteDistribution distr2,
+ boolean nullable1,
+ boolean nullable2
+ ) throws Exception {
+ IgniteSchema schema = new IgniteSchema(DEFAULT_SCHEMA);
+
+ IgniteTypeFactory f = Commons.typeFactory();
+
+ SqlTypeName[] numTypes = new SqlTypeName[] {SqlTypeName.TINYINT,
SqlTypeName.SMALLINT, SqlTypeName.REAL,
+ SqlTypeName.FLOAT, SqlTypeName.INTEGER, SqlTypeName.BIGINT,
SqlTypeName.DOUBLE, SqlTypeName.DECIMAL};
+
+ boolean notNull = !nullable1 && !nullable2;
+
+ for (SqlTypeName t1 : numTypes) {
+ for (SqlTypeName t2 : numTypes) {
+ RelDataType type = new RelDataTypeFactory.Builder(f)
+ .add("C1",
f.createTypeWithNullability(f.createSqlType(t1), nullable1))
+ .add("C2",
f.createTypeWithNullability(f.createSqlType(SqlTypeName.VARCHAR), true))
+ .build();
+
+ createTable(schema, "TABLE1", type, distr1, null);
+
+ type = new RelDataTypeFactory.Builder(f)
+ .add("C1",
f.createTypeWithNullability(f.createSqlType(t2), nullable2))
+ .add("C2",
f.createTypeWithNullability(f.createSqlType(SqlTypeName.VARCHAR), true))
+ .build();
+
+ createTable(schema, "TABLE2", type, distr2, null);
+
+ for (String op : Arrays.asList("UNION", "INTERSECT",
"EXCEPT")) {
+ String sql = "SELECT * FROM table1 " + op + " SELECT *
FROM table2";
+
+ if (t1 == t2 && (!nullable1 || !nullable2))
+ assertPlan(sql, schema,
nodeOrAnyChild(isInstanceOf(IgniteProject.class)).negate());
+ else {
+ RelDataType targetT =
f.leastRestrictive(Arrays.asList(f.createSqlType(t1), f.createSqlType(t2)));
+
+ assertPlan(sql, schema,
nodeOrAnyChild(isInstanceOf(SetOp.class)
+ .and(t1 == targetT.getSqlTypeName() ? input(0,
nodeOrAnyChild(isInstanceOf(IgniteProject.class)).negate())
+ : input(0, projectFromTable("TABLE1",
"CAST($0):" + targetT + (notNull ? " NOT NULL" : ""), "$1")))
+ .and(t2 == targetT.getSqlTypeName() ? input(1,
nodeOrAnyChild(isInstanceOf(IgniteProject.class)).negate())
+ : input(1, projectFromTable("TABLE2",
"CAST($0):" + targetT + (notNull ? " NOT NULL" : ""), "$1")))
+ ));
+ }
+ }
+ }
+ }
+ }
+
+ /** */
+ protected Predicate<? extends RelNode> projectFromTable(String tableName,
String... exprs) {
+ return nodeOrAnyChild(
+ isInstanceOf(IgniteProject.class)
+ .and(projection -> {
+ String actualProj = projection.getProjects().toString();
+
+ String expectedProj = Arrays.asList(exprs).toString();
+
+ return actualProj.equals(expectedProj);
+ })
+ .and(input(nodeOrAnyChild(isTableScan(tableName))))
+ );
+ }
+}
diff --git
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
index 372583f89f3..53ff68e605e 100644
---
a/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
+++
b/modules/calcite/src/test/java/org/apache/ignite/testsuites/PlannerTestSuite.java
@@ -21,6 +21,7 @@ import
org.apache.ignite.internal.processors.query.calcite.planner.AggregateDist
import
org.apache.ignite.internal.processors.query.calcite.planner.AggregatePlannerTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.CorrelatedNestedLoopJoinPlannerTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.CorrelatedSubqueryPlannerTest;
+import
org.apache.ignite.internal.processors.query.calcite.planner.DataTypesPlannerTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.HashAggregatePlannerTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.HashIndexSpoolPlannerTest;
import
org.apache.ignite.internal.processors.query.calcite.planner.IndexRebuildPlannerTest;
@@ -67,6 +68,7 @@ import org.junit.runners.Suite;
TableFunctionPlannerTest.class,
TableDmlPlannerTest.class,
UnionPlannerTest.class,
+ DataTypesPlannerTest.class,
JoinCommutePlannerTest.class,
LimitOffsetPlannerTest.class,
MergeJoinPlannerTest.class,