vlsi commented on code in PR #5297:
URL: https://github.com/apache/calcite/pull/5297#discussion_r4243105166


##########
core/src/test/java/org/apache/calcite/test/JdbcTest.java:
##########
@@ -1836,6 +1840,105 @@ private void checkResultSetMetaData(Connection 
connection, String sql)
         .returns("C0=5.5; C1=5; C2=1.4; C3=1; C4=0; C5=0\n");
   }
 
+  /** Installs {@link TypeCoercionTest.WideningTypeCoercion} via
+   * {@link Hook#STRING_TO_QUERY} for use in boundary tests for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]</a>. */
+  private static Consumer<Pair<FrameworkConfig, Holder<CalcitePrepare.Query>>>
+      wideningHook(String sql) {

Review Comment:
   The hook plans the `sql` argument and ignores the query it receives in 
`pair.right`, so each test has to pass the same string twice. If the two 
strings drift apart, the test plans one query and the failure report shows the 
other. Reading the SQL from the holder (`requireNonNull(pair.right.get().sql)`) 
removes the parameter.



##########
core/src/test/java/org/apache/calcite/test/JdbcTest.java:
##########
@@ -1836,6 +1840,105 @@ private void checkResultSetMetaData(Connection 
connection, String sql)
         .returns("C0=5.5; C1=5; C2=1.4; C3=1; C4=0; C5=0\n");
   }
 
+  /** Installs {@link TypeCoercionTest.WideningTypeCoercion} via
+   * {@link Hook#STRING_TO_QUERY} for use in boundary tests for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]</a>. */
+  private static Consumer<Pair<FrameworkConfig, Holder<CalcitePrepare.Query>>>
+      wideningHook(String sql) {
+    return pair -> {
+      final FrameworkConfig config =
+          Frameworks.newConfigBuilder(pair.left)
+              .sqlValidatorConfig(pair.left.getSqlValidatorConfig()
+                  
.withTypeCoercionFactory(TypeCoercionTest.WideningTypeCoercion::new))
+              .build();
+      final Planner planner = Frameworks.getPlanner(config);
+      try {
+        final RelRoot root = planner.rel(planner.validate(planner.parse(sql)));
+        pair.right.set(CalcitePrepare.Query.of(root.project()));
+      } catch (Exception e) {
+        throw TestUtil.rethrow(e);
+      }
+    };
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]
+   * Document precision loss in DECIMAL/REAL comparisons and how to widen them 
to DOUBLE</a>.
+   *
+   * <p>Default behavior: DECIMAL(18, 3) compared with REAL narrows to REAL
+   * (24-bit mantissa, about 7 decimal digits). These two values collide in
+   * REAL, so the join incorrectly returns 1 row. */
+  @Test void testDecimalRealComparisonDefaultNarrowsToReal() {
+    CalciteAssert.that()
+        .query("SELECT *\n"
+            + "FROM (VALUES (CAST(59999943.000 AS DECIMAL(18, 3)))) AS d(k)\n"
+            + "JOIN (VALUES (CAST(59999945 AS REAL))) AS f(k) ON d.k = f.k")
+        .returnsCount(1);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]
+   * Document precision loss in DECIMAL/REAL comparisons and how to widen them 
to DOUBLE</a>.
+   *
+   * <p>With {@link TypeCoercionTest.WideningTypeCoercion}, DECIMAL(18, 3)
+   * compared with REAL widens to DOUBLE. {@code CAST(140737488355328 AS REAL)}
+   * is exactly 2^47 (a power of two, exactly representable as REAL). DOUBLE
+   * has 53-bit mantissa; the unit in the last place (ULP) at 2^47 is
+   * 2^(47-52) = 0.03125. The DECIMAL value 140737488355328.016 exceeds the
+   * half-ULP (0.015625), so it rounds to the next DOUBLE above 2^47. The
+   * widened join correctly returns 0 rows. See
+   * {@link #testDecimalRealComparisonWidenedStillCollides} for the pair that
+   * still collides after widening. */
+  @Test void testDecimalRealComparisonWidenedToDoubleHelps() {
+    final String sql = "SELECT *\n"
+        + "FROM (VALUES (CAST(140737488355328.016 AS DECIMAL(18, 3)))) AS 
d(k)\n"
+        + "JOIN (VALUES (CAST(140737488355328 AS REAL))) AS f(k) ON d.k = f.k";
+    CalciteAssert.that()
+        .withHook(Hook.STRING_TO_QUERY, wideningHook(sql))
+        .query(sql)
+        .returnsCount(0);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]
+   * Document precision loss in DECIMAL/REAL comparisons and how to widen them 
to DOUBLE</a>.
+   *
+   * <p>With {@link TypeCoercionTest.WideningTypeCoercion}, DECIMAL(18, 3)
+   * compared with REAL widens to DOUBLE, but DOUBLE has only 53 bits of
+   * mantissa (about 15 significant decimal digits). The DECIMAL value
+   * 140737488355328.001 is within half the DOUBLE ULP at 2^47 (half-ULP =
+   * 0.015625), so it rounds to 2^47 = {@code CAST(140737488355328 AS REAL)}.
+   * The widened join incorrectly returns 1 row, showing where widening stops
+   * helping. */
+  @Test void testDecimalRealComparisonWidenedStillCollides() {

Review Comment:
   `testDecimalRealComparisonWidenedStillCollides` returns 1 row with or 
without `WideningTypeCoercion`: the REAL operand is exactly 2^47, and a DECIMAL 
that rounds to it in DOUBLE also rounds to it in REAL, so the test cannot show 
that the override is installed. `testIntegerRealComparisonNotWidened` checks 
only that the override leaves INTEGER alone, which `TypeCoercionTest.java:512` 
already asserts at the type level. The DOUBLE limit is stated in the 
`WideningTypeCoercion` javadoc, so I would drop both tests.



##########
core/src/main/java/org/apache/calcite/sql/validate/implicit/TypeCoercion.java:
##########
@@ -94,6 +94,10 @@ public interface TypeCoercion {
 
   /**
    * Determines common type for a comparison operator.
+   *
+   * <p>{@code NATURAL JOIN} and {@code JOIN USING} call this method directly
+   * (via {@link org.apache.calcite.sql2rel.SqlToRelConverter}); every
+   * implementation must handle those correctly.

Review Comment:
   "Every implementation must handle those correctly" does not state a rule an 
implementer can check. What `SqlToRelConverter` does with the result is the 
contract:
   
   ```suggestion
      * <p>{@link org.apache.calcite.sql2rel.SqlToRelConverter} calls this 
method
      * for each column of a {@code NATURAL JOIN} or {@code JOIN USING} without
      * going through {@link #binaryComparisonCoercion}: it casts both columns 
to
      * the returned type, and compares them uncast when it returns {@code 
null}.
   ```



##########
core/src/test/java/org/apache/calcite/test/TypeCoercionTest.java:
##########
@@ -484,6 +486,71 @@ private static ImmutableList<RelDataType> combine(
         f.recordType("a", f.arrayType(f.nullableIntType)));
   }
 
+  /**
+   * Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]
+   * Document precision loss in DECIMAL/REAL comparisons and how to widen them 
to DOUBLE</a>.
+   *
+   * <p>By default, comparing DECIMAL with REAL narrows to REAL. A custom
+   * {@link TypeCoercion} that overrides
+   * {@link TypeCoercion#commonTypeForBinaryComparison} can widen to DOUBLE,
+   * including for the element types of ARRAY and MAP and the fields of ROW. */
+  @Test void testComparisonCoercionDecimalWithApproximateNumericOverride() {
+    final Fixture f = fixture();
+    final RelDataType decimal54 = f.decimalType(5, 4);
+    final RelDataType nullableDecimal54 =
+        f.typeFactory.createTypeWithNullability(decimal54, true);
+
+    final SqlValidator validator = SqlTestFactory.INSTANCE.createValidator();
+    final Fixture w =
+        new Fixture(f.typeFactory,
+            new WideningTypeCoercion(f.typeFactory, validator));
+    w.comparisonCommonType(decimal54, f.realType, f.doubleType);
+    w.comparisonCommonType(nullableDecimal54, f.realType, 
f.nullableDoubleType);
+    w.comparisonCommonType(decimal54, f.nullableRealType, 
f.nullableDoubleType);
+    // No DECIMAL operand: unchanged.
+    w.comparisonCommonType(f.intType, f.realType, f.realType);

Review Comment:
   No case covers a DECIMAL operand whose common type is exact, so nothing 
tests the `isApproximateNumeric(type)` check in `WideningTypeCoercion`, which 
is the rule the documentation should state. Please add 
`w.comparisonCommonType(decimal54, f.intType, decimal144)`.



##########
core/src/test/java/org/apache/calcite/test/JdbcTest.java:
##########
@@ -1836,6 +1840,105 @@ private void checkResultSetMetaData(Connection 
connection, String sql)
         .returns("C0=5.5; C1=5; C2=1.4; C3=1; C4=0; C5=0\n");
   }
 
+  /** Installs {@link TypeCoercionTest.WideningTypeCoercion} via
+   * {@link Hook#STRING_TO_QUERY} for use in boundary tests for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]</a>. */
+  private static Consumer<Pair<FrameworkConfig, Holder<CalcitePrepare.Query>>>
+      wideningHook(String sql) {
+    return pair -> {
+      final FrameworkConfig config =
+          Frameworks.newConfigBuilder(pair.left)
+              .sqlValidatorConfig(pair.left.getSqlValidatorConfig()
+                  
.withTypeCoercionFactory(TypeCoercionTest.WideningTypeCoercion::new))
+              .build();
+      final Planner planner = Frameworks.getPlanner(config);
+      try {
+        final RelRoot root = planner.rel(planner.validate(planner.parse(sql)));
+        pair.right.set(CalcitePrepare.Query.of(root.project()));
+      } catch (Exception e) {
+        throw TestUtil.rethrow(e);
+      }
+    };
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]
+   * Document precision loss in DECIMAL/REAL comparisons and how to widen them 
to DOUBLE</a>.
+   *
+   * <p>Default behavior: DECIMAL(18, 3) compared with REAL narrows to REAL
+   * (24-bit mantissa, about 7 decimal digits). These two values collide in
+   * REAL, so the join incorrectly returns 1 row. */
+  @Test void testDecimalRealComparisonDefaultNarrowsToReal() {

Review Comment:
   This test and `testDecimalRealComparisonWidenedToDoubleHelps` use different 
values, so no test runs the same query with and without the override. Please 
give this test the SQL of `...WidenedToDoubleHelps` (`140737488355328.016` 
against `CAST(140737488355328 AS REAL)`), still with `returnsCount(1)`. The two 
tests then differ only in the hook, and their outcomes, 1 row and 0 rows, show 
what the override changes.



##########
core/src/main/java/org/apache/calcite/sql/validate/implicit/AbstractTypeCoercion.java:
##########
@@ -495,8 +495,25 @@ private RelDataType getTightestCommonTypeOrThrow(
 
   /**
    * Determines common type for a comparison operator.
-   * For date and timestamp operands, use timestamp as common type,
-   * i.e. Timestamp(2017-01-01 00:00 ...) &gt; Date(2018) evaluates to be 
false.
+   *
+   * <p>For date and timestamp operands, uses timestamp as common type,
+   * i.e. {@code Timestamp(2017-01-01 00:00 ...) > Date(2018)} evaluates to 
false.
+   *
+   * <p>Comparing an approximate numeric ({@code REAL}, {@code DOUBLE}) with

Review Comment:
   The approximate types are listed as `REAL`, `DOUBLE`; `FLOAT` narrows the 
same way. Line 507: "may compare as equal" should be "compare as equal": the 
two values do become equal. Line 514: for MULTISET the recursion returns an 
ARRAY type (`createArrayType` at line 726), so please drop MULTISET from the 
sentence; the ARRAY result is a separate bug.



##########
site/_docs/reference.md:
##########
@@ -1828,6 +1828,20 @@ i: implicit cast / e: explicit cast / x: not allowed
   make `1 = true` and `0 = false` always evaluate to `TRUE`;
   if there is numeric type operand, find common type for both operands;
   if operands are `UUID` and `CHARACTER` or `BINARY`, promote to `UUID`.
+  By default, comparing `DECIMAL` with `REAL` or `DOUBLE` narrows to the
+  approximate type (`REAL` has 24-bit mantissa, about 7 decimal digits),
+  so distinct values can compare as equal. The same applies to `BETWEEN`,
+  `IN`, quantified comparisons, `NATURAL JOIN`, and `JOIN USING`, and to
+  the element types of `ARRAY`, `MAP`, and `ROW`. (`CASE`, `COALESCE`,
+  and set operations are not affected: they already use `DOUBLE` for this
+  combination.) To widen to `DOUBLE` instead, override
+  `TypeCoercion#commonTypeForBinaryComparison` in a `TypeCoercionImpl`

Review Comment:
   The paragraph names the method to override but not the rule the override 
applies: keep `super`'s result, and replace it with DOUBLE only when `super` 
returns an approximate type and one operand is DECIMAL. An override that 
returns DOUBLE for every DECIMAL operand would also change DECIMAL vs VARCHAR 
and DECIMAL vs INTEGER. Please add a sentence stating that rule.
   
   The page also does not say that `INTEGER` and `BIGINT` compared with `REAL` 
lose precision the same way, and that this override leaves them alone. The 
javadoc says so, but a user hitting wrong results will look here.



##########
core/src/test/java/org/apache/calcite/test/JdbcTest.java:
##########
@@ -1836,6 +1840,105 @@ private void checkResultSetMetaData(Connection 
connection, String sql)
         .returns("C0=5.5; C1=5; C2=1.4; C3=1; C4=0; C5=0\n");
   }
 
+  /** Installs {@link TypeCoercionTest.WideningTypeCoercion} via
+   * {@link Hook#STRING_TO_QUERY} for use in boundary tests for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]</a>. */
+  private static Consumer<Pair<FrameworkConfig, Holder<CalcitePrepare.Query>>>
+      wideningHook(String sql) {
+    return pair -> {
+      final FrameworkConfig config =
+          Frameworks.newConfigBuilder(pair.left)
+              .sqlValidatorConfig(pair.left.getSqlValidatorConfig()
+                  
.withTypeCoercionFactory(TypeCoercionTest.WideningTypeCoercion::new))
+              .build();
+      final Planner planner = Frameworks.getPlanner(config);
+      try {
+        final RelRoot root = planner.rel(planner.validate(planner.parse(sql)));
+        pair.right.set(CalcitePrepare.Query.of(root.project()));
+      } catch (Exception e) {
+        throw TestUtil.rethrow(e);
+      }
+    };
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]
+   * Document precision loss in DECIMAL/REAL comparisons and how to widen them 
to DOUBLE</a>.
+   *
+   * <p>Default behavior: DECIMAL(18, 3) compared with REAL narrows to REAL
+   * (24-bit mantissa, about 7 decimal digits). These two values collide in
+   * REAL, so the join incorrectly returns 1 row. */
+  @Test void testDecimalRealComparisonDefaultNarrowsToReal() {
+    CalciteAssert.that()
+        .query("SELECT *\n"
+            + "FROM (VALUES (CAST(59999943.000 AS DECIMAL(18, 3)))) AS d(k)\n"
+            + "JOIN (VALUES (CAST(59999945 AS REAL))) AS f(k) ON d.k = f.k")
+        .returnsCount(1);
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]
+   * Document precision loss in DECIMAL/REAL comparisons and how to widen them 
to DOUBLE</a>.
+   *
+   * <p>With {@link TypeCoercionTest.WideningTypeCoercion}, DECIMAL(18, 3)
+   * compared with REAL widens to DOUBLE. {@code CAST(140737488355328 AS REAL)}
+   * is exactly 2^47 (a power of two, exactly representable as REAL). DOUBLE
+   * has 53-bit mantissa; the unit in the last place (ULP) at 2^47 is
+   * 2^(47-52) = 0.03125. The DECIMAL value 140737488355328.016 exceeds the
+   * half-ULP (0.015625), so it rounds to the next DOUBLE above 2^47. The
+   * widened join correctly returns 0 rows. See
+   * {@link #testDecimalRealComparisonWidenedStillCollides} for the pair that
+   * still collides after widening. */
+  @Test void testDecimalRealComparisonWidenedToDoubleHelps() {

Review Comment:
   `...WidenedToDoubleHelps` does not say what the test expects. Something like 
`testDecimalRealComparisonWidenedToDoubleDistinguishesValues` would.



##########
core/src/test/java/org/apache/calcite/test/TypeCoercionTest.java:
##########
@@ -484,6 +486,71 @@ private static ImmutableList<RelDataType> combine(
         f.recordType("a", f.arrayType(f.nullableIntType)));
   }
 
+  /**
+   * Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7827";>[CALCITE-7827]
+   * Document precision loss in DECIMAL/REAL comparisons and how to widen them 
to DOUBLE</a>.
+   *
+   * <p>By default, comparing DECIMAL with REAL narrows to REAL. A custom

Review Comment:
   "By default, comparing DECIMAL with REAL narrows to REAL" is not asserted in 
this test; the `decimal54, f.realType, f.realType` row is in 
`testComparisonCoercion`. Please drop the sentence.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to