This is an automated email from the ASF dual-hosted git repository.

asolimando pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 83882017b1 [CALCITE-7174] Improve lossless cast detection for numeric 
types
83882017b1 is described below

commit 83882017b188222a06916145b0f4867b6a7e2b1f
Author: Alessandro Solimando <[email protected]>
AuthorDate: Thu Sep 11 19:17:07 2025 +0200

    [CALCITE-7174] Improve lossless cast detection for numeric types
    
    Deprecated getMinValue/getMaxValue methods in favor of integerBound
---
 .../main/java/org/apache/calcite/rex/RexUtil.java  |  86 +++++-
 .../org/apache/calcite/sql/type/SqlTypeUtil.java   |  86 +++++-
 .../apache/calcite/rex/RexLosslessCastTest.java    | 325 ++++++++++++++++++++-
 .../apache/calcite/rex/RexProgramBuilderBase.java  |  34 ++-
 .../apache/calcite/sql/type/SqlTypeUtilTest.java   |  59 +++-
 core/src/test/resources/sql/planner.iq             |  38 +--
 core/src/test/resources/sql/sub-query.iq           |  14 +-
 7 files changed, 589 insertions(+), 53 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/rex/RexUtil.java 
b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
index cfa0f25b3c..d4cc04c6eb 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
@@ -1676,7 +1676,6 @@ public static boolean isLosslessCast(RexNode node) {
     return isLosslessCast(((RexCall) node).getOperands().get(0).getType(), 
node.getType());
   }
 
-
   /**
    * Returns whether the conversion from {@code source} to {@code target} type
    * is a 'loss-less' cast, that is, a cast from which
@@ -1693,22 +1692,31 @@ public static boolean isLosslessCast(RexNode node) {
    * @return 'true' when the conversion can certainly be determined to be 
loss-less cast,
    *         but may return 'false' for some lossless casts.
    */
-  @API(since = "1.22", status = API.Status.EXPERIMENTAL)
+  @API(since = "1.22", status = API.Status.STABLE)
   public static boolean isLosslessCast(RelDataType source, RelDataType target) 
{
     final SqlTypeName sourceSqlTypeName = source.getSqlTypeName();
     final SqlTypeName targetSqlTypeName = target.getSqlTypeName();
-    // 1) Both INT numeric types
-    if (SqlTypeFamily.INTEGER.getTypeNames().contains(sourceSqlTypeName)
-        && SqlTypeFamily.INTEGER.getTypeNames().contains(targetSqlTypeName)) {
-      return targetSqlTypeName.compareTo(sourceSqlTypeName) >= 0;
+
+    // INT -> INT: use range containment (signed/unsigned)
+    if (SqlTypeUtil.isIntType(source) && SqlTypeUtil.isIntType(target)) {
+      final boolean sourceIsUnsigned =
+          
SqlTypeFamily.UNSIGNED_NUMERIC.getTypeNames().contains(sourceSqlTypeName);
+      final boolean targetIsUnsigned =
+          
SqlTypeFamily.UNSIGNED_NUMERIC.getTypeNames().contains(targetSqlTypeName);
+      if (!sourceIsUnsigned && targetIsUnsigned) {
+        return false;
+      }
+      return SqlTypeUtil.integerRangeContains(target, source);
     }
-    // 2) Both CHARACTER types: it depends on the precision (length)
+
+    // CHARACTER -> CHARACTER: valid if target order >= source and length grows
     if (SqlTypeFamily.CHARACTER.getTypeNames().contains(sourceSqlTypeName)
         && SqlTypeFamily.CHARACTER.getTypeNames().contains(targetSqlTypeName)) 
{
       return targetSqlTypeName.compareTo(sourceSqlTypeName) >= 0
           && source.getPrecision() <= target.getPrecision();
     }
-    // 3) From NUMERIC family to CHARACTER family: it depends on the 
precision/scale
+
+    // NUMERIC -> CHARACTER: allow when target length accommodates sign/scale
     if (sourceSqlTypeName.getFamily() == SqlTypeFamily.NUMERIC
         && targetSqlTypeName.getFamily() == SqlTypeFamily.CHARACTER) {
       int sourceLength = source.getPrecision() + 1; // include sign
@@ -1718,7 +1726,67 @@ public static boolean isLosslessCast(RelDataType source, 
RelDataType target) {
       final int targetPrecision = target.getPrecision();
       return targetPrecision == PRECISION_NOT_SPECIFIED || targetPrecision >= 
sourceLength;
     }
-    // Return FALSE by default
+
+    // DECIMAL -> DECIMAL: allow when precision/scale can only expand
+    if (sourceSqlTypeName == SqlTypeName.DECIMAL
+        && targetSqlTypeName == SqlTypeName.DECIMAL) {
+      int sourcePrecision = source.getPrecision();
+      int sourceScale = Math.max(source.getScale(), 0);
+      int targetPrecision = target.getPrecision();
+      int targetScale = Math.max(target.getScale(), 0);
+      if (sourcePrecision <= 0 || targetPrecision <= 0) {
+        return false;
+      }
+      return targetScale >= sourceScale
+          && (targetPrecision - targetScale) >= (sourcePrecision - 
sourceScale);
+    }
+
+    // INT (signed/unsigned) -> DECIMAL: valid if integer digits fit within 
target precision
+    if (SqlTypeUtil.isIntType(source)
+        && targetSqlTypeName == SqlTypeName.DECIMAL) {
+      int targetPrecision = target.getPrecision();
+      int targetScale = Math.max(target.getScale(), 0);
+      int sourcePrecision = source.getPrecision();
+      return sourcePrecision > 0 && (targetPrecision - targetScale) >= 
sourcePrecision;
+    }
+
+    // DECIMAL -> INT: only when scale = 0 and range fits target integer type
+    if (sourceSqlTypeName == SqlTypeName.DECIMAL && 
SqlTypeUtil.isIntType(target)) {
+      if (source.getScale() != 0) {
+        return false;
+      }
+      return SqlTypeUtil.integerRangeContains(target, source);
+    }
+
+    // APPROXIMATE NUMERIC -> APPROXIMATE NUMERIC: allow when target precision 
>= source precision
+    if 
(SqlTypeFamily.APPROXIMATE_NUMERIC.getTypeNames().contains(sourceSqlTypeName)
+        && 
SqlTypeFamily.APPROXIMATE_NUMERIC.getTypeNames().contains(targetSqlTypeName)) {
+      // Lossless if target has at least as many significant digits as source
+      final int sourcePrecision = source.getPrecision();
+      final int targetPrecision = target.getPrecision();
+      return targetPrecision >= sourcePrecision;
+    }
+
+    // EXACT NUMERIC -> APPROXIMATE NUMERIC: allow only for scale=0 values 
within target digits
+    if (SqlTypeFamily.EXACT_NUMERIC.getTypeNames().contains(sourceSqlTypeName)
+        && 
SqlTypeFamily.APPROXIMATE_NUMERIC.getTypeNames().contains(targetSqlTypeName)) {
+      final int targetPrecision = target.getPrecision();
+
+      // DECIMAL -> APPROXIMATE NUMERIC
+      if (sourceSqlTypeName == SqlTypeName.DECIMAL) {
+        final int sourcePrecision = source.getPrecision();
+        if (sourcePrecision <= 0 || source.getScale() != 0) {
+          return false;
+        }
+        // scale is 0, just check precision
+        return sourcePrecision <= targetPrecision;
+      }
+
+      // INT (signed/unsigned) -> APPROXIMATE NUMERIC
+      int sourcePrecision = source.getPrecision();
+      return sourcePrecision > 0 && sourcePrecision <= targetPrecision;
+    }
+
     return false;
   }
 
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java 
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
index 3de5e17df9..6a952aeaf3 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java
@@ -51,6 +51,7 @@
 import org.checkerframework.checker.nullness.qual.Nullable;
 
 import java.math.BigDecimal;
+import java.math.BigInteger;
 import java.math.RoundingMode;
 import java.nio.charset.Charset;
 import java.util.AbstractList;
@@ -556,6 +557,85 @@ public static boolean isExactNumeric(RelDataType type) {
     }
   }
 
+  /**
+   * Returns whether {@code container} can represent every value produced by
+   * {@code content} without loss of information.
+   *
+   * <p>The {@code container} type must be one of the integer types (signed or
+   * unsigned). The {@code content} type can be integer, or a DECIMAL with
+   * scale {@code 0}. For all other types this method returns {@code false}.
+   *
+   * @throws IllegalArgumentException if {@code container} is not an integer 
type
+   */
+  public static boolean integerRangeContains(RelDataType container, 
RelDataType content) {
+    checkArgument(isIntType(container),
+        "container must be an integer type: %s", container);
+
+    final SqlTypeName contentType = content.getSqlTypeName();
+    final boolean contentIsDecimal = contentType == SqlTypeName.DECIMAL;
+    if (!isIntType(content) && (!contentIsDecimal || content.getScale() != 0)) 
{
+      return false;
+    }
+
+    final BigInteger containerMin = integerBound(container, false);
+    final BigInteger containerMax = integerBound(container, true);
+    if (containerMin == null || containerMax == null) {
+      return false;
+    }
+
+    final BigInteger contentMin = integerBound(content, false);
+    final BigInteger contentMax = integerBound(content, true);
+    if (contentMin == null || contentMax == null) {
+      return false;
+    }
+
+    return containerMin.compareTo(contentMin) <= 0
+        && containerMax.compareTo(contentMax) >= 0;
+  }
+
+  /**
+   * Returns the numeric bound for an integer or zero-scale decimal type.
+   *
+   * @param type Type whose bounds should be computed
+   * @param upper If {@code true}, returns the maximum inclusive bound;
+   *              otherwise returns the minimum bound
+   * @return Bound as {@link BigInteger}, or {@code null} if the bound cannot 
be
+   *         determined (for example, type is not integer or has non-zero 
scale)
+   */
+  public static @Nullable BigInteger integerBound(RelDataType type, boolean 
upper) {
+    final SqlTypeName typeName = type.getSqlTypeName();
+
+    final boolean isDecimal = typeName == SqlTypeName.DECIMAL;
+    if (!isDecimal && !isIntType(type)) {
+      return null;
+    }
+    if (isDecimal && type.getScale() != 0) {
+      return null;
+    }
+
+    final int precision = isDecimal ? type.getPrecision() : -1;
+    final int scale = isDecimal ? type.getScale() : -1;
+    final Object limit =
+        typeName.getLimit(upper, SqlTypeName.Limit.OVERFLOW,
+        false,
+        precision,
+        scale);
+    if (limit == null) {
+      return null;
+    }
+    if (limit instanceof BigDecimal) {
+      try {
+        return ((BigDecimal) limit).toBigIntegerExact();
+      } catch (ArithmeticException ignored) {
+        return null;
+      }
+    }
+    if (limit instanceof Number) {
+      return BigInteger.valueOf(((Number) limit).longValue());
+    }
+    return null;
+  }
+
   /** Returns whether a type's scale is set. */
   public static boolean hasScale(RelDataType type) {
     return type.getScale() != Integer.MIN_VALUE;
@@ -717,8 +797,9 @@ public static int getMaxByteSize(RelDataType type) {
 
   /** Returns the minimum unscaled value of a numeric type.
    *
-   * @param type a numeric type
+   * @deprecated Use {@link #integerBound(RelDataType, boolean)} with {@code 
upper = false}
    */
+  @Deprecated // to be removed before 2.0
   public static long getMinValue(RelDataType type) {
     SqlTypeName typeName = type.getSqlTypeName();
     switch (typeName) {
@@ -744,8 +825,9 @@ public static long getMinValue(RelDataType type) {
   /** Returns the maximum unscaled value of a numeric type.
    * DOES NOT WORK CORRECTLY FOR U/BIGINT and many DECIMAL types.
    *
-   * @param type a numeric type
+   * @deprecated Use {@link #integerBound(RelDataType, boolean)} with {@code 
upper = true}
    */
+  @Deprecated // to be removed before 2.0
   public static long getMaxValue(RelDataType type) {
     SqlTypeName typeName = type.getSqlTypeName();
     switch (typeName) {
diff --git a/core/src/test/java/org/apache/calcite/rex/RexLosslessCastTest.java 
b/core/src/test/java/org/apache/calcite/rex/RexLosslessCastTest.java
index 334b3d1ed4..86c13c7427 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexLosslessCastTest.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexLosslessCastTest.java
@@ -16,7 +16,10 @@
  */
 package org.apache.calcite.rex;
 
+import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
 import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeSystem;
+import org.apache.calcite.rel.type.RelDataTypeSystemImpl;
 import org.apache.calcite.sql.type.SqlTypeName;
 
 import org.junit.jupiter.api.Test;
@@ -34,13 +37,15 @@ class RexLosslessCastTest extends RexProgramTestBase {
     final RelDataType smallIntType = 
typeFactory.createSqlType(SqlTypeName.SMALLINT);
     final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER);
     final RelDataType bigIntType = 
typeFactory.createSqlType(SqlTypeName.BIGINT);
-    final RelDataType floatType = typeFactory.createSqlType(SqlTypeName.FLOAT);
+    final RelDataType doubleType = 
typeFactory.createSqlType(SqlTypeName.DOUBLE);
     final RelDataType booleanType = 
typeFactory.createSqlType(SqlTypeName.BOOLEAN);
+
     final RelDataType charType5 = typeFactory.createSqlType(SqlTypeName.CHAR, 
5);
     final RelDataType charType6 = typeFactory.createSqlType(SqlTypeName.CHAR, 
6);
+
+    final RelDataType varcharType = 
typeFactory.createSqlType(SqlTypeName.VARCHAR);
     final RelDataType varCharType10 = 
typeFactory.createSqlType(SqlTypeName.VARCHAR, 10);
     final RelDataType varCharType11 = 
typeFactory.createSqlType(SqlTypeName.VARCHAR, 11);
-    final RelDataType varcharType = 
typeFactory.createSqlType(SqlTypeName.VARCHAR);
 
     // Negative
     assertThat(RexUtil.isLosslessCast(rexBuilder.makeInputRef(intType, 0)), 
is(false));
@@ -59,7 +64,7 @@ class RexLosslessCastTest extends RexProgramTestBase {
     assertThat(
         RexUtil.isLosslessCast(
             rexBuilder.makeCast(
-                bigIntType, rexBuilder.makeInputRef(floatType, 0))), 
is(false));
+                bigIntType, rexBuilder.makeInputRef(doubleType, 0))), 
is(false));
     assertThat(
         RexUtil.isLosslessCast(
             rexBuilder.makeCast(
@@ -132,22 +137,300 @@ class RexLosslessCastTest extends RexProgramTestBase {
                 varcharType, rexBuilder.makeInputRef(intType, 0))), is(true));
   }
 
+  @Test void testLosslessCastIntegerToApproximate() {
+    final RelDataType tinyIntType = 
typeFactory.createSqlType(SqlTypeName.TINYINT);
+    final RelDataType smallIntType = 
typeFactory.createSqlType(SqlTypeName.SMALLINT);
+    final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER);
+    final RelDataType bigIntType = 
typeFactory.createSqlType(SqlTypeName.BIGINT);
+    final RelDataType doubleType = 
typeFactory.createSqlType(SqlTypeName.DOUBLE);
+    final RelDataType realType = typeFactory.createSqlType(SqlTypeName.REAL);
+
+    // Positive: tiny/small/int -> double
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(doubleType,
+        rexBuilder.makeInputRef(tinyIntType, 0))), is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(doubleType,
+        rexBuilder.makeInputRef(smallIntType, 0))), is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(doubleType,
+        rexBuilder.makeInputRef(intType, 0))), is(true));
+
+    // Negative: bigint -> double can be lossy
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(doubleType,
+        rexBuilder.makeInputRef(bigIntType, 0))), is(false));
+
+    // Positive: tiny/small -> real
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(realType,
+        rexBuilder.makeInputRef(tinyIntType, 0))), is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(realType,
+        rexBuilder.makeInputRef(smallIntType, 0))), is(true));
+
+    // Negative: int/bigint -> real can be lossy
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(realType,
+        rexBuilder.makeInputRef(intType, 0))), is(false));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(realType,
+        rexBuilder.makeInputRef(bigIntType, 0))), is(false));
+  }
+
+  @Test void testLosslessCastIntegerToDecimal() {
+    final RelDataType tinyIntType = 
typeFactory.createSqlType(SqlTypeName.TINYINT);
+    final RelDataType smallIntType = 
typeFactory.createSqlType(SqlTypeName.SMALLINT);
+    final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER);
+    final RelDataType bigIntType = 
typeFactory.createSqlType(SqlTypeName.BIGINT);
+
+    final RelDataType dec_3_0  = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 3, 0);
+    final RelDataType dec_5_0  = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 5, 0);
+    final RelDataType dec_9_0  = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 9, 0);
+    final RelDataType dec_10_0 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 10, 0);
+    final RelDataType dec_18_0 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 18, 0);
+    final RelDataType dec_19_0 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 19, 0);
+    final RelDataType dec_4_1  = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 4, 1);
+    final RelDataType dec_5_1  = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 5, 1);
+    final RelDataType dec_12_2 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 12, 2);
+
+    // Positive: integer digits "precision - scale" is large enough
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_3_0,
+        rexBuilder.makeInputRef(tinyIntType, 0))), is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_5_0,
+        rexBuilder.makeInputRef(smallIntType, 0))), is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_10_0,
+        rexBuilder.makeInputRef(intType, 0))), is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_19_0,
+        rexBuilder.makeInputRef(bigIntType, 0))), is(true));
+
+    // Negative: not enough integer digits
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_9_0,
+        rexBuilder.makeInputRef(intType, 0))), is(false));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_18_0,
+        rexBuilder.makeInputRef(bigIntType, 0))), is(false));
+
+    // Non-zero scale is still lossless if "precision - scale" covers integer 
digits
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_12_2,
+        rexBuilder.makeInputRef(intType, 0))), is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_4_1,
+        rexBuilder.makeInputRef(smallIntType, 0))), is(false));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_5_1,
+        rexBuilder.makeInputRef(tinyIntType, 0))), is(true));
+  }
+
+  @Test void testLosslessCastUnsignedAndSignedIntegers() {
+    final RelDataType uTinyIntType = 
typeFactory.createSqlType(SqlTypeName.UTINYINT);
+    final RelDataType uIntType = 
typeFactory.createSqlType(SqlTypeName.UINTEGER);
+    final RelDataType uBigIntType = 
typeFactory.createSqlType(SqlTypeName.UBIGINT);
+
+    final RelDataType tinyIntType = 
typeFactory.createSqlType(SqlTypeName.TINYINT);
+    final RelDataType smallIntType = 
typeFactory.createSqlType(SqlTypeName.SMALLINT);
+    final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER);
+    final RelDataType bigIntType = 
typeFactory.createSqlType(SqlTypeName.BIGINT);
+
+    // unsigned -> signed of same width should be considered lossy
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(tinyIntType, 
rexBuilder.makeInputRef(uTinyIntType, 0))),
+        is(false));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(intType, rexBuilder.makeInputRef(uIntType, 
0))),
+        is(false));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(bigIntType, 
rexBuilder.makeInputRef(uBigIntType, 0))),
+        is(false));
+
+    // unsigned -> wider signed is safe
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(smallIntType, 
rexBuilder.makeInputRef(uTinyIntType, 0))),
+        is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(bigIntType, rexBuilder.makeInputRef(uIntType, 
0))),
+        is(true));
+
+    // signed -> unsigned can be lossy
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(uIntType, rexBuilder.makeInputRef(intType, 
0))),
+        is(false));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(uBigIntType, 
rexBuilder.makeInputRef(bigIntType, 0))),
+        is(false));
+
+    final RelDataType dec_9_0 = typeFactory.createSqlType(SqlTypeName.DECIMAL, 
9, 0);
+    final RelDataType dec_10_0 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 10, 0);
+    final RelDataType dec_10_1 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 10, 1);
+    final RelDataType dec19_0 = typeFactory.createSqlType(SqlTypeName.DECIMAL, 
19, 0);
+
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_10_0, rexBuilder.makeInputRef(uIntType, 
0))),
+        is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(dec_9_0, rexBuilder.makeInputRef(uIntType, 
0))),
+        is(false));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(uBigIntType, rexBuilder.makeInputRef(dec19_0, 
0))),
+        is(false));
+
+    // DECIMAL -> integer: requires scale 0 and fitting range
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(intType, rexBuilder.makeInputRef(dec_9_0, 0))),
+        is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(intType, rexBuilder.makeInputRef(dec_10_0, 
0))),
+        is(false));
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(intType, rexBuilder.makeInputRef(dec_10_1, 
0))),
+        is(false));
+  }
+
+  // DECIMAL(s = 0) -> APPROX: lossless iff precision <= target digits
+  @Test void testLosslessCastDecimalToApproximate() {
+    final RelDataType realType = typeFactory.createSqlType(SqlTypeName.REAL);
+    final RelDataType doubleType = 
typeFactory.createSqlType(SqlTypeName.DOUBLE);
+
+    final RelDataType dec_7_0 = typeFactory.createSqlType(SqlTypeName.DECIMAL, 
7, 0);
+    final RelDataType dec_8_0 = typeFactory.createSqlType(SqlTypeName.DECIMAL, 
8, 0);
+    final RelDataType dec_15_0 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 15, 0);
+    final RelDataType dec_16_0 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 16, 0);
+
+    // real: 7 digits in default type system
+    assertThat(
+        RexUtil.isLosslessCast(
+        rexBuilder.makeCast(realType, rexBuilder.makeInputRef(dec_7_0, 0))), 
is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+        rexBuilder.makeCast(realType, rexBuilder.makeInputRef(dec_8_0, 0))), 
is(false));
+
+    // double: 15 digits in default type system
+    assertThat(
+        RexUtil.isLosslessCast(
+        rexBuilder.makeCast(doubleType, rexBuilder.makeInputRef(dec_15_0, 
0))), is(true));
+    assertThat(
+        RexUtil.isLosslessCast(
+        rexBuilder.makeCast(doubleType, rexBuilder.makeInputRef(dec_16_0, 
0))), is(false));
+
+    // DECIMAL with scale > 0 -> APPROX is potentially lossy
+    final RelDataType dec_10_2 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 10, 2);
+    assertThat(
+        RexUtil.isLosslessCast(
+        rexBuilder.makeCast(doubleType, rexBuilder.makeInputRef(dec_10_2, 
0))), is(false));
+  }
+
+  @Test void testLosslessCastApproximateToApproximate() {
+    final RelDataType realType = typeFactory.createSqlType(SqlTypeName.REAL);
+    final RelDataType doubleType = 
typeFactory.createSqlType(SqlTypeName.DOUBLE);
+
+    // real -> double: target has >= digits
+    assertThat(
+        RexUtil.isLosslessCast(
+        rexBuilder.makeCast(doubleType, rexBuilder.makeInputRef(realType, 
0))), is(true));
+    // double -> real: target has fewer digits
+    assertThat(
+        RexUtil.isLosslessCast(
+        rexBuilder.makeCast(realType, rexBuilder.makeInputRef(doubleType, 
0))), is(false));
+  }
+
+  @Test void testLosslessCastApproximateToExact() {
+    final RelDataType realType = typeFactory.createSqlType(SqlTypeName.REAL);
+    final RelDataType doubleType = 
typeFactory.createSqlType(SqlTypeName.DOUBLE);
+    final RelDataType intType = typeFactory.createSqlType(SqlTypeName.INTEGER);
+    final RelDataType dec_19_0 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 19, 0);
+
+    // approx -> exact can be lossy
+    assertThat(
+        RexUtil.isLosslessCast(
+        rexBuilder.makeCast(intType, rexBuilder.makeInputRef(realType, 0))), 
is(false));
+    assertThat(
+        RexUtil.isLosslessCast(
+        rexBuilder.makeCast(dec_19_0, rexBuilder.makeInputRef(doubleType, 
0))), is(false));
+  }
+
+  @Test void testLosslessCastWithCustomTypeSystem() {
+    final RelDataType dec_10_0 = 
typeFactory.createSqlType(SqlTypeName.DECIMAL, 10, 0);
+    final RelDataType decDefault = 
typeFactory.createSqlType(SqlTypeName.DECIMAL);
+    assertThat(
+        RexUtil.isLosslessCast(
+            rexBuilder.makeCast(
+        dec_10_0, rexBuilder.makeInputRef(decDefault, 0))), is(false));
+
+    RelDataTypeSystem customTypeSystem = new RelDataTypeSystemImpl() {
+      @Override public int getDefaultPrecision(SqlTypeName typeName) {
+        if (typeName == SqlTypeName.DECIMAL) {
+          return 9;
+        }
+        return super.getDefaultPrecision(typeName);
+      }
+    };
+    final JavaTypeFactoryImpl customFactory = new 
JavaTypeFactoryImpl(customTypeSystem);
+    final RexBuilder customBuilder = new RexBuilder(customFactory);
+
+    final RelDataType customDec_10_0 = 
customFactory.createSqlType(SqlTypeName.DECIMAL, 10, 0);
+    final RelDataType customDecDefault = 
customFactory.createSqlType(SqlTypeName.DECIMAL);
+
+    // custom target has 9 integer digits, so decimal with 10 won't fit
+    assertThat(
+        RexUtil.isLosslessCast(
+            customBuilder.makeCast(
+        customDecDefault, customBuilder.makeInputRef(customDec_10_0, 0))), 
is(false));
+  }
+
   @Test void removeRedundantCast() {
     checkSimplify(cast(vInt(), nullable(tInt())), "?0.int0");
     checkSimplifyUnchanged(cast(vInt(), tInt()));
     checkSimplify(cast(vIntNotNull(), nullable(tInt())), "?0.notNullInt0");
     checkSimplify(cast(vIntNotNull(), tInt()), "?0.notNullInt0");
 
-    // Nested int int cast is removed
+    // Nested int cast is removed
     checkSimplify(cast(cast(vVarchar(), tInt()), tInt()),
         "CAST(?0.varchar0):INTEGER NOT NULL");
     checkSimplifyUnchanged(cast(cast(vVarchar(), tInt()), tVarchar()));
   }
 
-  @Test void removeLosslesssCastInt() {
+  @Test void removeLosslessCastInt() {
     checkSimplifyUnchanged(cast(vInt(), tBigInt()));
-    // A.1
+    checkSimplifyUnchanged(cast(vInt(), tDouble()));
+    // A.1 INT -> BIGINT
     checkSimplify(cast(cast(vInt(), tBigInt()), tInt()), 
"CAST(?0.int0):INTEGER NOT NULL");
+    checkSimplify(cast(cast(vInt(), tDouble()), tInt()), 
"CAST(?0.int0):INTEGER NOT NULL");
     RexNode core = cast(vIntNotNull(), tBigInt());
     checkSimplify(cast(core, tInt()), "?0.notNullInt0");
     checkSimplify(
@@ -156,10 +439,38 @@ class RexLosslessCastTest extends RexProgramTestBase {
     checkSimplify(
         cast(cast(cast(core, tInt()), tBigInt()), tInt()),
         "?0.notNullInt0");
+    // A.1 INT -> DOUBLE
+    core = cast(vIntNotNull(), tDouble());
+    checkSimplify(cast(core, tInt()), "?0.notNullInt0");
+    checkSimplify(
+        cast(cast(core, tInt()), tDouble()),
+        "CAST(?0.notNullInt0):DOUBLE NOT NULL");
+    checkSimplify(
+        cast(cast(cast(core, tInt()), tDouble()), tInt()),
+        "?0.notNullInt0");
+    // A.1 INT -> DECIMAL
+    core = cast(vIntNotNull(), tDecimal());
+    checkSimplify(cast(core, tInt()), "?0.notNullInt0");
+    checkSimplify(
+        cast(cast(core, tInt()), tDecimal()),
+        "CAST(?0.notNullInt0):DECIMAL(19, 0) NOT NULL");
+    checkSimplify(
+        cast(cast(cast(core, tInt()), tDecimal()), tInt()),
+        "?0.notNullInt0");
+    // A.1 SMALLINT -> REAL
+    core = cast(vSmallIntNotNull(), tReal());
+    checkSimplify(cast(core, tSmallInt()), "?0.notNullSmallint0");
+    checkSimplify(
+        cast(cast(core, tSmallInt()), tReal()),
+        "CAST(?0.notNullSmallint0):REAL NOT NULL");
+    checkSimplify(
+        cast(cast(cast(core, tSmallInt()), tReal()), tSmallInt()),
+        "?0.notNullSmallint0");
+    // A.1 INT -> VARCHAR
     checkSimplify(cast(cast(vInt(), tVarchar()), tInt()), 
"CAST(?0.int0):INTEGER NOT NULL");
   }
 
-  @Test void removeLosslesssCastChar() {
+  @Test void removeLosslessCastChar() {
     checkSimplifyUnchanged(cast(vVarchar(), tChar(3)));
     checkSimplifyUnchanged(cast(cast(vVarchar(), tChar(3)), tVarchar(5)));
 
diff --git 
a/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java 
b/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java
index 6051e75801..5c451ca8e5 100644
--- a/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java
+++ b/core/src/test/java/org/apache/calcite/rex/RexProgramBuilderBase.java
@@ -62,6 +62,8 @@ public abstract class RexProgramBuilderBase {
   protected RexLiteral nullSmallInt;
   protected RexLiteral nullVarchar;
   protected RexLiteral nullDecimal;
+  protected RexLiteral nullReal;
+  protected RexLiteral nullDouble;
   protected RexLiteral nullVarbinary;
 
   private RelDataType nullableBool;
@@ -79,6 +81,12 @@ public abstract class RexProgramBuilderBase {
   private RelDataType nullableDecimal;
   private RelDataType nonNullableDecimal;
 
+  private RelDataType nullableReal;
+  private RelDataType nonNullableReal;
+
+  private RelDataType nullableDouble;
+  private RelDataType nonNullableDouble;
+
   private RelDataType nullableVarbinary;
   private RelDataType nonNullableVarbinary;
 
@@ -123,6 +131,14 @@ public abstract class RexProgramBuilderBase {
     nullableDecimal = 
typeFactory.createTypeWithNullability(nonNullableDecimal, true);
     nullDecimal = rexBuilder.makeNullLiteral(nullableDecimal);
 
+    nonNullableReal = typeFactory.createSqlType(SqlTypeName.REAL);
+    nullableReal = typeFactory.createTypeWithNullability(nonNullableReal, 
true);
+    nullReal = rexBuilder.makeNullLiteral(nullableReal);
+
+    nonNullableDouble = typeFactory.createSqlType(SqlTypeName.DOUBLE);
+    nullableDouble = typeFactory.createTypeWithNullability(nonNullableDouble, 
true);
+    nullDouble = rexBuilder.makeNullLiteral(nullableDouble);
+
     nonNullableVarbinary = typeFactory.createSqlType(SqlTypeName.VARBINARY);
     nullableVarbinary = 
typeFactory.createTypeWithNullability(nonNullableVarbinary, true);
     nullVarbinary = rexBuilder.makeNullLiteral(nullableVarbinary);
@@ -425,13 +441,29 @@ protected RelDataType tSmallInt(boolean nullable) {
   }
 
   protected RelDataType tDecimal() {
-    return nonNullableDecimal;
+    return tDecimal(false);
   }
 
   protected RelDataType tDecimal(boolean nullable) {
     return nullable ? nullableDecimal : nonNullableDecimal;
   }
 
+  protected RelDataType tReal() {
+    return tReal(false);
+  }
+
+  protected RelDataType tReal(boolean nullable) {
+    return nullable ? nullableReal : nonNullableReal;
+  }
+
+  protected RelDataType tDouble() {
+    return tDouble(false);
+  }
+
+  protected RelDataType tDouble(boolean nullable) {
+    return nullable ? nullableDouble : nonNullableDouble;
+  }
+
   protected RelDataType tBigInt() {
     return tBigInt(false);
   }
diff --git 
a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeUtilTest.java 
b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeUtilTest.java
index 11eb8e0e55..1f08bd232e 100644
--- a/core/src/test/java/org/apache/calcite/sql/type/SqlTypeUtilTest.java
+++ b/core/src/test/java/org/apache/calcite/sql/type/SqlTypeUtilTest.java
@@ -29,6 +29,7 @@
 import org.hamcrest.Matcher;
 import org.junit.jupiter.api.Test;
 
+import java.math.BigInteger;
 import java.util.List;
 import java.util.Locale;
 import java.util.stream.Collectors;
@@ -37,10 +38,13 @@
 import static org.apache.calcite.sql.type.SqlTypeUtil.convertTypeToSpec;
 import static 
org.apache.calcite.sql.type.SqlTypeUtil.equalAsCollectionSansNullability;
 import static 
org.apache.calcite.sql.type.SqlTypeUtil.equalAsMapSansNullability;
+import static org.apache.calcite.sql.type.SqlTypeUtil.integerBound;
+import static org.apache.calcite.sql.type.SqlTypeUtil.integerRangeContains;
 import static org.apache.calcite.test.Matchers.isListOf;
 
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
  * Test of {@link org.apache.calcite.sql.type.SqlTypeUtil}.
@@ -110,6 +114,14 @@ class SqlTypeUtilTest {
         is(false));
   }
 
+  private RelDataType struct(RelDataType...relDataTypes) {
+    final RelDataTypeFactory.Builder builder = f.typeFactory.builder();
+    for (int i = 0; i < relDataTypes.length; i++) {
+      builder.add("field" + i, relDataTypes[i]);
+    }
+    return builder.build();
+  }
+
   @Test void testModifyTypeCoercionMappings() {
     SqlTypeMappingRules.Builder builder = SqlTypeMappingRules.builder();
     final SqlTypeCoercionRule defaultRules = SqlTypeCoercionRule.instance();
@@ -208,18 +220,47 @@ private void checkConvert(SqlTypeCoercionRule rules,
         sqlTypeName, is(SqlTypeName.VARCHAR));
   }
 
-  @Test void testGetMaxPrecisionScaleDecimal() {
-    RelDataType decimal = 
SqlTypeUtil.getMaxPrecisionScaleDecimal(f.typeFactory);
-    assertThat(decimal, is(f.typeFactory.createSqlType(SqlTypeName.DECIMAL, 
19, 9)));
+  @Test void testIntegerRangeContainsForSignedAndUnsigned() {
+    RelDataType uTinyInt = f.typeFactory.createSqlType(SqlTypeName.UTINYINT);
+    RelDataType tinyInt = f.typeFactory.createSqlType(SqlTypeName.TINYINT);
+    RelDataType smallInt = f.typeFactory.createSqlType(SqlTypeName.SMALLINT);
+    RelDataType intType = f.typeFactory.createSqlType(SqlTypeName.INTEGER);
+
+    assertThat(integerRangeContains(intType, smallInt), is(true));
+    assertThat(integerRangeContains(intType, tinyInt), is(true));
+    assertThat(integerRangeContains(intType, uTinyInt), is(true));
+    assertThat(integerRangeContains(tinyInt, uTinyInt), is(false));
+    assertThrows(IllegalArgumentException.class,
+        () -> integerRangeContains(f.sqlVarchar, intType));
   }
 
+  @Test void testIntegerRangeContainsWithDecimal() {
+    RelDataType intType = f.typeFactory.createSqlType(SqlTypeName.INTEGER);
+    RelDataType dec9 = f.typeFactory.createSqlType(SqlTypeName.DECIMAL, 9, 0);
+    RelDataType dec10 = f.typeFactory.createSqlType(SqlTypeName.DECIMAL, 10, 
0);
+    RelDataType dec10Scale1 = f.typeFactory.createSqlType(SqlTypeName.DECIMAL, 
10, 1);
 
-  private RelDataType struct(RelDataType...relDataTypes) {
-    final RelDataTypeFactory.Builder builder = f.typeFactory.builder();
-    for (int i = 0; i < relDataTypes.length; i++) {
-      builder.add("field" + i, relDataTypes[i]);
-    }
-    return builder.build();
+    assertThat(integerRangeContains(intType, dec9), is(true));
+    assertThat(integerRangeContains(intType, dec10), is(false));
+    assertThat(integerRangeContains(intType, dec10Scale1), is(false));
+  }
+
+  @Test void testIntegerBound() {
+    RelDataType intType = f.typeFactory.createSqlType(SqlTypeName.INTEGER);
+    RelDataType uIntType = f.typeFactory.createSqlType(SqlTypeName.UINTEGER);
+    RelDataType dec10 = f.typeFactory.createSqlType(SqlTypeName.DECIMAL, 10, 
0);
+    RelDataType dec10Scale1 = f.typeFactory.createSqlType(SqlTypeName.DECIMAL, 
10, 1);
+
+    assertThat(integerBound(intType, false), 
is(BigInteger.valueOf(Integer.MIN_VALUE)));
+    assertThat(integerBound(intType, true), 
is(BigInteger.valueOf(Integer.MAX_VALUE)));
+    assertThat(integerBound(uIntType, true), is(BigInteger.valueOf((1L << 32) 
- 1)));
+    assertThat(integerBound(dec10, true), 
is(BigInteger.valueOf(9_999_999_999L)));
+    assertThat(integerBound(dec10Scale1, true), is((BigInteger) null));
+  }
+
+  @Test void testGetMaxPrecisionScaleDecimal() {
+    RelDataType decimal = 
SqlTypeUtil.getMaxPrecisionScaleDecimal(f.typeFactory);
+    assertThat(decimal, is(f.typeFactory.createSqlType(SqlTypeName.DECIMAL, 
19, 9)));
   }
 
   private void compareTypesIgnoringNullability(
diff --git a/core/src/test/resources/sql/planner.iq 
b/core/src/test/resources/sql/planner.iq
index 0e9fd43baf..e9e48f6c7a 100644
--- a/core/src/test/resources/sql/planner.iq
+++ b/core/src/test/resources/sql/planner.iq
@@ -166,15 +166,16 @@ select a from (values (1.0), (4.0), (null)) as t3 (a);
 
 !ok
 
-EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], A=[$t1])
-  EnumerableNestedLoopJoin(condition=[OR(AND(IS NULL(CAST($0):DECIMAL(11, 1)), 
IS NULL(CAST($1):DECIMAL(11, 1))), =(CAST($0):DECIMAL(11, 1), 
CAST($1):DECIMAL(11, 1)))], joinType=[semi])
-    EnumerableAggregate(group=[{0}])
-      EnumerableHashJoin(condition=[=($1, $3)], joinType=[semi])
-        EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT 
NULL], expr#2=[CAST($t1):DECIMAL(11, 1) NOT NULL], A=[$t1], A0=[$t2])
-          EnumerableValues(tuples=[[{ 1.0 }, { 2.0 }, { 3.0 }, { 4.0 }, { 5.0 
}]])
-        EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT 
NULL], expr#2=[CAST($t1):DECIMAL(11, 1) NOT NULL], A=[$t1], A0=[$t2])
-          EnumerableValues(tuples=[[{ 1 }, { 2 }]])
-    EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
A=[$t1])
+EnumerableCalc(expr#0..1=[{inputs}], expr#2=[CAST($t0):DECIMAL(11, 1)], 
A=[$t2])
+  EnumerableHashJoin(condition=[=($1, $3)], joinType=[semi])
+    EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
proj#0..1=[{exprs}])
+      EnumerableAggregate(group=[{0}])
+        EnumerableHashJoin(condition=[=($1, $3)], joinType=[semi])
+          EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) 
NOT NULL], A=[$t1], A0=[$t1])
+            EnumerableValues(tuples=[[{ 1.0 }, { 2.0 }, { 3.0 }, { 4.0 }, { 
5.0 }]])
+          EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) 
NOT NULL], A=[$t1], A0=[$t1])
+            EnumerableValues(tuples=[[{ 1 }, { 2 }]])
+    EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
A=[$t1], A0=[$t1])
       EnumerableValues(tuples=[[{ 1.0 }, { 4.0 }, { null }]])
 !plan
 !set planner-rules original
@@ -223,15 +224,16 @@ select a from (values (1.0), (4.0), (null)) as t3 (a);
 
 !ok
 
-EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], A=[$t1])
-  EnumerableNestedLoopJoin(condition=[OR(AND(IS NULL(CAST($0):DECIMAL(11, 1)), 
IS NULL(CAST($1):DECIMAL(11, 1))), =(CAST($0):DECIMAL(11, 1), 
CAST($1):DECIMAL(11, 1)))], joinType=[anti])
-    EnumerableAggregate(group=[{0}])
-      EnumerableNestedLoopJoin(condition=[=(CAST($0):DECIMAL(11, 1) NOT NULL, 
CAST($1):DECIMAL(11, 1) NOT NULL)], joinType=[anti])
-        EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT 
NULL], A=[$t1])
-          EnumerableValues(tuples=[[{ 1.0 }, { 2.0 }, { 3.0 }, { 4.0 }, { 5.0 
}]])
-        EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) NOT 
NULL], A=[$t1])
-          EnumerableValues(tuples=[[{ 1 }, { 2 }]])
-    EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
A=[$t1])
+EnumerableCalc(expr#0..1=[{inputs}], expr#2=[CAST($t0):DECIMAL(11, 1)], 
A=[$t2])
+  EnumerableHashJoin(condition=[=($1, $3)], joinType=[anti])
+    EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
proj#0..1=[{exprs}])
+      EnumerableAggregate(group=[{0}])
+        EnumerableNestedLoopJoin(condition=[=(CAST($0):DECIMAL(11, 1) NOT 
NULL, CAST($1):DECIMAL(11, 1) NOT NULL)], joinType=[anti])
+          EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) 
NOT NULL], A=[$t1])
+            EnumerableValues(tuples=[[{ 1.0 }, { 2.0 }, { 3.0 }, { 4.0 }, { 
5.0 }]])
+          EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1) 
NOT NULL], A=[$t1])
+            EnumerableValues(tuples=[[{ 1 }, { 2 }]])
+    EnumerableCalc(expr#0=[{inputs}], expr#1=[CAST($t0):DECIMAL(11, 1)], 
A=[$t1], A0=[$t1])
       EnumerableValues(tuples=[[{ 1.0 }, { 4.0 }, { null }]])
 !plan
 !set planner-rules original
diff --git a/core/src/test/resources/sql/sub-query.iq 
b/core/src/test/resources/sql/sub-query.iq
index 330e3d990b..f673e4b322 100644
--- a/core/src/test/resources/sql/sub-query.iq
+++ b/core/src/test/resources/sql/sub-query.iq
@@ -3678,7 +3678,7 @@ select * from "scott".emp where comm in (300, 500, null);
 
 !ok
 
-EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[300.00:DECIMAL(12, 2), 500.00:DECIMAL(12, 2)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:DECIMAL(12, 2)], expr#12=[=($t8, 
$t11)], expr#13=[OR($t10, $t12)], proj#0..7=[{exprs}], $condition=[$t13])
+EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[300.00:DECIMAL(12, 2), 500.00:DECIMAL(12, 2)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], proj#0..7=[{exprs}], $condition=[$t10])
   EnumerableTableScan(table=[[scott, EMP]])
 !plan
 
@@ -3706,7 +3706,7 @@ select *, comm in (300, 500, null) as i from "scott".emp;
 
 !ok
 
-EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[300.00:DECIMAL(12, 2), 500.00:DECIMAL(12, 2)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:DECIMAL(12, 2)], expr#12=[=($t8, 
$t11)], expr#13=[OR($t10, $t12)], proj#0..7=[{exprs}], I=[$t13])
+EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[300.00:DECIMAL(12, 2), 500.00:DECIMAL(12, 2)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:BOOLEAN], expr#12=[OR($t10, $t11)], 
proj#0..7=[{exprs}], I=[$t12])
   EnumerableTableScan(table=[[scott, EMP]])
 !plan
 
@@ -3747,7 +3747,7 @@ select *, comm not in (300, 500, null) as i from 
"scott".emp;
 
 !ok
 
-EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[(-∞..300.00:DECIMAL(12, 2)), (300.00:DECIMAL(12, 
2)..500.00:DECIMAL(12, 2)), (500.00:DECIMAL(12, 2)..+∞)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:DECIMAL(12, 2)], expr#12=[<>($t8, 
$t11)], expr#13=[AND($t10, $t12)], proj#0..7=[{exprs}], I=[$t13])
+EnumerableCalc(expr#0..7=[{inputs}], expr#8=[CAST($t6):DECIMAL(12, 2)], 
expr#9=[Sarg[(-∞..300.00:DECIMAL(12, 2)), (300.00:DECIMAL(12, 
2)..500.00:DECIMAL(12, 2)), (500.00:DECIMAL(12, 2)..+∞)]:DECIMAL(12, 2)], 
expr#10=[SEARCH($t8, $t9)], expr#11=[null:BOOLEAN], expr#12=[AND($t10, $t11)], 
proj#0..7=[{exprs}], I=[$t12])
   EnumerableTableScan(table=[[scott, EMP]])
 !plan
 
@@ -4627,7 +4627,7 @@ select comm, comm in (500, 300, 0) from emp;
 
 !ok
 
-EnumerableCalc(expr#0..6=[{inputs}], expr#7=[0], expr#8=[=($t2, $t7)], 
expr#9=[false], expr#10=[CAST($t1):DECIMAL(12, 2)], expr#11=[IS NULL($t10)], 
expr#12=[null:BOOLEAN], expr#13=[IS NOT NULL($t6)], expr#14=[true], 
expr#15=[<($t3, $t2)], expr#16=[CASE($t8, $t9, $t11, $t12, $t13, $t14, $t15, 
$t12, $t9)], COMM=[$t1], EXPR$1=[$t16])
+EnumerableCalc(expr#0..6=[{inputs}], expr#7=[IS NULL($t1)], 
expr#8=[null:BOOLEAN], expr#9=[0], expr#10=[<>($t2, $t9)], expr#11=[AND($t7, 
$t8, $t10)], expr#12=[IS NOT NULL($t6)], expr#13=[IS NOT NULL($t1)], 
expr#14=[AND($t12, $t10, $t13)], expr#15=[<($t3, $t2)], expr#16=[IS NULL($t6)], 
expr#17=[AND($t15, $t8, $t10, $t13, $t16)], expr#18=[OR($t11, $t14, $t17)], 
COMM=[$t1], EXPR$1=[$t18])
   EnumerableMergeJoin(condition=[=($4, $5)], joinType=[left])
     EnumerableSort(sort0=[$4], dir0=[ASC])
       EnumerableCalc(expr#0..3=[{inputs}], expr#4=[CAST($t1):DECIMAL(12, 2)], 
proj#0..4=[{exprs}])
@@ -4667,7 +4667,7 @@ select comm, comm in (500, 300, 0, null) from emp;
 
 !ok
 
-EnumerableCalc(expr#0..6=[{inputs}], expr#7=[0], expr#8=[=($t2, $t7)], 
expr#9=[false], expr#10=[CAST($t1):DECIMAL(12, 2)], expr#11=[IS NULL($t10)], 
expr#12=[null:BOOLEAN], expr#13=[IS NOT NULL($t6)], expr#14=[true], 
expr#15=[<($t3, $t2)], expr#16=[CASE($t8, $t9, $t11, $t12, $t13, $t14, $t15, 
$t12, $t9)], COMM=[$t1], EXPR$1=[$t16])
+EnumerableCalc(expr#0..6=[{inputs}], expr#7=[IS NULL($t1)], 
expr#8=[null:BOOLEAN], expr#9=[0], expr#10=[<>($t2, $t9)], expr#11=[AND($t7, 
$t8, $t10)], expr#12=[IS NOT NULL($t6)], expr#13=[IS NOT NULL($t1)], 
expr#14=[AND($t12, $t10, $t13)], expr#15=[<($t3, $t2)], expr#16=[IS NULL($t6)], 
expr#17=[AND($t15, $t8, $t10, $t13, $t16)], expr#18=[OR($t11, $t14, $t17)], 
COMM=[$t1], EXPR$1=[$t18])
   EnumerableMergeJoin(condition=[=($4, $5)], joinType=[left])
     EnumerableSort(sort0=[$4], dir0=[ASC])
       EnumerableCalc(expr#0..3=[{inputs}], expr#4=[CAST($t1):DECIMAL(12, 2)], 
proj#0..4=[{exprs}])
@@ -4740,7 +4740,7 @@ select comm, (comm, comm) in ((500, 500), (300, 300), (0, 
0)) from emp;
 
 !ok
 
-EnumerableCalc(expr#0..8=[{inputs}], expr#9=[0], expr#10=[=($t2, $t9)], 
expr#11=[false], expr#12=[CAST($t1):DECIMAL(12, 2)], expr#13=[IS NULL($t12)], 
expr#14=[null:BOOLEAN], expr#15=[IS NOT NULL($t8)], expr#16=[true], 
expr#17=[<($t3, $t2)], expr#18=[CASE($t10, $t11, $t13, $t14, $t15, $t16, $t17, 
$t14, $t11)], COMM=[$t1], EXPR$1=[$t18])
+EnumerableCalc(expr#0..8=[{inputs}], expr#9=[IS NULL($t1)], 
expr#10=[null:BOOLEAN], expr#11=[0], expr#12=[<>($t2, $t11)], expr#13=[AND($t9, 
$t10, $t12)], expr#14=[IS NOT NULL($t8)], expr#15=[IS NOT NULL($t1)], 
expr#16=[AND($t14, $t12, $t15)], expr#17=[<($t3, $t2)], expr#18=[IS NULL($t8)], 
expr#19=[AND($t17, $t10, $t12, $t15, $t18)], expr#20=[OR($t13, $t16, $t19)], 
COMM=[$t1], EXPR$1=[$t20])
   EnumerableMergeJoin(condition=[AND(=($4, $6), =($5, $7))], joinType=[left])
     EnumerableSort(sort0=[$4], sort1=[$5], dir0=[ASC], dir1=[ASC])
       EnumerableCalc(expr#0..3=[{inputs}], expr#4=[CAST($t1):DECIMAL(12, 2)], 
proj#0..4=[{exprs}], COMM1=[$t4])
@@ -4779,7 +4779,7 @@ select comm, (comm, comm) in ((500, 500), (300, 300), (0, 
0), (null , null)) fro
 
 !ok
 
-EnumerableCalc(expr#0..8=[{inputs}], expr#9=[0], expr#10=[=($t2, $t9)], 
expr#11=[false], expr#12=[CAST($t1):DECIMAL(12, 2)], expr#13=[IS NULL($t12)], 
expr#14=[null:BOOLEAN], expr#15=[IS NOT NULL($t8)], expr#16=[true], 
expr#17=[<($t3, $t2)], expr#18=[CASE($t10, $t11, $t13, $t14, $t15, $t16, $t17, 
$t14, $t11)], COMM=[$t1], EXPR$1=[$t18])
+EnumerableCalc(expr#0..8=[{inputs}], expr#9=[IS NULL($t1)], 
expr#10=[null:BOOLEAN], expr#11=[0], expr#12=[<>($t2, $t11)], expr#13=[AND($t9, 
$t10, $t12)], expr#14=[IS NOT NULL($t8)], expr#15=[IS NOT NULL($t1)], 
expr#16=[AND($t14, $t12, $t15)], expr#17=[<($t3, $t2)], expr#18=[IS NULL($t8)], 
expr#19=[AND($t17, $t10, $t12, $t15, $t18)], expr#20=[OR($t13, $t16, $t19)], 
COMM=[$t1], EXPR$1=[$t20])
   EnumerableMergeJoin(condition=[AND(=($4, $6), =($5, $7))], joinType=[left])
     EnumerableSort(sort0=[$4], sort1=[$5], dir0=[ASC], dir1=[ASC])
       EnumerableCalc(expr#0..3=[{inputs}], expr#4=[CAST($t1):DECIMAL(12, 2)], 
proj#0..4=[{exprs}], COMM1=[$t4])


Reply via email to