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 18177ecae30a..bd519351e2a5 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 @@ -1635,8 +1635,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 e3ad7c502d40..32fa29fbd026 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 c48bb846fb8b..4b1ede7e570e 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 83a4f87b288e..688c31be8b25 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 @@ -12973,10 +12973,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 e3827c12530e..ac040bd48c4b 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 List 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 List 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 List 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 7db6ab298db7..0075f4779acb 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);