diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java index 6ef0182b301..3a16d620bea 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java @@ -3364,6 +3364,40 @@ static Expression call(Method method, @Nullable Expression target, } } + /** Base class for the implementors of {@code JSON_VALUE} and + * {@code JSON_QUERY}. */ + private abstract static class JsonImplementor extends MethodImplementor { + JsonImplementor(Method method) { + super(method, NullPolicy.ARG0, false); + } + + /** Calls the runtime method with {@code operands}, followed by the + * {@link SqlTypeName}, precision and scale of {@code returningType} and + * the rounding mode that a {@code CAST} to it would use. + * + *
A null {@code returningType} is passed as
+ * {@link SqlTypeName#ANY}, meaning no conversion. */
+ Expression callWithReturningType(RexToLixTranslator translator,
+ List We should avoid this when we support
* variable arguments function.
*/
- private static class JsonValueImplementor extends MethodImplementor {
+ private static class JsonValueImplementor extends JsonImplementor {
JsonValueImplementor(Method method) {
- super(method, NullPolicy.ARG0, false);
+ super(method);
}
@Override Expression implementSafe(RexToLixTranslator translator,
@@ -3421,20 +3455,16 @@ private static class JsonValueImplementor extends MethodImplementor {
newOperands.add(defaultValueOnEmpty);
newOperands.add(errorBehavior);
newOperands.add(defaultValueOnError);
- List The value is a SQL value of the type it was written as, not a
+ * value read from a JSON document, so the only conversion it can need is
+ * between numeric types, as in
+ * {@code RETURNING DOUBLE DEFAULT 1 ON EMPTY}. */
+ private static @Nullable Object convertDefaultValue(@Nullable Object value,
+ SqlTypeName typeName, int precision, int scale,
+ RoundingMode roundingMode) {
+ return SqlTypeName.NUMERIC_TYPES.contains(typeName)
+ ? SqlFunctions.cast(value, typeName, precision, scale, roundingMode)
+ : value;
+ }
+
private static Object jsonQueryEmptyArray(boolean jsonize) {
return jsonize ? "[]" : Collections.emptyList();
}
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index 4e77144243e..5c5c1e72973 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -44,6 +44,7 @@
import org.apache.calcite.sql.SqlUtil;
import org.apache.calcite.sql.fun.SqlLibraryOperators;
import org.apache.calcite.sql.parser.SqlParserUtil;
+import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.NumberUtil;
import org.apache.calcite.util.TimeWithTimeZoneString;
import org.apache.calcite.util.TimestampWithTimeZoneString;
@@ -5636,6 +5637,194 @@ public static BigDecimal toBigDecimal(Object o) {
: toBigDecimal(o.toString());
}
+ /** Converts a value to the SQL type {@code typeName}, as {@code CAST}
+ * does, when the type of the value is not known until run time.
+ *
+ * Throws if the value cannot be converted, and for a target type that
+ * this method does not handle, so that a caller such as
+ * {@code JSON_VALUE} can apply its {@code ON ERROR} clause.
+ *
+ * @param value Value to convert
+ * @param typeName Type to convert it to; {@link SqlTypeName#ANY}
+ * returns the value unchanged
+ * @param precision Precision of the target type, or negative
+ * @param scale Scale of the target type, or negative
+ * @param roundingMode Rounding mode of the type system
+ */
+ static @Nullable Object cast(@Nullable Object value,
+ SqlTypeName typeName, int precision, int scale,
+ RoundingMode roundingMode) {
+ if (value == null || typeName == SqlTypeName.ANY) {
+ return value;
+ }
+ switch (typeName) {
+ case BOOLEAN:
+ return toBoolean(value);
+ case TINYINT:
+ return castToExact(value, Primitive.BYTE, roundingMode);
+ case SMALLINT:
+ return castToExact(value, Primitive.SHORT, roundingMode);
+ case INTEGER:
+ return castToExact(value, Primitive.INT, roundingMode);
+ case BIGINT:
+ return castToExact(value, Primitive.LONG, roundingMode);
+ case REAL:
+ return toFloat(value);
+ case FLOAT:
+ case DOUBLE:
+ return toDouble(value);
+ case DECIMAL:
+ return castToDecimal(value, precision, scale, roundingMode);
+ case CHAR:
+ return precision < 0 ? value.toString()
+ : truncateOrPad(value.toString(), precision);
+ case VARCHAR:
+ return precision < 0 ? value.toString()
+ : truncate(value.toString(), precision);
+ case DATE:
+ return DateTimeUtils.dateStringToUnixDate(charValue(value, typeName));
+ case TIME:
+ return (int) truncateFraction(
+ DateTimeUtils.timeStringToUnixDate(charValue(value, typeName)),
+ precision);
+ case TIME_WITH_LOCAL_TIME_ZONE:
+ return (int) truncateFraction(
+ castNonNull(toTimeWithLocalTimeZone(charValue(value, typeName))),
+ precision);
+ case TIMESTAMP:
+ return truncateFraction(
+ DateTimeUtils.timestampStringToUnixDate(charValue(value, typeName)),
+ precision);
+ case TIMESTAMP_WITH_LOCAL_TIME_ZONE:
+ return truncateFraction(
+ castNonNull(
+ toTimestampWithLocalTimeZone(charValue(value, typeName))),
+ precision);
+ default:
+ return cannotConvert(value, typeName);
+ }
+ }
+
+ /** Converts a value to an array, {@code depth} levels deep, whose
+ * innermost elements have the SQL type {@code elementType}, as a
+ * {@code CAST} to that type does, when the type of the value is not known
+ * until run time.
+ *
+ * The declared type drives the conversion: the value must be an array
+ * at each of the {@code depth} levels, and what lies below them must
+ * convert to {@code elementType}. A value of a different shape, such as a
+ * flat array where an array of arrays is wanted, is an error.
+ *
+ * Throws if the value does not have that shape, so that a caller such
+ * as {@code JSON_QUERY} can apply its {@code ON ERROR} clause.
+ * {@link SqlTypeName#ANY} returns the value unchanged.
+ *
+ * @see #cast(Object, SqlTypeName, int, int, RoundingMode)
+ */
+ static @Nullable Object castArray(@Nullable Object value,
+ SqlTypeName elementType, int precision, int scale,
+ RoundingMode roundingMode, int depth) {
+ if (value == null || elementType == SqlTypeName.ANY) {
+ return value;
+ }
+ if (depth == 0) {
+ return cast(value, elementType, precision, scale, roundingMode);
+ }
+ if (!(value instanceof Collection)) {
+ return cannotConvert(value, SqlTypeName.ARRAY);
+ }
+ final Collection> collection = (Collection>) value;
+ final List<@Nullable Object> list = new ArrayList<>(collection.size());
+ for (Object element : collection) {
+ list.add(
+ castArray(element, elementType, precision, scale, roundingMode,
+ depth - 1));
+ }
+ return list;
+ }
+
+ /** Converts a value to an exact numeric type, rounding as the type system
+ * requires and throwing {@link ArithmeticException} if it is out of range,
+ * as {@code CAST} does.
+ *
+ * Converts a number to {@link BigDecimal} first:
+ * {@link Primitive#integerCast} does not accept every {@link Number} a
+ * semi-structured value may hold, such as {@link BigInteger}. */
+ private static Object castToExact(Object value, Primitive primitive,
+ RoundingMode roundingMode) {
+ if (!(value instanceof Number)) {
+ // Take the same path as a CAST from a character value.
+ switch (primitive) {
+ case BYTE:
+ return toByte(value);
+ case SHORT:
+ return toShort(value);
+ case INT:
+ return toInt(value);
+ default:
+ return toLong(value);
+ }
+ }
+ return requireNonNull(
+ Primitive.integerCast(primitive, toBigDecimal((Number) value),
+ roundingMode), "integerCast");
+ }
+
+ /** Converts a value to {@code DECIMAL(precision, scale)}. */
+ private static @Nullable Object castToDecimal(Object value, int precision,
+ int scale, RoundingMode roundingMode) {
+ if (precision < 0 || scale < 0) {
+ // The type gives no precision and scale to enforce.
+ return toBigDecimal(value);
+ }
+ if (value instanceof BigDecimal) {
+ return Primitive.decimalDecimalCast((BigDecimal) value, precision, scale,
+ roundingMode);
+ }
+ if (value instanceof BigInteger) {
+ return Primitive.decimalDecimalCast(new BigDecimal((BigInteger) value),
+ precision, scale, roundingMode);
+ }
+ if (value instanceof Float || value instanceof Double) {
+ return Primitive.fpDecimalCast((Number) value, precision, scale,
+ roundingMode);
+ }
+ if (value instanceof Number) {
+ return Primitive.integerDecimalCast((Number) value, precision, scale,
+ roundingMode);
+ }
+ return Primitive.charToDecimalCast(value.toString(), precision, scale,
+ roundingMode);
+ }
+
+ /** Truncates a datetime value, held as a number of milliseconds, to
+ * {@code precision} fractional digits of a second.
+ *
+ * @see org.apache.calcite.util.TimestampString#round(int) */
+ private static long truncateFraction(long millis, int precision) {
+ if (precision < 0 || precision >= 3) {
+ return millis;
+ }
+ long unit = 1;
+ for (int i = precision; i < 3; i++) {
+ unit *= 10;
+ }
+ return truncate(millis, unit);
+ }
+
+ /** Returns {@code value} as a character value, throwing if it is not one;
+ * the datetime types convert only from a character value. */
+ private static String charValue(Object value, SqlTypeName typeName) {
+ if (value instanceof String) {
+ return (String) value;
+ }
+ return (String) cannotConvert(value, typeName);
+ }
+
+ private static Object cannotConvert(Object o, SqlTypeName typeName) {
+ throw RESOURCE.cannotConvert(String.valueOf(o), typeName.getName()).ex();
+ }
+
/**
* Converts a SQL DATE value from the internal representation type
* (number of days since January 1st, 1970) to the Java type
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index a8c006d61e7..c7abbd7212b 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -126,6 +126,7 @@
import org.apache.calcite.sql.SqlJsonQueryEmptyOrErrorBehavior;
import org.apache.calcite.sql.SqlJsonQueryWrapperBehavior;
import org.apache.calcite.sql.SqlJsonValueEmptyOrErrorBehavior;
+import org.apache.calcite.sql.type.SqlTypeName;
import com.google.common.collect.ImmutableMap;
@@ -521,12 +522,14 @@ public enum BuiltInMethod {
JSON_VALUE(JsonFunctions.StatefulFunction.class, "jsonValue",
String.class, String.class,
SqlJsonValueEmptyOrErrorBehavior.class, Object.class,
- SqlJsonValueEmptyOrErrorBehavior.class, Object.class),
+ SqlJsonValueEmptyOrErrorBehavior.class, Object.class,
+ SqlTypeName.class, int.class, int.class, RoundingMode.class),
JSON_QUERY(JsonFunctions.StatefulFunction.class, "jsonQuery", String.class,
String.class, SqlJsonQueryWrapperBehavior.class,
SqlJsonQueryEmptyOrErrorBehavior.class,
SqlJsonQueryEmptyOrErrorBehavior.class,
- boolean.class),
+ boolean.class, int.class, SqlTypeName.class, int.class, int.class,
+ RoundingMode.class),
JSON_OBJECT(JsonFunctions.class, "jsonObject",
SqlJsonConstructorNullClause.class),
JSON_TYPE(JsonFunctions.class, "jsonType", String.class),
diff --git a/core/src/test/java/org/apache/calcite/runtime/SqlFunctionsCastTest.java b/core/src/test/java/org/apache/calcite/runtime/SqlFunctionsCastTest.java
new file mode 100644
index 00000000000..597eeb0ebbb
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/runtime/SqlFunctionsCastTest.java
@@ -0,0 +1,167 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.calcite.runtime;
+
+import org.apache.calcite.sql.type.SqlTypeName;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.math.RoundingMode;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasToString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Tests {@link SqlFunctions#cast} and {@link SqlFunctions#castArray}, the
+ * conversions used when the type of a value is not known until run time.
+ *
+ * Test case for
+ * [CALCITE-7801]
+ * JSON_VALUE(..., RETURNING DOUBLE) throws ClassCastException when the JSON
+ * number is an integer.
+ */
+class SqlFunctionsCastTest {
+
+ @Test void testCastToScalarType() {
+ // ANY, and a null value, are returned unchanged.
+ assertThat(cast("x", SqlTypeName.ANY), is("x"));
+ assertThat(cast(null, SqlTypeName.INTEGER), nullValue());
+
+ // Exact numerics round towards zero and are range-checked.
+ assertThat(cast(1, SqlTypeName.TINYINT), is((byte) 1));
+ assertThat(cast(1, SqlTypeName.SMALLINT), is((short) 1));
+ assertThat(cast(1, SqlTypeName.BIGINT), is(1L));
+ assertThat(cast(1.7d, SqlTypeName.BIGINT), is(1L));
+ assertThat(cast(-1.7d, SqlTypeName.SMALLINT), is((short) -1));
+ assertThat(cast(new BigInteger("42"), SqlTypeName.BIGINT), is(42L));
+ assertThrows(ArithmeticException.class,
+ () -> cast(100000, SqlTypeName.TINYINT));
+ assertThrows(ArithmeticException.class,
+ () -> cast(100000, SqlTypeName.SMALLINT));
+
+ // Approximate numerics.
+ assertThat(cast(1, SqlTypeName.REAL), is(1.0f));
+ assertThat(cast("1.5", SqlTypeName.REAL), is(1.5f));
+ assertThat(cast(1, SqlTypeName.FLOAT), is(1.0d));
+ assertThat(cast(1, SqlTypeName.DOUBLE), is(1.0d));
+
+ // A character value takes the same path as a CAST from one.
+ assertThat(cast("100", SqlTypeName.BIGINT), is(100L));
+ assertThat(cast("true", SqlTypeName.BOOLEAN), is(true));
+ assertThrows(NumberFormatException.class,
+ () -> cast("abc", SqlTypeName.BIGINT));
+
+ // A target type that is not handled is an error, so that a caller can
+ // apply its own error clause rather than the failure escaping.
+ assertThrows(CalciteException.class,
+ () -> cast("0102", SqlTypeName.VARBINARY));
+ assertThrows(CalciteException.class,
+ () -> cast(1, SqlTypeName.ARRAY));
+ }
+
+ @Test void testCastAppliesPrecisionAndScale() {
+ assertThat(cast(100, SqlTypeName.DECIMAL, 5, 2), hasToString("100.00"));
+ assertThat(cast("abcdef", SqlTypeName.VARCHAR, 3, -1), is("abc"));
+ assertThat(cast("ab", SqlTypeName.CHAR, 4, -1), is("ab "));
+
+ // Fractional seconds beyond the precision of the type are truncated.
+ // The value is a number of milliseconds, so the truncation is visible
+ // here in a way that it is not through a result set.
+ assertThat(cast("10:20:30.987", SqlTypeName.TIME, 3, -1), is(37230987));
+ assertThat(cast("10:20:30.987", SqlTypeName.TIME, 0, -1), is(37230000));
+ assertThat(cast("2020-01-01 10:20:30.987", SqlTypeName.TIMESTAMP, 3, -1),
+ is(1577874030987L));
+ assertThat(cast("2020-01-01 10:20:30.987", SqlTypeName.TIMESTAMP, 0, -1),
+ is(1577874030000L));
+
+ // Only a character value converts to a datetime.
+ assertThrows(CalciteException.class,
+ () -> cast(20200101, SqlTypeName.DATE, -1, -1));
+ }
+
+ @Test void testCastArray() {
+ assertThat(castArray(Arrays.asList(1, 2), SqlTypeName.DOUBLE, 1),
+ is(Arrays.asList(1.0d, 2.0d)));
+
+ // A nested array is converted at every level.
+ assertThat(
+ castArray(
+ Arrays.asList(Arrays.asList(1, 2), Collections.singletonList(3)),
+ SqlTypeName.DOUBLE, 2),
+ is(
+ Arrays.asList(Arrays.asList(1.0d, 2.0d),
+ Collections.singletonList(3.0d))));
+
+ // Elements of any type the cast handles, here as the number of days
+ // that a DATE is held as. Asserting the value rather than how it is
+ // rendered keeps this independent of the default time zone.
+ assertThat(
+ castArray(Arrays.asList("2020-01-01", "2020-01-02"), SqlTypeName.DATE,
+ 1),
+ is(Arrays.asList(18262, 18263)));
+
+ // ANY, and a null value, are returned unchanged.
+ assertThat(castArray(Arrays.asList(1, 2), SqlTypeName.ANY, 1),
+ is(Arrays.asList(1, 2)));
+ assertThat(castArray(null, SqlTypeName.INTEGER, 1), nullValue());
+
+ // A value that is not an array cannot be converted to one.
+ assertThrows(CalciteException.class,
+ () ->
+ castArray(Collections.singletonMap("x", 1), SqlTypeName.INTEGER,
+ 1));
+ }
+
+ /** Tests that the declared type, not the shape of the value, drives the
+ * conversion: a value of the wrong shape is an error rather than an array
+ * that does not match the type it is declared to have. */
+ @Test void testCastArrayChecksShapeAgainstType() {
+ // Declared one deep, but an element is itself an array.
+ assertThrows(CalciteException.class,
+ () ->
+ castArray(Arrays.asList(1, Arrays.asList(2, 3)),
+ SqlTypeName.INTEGER, 1));
+
+ // Declared two deep, but the value is flat.
+ assertThrows(CalciteException.class,
+ () -> castArray(Arrays.asList(1, 2), SqlTypeName.INTEGER, 2));
+ }
+
+ private static @Nullable Object cast(@Nullable Object value,
+ SqlTypeName typeName) {
+ return cast(value, typeName, -1, -1);
+ }
+
+ private static @Nullable Object cast(@Nullable Object value,
+ SqlTypeName typeName, int precision, int scale) {
+ return SqlFunctions.cast(value, typeName, precision, scale,
+ RoundingMode.DOWN);
+ }
+
+ private static @Nullable Object castArray(@Nullable Object value,
+ SqlTypeName elementType, int depth) {
+ return SqlFunctions.castArray(value, elementType, -1, -1,
+ RoundingMode.DOWN, depth);
+ }
+}
diff --git a/core/src/test/java/org/apache/calcite/test/JdbcTest.java b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
index f0b675960eb..06826f82ed8 100644
--- a/core/src/test/java/org/apache/calcite/test/JdbcTest.java
+++ b/core/src/test/java/org/apache/calcite/test/JdbcTest.java
@@ -9228,18 +9228,39 @@ void checkCalciteSchemaGetSubSchemaMap(boolean cache) {
.returns("C1=[1,2]; C2=[1, 2]; C3=[]; C4=[[1, 2]]\n");
}
+ /** Tests that a value that cannot be converted to the type in the
+ * {@code RETURNING} clause is governed by the {@code ON ERROR} clause.
+ *
+ * Test case for
+ * [CALCITE-7801]
+ * JSON_VALUE(..., RETURNING DOUBLE) throws ClassCastException when the JSON
+ * number is an integer. */
@Test void testJsonValueError() {
+ // NULL ON ERROR is the default.
+ CalciteAssert.that()
+ .query("SELECT JSON_VALUE(v, 'lax $.a' RETURNING INTEGER) AS c1\n"
+ + "FROM (VALUES ('{\"a\": \"abc\"}')) AS t(v)\n"
+ + "LIMIT 10")
+ .returns("C1=null\n");
+
+ CalciteAssert.that()
+ .query("SELECT JSON_VALUE(v, 'lax $.a' RETURNING INTEGER"
+ + " DEFAULT 0 ON ERROR) AS c1\n"
+ + "FROM (VALUES ('{\"a\": \"abc\"}')) AS t(v)\n"
+ + "LIMIT 10")
+ .returns("C1=0\n");
+
java.sql.SQLException t =
assertThrows(
java.sql.SQLException.class,
() -> CalciteAssert.that()
- .query("SELECT JSON_VALUE(v, 'lax $.a' RETURNING INTEGER) AS c1\n"
+ .query("SELECT JSON_VALUE(v, 'lax $.a' RETURNING INTEGER"
+ + " ERROR ON ERROR) AS c1\n"
+ "FROM (VALUES ('{\"a\": \"abc\"}')) AS t(v)\n"
+ "LIMIT 10")
.returns(""));
- assertThat(
- t.getMessage(), containsString("java.lang.String cannot be cast to"));
+ assertThat(t.getMessage(), containsString("For input string: \"abc\""));
}
@Test void testJsonQueryError() {
diff --git a/core/src/test/java/org/apache/calcite/test/SqlJsonFunctionsTest.java b/core/src/test/java/org/apache/calcite/test/SqlJsonFunctionsTest.java
index 9ebc696f027..96ec16d8392 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlJsonFunctionsTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlJsonFunctionsTest.java
@@ -24,6 +24,7 @@
import org.apache.calcite.sql.SqlJsonQueryEmptyOrErrorBehavior;
import org.apache.calcite.sql.SqlJsonQueryWrapperBehavior;
import org.apache.calcite.sql.SqlJsonValueEmptyOrErrorBehavior;
+import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.util.BuiltInMethod;
import com.google.common.primitives.Longs;
@@ -35,6 +36,7 @@
import org.hamcrest.Matcher;
import org.junit.jupiter.api.Test;
+import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -710,7 +712,8 @@ private void assertJsonValueAny(JsonFunctions.JsonPathContext context,
invocationDesc(BuiltInMethod.JSON_VALUE, context, emptyBehavior,
defaultValueOnEmpty, errorBehavior, defaultValueOnError),
f.jsonValue(context, emptyBehavior, defaultValueOnEmpty,
- errorBehavior, defaultValueOnError),
+ errorBehavior, defaultValueOnError, SqlTypeName.ANY,
+ -1, -1, RoundingMode.DOWN),
matcher);
}
@@ -726,7 +729,8 @@ private void assertJsonValueAnyFailed(JsonFunctions.JsonPathContext input,
invocationDesc(BuiltInMethod.JSON_VALUE, input, emptyBehavior,
defaultValueOnEmpty, errorBehavior, defaultValueOnError),
() -> f.jsonValue(input, emptyBehavior,
- defaultValueOnEmpty, errorBehavior, defaultValueOnError),
+ defaultValueOnEmpty, errorBehavior, defaultValueOnError,
+ SqlTypeName.ANY, -1, -1, RoundingMode.DOWN),
matcher);
}
@@ -750,7 +754,7 @@ private void assertJsonQuery(JsonFunctions.JsonPathContext input,
invocationDesc(BuiltInMethod.JSON_QUERY, input, wrapperBehavior,
emptyBehavior, errorBehavior),
f.jsonQuery(input, wrapperBehavior, emptyBehavior,
- errorBehavior, jsonize),
+ errorBehavior, jsonize, 0, SqlTypeName.ANY, -1, -1, RoundingMode.DOWN),
matcher);
}
@@ -765,7 +769,8 @@ private void assertJsonQueryFailed(JsonFunctions.JsonPathContext input,
invocationDesc(BuiltInMethod.JSON_QUERY, input, wrapperBehavior,
emptyBehavior, errorBehavior),
() -> f.jsonQuery(input, wrapperBehavior, emptyBehavior,
- errorBehavior, true),
+ errorBehavior, true, 0, SqlTypeName.ANY, -1, -1,
+ RoundingMode.DOWN),
matcher);
}
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 54e47d58c05..0c0cebd6f85 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2878,13 +2878,24 @@ In the following:
|:---------------------- |:-----------
| JSON_EXISTS(jsonValue, path [ { TRUE | FALSE | UNKNOWN | ERROR } ON ERROR ] ) | Whether a *jsonValue* satisfies a search criterion described using JSON path expression *path*
| JSON_VALUE(jsonValue, path [ RETURNING type ] [ { ERROR | NULL | DEFAULT expr } ON EMPTY ] [ { ERROR | NULL | DEFAULT expr } ON ERROR ] ) | Extract an SQL scalar from a *jsonValue* using JSON path expression *path*
-| JSON_QUERY(jsonValue, path [ { WITHOUT [ ARRAY ] | WITH [ CONDITIONAL | UNCONDITIONAL ] [ ARRAY ] } WRAPPER ] [ { ERROR | NULL | EMPTY ARRAY | EMPTY OBJECT } ON EMPTY ] [ { ERROR | NULL | EMPTY ARRAY | EMPTY OBJECT } ON ERROR ] ) | Extract a JSON object or JSON array from *jsonValue* using the *path* JSON path expression
+| JSON_QUERY(jsonValue, path [ RETURNING type ] [ { WITHOUT [ ARRAY ] | WITH [ CONDITIONAL | UNCONDITIONAL ] [ ARRAY ] } WRAPPER ] [ { ERROR | NULL | EMPTY ARRAY | EMPTY OBJECT } ON EMPTY ] [ { ERROR | NULL | EMPTY ARRAY | EMPTY OBJECT } ON ERROR ] ) | Extract a JSON object or JSON array from *jsonValue* using the *path* JSON path expression
Note:
* The `ON ERROR` and `ON EMPTY` clauses define the fallback
behavior of the function when an error is thrown or a null value
is about to be returned.
+* The `RETURNING` clause gives the type of the value that the function
+ returns; the default is `VARCHAR(2000)`. The value extracted from the
+ document is converted to that type as if by `CAST`, and a conversion that
+ fails is an error, and is therefore governed by the `ON ERROR` clause. In
+ `JSON_QUERY`, a `RETURNING` type that is an `ARRAY` converts each element.
+ A datetime is parsed from a JSON string, as `CAST` parses a character
+ value; a JSON number is not a datetime, so converting one is an error.
+ The precision and scale of the type are applied: a `DECIMAL(p, s)` is
+ scaled, a `CHAR(n)` or `VARCHAR(n)` is truncated or padded to *n*
+ characters, and fractional seconds beyond the precision of a datetime type
+ are truncated.
* The `ARRAY WRAPPER` clause defines how to represent a JSON array result
in `JSON_QUERY` function. The following examples compare the wrapper
behaviors.
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index e584e59c379..af0ecbd66a7 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -6975,7 +6975,12 @@ void checkRegexpExtract(SqlOperatorFixture f0, FunctionAlias functionAlias) {
"100", "VARCHAR(2000)");
f.checkScalar("json_value('{\"foo\":100}', 'strict $.foo' returning integer)",
100, "INTEGER");
- f.checkFails("json_value('{\"foo\":\"100\"}', 'strict $.foo' returning boolean)",
+ // A value that cannot be converted to the RETURNING type is an error,
+ // so the ON ERROR clause applies; NULL ON ERROR is the default.
+ f.checkScalar("json_value('{\"foo\":\"100\"}', 'strict $.foo' returning boolean)",
+ isNullValue(), "BOOLEAN");
+ f.checkFails("json_value('{\"foo\":\"100\"}', 'strict $.foo' returning boolean "
+ + "error on error)",
INVALID_CHAR_MESSAGE, true);
f.checkScalar("json_value('{\"foo\":100}', 'lax $.foo1' returning integer "
+ "null on empty)", isNullValue(), "INTEGER");
@@ -7049,6 +7054,177 @@ void checkRegexpExtract(SqlOperatorFixture f0, FunctionAlias functionAlias) {
f.checkNull("json_value(cast(null as varchar), 'strict $')");
}
+ /** Tests the {@code RETURNING} clause of {@code JSON_VALUE}.
+ *
+ * Test case for
+ * [CALCITE-7801]
+ * JSON_VALUE(..., RETURNING DOUBLE) throws ClassCastException when the JSON
+ * number is an integer.
+ *
+ * The extracted value is converted to the {@code RETURNING} type as if
+ * by {@code CAST}; a failed conversion is governed by {@code ON ERROR}. */
+ @Test void testJsonValueReturning() {
+ final SqlOperatorFixture f = fixture();
+
+ // Exact numeric in the document, approximate numeric in the RETURNING
+ // clause, and vice versa.
+ f.checkScalar("json_value('{\"c\":0}', '$.c' returning double)",
+ 0.0, "DOUBLE");
+ f.checkScalar("json_value('{\"c\":100}', '$.c' returning double)",
+ 100.0, "DOUBLE");
+ f.checkScalar("json_value('{\"c\":0.5}', '$.c' returning double)",
+ 0.5, "DOUBLE");
+ f.checkScalar("json_value('{\"c\":0.5}', '$.c' returning integer)",
+ 0, "INTEGER");
+ f.checkScalar("json_value('{\"c\":1.5}', '$.c' returning integer)",
+ 1, "INTEGER");
+ f.checkScalar("json_value('{\"c\":-1.5}', '$.c' returning integer)",
+ -1, "INTEGER");
+
+ // All the numeric types are reachable from an integral JSON number.
+ f.checkScalar("json_value('{\"c\":1}', '$.c' returning tinyint)",
+ 1, "TINYINT");
+ f.checkScalar("json_value('{\"c\":1}', '$.c' returning smallint)",
+ 1, "SMALLINT");
+ f.checkScalar("json_value('{\"c\":1}', '$.c' returning integer)",
+ 1, "INTEGER");
+ f.checkScalar("json_value('{\"c\":1}', '$.c' returning bigint)",
+ 1, "BIGINT");
+ f.checkScalar("json_value('{\"c\":1}', '$.c' returning real)",
+ 1.0, "REAL");
+ f.checkScalar("json_value('{\"c\":1}', '$.c' returning float)",
+ 1.0, "FLOAT");
+ f.checkScalar("json_value('{\"c\":1}', '$.c' returning double)",
+ 1.0, "DOUBLE");
+
+ // A JSON string converts to the RETURNING type just as CAST would.
+ f.checkScalar("json_value('{\"c\":\"100\"}', '$.c' returning integer)",
+ 100, "INTEGER");
+ f.checkScalar("json_value('{\"c\":\"100\"}', '$.c' returning double)",
+ 100.0, "DOUBLE");
+ f.checkScalar("json_value('{\"c\":\"true\"}', '$.c' returning boolean)",
+ true, "BOOLEAN");
+ f.checkScalar("json_value('{\"c\":true}', '$.c' returning boolean)",
+ true, "BOOLEAN");
+ f.checkScalar("json_value('{\"c\":false}', '$.c' returning boolean)",
+ false, "BOOLEAN");
+
+ // Every JSON scalar converts to a character type.
+ f.checkScalar("json_value('{\"c\":1}', '$.c' returning varchar(10))",
+ "1", "VARCHAR(10)");
+ f.checkScalar("json_value('{\"c\":1.5}', '$.c' returning varchar(10))",
+ "1.5", "VARCHAR(10)");
+ f.checkScalar("json_value('{\"c\":true}', '$.c' returning varchar(10))",
+ "true", "VARCHAR(10)");
+
+ // A conversion that fails is an error, and is therefore governed by the
+ // ON ERROR clause. NULL ON ERROR is the default.
+ f.checkScalar("json_value('{\"c\":\"abc\"}', '$.c' returning integer)",
+ isNullValue(), "INTEGER");
+ f.checkScalar("json_value('{\"c\":\"abc\"}', '$.c' returning integer "
+ + "null on error)",
+ isNullValue(), "INTEGER");
+ f.checkScalar("json_value('{\"c\":\"abc\"}', '$.c' returning integer "
+ + "default 42 on error)",
+ 42, "INTEGER");
+ f.checkFails("json_value('{\"c\":\"abc\"}', '$.c' returning integer "
+ + "error on error)",
+ "(?s).*For input string: \"abc\".*", true);
+ f.checkScalar("json_value('{\"c\":\"100\"}', '$.c' returning boolean "
+ + "null on error)",
+ isNullValue(), "BOOLEAN");
+ f.checkFails("json_value('{\"c\":\"100\"}', '$.c' returning boolean "
+ + "error on error)",
+ "(?s).*Invalid character for cast: 100.*", true);
+
+ // An out-of-range value is an error too.
+ f.checkScalar("json_value('{\"c\":100000}', '$.c' returning tinyint "
+ + "null on error)",
+ isNullValue(), "TINYINT");
+ f.checkScalar("json_value('{\"c\":100000}', '$.c' returning tinyint "
+ + "default 0 on error)",
+ 0, "TINYINT");
+
+
+ // A datetime is parsed from a JSON string, as CAST parses a character
+ // value; a JSON number is not a datetime, so converting one is an error.
+ f.checkScalar("json_value('{\"c\":\"2020-01-01\"}', '$.c' returning date)",
+ "2020-01-01", "DATE");
+ f.checkScalar("json_value('{\"c\":\"10:20:30\"}', '$.c' returning time)",
+ "10:20:30", "TIME(0)");
+ f.checkScalar("json_value('{\"c\":\"2020-01-01 10:20:30\"}', '$.c' "
+ + "returning timestamp)",
+ "2020-01-01 10:20:30", "TIMESTAMP(0)");
+ f.checkScalar("json_value('{\"c\":\"2020-01-01 10:20:30 UTC\"}', '$.c' "
+ + "returning timestamp with local time zone)",
+ "2020-01-01 10:20:30", "TIMESTAMP_WITH_LOCAL_TIME_ZONE(0)");
+ f.checkScalar("json_value('{\"c\":\"10:20:30 UTC\"}', '$.c' "
+ + "returning time with local time zone)",
+ "10:20:30", "TIME_WITH_LOCAL_TIME_ZONE(0)");
+ f.checkScalar("json_value('{\"c\":\"nope\"}', '$.c' returning date)",
+ isNullValue(), "DATE");
+ f.checkFails("json_value('{\"c\":\"nope\"}', '$.c' returning date "
+ + "error on error)",
+ "(?s).*Invalid DATE value, 'nope'.*", true);
+ f.checkScalar("json_value('{\"c\":\"nope\"}', '$.c' returning date "
+ + "default date '1970-01-02' on error)",
+ "1970-01-02", "DATE");
+ f.checkScalar("json_value('{\"c\":20200101}', '$.c' returning date)",
+ isNullValue(), "DATE");
+ f.checkFails("json_value('{\"c\":20200101}', '$.c' returning date "
+ + "error on error)",
+ "(?s).*Cannot convert 20200101 to DATE.*", true);
+
+ // The precision and scale of the RETURNING type are applied, as by CAST.
+ f.checkScalar("json_value('{\"c\":100}', '$.c' returning decimal(5,2))",
+ "100.00", "DECIMAL(5, 2)");
+ f.checkScalar("json_value('{\"c\":1.005}', '$.c' returning decimal(5,2))",
+ "1.00", "DECIMAL(5, 2)");
+ f.checkScalar("json_value('{\"c\":\"1.005\"}', '$.c' returning decimal(5,2))",
+ "1.00", "DECIMAL(5, 2)");
+ f.checkFails("json_value('{\"c\":123456}', '$.c' returning decimal(5,2) "
+ + "error on error)",
+ "(?s).*cannot be represented as a DECIMAL.*", true);
+
+ // A character type longer than the value pads, and shorter truncates.
+ f.checkScalar("json_value('{\"c\":\"abcdef\"}', '$.c' returning varchar(3))",
+ "abc", "VARCHAR(3)");
+ f.checkScalar("json_value('{\"c\":\"abcdef\"}', '$.c' returning char(3))",
+ "abc", "CHAR(3)");
+ f.checkScalar("'[' || json_value('{\"c\":\"ab\"}', '$.c' returning char(9))"
+ + " || ']'",
+ "[ab ]", "CHAR(11)");
+
+ // Fractional seconds beyond the precision of the type are truncated.
+ f.checkScalar("json_value('{\"c\":\"2020-01-01 10:20:30.987\"}', '$.c' "
+ + "returning timestamp(0))",
+ "2020-01-01 10:20:30", "TIMESTAMP(0)");
+ f.checkScalar("json_value('{\"c\":\"2020-01-01 10:20:30.987\"}', '$.c' "
+ + "returning timestamp(3))",
+ "2020-01-01 10:20:30.987", "TIMESTAMP(3)");
+
+ // A RETURNING type that cannot be converted to is an error, so the
+ // ON ERROR clause applies rather than the failure escaping.
+ f.checkScalar("json_value('{\"c\":\"0102\"}', '$.c' returning varbinary(2))",
+ isNullValue(), "VARBINARY(2)");
+ f.checkFails("json_value('{\"c\":\"0102\"}', '$.c' returning varbinary(2) "
+ + "error on error)",
+ "(?s).*Cannot convert 0102 to VARBINARY.*", true);
+
+ // JSON_VALUE returns a scalar, so an array RETURNING type never matches.
+ f.checkScalar("json_value('{\"c\":[1,2]}', '$.c' returning integer array)",
+ isNullValue(), "INTEGER ARRAY");
+
+ // The ON EMPTY clause still applies to an empty result, and its default
+ // value is converted to the RETURNING type as well.
+ f.checkScalar("json_value('{\"c\":1}', 'lax $.d' returning integer "
+ + "null on empty)",
+ isNullValue(), "INTEGER");
+ f.checkScalar("json_value('{\"c\":1}', 'lax $.d' returning double "
+ + "default 1 on empty)",
+ 1.0, "DOUBLE");
+ }
+
@Test void testJsonQuery() {
final SqlOperatorFixture f = fixture();
// default pathmode the default is: strict mode
@@ -7148,6 +7324,71 @@ void checkRegexpExtract(SqlOperatorFixture f0, FunctionAlias functionAlias) {
f.checkNull("json_query(cast(null as varchar), 'lax $')");
}
+ /** Tests the {@code RETURNING ... ARRAY} clause of {@code JSON_QUERY}.
+ *
+ * Test case for
+ * [CALCITE-7801]
+ * JSON_VALUE(..., RETURNING DOUBLE) throws ClassCastException when the JSON
+ * number is an integer. Each element of the array is converted in the
+ * same way that {@code JSON_VALUE} converts a scalar. */
+ @Test void testJsonQueryReturningArray() {
+ final SqlOperatorFixture f = fixture();
+
+ f.checkScalar("json_query('{\"c\":[0,1]}', '$.c' returning integer array)",
+ "[0, 1]", "INTEGER ARRAY");
+ f.checkScalar("json_query('{\"c\":[0,1]}', '$.c' returning double array)",
+ "[0.0, 1.0]", "DOUBLE ARRAY");
+ f.checkScalar("json_query('{\"c\":[0,1]}', '$.c' returning bigint array)",
+ "[0, 1]", "BIGINT ARRAY");
+ f.checkScalar("json_query('{\"c\":[0.5,1.5]}', '$.c' "
+ + "returning integer array)",
+ "[0, 1]", "INTEGER ARRAY");
+ f.checkScalar("json_query('{\"c\":[\"0\",\"1\"]}', '$.c' "
+ + "returning integer array)",
+ "[0, 1]", "INTEGER ARRAY");
+ f.checkScalar("json_query('{\"c\":[0,1]}', '$.c' returning varchar array)",
+ "[0, 1]", "VARCHAR ARRAY");
+
+
+ f.checkScalar("json_query('{\"c\":[100,2]}', '$.c' "
+ + "returning decimal(5,2) array)",
+ "[100.00, 2.00]", "DECIMAL(5, 2) ARRAY");
+ f.checkScalar("json_query('{\"c\":[\"abcdef\"]}', '$.c' "
+ + "returning varchar(3) array)",
+ "[abc]", "VARCHAR(3) ARRAY");
+
+
+ // A nested array is converted element by element, to the depth of the
+ // value.
+ f.checkScalar("json_query('{\"c\":[[1,2],[3]]}', '$.c' "
+ + "returning double array array)",
+ "[[1.0, 2.0], [3.0]]", "DOUBLE ARRAY ARRAY");
+
+ // A value that is not an array cannot be converted to one, so the
+ // ON ERROR clause applies.
+ f.checkScalar("json_query('{\"c\":{\"x\":1}}', '$.c' "
+ + "returning integer array)",
+ isNullValue(), "INTEGER ARRAY");
+ f.checkScalar("json_query('{\"c\":{\"x\":1}}', '$.c' "
+ + "returning integer array empty array on error)",
+ "[]", "INTEGER ARRAY");
+
+ // A conversion that fails is governed by the ON ERROR clause;
+ // NULL ON ERROR is the default.
+ f.checkScalar("json_query('{\"c\":[\"a\"]}', '$.c' "
+ + "returning integer array)",
+ isNullValue(), "INTEGER ARRAY");
+ f.checkScalar("json_query('{\"c\":[\"a\"]}', '$.c' "
+ + "returning integer array null on error)",
+ isNullValue(), "INTEGER ARRAY");
+ f.checkScalar("json_query('{\"c\":[\"a\"]}', '$.c' "
+ + "returning integer array empty array on error)",
+ "[]", "INTEGER ARRAY");
+ f.checkFails("json_query('{\"c\":[\"a\"]}', '$.c' "
+ + "returning integer array error on error)",
+ "(?s).*For input string: \"a\".*", true);
+ }
+
@Test void testJsonPretty() {
final SqlOperatorFixture f = fixture();
f.checkString("json_pretty('{\"foo\":100}')",