diff --git a/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java b/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java index 8fa5abd149d..c658914a435 100644 --- a/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java +++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java @@ -459,6 +459,17 @@ public Object toJson(AggregateCall node) { return map; } + /** Converts a literal's value to JSON. A non-finite approximate value becomes + * a string, because JSON has no syntax for NaN or infinity and the + * {@link java.math.BigDecimal} that the reader parses numbers into cannot + * represent one either. */ + private @Nullable Object toJsonLiteralValue(@Nullable Object value) { + if (value instanceof Double && !Double.isFinite((Double) value)) { + return value.toString(); + } + return toJson(value); + } + public @Nullable Object toJson(@Nullable Object value) { if (value == null || value instanceof Number @@ -610,7 +621,7 @@ public Object toJson(RexNode node) { map.put("literal", value instanceof Enum ? RelEnumTypes.fromEnum((Enum) value) - : toJson(value)); + : toJsonLiteralValue(value)); map.put("type", toJson(node.getType())); return map; case INPUT_REF: @@ -876,6 +887,11 @@ public RexNode toRex(RelOptCluster cluster, Object o) { literal = ByteString.of((String) literal, 16); } else if (sqlTypeName == SqlTypeName.UUID) { literal = SqlFunctions.stringToUuid((String) literal); + } else if (literal instanceof String + && SqlTypeName.APPROX_TYPES.contains(sqlTypeName)) { + // A non-finite value that toJson wrote as a string because JSON + // cannot represent it as a number. + literal = Double.valueOf((String) literal); } return rexBuilder.makeLiteral(literal, type); } diff --git a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java index a107afd17f0..0fe768c242e 100644 --- a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java +++ b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java @@ -648,6 +648,91 @@ private static Fixture relFn(Function relFn) { .assertThatJson(isLinux(UUID_LITERAL)); } + static final String NON_FINITE_APPROX_LITERAL = "{\n" + + " \"rels\": [\n" + + " {\n" + + " \"id\": \"0\",\n" + + " \"relOp\": \"LogicalTableScan\",\n" + + " \"table\": [\n" + + " \"scott\",\n" + + " \"EMP\"\n" + + " ],\n" + + " \"inputs\": []\n" + + " },\n" + + " {\n" + + " \"id\": \"1\",\n" + + " \"relOp\": \"LogicalProject\",\n" + + " \"fields\": [\n" + + " \"$f0\",\n" + + " \"$f1\",\n" + + " \"$f2\",\n" + + " \"$f3\",\n" + + " \"$f4\"\n" + + " ],\n" + + " \"exprs\": [\n" + + " {\n" + + " \"literal\": \"NaN\",\n" + + " \"type\": {\n" + + " \"type\": \"DOUBLE\",\n" + + " \"nullable\": false\n" + + " }\n" + + " },\n" + + " {\n" + + " \"literal\": \"Infinity\",\n" + + " \"type\": {\n" + + " \"type\": \"DOUBLE\",\n" + + " \"nullable\": false\n" + + " }\n" + + " },\n" + + " {\n" + + " \"literal\": \"-Infinity\",\n" + + " \"type\": {\n" + + " \"type\": \"DOUBLE\",\n" + + " \"nullable\": false\n" + + " }\n" + + " },\n" + + " {\n" + + " \"literal\": 1.5,\n" + + " \"type\": {\n" + + " \"type\": \"DOUBLE\",\n" + + " \"nullable\": false\n" + + " }\n" + + " },\n" + + " {\n" + + " \"literal\": \"Infinity\",\n" + + " \"type\": {\n" + + " \"type\": \"CHAR\",\n" + + " \"nullable\": false,\n" + + " \"precision\": 8\n" + + " }\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + + /** Test case for + * [CALCITE-6792] + * RelJsonReader failed to read RelJsonWriter output if 'NaN' or 'Infinity' + * approximate numerics are present. */ + @Test void testNonFiniteApproxLiteral() { + final Function relFn = b -> { + final RexBuilder rexBuilder = b.getRexBuilder(); + final RelDataType doubleType = + b.getTypeFactory().createSqlType(SqlTypeName.DOUBLE); + return b.scan("EMP") + .project(rexBuilder.makeApproxLiteral(Double.NaN, doubleType), + rexBuilder.makeApproxLiteral(Double.POSITIVE_INFINITY, doubleType), + rexBuilder.makeApproxLiteral(Double.NEGATIVE_INFINITY, doubleType), + rexBuilder.makeApproxLiteral(1.5d, doubleType), + b.literal("Infinity")) + .build(); + }; + relFn(relFn) + .assertThatJson(isLinux(NON_FINITE_APPROX_LITERAL)) + .assertThatPlanRoundTrips(); + } + /** * Unit test for {@link org.apache.calcite.rel.externalize.RelJsonWriter} on * a simple tree of relational expressions, consisting of a table, a filter @@ -1931,6 +2016,22 @@ Fixture assertThatJson(Matcher matcher) { return this; } + @SuppressWarnings("UnusedReturnValue") + Fixture assertThatPlanRoundTrips() { + final FrameworkConfig config = RelBuilderTest.config().build(); + final RelBuilder b = RelBuilder.create(config); + final RelNode rel = relFn.apply(b); + final String relJson = + RelOptUtil.dumpPlan("", rel, SqlExplainFormat.JSON, + SqlExplainLevel.EXPPLAN_ATTRIBUTES); + final String originalPlan = + RelOptUtil.dumpPlan("", rel, format, SqlExplainLevel.EXPPLAN_ATTRIBUTES); + final String roundTrippedPlan = + deserializeAndDump(getSchema(rel), relJson, format); + assertThat(roundTrippedPlan, is(originalPlan)); + return this; + } + @SuppressWarnings("UnusedReturnValue") Fixture assertThatPlan(Matcher matcher) { final FrameworkConfig config = RelBuilderTest.config().build();