diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/JsonOperators.java b/core/trino-main/src/main/java/io/trino/operator/scalar/JsonOperators.java index ee017d88e23b..4d55bd501d88 100644 --- a/core/trino-main/src/main/java/io/trino/operator/scalar/JsonOperators.java +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/JsonOperators.java @@ -25,9 +25,11 @@ import io.trino.spi.function.ScalarOperator; import io.trino.spi.function.SqlNullable; import io.trino.spi.function.SqlType; +import io.trino.spi.type.TrinoNumber; import io.trino.util.JsonCastException; import java.io.IOException; +import java.math.BigDecimal; import static io.airlift.slice.SliceUtf8.countCodePoints; import static io.trino.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; @@ -39,6 +41,7 @@ import static io.trino.spi.type.StandardTypes.DOUBLE; import static io.trino.spi.type.StandardTypes.INTEGER; import static io.trino.spi.type.StandardTypes.JSON; +import static io.trino.spi.type.StandardTypes.NUMBER; import static io.trino.spi.type.StandardTypes.REAL; import static io.trino.spi.type.StandardTypes.SMALLINT; import static io.trino.spi.type.StandardTypes.TINYINT; @@ -51,6 +54,7 @@ import static io.trino.util.JsonUtil.currentTokenAsBoolean; import static io.trino.util.JsonUtil.currentTokenAsDouble; import static io.trino.util.JsonUtil.currentTokenAsInteger; +import static io.trino.util.JsonUtil.currentTokenAsNumber; import static io.trino.util.JsonUtil.currentTokenAsReal; import static io.trino.util.JsonUtil.currentTokenAsSmallint; import static io.trino.util.JsonUtil.currentTokenAsTinyint; @@ -198,6 +202,22 @@ public static Long castToReal(@SqlType(JSON) Slice json) } } + @ScalarOperator(CAST) + @SqlNullable + @SqlType(NUMBER) + public static TrinoNumber castToNumber(@SqlType(JSON) Slice json) + { + try (JsonParser parser = createJsonParser(JSON_MAPPER, json)) { + parser.nextToken(); + TrinoNumber result = currentTokenAsNumber(parser); + checkCondition(parser.nextToken() == null, INVALID_CAST_ARGUMENT, "Cannot cast input json to NUMBER"); // check no trailing token + return result; + } + catch (IOException | JsonCastException e) { + throw new TrinoException(INVALID_CAST_ARGUMENT, format("Cannot cast '%s' to %s", json.toStringUtf8(), NUMBER), e); + } + } + @ScalarOperator(CAST) @SqlNullable @SqlType(BOOLEAN) @@ -305,6 +325,26 @@ public static Slice castFromReal(@SqlType(REAL) long value) } } + @ScalarOperator(CAST) + @SqlType(JSON) + public static Slice castFromNumber(@SqlType(NUMBER) TrinoNumber value) + { + try { + SliceOutput output = new DynamicSliceOutput(32); + try (JsonGenerator jsonGenerator = createJsonGenerator(JSON_MAPPER, output)) { + switch (value.toBigDecimal()) { + case TrinoNumber.NotANumber() -> jsonGenerator.writeString("NaN"); + case TrinoNumber.Infinity(boolean negative) -> jsonGenerator.writeString(negative ? "-Infinity" : "+Infinity"); + case TrinoNumber.BigDecimalValue(BigDecimal bigDecimal) -> jsonGenerator.writeNumber(bigDecimal); + } + } + return output.slice(); + } + catch (IOException e) { + throw new TrinoException(INVALID_CAST_ARGUMENT, format("Cannot cast NUMBER '%s' to %s", value.toBigDecimal(), JSON), e); + } + } + @ScalarOperator(CAST) @SqlType(JSON) public static Slice castFromBoolean(@SqlType(BOOLEAN) boolean value) diff --git a/core/trino-main/src/main/java/io/trino/util/JsonUtil.java b/core/trino-main/src/main/java/io/trino/util/JsonUtil.java index 16b7f15dfbe7..ce00e5839071 100644 --- a/core/trino-main/src/main/java/io/trino/util/JsonUtil.java +++ b/core/trino-main/src/main/java/io/trino/util/JsonUtil.java @@ -42,6 +42,7 @@ import io.trino.spi.type.IntegerType; import io.trino.spi.type.LongTimestamp; import io.trino.spi.type.MapType; +import io.trino.spi.type.NumberType; import io.trino.spi.type.RealType; import io.trino.spi.type.RowType; import io.trino.spi.type.RowType.Field; @@ -49,6 +50,7 @@ import io.trino.spi.type.StandardTypes; import io.trino.spi.type.TimestampType; import io.trino.spi.type.TinyintType; +import io.trino.spi.type.TrinoNumber; import io.trino.spi.type.Type; import io.trino.spi.type.VarcharType; import io.trino.type.BigintOperators; @@ -57,6 +59,7 @@ import io.trino.type.JsonType; import io.trino.type.UnknownType; import io.trino.type.VarcharOperators; +import jakarta.annotation.Nullable; import java.io.IOException; import java.io.InputStreamReader; @@ -89,6 +92,7 @@ import static io.trino.spi.type.DateType.DATE; import static io.trino.spi.type.DoubleType.DOUBLE; import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.NumberType.NUMBER; import static io.trino.spi.type.RealType.REAL; import static io.trino.spi.type.SmallintType.SMALLINT; import static io.trino.spi.type.TinyintType.TINYINT; @@ -180,6 +184,7 @@ public static boolean canCastToJson(Type type) type instanceof RealType || type instanceof DoubleType || type instanceof DecimalType || + type instanceof NumberType || type instanceof VarcharType || type instanceof JsonType || type instanceof TimestampType || @@ -210,6 +215,7 @@ public static boolean canCastFromJson(Type type) type instanceof RealType || type instanceof DoubleType || type instanceof DecimalType || + type instanceof NumberType || type instanceof VarcharType || type instanceof JsonType) { return true; @@ -316,6 +322,9 @@ static JsonGeneratorWriter createJsonGeneratorWriter(Type type) } return new LongDecimalJsonGeneratorWriter(decimalType); } + if (type instanceof NumberType) { + return new NumberJsonGeneratorWriter(); + } if (type instanceof VarcharType) { return new VarcharJsonGeneratorWriter(type); } @@ -488,6 +497,27 @@ public void writeJsonValue(JsonGenerator jsonGenerator, Block block, int positio } } + private static class NumberJsonGeneratorWriter + implements JsonGeneratorWriter + { + @Override + public void writeJsonValue(JsonGenerator jsonGenerator, Block block, int position) + throws IOException + { + if (block.isNull(position)) { + jsonGenerator.writeNull(); + } + else { + TrinoNumber value = (TrinoNumber) NUMBER.getObject(block, position); + switch (value.toBigDecimal()) { + case TrinoNumber.NotANumber() -> jsonGenerator.writeString("NaN"); + case TrinoNumber.Infinity(boolean negative) -> jsonGenerator.writeString(negative ? "-Infinity" : "+Infinity"); + case TrinoNumber.BigDecimalValue(BigDecimal bigDecimal) -> jsonGenerator.writeNumber(bigDecimal); + } + } + } + } + private static class VarcharJsonGeneratorWriter implements JsonGeneratorWriter { @@ -796,6 +826,20 @@ public static Long currentTokenAsReal(JsonParser parser) }; } + @Nullable + public static TrinoNumber currentTokenAsNumber(JsonParser parser) + throws IOException + { + return switch (parser.currentToken()) { + case VALUE_NULL -> null; + case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToNumber(utf8Slice(parser.getText())); + case VALUE_NUMBER_INT, VALUE_NUMBER_FLOAT -> TrinoNumber.from(parser.getDecimalValue()); + case VALUE_TRUE -> TrinoNumber.from(BigDecimal.ONE); + case VALUE_FALSE -> TrinoNumber.from(BigDecimal.ZERO); + default -> throw new JsonCastException(format("Unexpected token when cast to %s: %s", StandardTypes.NUMBER, parser.getText())); + }; + } + public static Boolean currentTokenAsBoolean(JsonParser parser) throws IOException { @@ -901,6 +945,9 @@ static BlockBuilderAppender createBlockBuilderAppender(Type type) return new LongDecimalBlockBuilderAppender(decimalType); } + if (type instanceof NumberType) { + return new NumberBlockBuilderAppender(); + } if (type instanceof VarcharType) { return new VarcharBlockBuilderAppender(type); } @@ -1100,6 +1147,24 @@ public void append(JsonParser parser, BlockBuilder blockBuilder) } } + private static class NumberBlockBuilderAppender + implements BlockBuilderAppender + { + @Override + public void append(JsonParser parser, BlockBuilder blockBuilder) + throws IOException + { + TrinoNumber result = currentTokenAsNumber(parser); + + if (result == null) { + blockBuilder.appendNull(); + } + else { + NUMBER.writeObject(blockBuilder, result); + } + } + } + private static class VarcharBlockBuilderAppender implements BlockBuilderAppender { diff --git a/core/trino-main/src/test/java/io/trino/type/TestArrayOperators.java b/core/trino-main/src/test/java/io/trino/type/TestArrayOperators.java index 5c0962620f03..c6f3bd6ac1f1 100644 --- a/core/trino-main/src/test/java/io/trino/type/TestArrayOperators.java +++ b/core/trino-main/src/test/java/io/trino/type/TestArrayOperators.java @@ -13,13 +13,19 @@ */ package io.trino.type; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.primitives.Ints; +import io.airlift.json.JsonMapperProvider; import io.airlift.slice.DynamicSliceOutput; import io.airlift.slice.Slice; +import io.airlift.slice.SliceOutput; import io.trino.metadata.InternalFunctionBundle; +import io.trino.plugin.base.util.JsonTypeUtil; +import io.trino.spi.TrinoException; import io.trino.spi.block.ArrayBlockBuilder; import io.trino.spi.block.Block; import io.trino.spi.function.LiteralParameters; @@ -36,8 +42,14 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.parallel.Execution; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; import java.util.Collections; +import static com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS; +import static com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS; +import static com.google.common.base.Preconditions.checkState; import static io.trino.block.BlockSerdeUtil.writeBlock; import static io.trino.operator.scalar.BlockSet.MAX_FUNCTION_MEMORY; import static io.trino.spi.StandardErrorCode.AMBIGUOUS_FUNCTION_CALL; @@ -55,6 +67,7 @@ import static io.trino.spi.type.DecimalType.createDecimalType; import static io.trino.spi.type.DoubleType.DOUBLE; import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.NumberType.NUMBER; import static io.trino.spi.type.RealType.REAL; import static io.trino.spi.type.RowType.anonymousRow; import static io.trino.spi.type.RowType.field; @@ -77,6 +90,7 @@ import static java.lang.Double.POSITIVE_INFINITY; import static java.lang.Math.toIntExact; import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; @@ -115,6 +129,35 @@ public static Slice uncheckedToJson(@SqlType("varchar(x)") Slice slice) return slice; } + // TODO (https://github.com/trinodb/trino/issues/28867) remove when json_parse and JSON literals are no longer lossy. + @ScalarFunction("json_literal_fixed") + @LiteralParameters("x") + @SqlType(StandardTypes.JSON) + public static Slice workaroundBrokenJsonLiteralParsing(@SqlType("varchar(x)") Slice slice) + { + // This is copy of JsonTypeUtil.jsonParse with addition of USE_BIG_DECIMAL_FOR_FLOATS to prevent numeric precision loss during JSON parsing. + JsonMapper sortingMapper = new JsonMapperProvider().get() + .rebuild() + .configure(ORDER_MAP_ENTRIES_BY_KEYS, true) + .configure(USE_BIG_DECIMAL_FOR_FLOATS, true) + .build(); + + Slice json; + try (JsonParser parser = sortingMapper.createParser(new InputStreamReader(slice.getInput(), UTF_8))) { + SliceOutput output = new DynamicSliceOutput(slice.length()); + sortingMapper.writeValue((OutputStream) output, sortingMapper.readValue(parser, Object.class)); + checkState(parser.nextToken() == null, "Found characters after the expected end of input"); + json = output.slice(); + } + catch (IOException | RuntimeException e) { + throw new TrinoException(INVALID_FUNCTION_ARGUMENT, format("Cannot convert value to JSON: '%s'", slice.toStringUtf8()), e); + } + + Slice lossyJson = JsonTypeUtil.jsonParse(slice); + checkState(!json.equals(lossyJson), "json_literal_fixed is used unnecessarily here, or jsonParse has been fixed"); + return json; + } + @Test public void testStackRepresentation() { @@ -1121,6 +1164,48 @@ public void testCastJsonToArrayDecimal() .hasErrorCode(INVALID_CAST_ARGUMENT); } + @Test + public void testCastJsonToArrayNumber() + { + assertThat(assertions.expression("CAST(a AS ARRAY(NUMBER))") + .binding("a", "CAST(null AS JSON)")) + .isNull(new ArrayType(NUMBER)); + + assertThat(assertions.expression("CAST(a AS ARRAY(NUMBER))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(NUMBER)); + + assertThat(assertions.expression("CAST(a AS ARRAY(NUMBER))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(NUMBER)) + .matches("CAST(ARRAY[] AS ARRAY(NUMBER))"); + + assertThat(assertions.expression("CAST(a AS ARRAY(NUMBER))") + .binding("a", "JSON '[1, 2.5, 3.14159]'")) + .hasType(new ArrayType(NUMBER)) + .matches("ARRAY[NUMBER '1', NUMBER '2.5', NUMBER '3.14159']"); + + assertThat(assertions.expression("CAST(a AS ARRAY(NUMBER))") + .binding("a", "json_literal_fixed('[12345678901234567890.123456789012345678, 123456789012345678901234567890.123456789012345678901234567890]')")) + .hasType(new ArrayType(NUMBER)) + .matches("ARRAY[NUMBER '12345678901234567890.123456789012345678', NUMBER '123456789012345678901234567890.123456789012345678901234567890']"); + + assertThat(assertions.expression("CAST(a AS ARRAY(NUMBER))") + .binding("a", "JSON '[\"NaN\", \"Infinity\", \"-Infinity\", null]'")) + .hasType(new ArrayType(NUMBER)) + .matches("ARRAY[NUMBER 'NaN', NUMBER '+Infinity', NUMBER '-Infinity', null]"); + + assertThat(assertions.expression("CAST(a AS ARRAY(NUMBER))") + .binding("a", "JSON '[true, false, 128, 123.456, \"3.14\", null]'")) + .hasType(new ArrayType(NUMBER)) + .matches("ARRAY[NUMBER '1', NUMBER '0', NUMBER '128', NUMBER '123.456', NUMBER '3.14', null]"); + + assertTrinoExceptionThrownBy(() -> assertions.expression("CAST(a AS ARRAY(NUMBER))") + .binding("a", "JSON '[\"not a number\"]'") + .evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + @Test public void testCastJsonToArrayVarchar() { @@ -1549,6 +1634,34 @@ public void testCastArrayDecimalToJson() .isEqualTo("[null,null]"); } + @Test + public void testCastArrayNumberToJson() + { + assertThat(assertions.expression("CAST(a AS JSON)") + .binding("a", "CAST(null AS ARRAY(NUMBER))")) + .isNull(JSON); + + assertThat(assertions.expression("CAST(a AS JSON)") + .binding("a", "ARRAY[]")) + .hasType(JSON) + .isEqualTo("[]"); + + assertThat(assertions.expression("CAST(a AS JSON)") + .binding("a", "ARRAY[NUMBER '1', NUMBER '2.5', NUMBER '3.14159', null]")) + .hasType(JSON) + .isEqualTo("[1,2.5,3.14159,null]"); + + assertThat(assertions.expression("CAST(a AS JSON)") + .binding("a", "ARRAY[NUMBER '12345678901234567890.123456789012345678', null, NUMBER '123456789012345678901234567890.123456789012345678901234567890']")) + .hasType(JSON) + .isEqualTo("[12345678901234567890.123456789012345678,null,123456789012345678901234567890.12345678901234567890123456789]"); + + assertThat(assertions.expression("CAST(a AS JSON)") + .binding("a", "ARRAY[NUMBER 'NaN', NUMBER '+Infinity', NUMBER '-Infinity', NUMBER '0', null]")) + .hasType(JSON) + .isEqualTo("[\"NaN\",\"+Infinity\",\"-Infinity\",0,null]"); + } + @Test public void testCastArrayVarcharToJson() { diff --git a/core/trino-main/src/test/java/io/trino/type/TestJsonOperators.java b/core/trino-main/src/test/java/io/trino/type/TestJsonOperators.java index a0c729d5985a..8ce916a9814f 100644 --- a/core/trino-main/src/test/java/io/trino/type/TestJsonOperators.java +++ b/core/trino-main/src/test/java/io/trino/type/TestJsonOperators.java @@ -13,10 +13,23 @@ */ package io.trino.type; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.json.JsonMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import io.airlift.json.JsonMapperProvider; +import io.airlift.slice.DynamicSliceOutput; +import io.airlift.slice.Slice; +import io.airlift.slice.SliceOutput; +import io.trino.metadata.InternalFunctionBundle; +import io.trino.plugin.base.util.JsonTypeUtil; +import io.trino.spi.TrinoException; +import io.trino.spi.function.LiteralParameters; +import io.trino.spi.function.ScalarFunction; +import io.trino.spi.function.SqlType; import io.trino.spi.type.ArrayType; import io.trino.spi.type.RowType; +import io.trino.spi.type.StandardTypes; import io.trino.sql.query.QueryAssertions; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -24,6 +37,13 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.parallel.Execution; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; + +import static com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS; +import static com.fasterxml.jackson.databind.SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS; +import static com.google.common.base.Preconditions.checkState; import static io.trino.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; import static io.trino.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; import static io.trino.spi.StandardErrorCode.INVALID_LITERAL; @@ -35,6 +55,7 @@ import static io.trino.spi.type.DecimalType.createDecimalType; import static io.trino.spi.type.DoubleType.DOUBLE; import static io.trino.spi.type.IntegerType.INTEGER; +import static io.trino.spi.type.NumberType.NUMBER; import static io.trino.spi.type.RealType.REAL; import static io.trino.spi.type.SmallintType.SMALLINT; import static io.trino.spi.type.SqlDecimal.decimal; @@ -48,6 +69,7 @@ import static java.lang.Double.NEGATIVE_INFINITY; import static java.lang.Double.POSITIVE_INFINITY; import static java.lang.String.format; +import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.junit.jupiter.api.parallel.ExecutionMode.CONCURRENT; @@ -64,6 +86,9 @@ public class TestJsonOperators public void init() { assertions = new QueryAssertions(); + assertions.addFunctions(InternalFunctionBundle.builder() + .scalars(TestJsonOperators.class) + .build()); } @AfterAll @@ -73,6 +98,35 @@ public void teardown() assertions = null; } + // TODO (https://github.com/trinodb/trino/issues/28867) remove when json_parse and JSON literals are no longer lossy. + @ScalarFunction("json_literal_fixed") + @LiteralParameters("x") + @SqlType(StandardTypes.JSON) + public static Slice workaroundBrokenJsonLiteralParsing(@SqlType("varchar(x)") Slice slice) + { + // This is copy of JsonTypeUtil.jsonParse with addition of USE_BIG_DECIMAL_FOR_FLOATS to prevent numeric precision loss during JSON parsing. + JsonMapper sortingMapper = new JsonMapperProvider().get() + .rebuild() + .configure(ORDER_MAP_ENTRIES_BY_KEYS, true) + .configure(USE_BIG_DECIMAL_FOR_FLOATS, true) + .build(); + + Slice json; + try (JsonParser parser = sortingMapper.createParser(new InputStreamReader(slice.getInput(), UTF_8))) { + SliceOutput output = new DynamicSliceOutput(slice.length()); + sortingMapper.writeValue((OutputStream) output, sortingMapper.readValue(parser, Object.class)); + checkState(parser.nextToken() == null, "Found characters after the expected end of input"); + json = output.slice(); + } + catch (IOException | RuntimeException e) { + throw new TrinoException(INVALID_FUNCTION_ARGUMENT, format("Cannot convert value to JSON: '%s'", slice.toStringUtf8()), e); + } + + Slice lossyJson = JsonTypeUtil.jsonParse(slice); + checkState(!json.equals(lossyJson), "json_literal_fixed is used unnecessarily here, or jsonParse has been fixed"); + return json; + } + @Test public void testCastToBigint() { @@ -1236,4 +1290,134 @@ public void testIndeterminate() assertThat(assertions.operator(INDETERMINATE, "JSON '\"-Infinity\"'")) .isEqualTo(false); } + + @Test + public void testCastToNumber() + { + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON 'null'")) + .isNull(NUMBER); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '128'")) + .matches("NUMBER '128'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '12345678901234567890'")) + .matches("NUMBER '12345678901234567890'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '12345678901234567890123456789012345678'")) + .matches("NUMBER '12345678901234567890123456789012345678'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "json_literal_fixed('123456789012345678901234567890.123456789012345678901234567890')")) + .matches("NUMBER '123456789012345678901234567890.123456789012345678901234567890'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '128.9'")) + .matches("NUMBER '128.9'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "json_literal_fixed('1e-324')")) + .matches("NUMBER '1E-324'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "json_literal_fixed('1e308')")) + .matches("NUMBER '1e308'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON 'true'")) + .matches("NUMBER '1'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON 'false'")) + .matches("NUMBER '0'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '\"128\"'")) + .matches("NUMBER '128'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '\"128.456\"'")) + .matches("NUMBER '128.456'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '\"NaN\"'")) + .matches("NUMBER 'NaN'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '\"Infinity\"'")) + .matches("NUMBER '+Infinity'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '\"+Infinity\"'")) + .matches("NUMBER '+Infinity'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '\"-Infinity\"'")) + .matches("NUMBER '-Infinity'"); + + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '\"abc\"'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON '{ \"x\" : 123}'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // with spaces + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON ' \" -128.456 \" '")) + .matches("NUMBER '-128.456'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON ' \" NaN \" '")) + .matches("NUMBER 'NaN'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON ' \" Infinity \" '")) + .matches("NUMBER '+Infinity'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON ' \" +Infinity \" '")) + .matches("NUMBER '+Infinity'"); + + assertThat(assertions.expression("cast(a as NUMBER)") + .binding("a", "JSON ' \" -Infinity \" '")) + .matches("NUMBER '-Infinity'"); + } + + @Test + public void testCastFromNumber() + { + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as NUMBER)")) + .isNull(JSON); + + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "NUMBER '3.14'")) + .hasType(JSON) + .isEqualTo("3.14"); + + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "NUMBER '12345678901234567890.123456789012345678'")) + .hasType(JSON) + .isEqualTo("12345678901234567890.123456789012345678"); + + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "NUMBER 'NaN'")) + .hasType(JSON) + .isEqualTo("\"NaN\""); + + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "NUMBER '+Infinity'")) + .hasType(JSON) + .isEqualTo("\"+Infinity\""); + + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "NUMBER '-Infinity'")) + .hasType(JSON) + .isEqualTo("\"-Infinity\""); + } }