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);