diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToArrayCast.java b/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToArrayCast.java index dce9acc4d026..b79412579a03 100644 --- a/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToArrayCast.java +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToArrayCast.java @@ -90,6 +90,9 @@ public static Block toArray(ArrayType arrayType, BlockBuilderAppender arrayAppen try (JsonParser jsonParser = createJsonParser(JSON_MAPPER, json)) { jsonParser.nextToken(); if (jsonParser.getCurrentToken() == JsonToken.VALUE_NULL) { + if (jsonParser.nextToken() != null) { + throw new JsonCastException(format("Unexpected trailing token: %s", jsonParser.getText())); + } return null; } diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToMapCast.java b/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToMapCast.java index b005656a5531..aede2a3b4f9a 100644 --- a/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToMapCast.java +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToMapCast.java @@ -94,6 +94,9 @@ public static SqlMap toMap(MapType mapType, BlockBuilderAppender mapAppender, Sl try (JsonParser jsonParser = createJsonParser(JSON_MAPPER, json)) { jsonParser.nextToken(); if (jsonParser.getCurrentToken() == JsonToken.VALUE_NULL) { + if (jsonParser.nextToken() != null) { + throw new JsonCastException(format("Unexpected trailing token: %s", jsonParser.getText())); + } return null; } diff --git a/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToRowCast.java b/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToRowCast.java index bae601d72d2d..c513fb6d6d32 100644 --- a/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToRowCast.java +++ b/core/trino-main/src/main/java/io/trino/operator/scalar/JsonToRowCast.java @@ -95,6 +95,9 @@ public static SqlRow toRow(RowType rowType, BlockBuilderAppender rowAppender, Sl try (JsonParser jsonParser = createJsonParser(JSON_MAPPER, json)) { jsonParser.nextToken(); if (jsonParser.getCurrentToken() == JsonToken.VALUE_NULL) { + if (jsonParser.nextToken() != null) { + throw new JsonCastException(format("Unexpected trailing token: %s", jsonParser.getText())); + } return null; } 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 00d396aa9885..5c0962620f03 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 @@ -411,6 +411,14 @@ public void testJsonToArraySmoke() .binding("a", "JSON 'null'")) .isNull(new ArrayType(BIGINT)); + assertThat(assertions.expression("CAST(json_parse(a) AS array(BIGINT))") + .binding("a", "'null'")) + .isNull(new ArrayType(BIGINT)); + + assertTrinoExceptionThrownBy(assertions.expression("CAST(json_parse(a) AS array(BIGINT))") + .binding("a", "'null 123 some invalid JSON content'")::evaluate) + .hasMessage("Cannot cast to array(bigint). Unexpected trailing token: 123\nnull 123 some invalid JSON content"); + assertThat(assertions.expression("CAST(a AS array(BIGINT))") .binding("a", "JSON '[]'")) .hasType(new ArrayType(BIGINT)) @@ -1158,10 +1166,34 @@ public void testCastJsonToArrayVarchar() .matches("CAST(ARRAY['test', '', 'data'] AS ARRAY(VARCHAR))"); // array with various types including scientific notation and string "null" + String inputJsonArray = "[true, false, 12, 12.3, 1.23E1, 0, 0.000000000000000, 0e1000, 0e-1000, 1, 100000000000000000000000000000000000000000000000000000000000000000000e-68, 0.100000000000000, \"puppies\", \"kittens\", \"null\", null]"; + String expectedVarcharArray = "ARRAY[VARCHAR 'true', 'false', '12', '1.23E1', '1.23E1', '0', '0E0', '0E0', '0E0', '1', '1.0E0', '1.0E-1', 'puppies', 'kittens', 'null', null]"; assertThat(assertions.expression("cast(a as ARRAY(VARCHAR))") - .binding("a", "JSON '[true, false, 12, 12.3, 1.23E1, \"puppies\", \"kittens\", \"null\", null]'")) + .binding("a", "JSON '" + inputJsonArray + "'")) + .hasType(new ArrayType(VARCHAR)) + .matches(expectedVarcharArray); + // Same with json_parse, exercising SpecializeCastWithJsonParse + assertThat(assertions.expression("cast(json_parse(a) as ARRAY(VARCHAR))") + .binding("a", "'" + inputJsonArray + "'")) .hasType(new ArrayType(VARCHAR)) - .matches("CAST(ARRAY['true', 'false', '12', '1.23E1', '1.23E1', 'puppies', 'kittens', 'null', null] AS ARRAY(VARCHAR))"); + .matches(expectedVarcharArray); + + // Number with leading zeros + assertTrinoExceptionThrownBy(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '[000]'")::evaluate) + .hasMessage("line 3:16: '[000]' is not a valid JSON literal"); + assertTrinoExceptionThrownBy(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '[000.0]'")::evaluate) + .hasMessage("line 3:16: '[000.0]' is not a valid JSON literal"); + // Number with leading zeros with json_parse, exercising SpecializeCastWithJsonParse + assertTrinoExceptionThrownBy(assertions.expression("cast(json_parse(a) as ARRAY(VARCHAR))") + .binding("a", "'[000]'")::evaluate) + // TODO the exception message could be better + .hasMessage("Cannot cast to array(varchar).\n[000]"); + assertTrinoExceptionThrownBy(assertions.expression("cast(json_parse(a) as ARRAY(VARCHAR))") + .binding("a", "'[000.0]'")::evaluate) + // TODO the exception message could be better + .hasMessage("Cannot cast to array(varchar).\n[000.0]"); // non-array JSON should fail assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(VARCHAR))") 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 37fca68347c0..a0c729d5985a 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 @@ -873,7 +873,42 @@ public void testCastToVarchar() .hasType(VARCHAR) .isEqualTo("128"); - // overflow, no loss of precision + assertThat(assertions.expression("cast(a as VARCHAR)") + .binding("a", "JSON '0'")) + .hasType(VARCHAR) + .isEqualTo("0"); + + assertThat(assertions.expression("cast(a as VARCHAR)") + .binding("a", "JSON '0.000000000000000'")) + .hasType(VARCHAR) + .isEqualTo("0E0"); + + assertThat(assertions.expression("cast(a as VARCHAR)") + .binding("a", "JSON '0e1000'")) + .hasType(VARCHAR) + .isEqualTo("0E0"); + + assertThat(assertions.expression("cast(a as VARCHAR)") + .binding("a", "JSON '0e-1000'")) + .hasType(VARCHAR) + .isEqualTo("0E0"); + + assertThat(assertions.expression("cast(a as VARCHAR)") + .binding("a", "JSON '1'")) + .hasType(VARCHAR) + .isEqualTo("1"); + + assertThat(assertions.expression("cast(a as VARCHAR)") + .binding("a", "JSON '100000000000000000000000000000000000000000000000000000000000000000000e-68'")) + .hasType(VARCHAR) + .isEqualTo("1.0E0"); + + assertThat(assertions.expression("cast(a as VARCHAR)") + .binding("a", "JSON '0.100000000000000'")) + .hasType(VARCHAR) + .isEqualTo("1.0E-1"); + + // overflow if parsed as long, no loss of precision assertThat(assertions.expression("cast(a as VARCHAR)") .binding("a", "JSON '12345678901234567890'")) .hasType(VARCHAR) diff --git a/core/trino-main/src/test/java/io/trino/type/TestMapOperators.java b/core/trino-main/src/test/java/io/trino/type/TestMapOperators.java index 4a8971f966e9..7280ff1a3d7e 100644 --- a/core/trino-main/src/test/java/io/trino/type/TestMapOperators.java +++ b/core/trino-main/src/test/java/io/trino/type/TestMapOperators.java @@ -402,6 +402,14 @@ public void testJsonToMap() .binding("a", "JSON 'null'")) .isNull(mapType(BIGINT, BIGINT)); + assertThat(assertions.expression("cast(json_parse(a) as MAP(BIGINT, BIGINT))") + .binding("a", "'null'")) + .isNull(mapType(BIGINT, BIGINT)); + + assertTrinoExceptionThrownBy(assertions.expression("cast(json_parse(a) as MAP(BIGINT, BIGINT))") + .binding("a", "'null 123 some invalid JSON content'")::evaluate) + .hasMessage("Cannot cast to map(bigint, bigint). Unexpected trailing token: 123\nnull 123 some invalid JSON content"); + assertThat(assertions.expression("cast(a as MAP(BIGINT, BIGINT))") .binding("a", "JSON '{}'")) .hasType(mapType(BIGINT, BIGINT)) diff --git a/core/trino-main/src/test/java/io/trino/type/TestRowOperators.java b/core/trino-main/src/test/java/io/trino/type/TestRowOperators.java index 53b6bc2d7dd5..5a7474e17af3 100644 --- a/core/trino-main/src/test/java/io/trino/type/TestRowOperators.java +++ b/core/trino-main/src/test/java/io/trino/type/TestRowOperators.java @@ -240,6 +240,14 @@ public void testJsonToRow() .binding("a", "JSON 'null'")) .isNull(RowType.anonymous(ImmutableList.of(BIGINT))); + assertThat(assertions.expression("CAST(json_parse(a) as ROW(BIGINT))") + .binding("a", "'null'")) + .isNull(RowType.anonymous(ImmutableList.of(BIGINT))); + + assertTrinoExceptionThrownBy(assertions.expression("CAST(json_parse(a) as ROW(BIGINT))") + .binding("a", "'null 123 some invalid JSON content'")::evaluate) + .hasMessage("Cannot cast to row(bigint). Unexpected trailing token: 123\nnull 123 some invalid JSON content"); + assertThat(assertions.expression("CAST(a as ROW(VARCHAR, BIGINT))") .binding("a", "JSON '[null, null]'")) .hasType(RowType.anonymous(ImmutableList.of(VARCHAR, BIGINT))) diff --git a/lib/trino-plugin-toolkit/src/test/java/io/trino/plugin/base/util/TestJsonTypeUtil.java b/lib/trino-plugin-toolkit/src/test/java/io/trino/plugin/base/util/TestJsonTypeUtil.java index e381b7eaa3ea..0a00561caf3f 100644 --- a/lib/trino-plugin-toolkit/src/test/java/io/trino/plugin/base/util/TestJsonTypeUtil.java +++ b/lib/trino-plugin-toolkit/src/test/java/io/trino/plugin/base/util/TestJsonTypeUtil.java @@ -61,7 +61,7 @@ void testJsonParseInteger() } @Test - void testJsonParseDouble() + void testJsonParseDecimal() { assertThat(jsonParse(utf8Slice("3.14")).toStringUtf8()) .isEqualTo("3.14"); @@ -69,6 +69,12 @@ void testJsonParseDouble() .isEqualTo("-2.5"); assertThat(jsonParse(utf8Slice("1.0")).toStringUtf8()) .isEqualTo("1.0"); + assertThat(jsonParse(utf8Slice("100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e-106")).toStringUtf8()) + .isEqualTo("10.0"); + assertThat(jsonParse(utf8Slice("100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e-107")).toStringUtf8()) + .isEqualTo("1.0"); + assertThat(jsonParse(utf8Slice("100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e-108")).toStringUtf8()) + .isEqualTo("0.1"); } @Test @@ -81,6 +87,32 @@ void testJsonParseLargeNumber() .isEqualTo("1.2345678901234568E29"); } + @Test + void testJsonParseNumberWithLeadingZeros() + { + assertThatThrownBy(() -> jsonParse(utf8Slice("007"))) + .isInstanceOf(TrinoException.class) + .hasMessage("Cannot convert value to JSON: '007'"); + assertThatThrownBy(() -> jsonParse(utf8Slice("007.0"))) + .isInstanceOf(TrinoException.class) + .hasMessage("Cannot convert value to JSON: '007.0'"); + } + + @Test + void testJsonParseNumberWithTrailingZeros() + { + assertThat(jsonParse(utf8Slice("7000000100000000")).toStringUtf8()) + .isEqualTo("7000000100000000"); + assertThat(jsonParse(utf8Slice("7010.00")).toStringUtf8()) + .isEqualTo("7010.0"); + assertThat(jsonParse(utf8Slice("7000000100000000.000000000000")).toStringUtf8()) + .isEqualTo("7.0000001E15"); + assertThat(jsonParse(utf8Slice("7010.030")).toStringUtf8()) + .isEqualTo("7010.03"); + assertThat(jsonParse(utf8Slice("7000000100000000.0000030000000")).toStringUtf8()) + .isEqualTo("7.0000001E15"); + } + @Test void testJsonParseArray() {