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

mihaibudiu 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 92ff2a6b5f [CALCITE-6059] Optimizer does not correctly handle special 
floating point value -0.0E0
92ff2a6b5f is described below

commit 92ff2a6b5f3374c330d384ffef99db9b1f287e16
Author: Mihai Budiu <[email protected]>
AuthorDate: Thu Aug 6 13:34:23 2026 -0700

    [CALCITE-6059] Optimizer does not correctly handle special floating point 
value -0.0E0
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../apache/calcite/rel/rel2sql/SqlImplementor.java |  6 +-
 .../org/apache/calcite/runtime/SqlFunctions.java   | 16 +++--
 .../apache/calcite/sql/parser/SqlParserUtil.java   | 10 ++-
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java |  5 +-
 .../org/apache/calcite/test/SqlFunctionsTest.java  |  8 +++
 .../org/apache/calcite/test/SqlOperatorTest.java   | 78 ++++++++++++++++++++++
 6 files changed, 111 insertions(+), 12 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java 
b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index e5b1d0d0c6..fb003caa95 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -1653,8 +1653,10 @@ public static SqlNode toSql(RexLiteral literal) {
     case EXACT_NUMERIC: {
       if (SqlTypeName.APPROX_TYPES.contains(typeName)) {
         final Double d = castNonNull(literal.getValueAs(Double.class));
-        // BigDecimal cannot represent IEEE 754 special values (NaN, 
±Infinity).
-        if (!Double.isFinite(d)) {
+        // BigDecimal cannot represent IEEE 754 special values
+        // (NaN, ±Infinity) or negative zero.
+        if (!Double.isFinite(d)
+            || (d == 0 && Double.doubleToRawLongBits(d) != 0L)) {
           final SqlNode strLiteral =
               SqlLiteral.createCharString(d.toString(), POS);
           final SqlDataTypeSpec typeSpec =
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java 
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index e3ad7c502d..32fa29fbd0 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -5101,7 +5101,9 @@ public static double lesser(double b0, double b1) {
   /** CAST(FLOAT AS VARCHAR). */
   public static String toString(float x) {
     if (x == 0) {
-      return "0E0";
+      // The comparison 'x == 0' does not distinguish -0.0 from 0.0,
+      // but the bit pattern does
+      return Float.floatToRawIntBits(x) != 0 ? "-0E0" : "0E0";
     }
     return Float.toString(x);
   }
@@ -5109,7 +5111,9 @@ public static String toString(float x) {
   /** CAST(DOUBLE AS VARCHAR). */
   public static String toString(double x) {
     if (x == 0) {
-      return "0E0";
+      // The comparison 'x == 0' does not distinguish -0.0 from 0.0,
+      // but the bit pattern does
+      return Double.doubleToRawLongBits(x) != 0L ? "-0E0" : "0E0";
     }
     return Double.toString(x);
   }
@@ -5159,12 +5163,12 @@ public static boolean toBoolean(Number number) {
       return decimal.compareTo(BigDecimal.ZERO) != 0;
     }
     if (number instanceof Double) {
-      Double d = (Double) number;
-      return !d.equals(Double.valueOf(0));
+      // Compare primitives: IEEE 754 treats -0.0 as equal to 0.0,
+      // whereas Double.equals does not
+      return ((Double) number).doubleValue() != 0d;
     }
     if (number instanceof Float) {
-      Float f = (Float) number;
-      return !f.equals(Float.valueOf(0));
+      return ((Float) number).floatValue() != 0f;
     }
     return !number.equals(0);
   }
diff --git 
a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java 
b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
index c48bb846fb..4b1ede7e57 100644
--- a/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/parser/SqlParserUtil.java
@@ -983,8 +983,14 @@ private static SqlNode 
convert(PrecedenceClimbingParser.Token token) {
         SqlNode firstItem = list.get(0);
         if (item.op == SqlStdOperatorTable.UNARY_MINUS
             && firstItem instanceof SqlNumericLiteral) {
-          return SqlLiteral.createNegative((SqlNumericLiteral) firstItem,
-              item.pos.plusAll(list));
+          final SqlNumericLiteral num = (SqlNumericLiteral) firstItem;
+          // Do not fold "-0.0E0" into a literal: BigDecimal, which backs
+          // SqlNumericLiteral, cannot represent IEEE 754 negative zero.
+          // Keeping the unary minus call preserves the sign at runtime.
+          if (num.isExact()
+              || ((BigDecimal) requireNonNull(num.getValue())).signum() != 0) {
+            return SqlLiteral.createNegative(num, item.pos.plusAll(list));
+          }
         }
         if (item.op == SqlStdOperatorTable.UNARY_PLUS
             && firstItem instanceof SqlNumericLiteral) {
diff --git 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index e9cede1a4d..2a500bcce7 100644
--- 
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++ 
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -13070,10 +13070,11 @@ generated, isLinux("SELECT \"$cor0\".\"id\"\n"
         .ok("SELECT *\n"
             + "FROM (VALUES (CAST('-Infinity' AS DOUBLE))) AS \"t\" 
(\"EXPR$0\")");
 
-    // Test Negative Zero
+    // Test Negative Zero: must round-trip through a CAST, because a
+    // SqlNumericLiteral cannot represent it
     sql("select cast('-0.0' as DOUBLE)")
         .ok("SELECT *\n"
-            + "FROM (VALUES (0E0)) AS \"t\" (\"EXPR$0\")");
+            + "FROM (VALUES (CAST('-0.0' AS DOUBLE))) AS \"t\" (\"EXPR$0\")");
 
     // Test Subnormal values
     sql("select cast('1e-310' as DOUBLE)")
diff --git a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
index e3827c1253..ac040bd48c 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlFunctionsTest.java
@@ -152,6 +152,7 @@ static <E> List<E> list() {
 
   @Test void testToString() {
     assertThat(SqlFunctions.toString(0f), is("0E0"));
+    assertThat(SqlFunctions.toString(-0f), is("-0E0"));
     assertThat(SqlFunctions.toString(1f), is("1.0"));
     assertThat(SqlFunctions.toString(1.5f), is("1.5"));
     assertThat(SqlFunctions.toString(-1.5f), is("-1.5"));
@@ -159,8 +160,12 @@ static <E> List<E> list() {
     assertThat(SqlFunctions.toString(-0.0625f), is("-0.0625"));
     assertThat(SqlFunctions.toString(0.0625f), is("0.0625"));
     assertThat(SqlFunctions.toString(-5e-12f), is("-5.0E-12"));
+    assertThat(SqlFunctions.toString(Float.NaN), is("NaN"));
+    assertThat(SqlFunctions.toString(Float.POSITIVE_INFINITY), is("Infinity"));
+    assertThat(SqlFunctions.toString(Float.NEGATIVE_INFINITY), 
is("-Infinity"));
 
     assertThat(SqlFunctions.toString(0d), is("0E0"));
+    assertThat(SqlFunctions.toString(-0d), is("-0E0"));
     assertThat(SqlFunctions.toString(1d), is("1.0"));
     assertThat(SqlFunctions.toString(1.5d), is("1.5"));
     assertThat(SqlFunctions.toString(-1.5d), is("-1.5"));
@@ -168,6 +173,9 @@ static <E> List<E> list() {
     assertThat(SqlFunctions.toString(-0.0625d), is("-0.0625"));
     assertThat(SqlFunctions.toString(0.0625d), is("0.0625"));
     assertThat(SqlFunctions.toString(-5e-12d), is("-5.0E-12"));
+    assertThat(SqlFunctions.toString(Double.NaN), is("NaN"));
+    assertThat(SqlFunctions.toString(Double.POSITIVE_INFINITY), 
is("Infinity"));
+    assertThat(SqlFunctions.toString(Double.NEGATIVE_INFINITY), 
is("-Infinity"));
 
     assertThat(SqlFunctions.toString(new BigDecimal("0")), is("0"));
     assertThat(SqlFunctions.toString(new BigDecimal("1")), is("1"));
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java 
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 93b2f90aae..1400f90a06 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -405,6 +405,84 @@ protected SqlOperatorFixture fixture() {
     }
   }
 
+  @Test void testFPSpecialValues() {
+    SqlOperatorFixture f = fixture();
+    f.checkScalarApprox("CAST('Infinity' AS REAL)",
+        "REAL NOT NULL", "Infinity");
+    f.checkScalarApprox("CAST('Infinity' AS DOUBLE)",
+        "DOUBLE NOT NULL", "Infinity");
+    f.checkScalarApprox("CAST('Infinity' AS FLOAT)",
+        "FLOAT NOT NULL", "Infinity");
+    f.checkScalarApprox("CAST('-Infinity' AS REAL)",
+        "REAL NOT NULL", "-Infinity");
+    f.checkScalarApprox("CAST('-Infinity' AS DOUBLE)",
+        "DOUBLE NOT NULL", "-Infinity");
+    f.checkScalarApprox("CAST('-Infinity' AS FLOAT)",
+        "FLOAT NOT NULL", "-Infinity");
+    // Note: IEEE 754 specifies that there are several types of NaN: quiet and 
signaling.
+    // There is however only one way to write them.
+    // But when compared for equality they may not match.
+    f.checkScalarApprox("CAST('NaN' AS REAL)",
+        "REAL NOT NULL", "NaN");
+    f.checkScalarApprox("CAST('NaN' AS DOUBLE)",
+        "DOUBLE NOT NULL", "NaN");
+    f.checkScalarApprox("CAST('NaN' AS FLOAT)",
+        "FLOAT NOT NULL", "NaN");
+    // [CALCITE-6059] Optimizer does not correctly handle
+    // special floating point value -0.0E0
+    // The matcher is(-0.0d) checks the value bit-exactly:
+    // Double.equals distinguishes -0.0 from 0.0.
+    f.checkScalarApprox("CAST('-0E0' AS REAL)",
+        "REAL NOT NULL", is(-0.0d));
+    f.checkScalarApprox("CAST('-0E0' AS DOUBLE)",
+        "DOUBLE NOT NULL", is(-0.0d));
+    f.checkScalarApprox("CAST('-0E0' AS FLOAT)",
+        "FLOAT NOT NULL", is(-0.0d));
+    f.checkScalarApprox("CAST('0E0' AS REAL)",
+        "REAL NOT NULL", is(0.0d));
+    f.checkScalarApprox("CAST('0E0' AS DOUBLE)",
+        "DOUBLE NOT NULL", is(0.0d));
+    f.checkScalarApprox("CAST('0E0' AS FLOAT)",
+        "FLOAT NOT NULL", is(0.0d));
+    // Casting an approximate numeric to VARCHAR uses E notation,
+    // and must preserve the sign of a negative zero
+    f.checkString("CAST(CAST('-0E0' AS REAL) AS VARCHAR)",
+        "-0E0", "VARCHAR NOT NULL");
+    f.checkString("CAST(CAST('-0E0' AS DOUBLE) AS VARCHAR)",
+        "-0E0", "VARCHAR NOT NULL");
+    f.checkString("CAST(CAST('0E0' AS REAL) AS VARCHAR)",
+        "0E0", "VARCHAR NOT NULL");
+    f.checkString("CAST(CAST('0E0' AS DOUBLE) AS VARCHAR)",
+        "0E0", "VARCHAR NOT NULL");
+    // A nullable value is boxed at runtime; formatting must not depend
+    // on nullability. RAND() prevents constant folding, so the CAST
+    // executes at runtime on the boxed value.
+    f.checkString("CAST(CASE WHEN RAND() >= 0 THEN 0.0E0 ELSE NULL END"
+            + " AS VARCHAR)",
+        "0E0", "VARCHAR");
+    // An array element is a boxed Double in the generated code
+    f.checkString("CAST(ARRAY[-0.0E0][1] AS VARCHAR)",
+        "-0E0", "VARCHAR");
+    // 1/-0.0 = -Infinity: proves that the sign of the zero
+    // survives arithmetic at runtime.
+    f.checkScalarApprox("1E0 / CAST('-0E0' AS REAL)",
+        "DOUBLE NOT NULL", "-Infinity");
+    f.checkScalarApprox("1E0 / CAST('-0E0' AS DOUBLE)",
+        "DOUBLE NOT NULL", "-Infinity");
+    f.checkScalarApprox("1E0 / CAST('0E0' AS DOUBLE)",
+        "DOUBLE NOT NULL", "Infinity");
+    // A negative zero written as a literal, rather than computed by a CAST
+    f.checkScalarApprox("1E0 / -0.0E0",
+        "DOUBLE NOT NULL", "-Infinity");
+    f.checkString("CAST(-0.0E0 AS VARCHAR)",
+        "-0E0", "VARCHAR NOT NULL");
+    // In comparisons -0.0 is equal to 0.0, so casting either to
+    // BOOLEAN yields FALSE
+    f.checkBoolean("CAST(CAST('-0E0' AS DOUBLE) AS BOOLEAN)", false);
+    f.checkBoolean("CAST(CAST('-0E0' AS REAL) AS BOOLEAN)", false);
+    f.checkBoolean("CAST(-0.0E0 AS BOOLEAN)", false);
+  }
+
   @Test void testBetween() {
     final SqlOperatorFixture f = fixture();
     f.setFor(SqlStdOperatorTable.BETWEEN, VmName.EXPAND);

Reply via email to