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 8590959f4d2e..dce9acc4d026 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 @@ -23,7 +23,6 @@ import io.trino.spi.TrinoException; import io.trino.spi.block.Block; import io.trino.spi.block.BlockBuilder; -import io.trino.spi.connector.ConnectorSession; import io.trino.spi.function.BoundSignature; import io.trino.spi.function.FunctionMetadata; import io.trino.spi.function.Signature; @@ -53,7 +52,7 @@ public class JsonToArrayCast extends SqlScalarFunction { public static final JsonToArrayCast JSON_TO_ARRAY = new JsonToArrayCast(); - private static final MethodHandle METHOD_HANDLE = methodHandle(JsonToArrayCast.class, "toArray", ArrayType.class, BlockBuilderAppender.class, ConnectorSession.class, Slice.class); + private static final MethodHandle METHOD_HANDLE = methodHandle(JsonToArrayCast.class, "toArray", ArrayType.class, BlockBuilderAppender.class, Slice.class); private static final JsonMapper JSON_MAPPER = new JsonMapper(createJsonFactory()); @@ -86,7 +85,7 @@ protected SpecializedSqlScalarFunction specialize(BoundSignature boundSignature) } @UsedByGeneratedCode - public static Block toArray(ArrayType arrayType, BlockBuilderAppender arrayAppender, ConnectorSession connectorSession, Slice json) + public static Block toArray(ArrayType arrayType, BlockBuilderAppender arrayAppender, Slice json) { try (JsonParser jsonParser = createJsonParser(JSON_MAPPER, json)) { jsonParser.nextToken(); 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 db47d17a8287..b005656a5531 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 @@ -24,7 +24,6 @@ import io.trino.spi.block.Block; import io.trino.spi.block.BlockBuilder; import io.trino.spi.block.SqlMap; -import io.trino.spi.connector.ConnectorSession; import io.trino.spi.function.BoundSignature; import io.trino.spi.function.FunctionMetadata; import io.trino.spi.function.Signature; @@ -56,7 +55,7 @@ public class JsonToMapCast extends SqlScalarFunction { public static final JsonToMapCast JSON_TO_MAP = new JsonToMapCast(); - private static final MethodHandle METHOD_HANDLE = methodHandle(JsonToMapCast.class, "toMap", MapType.class, BlockBuilderAppender.class, ConnectorSession.class, Slice.class); + private static final MethodHandle METHOD_HANDLE = methodHandle(JsonToMapCast.class, "toMap", MapType.class, BlockBuilderAppender.class, Slice.class); private static final JsonMapper JSON_MAPPER = new JsonMapper(createJsonFactory()); @@ -90,7 +89,7 @@ protected SpecializedSqlScalarFunction specialize(BoundSignature boundSignature) } @UsedByGeneratedCode - public static SqlMap toMap(MapType mapType, BlockBuilderAppender mapAppender, ConnectorSession connectorSession, Slice json) + public static SqlMap toMap(MapType mapType, BlockBuilderAppender mapAppender, Slice json) { try (JsonParser jsonParser = createJsonParser(JSON_MAPPER, json)) { jsonParser.nextToken(); 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 65f925295d64..bae601d72d2d 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 @@ -24,7 +24,6 @@ import io.trino.spi.block.Block; import io.trino.spi.block.BlockBuilder; import io.trino.spi.block.SqlRow; -import io.trino.spi.connector.ConnectorSession; import io.trino.spi.function.BoundSignature; import io.trino.spi.function.FunctionMetadata; import io.trino.spi.function.Signature; @@ -54,7 +53,7 @@ public class JsonToRowCast extends SqlScalarFunction { public static final JsonToRowCast JSON_TO_ROW = new JsonToRowCast(); - private static final MethodHandle METHOD_HANDLE = methodHandle(JsonToRowCast.class, "toRow", RowType.class, BlockBuilderAppender.class, ConnectorSession.class, Slice.class); + private static final MethodHandle METHOD_HANDLE = methodHandle(JsonToRowCast.class, "toRow", RowType.class, BlockBuilderAppender.class, Slice.class); private static final JsonMapper JSON_MAPPER = new JsonMapper(createJsonFactory()); @@ -91,11 +90,7 @@ protected SpecializedSqlScalarFunction specialize(BoundSignature boundSignature) } @UsedByGeneratedCode - public static SqlRow toRow( - RowType rowType, - BlockBuilderAppender rowAppender, - ConnectorSession connectorSession, - Slice json) + public static SqlRow toRow(RowType rowType, BlockBuilderAppender rowAppender, Slice json) { try (JsonParser jsonParser = createJsonParser(JSON_MAPPER, json)) { jsonParser.nextToken(); diff --git a/core/trino-main/src/main/java/io/trino/sql/ir/optimizer/rule/SpecializeCastWithJsonParse.java b/core/trino-main/src/main/java/io/trino/sql/ir/optimizer/rule/SpecializeCastWithJsonParse.java index 9c936db10530..e52cf0c4c2be 100644 --- a/core/trino-main/src/main/java/io/trino/sql/ir/optimizer/rule/SpecializeCastWithJsonParse.java +++ b/core/trino-main/src/main/java/io/trino/sql/ir/optimizer/rule/SpecializeCastWithJsonParse.java @@ -34,6 +34,12 @@ import static io.trino.operator.scalar.JsonStringToMapCast.JSON_STRING_TO_MAP_NAME; import static io.trino.operator.scalar.JsonStringToRowCast.JSON_STRING_TO_ROW_NAME; +/** + * Replaces certain {@code CAST(json_parse(x) AS T)} with functions logically + * implementing {@code CAST(a_json AS T)} along with validation that input is + * well-formed JSON. This avoids cost of validation and canonicalization done + * by {@code json_parse}. + */ public class SpecializeCastWithJsonParse implements IrOptimizerRule { 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 a3a3c5ba70c8..d92448360def 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 @@ -22,7 +22,6 @@ import com.google.common.primitives.SignedBytes; import io.airlift.slice.Slice; import io.airlift.slice.SliceOutput; -import io.airlift.slice.Slices; import io.trino.spi.TrinoException; import io.trino.spi.block.ArrayBlockBuilder; import io.trino.spi.block.Block; @@ -81,6 +80,7 @@ import static com.fasterxml.jackson.core.JsonToken.START_ARRAY; import static com.fasterxml.jackson.core.JsonToken.START_OBJECT; import static com.google.common.base.Verify.verify; +import static io.airlift.slice.Slices.utf8Slice; import static io.trino.plugin.base.util.JsonUtils.jsonFactoryBuilder; import static io.trino.spi.StandardErrorCode.INVALID_CAST_ARGUMENT; import static io.trino.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; @@ -696,12 +696,8 @@ public static Slice currentTokenAsVarchar(JsonParser parser) { return switch (parser.currentToken()) { case VALUE_NULL -> null; - case VALUE_STRING, FIELD_NAME -> Slices.utf8Slice(parser.getText()); - // Avoidance of loss of precision does not seem to be possible here because of Jackson implementation. - case VALUE_NUMBER_FLOAT -> DoubleOperators.castToVarchar(UNBOUNDED_LENGTH, parser.getDoubleValue()); - // An alternative is calling getLongValue and then BigintOperators.castToVarchar. - // It doesn't work as well because it can result in overflow and underflow exceptions for large integral numbers. - case VALUE_NUMBER_INT -> Slices.utf8Slice(parser.getText()); + case VALUE_STRING, FIELD_NAME -> utf8Slice(parser.getText()); + case VALUE_NUMBER_INT, VALUE_NUMBER_FLOAT -> utf8Slice(parser.getDecimalValue().toString()); case VALUE_TRUE -> BooleanOperators.castToVarchar(UNBOUNDED_LENGTH, true); case VALUE_FALSE -> BooleanOperators.castToVarchar(UNBOUNDED_LENGTH, false); default -> throw new JsonCastException(format("Unexpected token when cast to %s: %s", StandardTypes.VARCHAR, parser.getText())); @@ -713,7 +709,7 @@ public static Long currentTokenAsBigint(JsonParser parser) { return switch (parser.currentToken()) { case VALUE_NULL -> null; - case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToBigint(Slices.utf8Slice(parser.getText())); + case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToBigint(utf8Slice(parser.getText())); case VALUE_NUMBER_FLOAT -> DoubleOperators.castToLong(parser.getDoubleValue()); case VALUE_NUMBER_INT -> parser.getLongValue(); case VALUE_TRUE -> BooleanOperators.castToBigint(true); @@ -727,7 +723,7 @@ public static Long currentTokenAsInteger(JsonParser parser) { return switch (parser.currentToken()) { case VALUE_NULL -> null; - case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToInteger(Slices.utf8Slice(parser.getText())); + case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToInteger(utf8Slice(parser.getText())); case VALUE_NUMBER_FLOAT -> DoubleOperators.castToInteger(parser.getDoubleValue()); case VALUE_NUMBER_INT -> (long) toIntExact(parser.getLongValue()); case VALUE_TRUE -> BooleanOperators.castToInteger(true); @@ -741,7 +737,7 @@ public static Long currentTokenAsSmallint(JsonParser parser) { return switch (parser.currentToken()) { case VALUE_NULL -> null; - case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToSmallint(Slices.utf8Slice(parser.getText())); + case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToSmallint(utf8Slice(parser.getText())); case VALUE_NUMBER_FLOAT -> DoubleOperators.castToSmallint(parser.getDoubleValue()); case VALUE_NUMBER_INT -> (long) Shorts.checkedCast(parser.getLongValue()); case VALUE_TRUE -> BooleanOperators.castToSmallint(true); @@ -755,7 +751,7 @@ public static Long currentTokenAsTinyint(JsonParser parser) { return switch (parser.currentToken()) { case VALUE_NULL -> null; - case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToTinyint(Slices.utf8Slice(parser.getText())); + case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToTinyint(utf8Slice(parser.getText())); case VALUE_NUMBER_FLOAT -> DoubleOperators.castToTinyint(parser.getDoubleValue()); case VALUE_NUMBER_INT -> (long) SignedBytes.checkedCast(parser.getLongValue()); case VALUE_TRUE -> BooleanOperators.castToTinyint(true); @@ -769,7 +765,7 @@ public static Double currentTokenAsDouble(JsonParser parser) { return switch (parser.currentToken()) { case VALUE_NULL -> null; - case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToDouble(Slices.utf8Slice(parser.getText())); + case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToDouble(utf8Slice(parser.getText())); case VALUE_NUMBER_FLOAT -> parser.getDoubleValue(); // An alternative is calling getLongValue and then BigintOperators.castToDouble. // It doesn't work as well because it can result in overflow and underflow exceptions for large integral numbers. @@ -785,7 +781,7 @@ public static Long currentTokenAsReal(JsonParser parser) { return switch (parser.currentToken()) { case VALUE_NULL -> null; - case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToFloat(Slices.utf8Slice(parser.getText())); + case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToFloat(utf8Slice(parser.getText())); case VALUE_NUMBER_FLOAT -> (long) floatToRawIntBits(parser.getFloatValue()); // An alternative is calling getLongValue and then BigintOperators.castToReal. // It doesn't work as well because it can result in overflow and underflow exceptions for large integral numbers. @@ -801,7 +797,7 @@ public static Boolean currentTokenAsBoolean(JsonParser parser) { return switch (parser.currentToken()) { case VALUE_NULL -> null; - case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToBoolean(Slices.utf8Slice(parser.getText())); + case VALUE_STRING, FIELD_NAME -> VarcharOperators.castToBoolean(utf8Slice(parser.getText())); case VALUE_NUMBER_FLOAT -> DoubleOperators.castToBoolean(parser.getDoubleValue()); case VALUE_NUMBER_INT -> BigintOperators.castToBoolean(parser.getLongValue()); case VALUE_TRUE -> true; @@ -907,7 +903,7 @@ static BlockBuilderAppender createBlockBuilderAppender(Type type) if (type instanceof JsonType) { return (parser, blockBuilder) -> { String json = JSON_MAPPED_UNORDERED.writeValueAsString(parser.readValueAsTree()); - JSON.writeSlice(blockBuilder, Slices.utf8Slice(json)); + JSON.writeSlice(blockBuilder, utf8Slice(json)); }; } if (type instanceof ArrayType arrayType) { 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 2b20ae50a514..d6a42824b06c 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 @@ -292,7 +292,7 @@ public void testArraySize() } @Test - public void testArrayToJson() + public void testArrayToJsonSmoke() { assertThat(assertions.expression("CAST(a AS JSON)") .binding("a", "CAST(null as ARRAY(BIGINT))")) @@ -400,7 +400,7 @@ public void testArrayToJson() } @Test - public void testJsonToArray() + public void testJsonToArraySmoke() { // special values assertThat(assertions.expression("CAST(a AS array(BIGINT))") @@ -485,7 +485,7 @@ public void testJsonToArray() assertThat(assertions.expression("CAST(a AS array(VARCHAR))") .binding("a", "JSON '[true, false, 12, 12.3, \"puppies\", \"kittens\", \"null\", \"\", null]'")) .hasType(new ArrayType(VARCHAR)) - .isEqualTo(asList("true", "false", "12", "1.23E1", "puppies", "kittens", "null", "", null)); + .isEqualTo(asList("true", "false", "12", "12.3", "puppies", "kittens", "null", "", null)); assertThat(assertions.expression("CAST(a AS array(JSON))") .binding("a", "JSON '[5, 3.14, [1, 2, 3], \"e\", {\"a\": \"b\"}, null, \"null\", [null]]'")) @@ -583,7 +583,7 @@ public void testJsonToArray() assertTrinoExceptionThrownBy(() -> assertions.expression("CAST(a AS array(INTEGER))") .binding("a", "JSON '[1234567890123.456]'").evaluate()) - .hasMessage("Cannot cast to array(integer). Out of range for integer: 1.234567890123456E12\n[1.234567890123456E12]") + .hasMessage("Cannot cast to array(integer). Out of range for integer: 1.234567890123456E12\n[1234567890123.456]") .hasErrorCode(INVALID_CAST_ARGUMENT); assertThat(assertions.expression("CAST(a AS array(DECIMAL(10,5)))") @@ -620,7 +620,943 @@ public void testJsonToArray() } @Test - public void testConstructor() // TODO + public void testCastJsonToArrayBoolean() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(BOOLEAN))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(BOOLEAN)); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(BOOLEAN))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(BOOLEAN)) + .matches("CAST(ARRAY[] AS ARRAY(BOOLEAN))"); + + // array with boolean elements + assertThat(assertions.expression("cast(a as ARRAY(BOOLEAN))") + .binding("a", "JSON '[true, false, true]'")) + .hasType(new ArrayType(BOOLEAN)) + .matches("ARRAY[true, false, true]"); + + // array with null element + assertThat(assertions.expression("cast(a as ARRAY(BOOLEAN))") + .binding("a", "JSON '[true, null, false]'")) + .hasType(new ArrayType(BOOLEAN)) + .matches("ARRAY[true, null, false]"); + + // array with number elements (0 -> false, non-zero -> true) + assertThat(assertions.expression("cast(a as ARRAY(BOOLEAN))") + .binding("a", "JSON '[0, 1, 128, -5]'")) + .hasType(new ArrayType(BOOLEAN)) + .matches("ARRAY[false, true, true, true]"); + + // array with string elements + assertThat(assertions.expression("cast(a as ARRAY(BOOLEAN))") + .binding("a", "JSON '[\"true\", \"false\", \"True\"]'")) + .hasType(new ArrayType(BOOLEAN)) + .matches("ARRAY[true, false, true]"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(BOOLEAN))") + .binding("a", "JSON 'true'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // array with invalid string + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(BOOLEAN))") + .binding("a", "JSON '[\"abc\"]'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastJsonToArrayTinyint() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(TINYINT)); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(TINYINT)) + .matches("CAST(ARRAY[] AS ARRAY(TINYINT))"); + + // array with elements + assertThat(assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON '[12, 34, 56]'")) + .hasType(new ArrayType(TINYINT)) + .matches("ARRAY[TINYINT '12', TINYINT '34', TINYINT '56']"); + + // array with null element + assertThat(assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON '[1, null, 3]'")) + .hasType(new ArrayType(TINYINT)) + .matches("ARRAY[TINYINT '1', null, TINYINT '3']"); + + // array with decimal numbers (should round) + assertThat(assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON '[12.9, 42.1]'")) + .hasType(new ArrayType(TINYINT)) + .matches("ARRAY[TINYINT '13', TINYINT '42']"); + + // array with extreme values + assertThat(assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON '[127, -128]'")) + .hasType(new ArrayType(TINYINT)) + .matches("ARRAY[TINYINT '127', TINYINT '-128']"); + + // array with boolean values + assertThat(assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON '[true, false]'")) + .hasType(new ArrayType(TINYINT)) + .matches("ARRAY[TINYINT '1', TINYINT '0']"); + + // array with string numbers + assertThat(assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON '[\"12\", \"34\"]'")) + .hasType(new ArrayType(TINYINT)) + .matches("ARRAY[TINYINT '12', TINYINT '34']"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON '12'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // array with number overflow + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(TINYINT))") + .binding("a", "JSON '[1234]'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastJsonToArraySmallint() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(SMALLINT)); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(SMALLINT)) + .matches("CAST(ARRAY[] AS ARRAY(SMALLINT))"); + + // array with elements + assertThat(assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON '[128, 256, 512]'")) + .hasType(new ArrayType(SMALLINT)) + .matches("ARRAY[SMALLINT '128', SMALLINT '256', SMALLINT '512']"); + + // array with null element + assertThat(assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON '[1, null, 3]'")) + .hasType(new ArrayType(SMALLINT)) + .matches("ARRAY[SMALLINT '1', null, SMALLINT '3']"); + + // array with decimal numbers (should round) + assertThat(assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON '[128.9, 42.1]'")) + .hasType(new ArrayType(SMALLINT)) + .matches("ARRAY[SMALLINT '129', SMALLINT '42']"); + + // array with extreme values + assertThat(assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON '[32767, -32768]'")) + .hasType(new ArrayType(SMALLINT)) + .matches("ARRAY[SMALLINT '32767', SMALLINT '-32768']"); + + // array with boolean values + assertThat(assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON '[true, false]'")) + .hasType(new ArrayType(SMALLINT)) + .matches("ARRAY[SMALLINT '1', SMALLINT '0']"); + + // array with string numbers + assertThat(assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON '[\"12345\", \"-12345\"]'")) + .hasType(new ArrayType(SMALLINT)) + .matches("ARRAY[SMALLINT '12345', SMALLINT '-12345']"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON '128'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // array with number overflow + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(SMALLINT))") + .binding("a", "JSON '[123456]'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastJsonToArrayInteger() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(INTEGER)); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(INTEGER)) + .matches("CAST(ARRAY[] AS ARRAY(INTEGER))"); + + // array with elements + assertThat(assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON '[128, 256, 512]'")) + .hasType(new ArrayType(INTEGER)) + .matches("ARRAY[128, 256, 512]"); + + // array with null element + assertThat(assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON '[1, null, 3]'")) + .hasType(new ArrayType(INTEGER)) + .matches("ARRAY[1, null, 3]"); + + // array with decimal numbers (should round) + assertThat(assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON '[128.9, 42.1]'")) + .hasType(new ArrayType(INTEGER)) + .matches("ARRAY[129, 42]"); + + // array with extreme values + assertThat(assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON '[2147483647, -2147483648]'")) + .hasType(new ArrayType(INTEGER)) + .matches("ARRAY[2147483647, -2147483648]"); + + // array with boolean values + assertThat(assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON '[true, false]'")) + .hasType(new ArrayType(INTEGER)) + .matches("ARRAY[1, 0]"); + + // array with string numbers + assertThat(assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON '[\"12345678\", \"-12345678\"]'")) + .hasType(new ArrayType(INTEGER)) + .matches("ARRAY[12345678, -12345678]"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON '128'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // array with number overflow + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(INTEGER))") + .binding("a", "JSON '[12345678901]'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastJsonToArrayBigint() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(BIGINT)); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(BIGINT)) + .matches("CAST(ARRAY[] AS ARRAY(BIGINT))"); + + // array with single element + assertThat(assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[128]'")) + .hasType(new ArrayType(BIGINT)) + .matches("ARRAY[BIGINT '128']"); + + // array with multiple elements + assertThat(assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[1, 2, 3, 4, 5]'")) + .hasType(new ArrayType(BIGINT)) + .matches("ARRAY[BIGINT '1', BIGINT '2', BIGINT '3', BIGINT '4', BIGINT '5']"); + + // array with null element + assertThat(assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[1, null, 3]'")) + .hasType(new ArrayType(BIGINT)) + .matches("ARRAY[BIGINT '1', null, BIGINT '3']"); + + // array with decimal numbers (should round) + assertThat(assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[128.9, 42.1]'")) + .hasType(new ArrayType(BIGINT)) + .matches("ARRAY[BIGINT '129', BIGINT '42']"); + + // array with extreme values + assertThat(assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[9223372036854775807, -9223372036854775808]'")) + .hasType(new ArrayType(BIGINT)) + .matches("ARRAY[BIGINT '9223372036854775807', BIGINT '-9223372036854775808']"); + + // array with boolean values + assertThat(assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[true, false, true]'")) + .hasType(new ArrayType(BIGINT)) + .matches("ARRAY[BIGINT '1', BIGINT '0', BIGINT '1']"); + + // array with string numbers + assertThat(assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[\"1234567891234567\", \"-9999999999\"]'")) + .hasType(new ArrayType(BIGINT)) + .matches("ARRAY[BIGINT '1234567891234567', BIGINT '-9999999999']"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '128'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '{\"a\": 1}'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // array with string that cannot be cast to bigint + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[\"abc\"]'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // array with number overflow + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(BIGINT))") + .binding("a", "JSON '[12345678901234567890]'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastJsonToArrayReal() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(REAL))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(REAL)); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(REAL))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(REAL)) + .matches("CAST(ARRAY[] AS ARRAY(REAL))"); + + // array with elements + assertThat(assertions.expression("cast(a as ARRAY(REAL))") + .binding("a", "JSON '[128.9, 42.1, -3.14]'")) + .hasType(new ArrayType(REAL)) + .matches("ARRAY[REAL '128.9', REAL '42.1', REAL '-3.14']"); + + // array with null element + assertThat(assertions.expression("cast(a as ARRAY(REAL))") + .binding("a", "JSON '[1.5, null, 3.7]'")) + .hasType(new ArrayType(REAL)) + .matches("ARRAY[REAL '1.5', null, REAL '3.7']"); + + // array with boolean values + assertThat(assertions.expression("cast(a as ARRAY(REAL))") + .binding("a", "JSON '[true, false]'")) + .hasType(new ArrayType(REAL)) + .matches("ARRAY[REAL '1.0', REAL '0.0']"); + + // array with string elements + assertThat(assertions.expression("cast(a as ARRAY(REAL))") + .binding("a", "JSON '[\"128.9\", \"NaN\", \"Infinity\", \"-Infinity\"]'")) + .hasType(new ArrayType(REAL)) + .matches("ARRAY[REAL '128.9', CAST(nan() AS REAL), CAST(infinity() AS REAL), CAST(-infinity() AS REAL)]"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(REAL))") + .binding("a", "JSON '128.9'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastJsonToArrayDouble() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(DOUBLE))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(DOUBLE)); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(DOUBLE))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(DOUBLE)) + .matches("CAST(ARRAY[] AS ARRAY(DOUBLE))"); + + // array with elements + assertThat(assertions.expression("cast(a as ARRAY(DOUBLE))") + .binding("a", "JSON '[128.9, 42.1, -3.14]'")) + .hasType(new ArrayType(DOUBLE)) + .matches("ARRAY[DOUBLE '128.9', DOUBLE '42.1', DOUBLE '-3.14']"); + + // array with null element + assertThat(assertions.expression("cast(a as ARRAY(DOUBLE))") + .binding("a", "JSON '[1.5, null, 3.7]'")) + .hasType(new ArrayType(DOUBLE)) + .matches("ARRAY[DOUBLE '1.5', null, DOUBLE '3.7']"); + + // array with boolean values + assertThat(assertions.expression("cast(a as ARRAY(DOUBLE))") + .binding("a", "JSON '[true, false]'")) + .hasType(new ArrayType(DOUBLE)) + .matches("ARRAY[DOUBLE '1.0', DOUBLE '0.0']"); + + // array with large numbers + assertThat(assertions.expression("cast(a as ARRAY(DOUBLE))") + .binding("a", "JSON '[12345678901234567890]'")) + .hasType(new ArrayType(DOUBLE)) + .matches("ARRAY[DOUBLE '1.2345678901234567e19']"); + + // array with string elements + assertThat(assertions.expression("cast(a as ARRAY(DOUBLE))") + .binding("a", "JSON '[\"128.9\", \"NaN\", \"Infinity\", \"-Infinity\"]'")) + .hasType(new ArrayType(DOUBLE)) + .matches("ARRAY[DOUBLE '128.9', nan(), infinity(), -infinity()]"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(DOUBLE))") + .binding("a", "JSON '128.9'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastJsonToArrayDecimal() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(10,3)))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(createDecimalType(10, 3))); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(10,3)))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(createDecimalType(10, 3))) + .matches("CAST(ARRAY[] AS ARRAY(DECIMAL(10,3)))"); + + // array with single element + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(10,3)))") + .binding("a", "JSON '[128]'")) + .hasType(new ArrayType(createDecimalType(10, 3))) + .matches("CAST(ARRAY[DECIMAL '128.000'] AS ARRAY(DECIMAL(10,3)))"); + + // array with multiple elements + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(10,5)))") + .binding("a", "JSON '[123.456, 789.012, 345.678]'")) + .hasType(new ArrayType(createDecimalType(10, 5))) + .matches("CAST(ARRAY[DECIMAL '123.45600', DECIMAL '789.01200', DECIMAL '345.67800'] AS ARRAY(DECIMAL(10,5)))"); + + // array with null element + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(10,3)))") + .binding("a", "JSON '[1.5, null, 3.7]'")) + .hasType(new ArrayType(createDecimalType(10, 3))) + .matches("CAST(ARRAY[DECIMAL '1.500', null, DECIMAL '3.700'] AS ARRAY(DECIMAL(10,3)))"); + + // array with boolean values + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(10,5)))") + .binding("a", "JSON '[true, false]'")) + .hasType(new ArrayType(createDecimalType(10, 5))) + .matches("CAST(ARRAY[DECIMAL '1.00000', DECIMAL '0.00000'] AS ARRAY(DECIMAL(10,5)))"); + + // array with string numbers + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(10,5)))") + .binding("a", "JSON '[\"3.14\", \"123.456\"]'")) + .hasType(new ArrayType(createDecimalType(10, 5))) + .matches("CAST(ARRAY[DECIMAL '3.14000', DECIMAL '123.45600'] AS ARRAY(DECIMAL(10,5)))"); + + // round-trip through JSON cast + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(10,5)))") + .binding("a", "CAST(ARRAY[1, 2.0, 3] as JSON)")) + .hasType(new ArrayType(createDecimalType(10, 5))) + .matches("CAST(ARRAY[DECIMAL '1.00000', DECIMAL '2.00000', DECIMAL '3.00000'] AS ARRAY(DECIMAL(10,5)))"); + + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(38,8)))") + .binding("a", "CAST(ARRAY[123456789012345678901234567890.12345678, 1.2] as JSON)")) + .hasType(new ArrayType(createDecimalType(38, 8))) + .matches("CAST(ARRAY[DECIMAL '123456789012345678901234567890.12345678', DECIMAL '1.20000000'] AS ARRAY(DECIMAL(38,8)))"); + + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(7,2)))") + .binding("a", "CAST(ARRAY[12345.87654] as JSON)")) + .hasType(new ArrayType(createDecimalType(7, 2))) + .matches("CAST(ARRAY[DECIMAL '12345.88'] AS ARRAY(DECIMAL(7,2)))"); + + // array with large decimal + assertThat(assertions.expression("cast(a as ARRAY(DECIMAL(38,8)))") + .binding("a", "JSON '[123456789012345678901234567890.12345678]'")) + .hasType(new ArrayType(createDecimalType(38, 8))) + .matches("CAST(ARRAY[DECIMAL '123456789012345678901234567890.12345678'] AS ARRAY(DECIMAL(38,8)))"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(DECIMAL(10,3)))") + .binding("a", "JSON '128.9'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(DECIMAL(10,3)))") + .binding("a", "JSON '{\"a\": 1.5}'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // array with string that cannot be cast to decimal + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(DECIMAL(10,3)))") + .binding("a", "JSON '[\"abc\"]'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // array with number overflow + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(DECIMAL(10,3)))") + .binding("a", "JSON '[1234567890123456]'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + // round-trip with insufficient precision + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(DECIMAL(6,2)))") + .binding("a", "CAST(ARRAY[12345.12345] as JSON)").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastJsonToArrayVarchar() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(VARCHAR)); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(VARCHAR)) + .matches("CAST(ARRAY[] AS ARRAY(VARCHAR))"); + + // array with string elements + assertThat(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '[\"hello\", \"world\", \"test\"]'")) + .hasType(new ArrayType(VARCHAR)) + .matches("CAST(ARRAY['hello', 'world', 'test'] AS ARRAY(VARCHAR))"); + + // array with null element + assertThat(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '[\"foo\", null, \"bar\"]'")) + .hasType(new ArrayType(VARCHAR)) + .matches("CAST(ARRAY['foo', null, 'bar'] AS ARRAY(VARCHAR))"); + + // array with number elements (converted to string) + assertThat(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '[128, 12345678901234567890]'")) + .hasType(new ArrayType(VARCHAR)) + .matches("CAST(ARRAY['128', '12345678901234567890'] AS ARRAY(VARCHAR))"); + + // array with boolean elements + assertThat(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '[true, false]'")) + .hasType(new ArrayType(VARCHAR)) + .matches("CAST(ARRAY['true', 'false'] AS ARRAY(VARCHAR))"); + + // array with empty string + assertThat(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '[\"test\", \"\", \"data\"]'")) + .hasType(new ArrayType(VARCHAR)) + .matches("CAST(ARRAY['test', '', 'data'] AS ARRAY(VARCHAR))"); + + // array with various types including scientific notation and string "null" + assertThat(assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '[true, false, 12, 12.3, 1.23E1, \"puppies\", \"kittens\", \"null\", null]'")) + .hasType(new ArrayType(VARCHAR)) + .matches("CAST(ARRAY['true', 'false', '12', '12.3', '12.3', 'puppies', 'kittens', 'null', null] AS ARRAY(VARCHAR))"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '\"test\"'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(VARCHAR))") + .binding("a", "JSON '{\"a\": \"test\"}'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastJsonToArrayJson() + { + // null JSON -> null array + assertThat(assertions.expression("cast(a as ARRAY(JSON))") + .binding("a", "JSON 'null'")) + .isNull(new ArrayType(JSON)); + + // empty array + assertThat(assertions.expression("cast(a as ARRAY(JSON))") + .binding("a", "JSON '[]'")) + .hasType(new ArrayType(JSON)) + .matches("CAST(ARRAY[] AS ARRAY(JSON))"); + + // array with various JSON elements + assertThat(assertions.expression("cast(a as ARRAY(JSON))") + .binding("a", "JSON '[5, 3.14, [1, 2, 3], \"e\", {\"a\": \"b\"}, null, \"null\", [null]]'")) + .hasType(new ArrayType(JSON)) + .matches("ARRAY[JSON '5', JSON '3.14', JSON '[1,2,3]', JSON '\"e\"', JSON '{\"a\":\"b\"}', JSON 'null', JSON '\"null\"', JSON '[null]']"); + + // non-array JSON should fail + assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as ARRAY(JSON))") + .binding("a", "JSON '\"test\"'").evaluate()) + .hasErrorCode(INVALID_CAST_ARGUMENT); + } + + @Test + public void testCastArrayBooleanToJson() + { + // null array -> null JSON + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as ARRAY(BOOLEAN))")) + .isNull(JSON); + + // empty array + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "CAST(ARRAY[] AS ARRAY(BOOLEAN))")) + .hasType(JSON) + .isEqualTo("[]"); + + // array with elements + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[true, false, true]")) + .hasType(JSON) + .isEqualTo("[true,false,true]"); + + // array with null element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[true, null, false]")) + .hasType(JSON) + .isEqualTo("[true,null,false]"); + + // array with all true + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[true, true, true]")) + .hasType(JSON) + .isEqualTo("[true,true,true]"); + + // array with all false + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[false, false]")) + .hasType(JSON) + .isEqualTo("[false,false]"); + } + + @Test + public void testCastArrayTinyintToJson() + { + // null array -> null JSON + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as ARRAY(TINYINT))")) + .isNull(JSON); + + // empty array + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "CAST(ARRAY[] AS ARRAY(TINYINT))")) + .hasType(JSON) + .isEqualTo("[]"); + + // array with elements + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[TINYINT '12', TINYINT '34', TINYINT '56']")) + .hasType(JSON) + .isEqualTo("[12,34,56]"); + + // array with null element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[TINYINT '1', null, TINYINT '3']")) + .hasType(JSON) + .isEqualTo("[1,null,3]"); + + // array with extreme values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[TINYINT '127', TINYINT '-128']")) + .hasType(JSON) + .isEqualTo("[127,-128]"); + } + + @Test + public void testCastArraySmallintToJson() + { + // null array -> null JSON + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as ARRAY(SMALLINT))")) + .isNull(JSON); + + // empty array + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "CAST(ARRAY[] AS ARRAY(SMALLINT))")) + .hasType(JSON) + .isEqualTo("[]"); + + // array with elements + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[SMALLINT '128', SMALLINT '256', SMALLINT '512']")) + .hasType(JSON) + .isEqualTo("[128,256,512]"); + + // array with null element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[SMALLINT '1', null, SMALLINT '3']")) + .hasType(JSON) + .isEqualTo("[1,null,3]"); + + // array with extreme values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[SMALLINT '32767', SMALLINT '-32768']")) + .hasType(JSON) + .isEqualTo("[32767,-32768]"); + } + + @Test + public void testCastArrayIntegerToJson() + { + // null array -> null JSON + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as ARRAY(INTEGER))")) + .isNull(JSON); + + // empty array + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "CAST(ARRAY[] AS ARRAY(INTEGER))")) + .hasType(JSON) + .isEqualTo("[]"); + + // array with elements + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[128, 256, 512]")) + .hasType(JSON) + .isEqualTo("[128,256,512]"); + + // array with null element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[1, null, 3]")) + .hasType(JSON) + .isEqualTo("[1,null,3]"); + + // array with negative values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[-100, 0, 100]")) + .hasType(JSON) + .isEqualTo("[-100,0,100]"); + + // array with extreme values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[2147483647, -2147483648]")) + .hasType(JSON) + .isEqualTo("[2147483647,-2147483648]"); + } + + @Test + public void testCastArrayBigintToJson() + { + // null array -> null JSON + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as ARRAY(BIGINT))")) + .isNull(JSON); + + // empty array + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "CAST(ARRAY[] AS ARRAY(BIGINT))")) + .hasType(JSON) + .isEqualTo("[]"); + + // array with single element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[BIGINT '128']")) + .hasType(JSON) + .isEqualTo("[128]"); + + // array with multiple elements + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[BIGINT '1', BIGINT '2', BIGINT '3', BIGINT '4', BIGINT '5']")) + .hasType(JSON) + .isEqualTo("[1,2,3,4,5]"); + + // array with null element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[BIGINT '1', null, BIGINT '3']")) + .hasType(JSON) + .isEqualTo("[1,null,3]"); + + // array with negative values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[BIGINT '-100', BIGINT '0', BIGINT '100']")) + .hasType(JSON) + .isEqualTo("[-100,0,100]"); + + // array with extreme values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[BIGINT '9223372036854775807', BIGINT '-9223372036854775808']")) + .hasType(JSON) + .isEqualTo("[9223372036854775807,-9223372036854775808]"); + } + + @Test + public void testCastArrayRealToJson() + { + // null array -> null JSON + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as ARRAY(REAL))")) + .isNull(JSON); + + // empty array + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "CAST(ARRAY[] AS ARRAY(REAL))")) + .hasType(JSON) + .isEqualTo("[]"); + + // array with elements + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[REAL '3.14', REAL '2.71', REAL '1.41']")) + .hasType(JSON) + .isEqualTo("[3.14,2.71,1.41]"); + + // array with null element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[REAL '1.5', null, REAL '3.7']")) + .hasType(JSON) + .isEqualTo("[1.5,null,3.7]"); + + // array with special values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[cast(nan() as REAL), cast(infinity() as REAL), cast(-infinity() as REAL)]")) + .hasType(JSON) + .isEqualTo("[\"NaN\",\"Infinity\",\"-Infinity\"]"); + + // array with negative values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[REAL '-100.5', REAL '0.0', REAL '100.5']")) + .hasType(JSON) + .isEqualTo("[-100.5,0.0,100.5]"); + } + + @Test + public void testCastArrayDoubleToJson() + { + // null array -> null JSON + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as ARRAY(DOUBLE))")) + .isNull(JSON); + + // empty array + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "CAST(ARRAY[] AS ARRAY(DOUBLE))")) + .hasType(JSON) + .isEqualTo("[]"); + + // array with elements + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[3.14, 2.71, 1.41]")) + .hasType(JSON) + .isEqualTo("[3.14,2.71,1.41]"); + + // array with null element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[1.5, null, 3.7]")) + .hasType(JSON) + .isEqualTo("[1.5,null,3.7]"); + + // array with special values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[nan(), infinity(), -infinity()]")) + .hasType(JSON) + .isEqualTo("[\"NaN\",\"Infinity\",\"-Infinity\"]"); + + // array with negative values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[-100.123, 0.0, 100.456]")) + .hasType(JSON) + .isEqualTo("[-100.123,0.000,100.456]"); + } + + @Test + public void testCastArrayDecimalToJson() + { + // null array -> null JSON + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as ARRAY(DECIMAL(10,3)))")) + .isNull(JSON); + + // empty array + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "CAST(ARRAY[] AS ARRAY(DECIMAL(10,3)))")) + .hasType(JSON) + .isEqualTo("[]"); + + // array with single element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[DECIMAL '3.14']")) + .hasType(JSON) + .isEqualTo("[3.14]"); + + // array with multiple elements + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[DECIMAL '123.456', DECIMAL '789.012', DECIMAL '345.678']")) + .hasType(JSON) + .isEqualTo("[123.456,789.012,345.678]"); + + // array with null element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[DECIMAL '1.5', null, DECIMAL '3.7']")) + .hasType(JSON) + .isEqualTo("[1.5,null,3.7]"); + + // array with negative values + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[DECIMAL '-100.123', DECIMAL '0.000', DECIMAL '100.456']")) + .hasType(JSON) + .isEqualTo("[-100.123,0.000,100.456]"); + + // array with large decimal + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[DECIMAL '12345678901234567890.123456789012345678']")) + .hasType(JSON) + .isEqualTo("[12345678901234567890.123456789012345678]"); + + // array with all nulls + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY[cast(null as DECIMAL(10,3)), cast(null as DECIMAL(10,3))]")) + .hasType(JSON) + .isEqualTo("[null,null]"); + } + + @Test + public void testCastArrayVarcharToJson() + { + // null array -> null JSON + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "cast(null as ARRAY(VARCHAR))")) + .isNull(JSON); + + // empty array + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "CAST(ARRAY[] AS ARRAY(VARCHAR))")) + .hasType(JSON) + .isEqualTo("[]"); + + // array with elements + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY['hello', 'world', 'test']")) + .hasType(JSON) + .isEqualTo("[\"hello\",\"world\",\"test\"]"); + + // array with null element + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY['foo', null, 'bar']")) + .hasType(JSON) + .isEqualTo("[\"foo\",null,\"bar\"]"); + + // array with empty string + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY['test', '', 'data']")) + .hasType(JSON) + .isEqualTo("[\"test\",\"\",\"data\"]"); + + // array with special characters + assertThat(assertions.expression("cast(a as JSON)") + .binding("a", "ARRAY['\"quoted\"', 'tab\tchar', 'new\nline']")) + .hasType(JSON) + .isEqualTo("[\"\\\"quoted\\\"\",\"tab\\tchar\",\"new\\nline\"]"); + } + + @Test + public void testConstructor() { assertThat(assertions.expression("ARRAY[]")) .hasType(new ArrayType(UNKNOWN)) 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 19316c61c2f5..c34ebb878dcf 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 @@ -73,8 +73,6 @@ public void teardown() assertions = null; } - // todo add cases for decimal - @Test public void testCastToBigint() { @@ -151,6 +149,14 @@ public void testCastToBigint() assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as BIGINT)") .binding("a", "JSON '{ \"x\" : 123}'").evaluate()) .hasErrorCode(INVALID_CAST_ARGUMENT); + + // Extreme values + assertThat(assertions.expression("cast(a as BIGINT)") + .binding("a", "JSON '9223372036854775807'")) + .isEqualTo(9223372036854775807L); + assertThat(assertions.expression("cast(a as BIGINT)") + .binding("a", "JSON '-9223372036854775808'")) + .isEqualTo(-9223372036854775808L); } @Test @@ -699,6 +705,11 @@ public void testCastToDecimal() .hasType(createDecimalType(10, 3)) .isEqualTo(decimal("128.000", createDecimalType(10, 3))); + assertThat(assertions.expression("cast(a as DECIMAL(38,8))") + .binding("a", "JSON '123456789012345678901234567890.12345678'")) + .hasType(createDecimalType(38, 8)) + .isEqualTo(decimal("123456789012345678901234567890.12345678", createDecimalType(38, 8))); + assertThat(assertions.expression("cast(a as DECIMAL(38,8))") .binding("a", "cast(DECIMAL '123456789012345678901234567890.12345678' as JSON)")) .hasType(createDecimalType(38, 8)) @@ -784,10 +795,10 @@ public void testCastToBoolean() .binding("a", "JSON '1e-324'")) .isEqualTo(false); - // overflow - assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as BOOLEAN)") - .binding("a", "JSON '1e309'").evaluate()) - .hasErrorCode(INVALID_CAST_ARGUMENT); + // overflow if parsed as double + assertThat(assertions.expression("cast(a as BOOLEAN)") + .binding("a", "JSON '1e309'")) + .isEqualTo(true); assertThat(assertions.expression("cast(a as BOOLEAN)") .binding("a", "JSON 'true'")) @@ -861,7 +872,6 @@ public void testCastToVarchar() .hasType(VARCHAR) .isEqualTo("128"); - // overflow, no loss of precision assertThat(assertions.expression("cast(a as VARCHAR)") .binding("a", "JSON '12345678901234567890'")) .hasType(VARCHAR) @@ -870,25 +880,30 @@ public void testCastToVarchar() assertThat(assertions.expression("cast(a as VARCHAR)") .binding("a", "JSON '128.9'")) .hasType(VARCHAR) - .isEqualTo("1.289E2"); + .isEqualTo("128.9"); - // smaller than minimum subnormal positive + // smaller than double's minimum subnormal positive assertThat(assertions.expression("cast(a as VARCHAR)") .binding("a", "JSON '1e-324'")) .hasType(VARCHAR) - .isEqualTo("0E0"); + .isEqualTo("1E-324"); - // overflow + assertThat(assertions.expression("cast(a as VARCHAR)") + .binding("a", "JSON '123456789012345678901234567890.123456789012345678901234567890'")) + .hasType(VARCHAR) + .isEqualTo("123456789012345678901234567890.123456789012345678901234567890"); + + // overflow if parsed as double assertThat(assertions.expression("cast(a as VARCHAR)") .binding("a", "JSON '1e309'")) .hasType(VARCHAR) - .isEqualTo("Infinity"); + .isEqualTo("1E+309"); - // underflow + // underflow if parsed as double assertThat(assertions.expression("cast(a as VARCHAR)") .binding("a", "JSON '-1e309'")) .hasType(VARCHAR) - .isEqualTo("-Infinity"); + .isEqualTo("-1E+309"); assertThat(assertions.expression("cast(a as VARCHAR)") .binding("a", "JSON 'true'")) 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..0402527f9023 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 @@ -562,7 +562,7 @@ public void testJsonToMap() .hasType(mapType(BIGINT, VARCHAR)) .isEqualTo(asMap( ImmutableList.of(1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L, 55L), - asList("true", "false", "12", "1.23E1", "puppies", "kittens", "null", "", null))); + asList("true", "false", "12", "12.3", "puppies", "kittens", "null", "", null))); assertThat(assertions.expression("cast(a as MAP(VARCHAR, JSON))") .binding("a", "JSON '{\"k1\": 5, \"k2\": 3.14, \"k3\":[1, 2, 3], \"k4\":\"e\", \"k5\":{\"a\": \"b\"}, \"k6\":null, \"k7\":\"null\", \"k8\":[null]}'")) @@ -683,7 +683,7 @@ public void testJsonToMap() assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as MAP(VARCHAR, INTEGER))") .binding("a", "JSON '{\"a\": 1234567890123.456}'").evaluate()) - .hasMessage("Cannot cast to map(varchar, integer). Out of range for integer: 1.234567890123456E12\n{\"a\":1.234567890123456E12}") + .hasMessage("Cannot cast to map(varchar, integer). Out of range for integer: 1.234567890123456E12\n{\"a\":1234567890123.456}") .hasErrorCode(INVALID_CAST_ARGUMENT); assertTrinoExceptionThrownBy(() -> assertions.expression("cast(a as MAP(BIGINT, BIGINT))") diff --git a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/util/JsonTypeUtil.java b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/util/JsonTypeUtil.java index 2522377f1e4c..a6c199e5001f 100644 --- a/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/util/JsonTypeUtil.java +++ b/lib/trino-plugin-toolkit/src/main/java/io/trino/plugin/base/util/JsonTypeUtil.java @@ -36,6 +36,7 @@ import java.util.StringJoiner; import static com.fasterxml.jackson.core.JsonFactory.Feature.CANONICALIZE_FIELD_NAMES; +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.plugin.base.util.JsonUtils.jsonFactoryBuilder; @@ -52,11 +53,11 @@ public final class JsonTypeUtil private static final JsonMapper SORTED_MAPPER = new JsonMapperProvider().get() .rebuild() .configure(ORDER_MAP_ENTRIES_BY_KEYS, true) + .configure(USE_BIG_DECIMAL_FOR_FLOATS, true) .build(); private JsonTypeUtil() {} - // TODO: this should be available from the engine public static Slice jsonParse(Slice slice) { // cast(json_parse(x) AS t)` will be optimized into `$internal$json_string_to_array/map/row_cast` in ExpressionOptimizer 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 new file mode 100644 index 000000000000..54983368a577 --- /dev/null +++ b/lib/trino-plugin-toolkit/src/test/java/io/trino/plugin/base/util/TestJsonTypeUtil.java @@ -0,0 +1,147 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.plugin.base.util; + +import io.trino.spi.TrinoException; +import org.junit.jupiter.api.Test; + +import static io.airlift.slice.Slices.utf8Slice; +import static io.trino.plugin.base.util.JsonTypeUtil.jsonParse; +import static io.trino.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class TestJsonTypeUtil +{ + @Test + void testJsonParseNull() + { + assertThat(jsonParse(utf8Slice("null")).toStringUtf8()) + .isEqualTo("null"); + } + + @Test + void testJsonParseBoolean() + { + assertThat(jsonParse(utf8Slice("true")).toStringUtf8()) + .isEqualTo("true"); + assertThat(jsonParse(utf8Slice("false")).toStringUtf8()) + .isEqualTo("false"); + } + + @Test + void testJsonParseString() + { + assertThat(jsonParse(utf8Slice("\"hello\"")).toStringUtf8()) + .isEqualTo("\"hello\""); + assertThat(jsonParse(utf8Slice("\"with\\nescapes\"")).toStringUtf8()) + .isEqualTo("\"with\\nescapes\""); + } + + @Test + void testJsonParseInteger() + { + assertThat(jsonParse(utf8Slice("123")).toStringUtf8()) + .isEqualTo("123"); + assertThat(jsonParse(utf8Slice("-456")).toStringUtf8()) + .isEqualTo("-456"); + assertThat(jsonParse(utf8Slice("0")).toStringUtf8()) + .isEqualTo("0"); + } + + @Test + void testJsonParseDouble() + { + assertThat(jsonParse(utf8Slice("3.14")).toStringUtf8()) + .isEqualTo("3.14"); + assertThat(jsonParse(utf8Slice("-2.5")).toStringUtf8()) + .isEqualTo("-2.5"); + assertThat(jsonParse(utf8Slice("1.0")).toStringUtf8()) + .isEqualTo("1.0"); + } + + @Test + void testJsonParseLargeNumber() + { + assertThat(jsonParse(utf8Slice("12345678901234567890123456789012345678")).toStringUtf8()) + .isEqualTo("12345678901234567890123456789012345678"); + assertThat(jsonParse(utf8Slice("123456789012345678901234567890.12345678")).toStringUtf8()) + .isEqualTo("123456789012345678901234567890.12345678"); + } + + @Test + void testJsonParseArray() + { + assertThat(jsonParse(utf8Slice("[]")).toStringUtf8()) + .isEqualTo("[]"); + assertThat(jsonParse(utf8Slice("[1,2,3]")).toStringUtf8()) + .isEqualTo("[1,2,3]"); + assertThat(jsonParse(utf8Slice("[\"a\",\"b\",\"c\"]")).toStringUtf8()) + .isEqualTo("[\"a\",\"b\",\"c\"]"); + assertThat(jsonParse(utf8Slice("[null,true,false]")).toStringUtf8()) + .isEqualTo("[null,true,false]"); + } + + @Test + void testJsonParseObject() + { + assertThat(jsonParse(utf8Slice("{}")).toStringUtf8()) + .isEqualTo("{}"); + // Note: jsonParse sorts object keys (normalization) + assertThat(jsonParse(utf8Slice("{\"b\":2,\"a\":1}")).toStringUtf8()) + .isEqualTo("{\"a\":1,\"b\":2}"); + assertThat(jsonParse(utf8Slice("{\"name\":\"value\"}")).toStringUtf8()) + .isEqualTo("{\"name\":\"value\"}"); + } + + @Test + void testJsonParseNested() + { + assertThat(jsonParse(utf8Slice("{\"arr\":[1,2,3],\"obj\":{\"x\":10}}")).toStringUtf8()) + .isEqualTo("{\"arr\":[1,2,3],\"obj\":{\"x\":10}}"); + + assertThat(jsonParse(utf8Slice("[{\"b\":2,\"c\":3,\"a\":1}]")).toStringUtf8()) + .isEqualTo("[{\"a\":1,\"b\":2,\"c\":3}]"); + } + + @Test + void testJsonParseInvalidJson() + { + assertThatThrownBy(() -> jsonParse(utf8Slice("invalid"))) + .isInstanceOf(TrinoException.class) + .hasMessageContaining("Cannot convert value to JSON") + .extracting(e -> ((TrinoException) e).getErrorCode().getType()) + .isEqualTo(INVALID_FUNCTION_ARGUMENT.toErrorCode().getType()); + + assertThatThrownBy(() -> jsonParse(utf8Slice("{incomplete"))) + .isInstanceOf(TrinoException.class) + .hasMessageContaining("Cannot convert value to JSON"); + + assertThatThrownBy(() -> jsonParse(utf8Slice("[1,2,]"))) + .isInstanceOf(TrinoException.class) + .hasMessageContaining("Cannot convert value to JSON"); + } + + @Test + void testJsonParseTrailingCharacters() + { + assertThatThrownBy(() -> jsonParse(utf8Slice("123 extra"))) + .isInstanceOf(TrinoException.class) + .hasMessageContaining("Cannot convert value to JSON"); + + assertThatThrownBy(() -> jsonParse(utf8Slice("{}abc"))) + .isInstanceOf(TrinoException.class) + .hasMessageContaining("Cannot convert value to JSON"); + } +}