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 20160fa66f5 IGNITE-23179 SQL Calcite: Fix wrong numeric type coercion 
with 'IS DISTINCT FROM' - Fixes #11525.
20160fa66f5 is described below

commit 20160fa66f517ca270b564690b5373ab428e141a
Author: Vladimir Steshin <[email protected]>
AuthorDate: Tue Oct 1 17:40:34 2024 +0300

    IGNITE-23179 SQL Calcite: Fix wrong numeric type coercion with 'IS DISTINCT 
FROM' - Fixes #11525.
    
    Signed-off-by: Aleksey Plekhanov <[email protected]>
---
 .../query/calcite/exec/LogicalRelImplementor.java  |   5 +-
 .../query/calcite/exec/exp/ExpressionFactory.java  |   4 +-
 .../calcite/exec/exp/ExpressionFactoryImpl.java    |   5 +-
 .../query/calcite/prepare/IgniteTypeCoercion.java  |  38 ++++++++
 .../processors/query/calcite/util/TypeUtils.java   |   5 +
 .../query/calcite/integration/DataTypesTest.java   | 102 +++++++++++++++++++++
 6 files changed, 155 insertions(+), 4 deletions(-)

diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
index 974113a0401..6b5a2661939 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/LogicalRelImplementor.java
@@ -114,6 +114,8 @@ import 
org.apache.ignite.internal.processors.query.calcite.util.RexUtils;
 import org.apache.ignite.internal.util.typedef.F;
 
 import static org.apache.calcite.rel.RelDistribution.Type.HASH_DISTRIBUTED;
+import static org.apache.calcite.sql.SqlKind.IS_DISTINCT_FROM;
+import static org.apache.calcite.sql.SqlKind.IS_NOT_DISTINCT_FROM;
 import static 
org.apache.ignite.internal.processors.query.calcite.util.TypeUtils.combinedRowType;
 
 /**
@@ -298,7 +300,8 @@ public class LogicalRelImplementor<Row> implements 
IgniteRelVisitor<Node<Row>> {
 
         Comparator<Row> comp = expressionFactory.comparator(
             rel.leftCollation().getFieldCollations().subList(0, pairsCnt),
-            rel.rightCollation().getFieldCollations().subList(0, pairsCnt)
+            rel.rightCollation().getFieldCollations().subList(0, pairsCnt),
+            rel.getCondition().getKind() == IS_NOT_DISTINCT_FROM || 
rel.getCondition().getKind() == IS_DISTINCT_FROM
         );
 
         Node<Row> node = MergeJoinNode.create(ctx, outType, leftType, 
rightType, joinType, comp, hasExchange(rel));
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactory.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactory.java
index a68078f6c42..3fb64377322 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactory.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactory.java
@@ -60,9 +60,11 @@ public interface ExpressionFactory<Row> {
      *
      * @param left Collations of left row.
      * @param right Collations of right row.
+     * @param nullsEqual If {@code true}, nulls are considered equal. Usually, 
NULL <> NULL in SQL. So, the value should
+     *                   be {@code false}. Except cases with IS DISTINCT / IS 
NOT DISTINCT.
      * @return Rows comparator.
      */
-    Comparator<Row> comparator(List<RelFieldCollation> left, 
List<RelFieldCollation> right);
+    Comparator<Row> comparator(List<RelFieldCollation> left, 
List<RelFieldCollation> right, boolean nullsEqual);
 
     /**
      * Creates a Filter predicate.
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
index 187d2739457..910cedc8d14 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/exec/exp/ExpressionFactoryImpl.java
@@ -192,7 +192,7 @@ public class ExpressionFactoryImpl<Row> implements 
ExpressionFactory<Row> {
     }
 
     /** {@inheritDoc} */
-    @Override public Comparator<Row> comparator(List<RelFieldCollation> left, 
List<RelFieldCollation> right) {
+    @Override public Comparator<Row> comparator(List<RelFieldCollation> left, 
List<RelFieldCollation> right, boolean nullsEqual) {
         if (F.isEmpty(left) || F.isEmpty(right) || left.size() != right.size())
             throw new IllegalArgumentException("Both inputs should be 
non-empty and have the same size: left="
                 + (left != null ? left.size() : "null") + ", right=" + (right 
!= null ? right.size() : "null"));
@@ -237,7 +237,8 @@ public class ExpressionFactoryImpl<Row> implements 
ExpressionFactory<Row> {
                 }
 
                 // If compared rows contain NULLs, they shouldn't be treated 
as equals, since NULL <> NULL in SQL.
-                return hasNulls ? 1 : 0;
+                // Except cases with IS DISTINCT / IS NOT DISTINCT.
+                return hasNulls && !nullsEqual ? 1 : 0;
             }
         };
     }
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteTypeCoercion.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteTypeCoercion.java
index 0528f37a090..8cba0419885 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteTypeCoercion.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/prepare/IgniteTypeCoercion.java
@@ -18,12 +18,14 @@
 package org.apache.ignite.internal.processors.query.calcite.prepare;
 
 import java.nio.charset.Charset;
+import java.util.Arrays;
 import org.apache.calcite.adapter.java.JavaTypeFactory;
 import org.apache.calcite.rel.type.DynamicRecordType;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelDataTypeFactoryImpl;
 import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlCallBinding;
 import org.apache.calcite.sql.SqlCollation;
 import org.apache.calcite.sql.SqlDataTypeSpec;
 import org.apache.calcite.sql.SqlIdentifier;
@@ -33,6 +35,7 @@ import org.apache.calcite.sql.SqlNodeList;
 import org.apache.calcite.sql.SqlUserDefinedTypeNameSpec;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlTypeFamily;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.sql.validate.SqlValidator;
 import org.apache.calcite.sql.validate.SqlValidatorScope;
@@ -55,6 +58,41 @@ public class IgniteTypeCoercion extends TypeCoercionImpl {
         super(typeFactory, validator);
     }
 
+    /** {@inheritDoc} **/
+    @Override public boolean binaryComparisonCoercion(SqlCallBinding binding) {
+        // Although it is not reflected in the docs, this method is also 
invoked for MAX, MIN (and other similar operators)
+        // by ComparableOperandTypeChecker. When that is the case, fallback to 
default rules.
+        SqlCall call = binding.getCall();
+
+        if (binding.getOperandCount() != 2 || 
!SqlKind.BINARY_COMPARISON.contains(call.getKind()))
+            return super.binaryComparisonCoercion(binding);
+
+        SqlValidatorScope scope = binding.getScope();
+
+        RelDataType leftType = validator.deriveType(scope, call.operand(0));
+        RelDataType rightType = validator.deriveType(scope, call.operand(1));
+
+        if (leftType.equals(rightType))
+            return super.binaryComparisonCoercion(binding);
+        else {
+            // Find the least restrictive type among the operand types
+            // and coerce the operands to that type if such type exists.
+            //
+            // An example of a least restrictive type from the javadoc for 
RelDataTypeFactory::leastRestrictive:
+            // leastRestrictive(INT, NUMERIC(3, 2)) could be NUMERIC(12, 2)
+            //
+            // A least restrictive type between types of different type 
families does not exist -
+            // the method returns null (See 
SqlTypeFactoryImpl::leastRestrictive).
+            //
+            RelDataType targetType = 
factory.leastRestrictive(Arrays.asList(leftType, rightType));
+
+            if (targetType == null || targetType.getFamily() == 
SqlTypeFamily.ANY)
+                return super.binaryComparisonCoercion(binding);
+            else
+                return coerceOperandsType(scope, call, targetType);
+        }
+    }
+
     /** {@inheritDoc} */
     @Override protected boolean coerceOperandType(
         SqlValidatorScope scope,
diff --git 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
index 811f6c692d1..7fda6a03b6f 100644
--- 
a/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
+++ 
b/modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/util/TypeUtils.java
@@ -145,6 +145,11 @@ public class TypeUtils {
             return false;
         }
 
+        // Currently, RelDataTypeFactoryImpl#CLASS_FAMILIES doesn't consider 
the byte type as an integer.
+        if ((fromType.getSqlTypeName() == SqlTypeName.TINYINT && 
SqlTypeUtil.isIntType(toType))
+            || (toType.getSqlTypeName() == SqlTypeName.TINYINT && 
SqlTypeUtil.isIntType(fromType)))
+            return false;
+
         // Implicit type coercion does not handle nullability.
         if (SqlTypeUtil.equalSansNullability(factory, fromType, toType))
             return false;
diff --git 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
index 23474698fe5..c350fc2d80f 100644
--- 
a/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
+++ 
b/modules/calcite/src/test/java/org/apache/ignite/internal/processors/query/calcite/integration/DataTypesTest.java
@@ -18,18 +18,21 @@
 package org.apache.ignite.internal.processors.query.calcite.integration;
 
 import java.math.BigDecimal;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
 import java.util.UUID;
 import java.util.stream.Collectors;
 import com.google.common.collect.ImmutableSet;
 import org.apache.calcite.runtime.CalciteException;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.tools.FrameworkConfig;
 import org.apache.calcite.tools.Frameworks;
 import org.apache.ignite.IgniteCache;
 import org.apache.ignite.cache.QueryEntity;
 import org.apache.ignite.configuration.CacheConfiguration;
 import org.apache.ignite.internal.processors.query.IgniteSQLException;
+import org.apache.ignite.internal.processors.query.calcite.hint.HintDefinition;
 import org.apache.ignite.internal.util.typedef.F;
 import org.junit.Test;
 
@@ -450,6 +453,105 @@ public class DataTypesTest extends 
AbstractBasicIntegrationTest {
             .check();
     }
 
+    /** */
+    @Test
+    public void testIsNotDistinctFromTypeConversion() {
+        SqlTypeName[] numerics = new SqlTypeName[] {SqlTypeName.TINYINT, 
SqlTypeName.SMALLINT, SqlTypeName.INTEGER,
+            SqlTypeName.BIGINT, SqlTypeName.DECIMAL, SqlTypeName.FLOAT, 
SqlTypeName.DOUBLE};
+
+        sql("CREATE TABLE t1(key1 INTEGER, i1idx INTEGER, i1 INTEGER, chr1 
VARCHAR, PRIMARY KEY(key1))");
+        sql("CREATE INDEX t1_idx ON t1(i1idx)");
+        sql("INSERT INTO t1 VALUES (1, 1, null, '1'), (2, 2, 2, '22'), (3, 33, 
3, null), (4, null, 4, '4')");
+
+        for (SqlTypeName type : numerics) {
+            String t = type.getName();
+
+            sql("CREATE TABLE t2(key2 " + t + ", i2idx " + t + ", i2 " + t + 
", i3 INTEGER, chr2 VARCHAR, PRIMARY KEY(key2))");
+            sql("CREATE INDEX t2_idx ON t2(i2idx)");
+            sql("INSERT INTO t2 VALUES (0, 0, 0, null, '0'), (11, null, 1, 1, 
'1'), (2, 2, 2, 2, '22'), (3, 3, null, 3, null)");
+
+            for (HintDefinition hint : 
Arrays.asList(HintDefinition.MERGE_JOIN, HintDefinition.NL_JOIN, 
HintDefinition.CNL_JOIN)) {
+                String h = "/*+ " + hint.name() + " */ ";
+
+                // Primary keys, indexed.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON key2 
IS NOT DISTINCT FROM key1")
+                    .returns(2, 2)
+                    .returns(3, 3)
+                    .check();
+
+                // Indexed and not indexed.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON i2idx 
IS NOT DISTINCT FROM i1")
+                    .returns(1, 1)
+                    .returns(2, 2)
+                    .returns(3, 3)
+                    .check();
+
+                // Both not indexed.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON i2 IS 
NOT DISTINCT FROM i1")
+                    .returns(1, 3)
+                    .returns(2, 2)
+                    .check();
+
+                // Indexed and casted.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON i2idx 
IS NOT DISTINCT FROM CAST(chr1 as INTEGER)")
+                    .returns(3, 1)
+                    .check();
+
+                // Not indexed and casted.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON i2 IS 
NOT DISTINCT FROM CAST(chr1 as INTEGER)")
+                    .returns(1, 1)
+                    .returns(3, 3)
+                    .check();
+
+                // @see MergeJoinConverterRule#matchesJoin(RelOptRuleCall)
+                if (hint == HintDefinition.MERGE_JOIN)
+                    continue;
+
+                // Primary keys, indexed.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON key2 
IS DISTINCT FROM key1 and key1<2")
+                    .returns(1, 1)
+                    .returns(1, 2)
+                    .returns(1, 3)
+                    .returns(1, null)
+                    .check();
+
+                // Indexed and not indexed.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON i2idx 
IS NOT DISTINCT FROM i1")
+                    .returns(1, 1)
+                    .returns(2, 2)
+                    .returns(3, 3)
+                    .check();
+
+                // Both not indexed.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON i2 IS 
DISTINCT FROM i1 and key1<3")
+                    .returns(1, 1)
+                    .returns(1, 2)
+                    .returns(1, null)
+                    .returns(2, 1)
+                    .returns(2, 3)
+                    .returns(2, null)
+                    .check();
+
+                // Indexed and casted.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON i2idx 
IS DISTINCT FROM CAST(chr1 as INTEGER) and key1<2")
+                    .returns(1, null)
+                    .returns(1, 2)
+                    .returns(1, 3)
+                    .returns(1, 1)
+                    .check();
+
+                // Not indexed and casted.
+                assertQuery("SELECT " + h + "key1, i3 FROM t1 JOIN t2 ON i2 IS 
DISTINCT FROM CAST(chr1 as INTEGER) and key1<2")
+                    .returns(1, null)
+                    .returns(1, 2)
+                    .returns(1, 3)
+                    .check();
+            }
+
+            sql("DROP TABLE t2");
+        }
+    }
+
     /** */
     @Test
     public void testNumericConversion() {

Reply via email to