Vamsi-klu commented on code in PR #18979:
URL: https://github.com/apache/pinot/pull/18979#discussion_r4051881581
##########
pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java:
##########
@@ -611,6 +634,396 @@ public static Object jsonExtractObject(@Nullable Object
object) {
return null;
}
+ /// Extract a scalar (or scalar-array) value from a JSON document and coerce
it to `resultsType`.
+ ///
+ /// Scalar-function counterpart of the `jsonExtractScalar` transform
(`JsonExtractScalarTransformFunction` in
+ /// pinot-core), so that `json_extract_scalar(...)` resolves in the
multi-stage engine and in ad-hoc scalar
+ /// contexts. `resultsType` is a Pinot [DataType] name, optionally suffixed
with `_ARRAY` for a multi-value
+ /// result. Supported types are
`INT/LONG/FLOAT/DOUBLE/BIG_DECIMAL/BOOLEAN/TIMESTAMP/STRING/JSON/BYTES` and
+ /// the `INT/LONG/FLOAT/DOUBLE/BIG_DECIMAL/BOOLEAN/TIMESTAMP/STRING` array
variants.
+ ///
+ /// The document may be a `String`, a UTF-8 encoded `byte[]` (BYTES columns)
or an already-parsed container.
+ /// Coercion mirrors the transform exactly: `BOOLEAN` is returned as its
stored `INT` (0/1), `TIMESTAMP` as
+ /// epoch millis (numeric values as-is, strings via ISO-8601), `BIG_DECIMAL`
/ `STRING` / `JSON` use a
+ /// BigDecimal-preserving parser. The 3-argument form throws on an
unresolved single-value path. The
+ /// 4-argument form returns `defaultValue` (including SQL `NULL`). A
multi-value path yields an empty
+ /// array when unresolved, but a `null` element inside a resolved array
still throws unless a default is
+ /// supplied. A malformed JSON document is treated as unresolved.
+ @ScalarFunction
+ public static Object jsonExtractScalar(Object jsonInput, String jsonPath,
String resultsType) {
+ return jsonExtractScalarInternal(jsonInput, jsonPath, resultsType, null,
false);
+ }
+
+ /// See [#jsonExtractScalar(Object, String, String)]. `defaultValue` is
returned (coerced to `resultsType`)
+ /// when the path resolves to `null` or the document is malformed. An
explicit SQL `NULL` default returns
+ /// Java `null` rather than throwing.
+ @ScalarFunction(nullableParameters = true)
Review Comment:
Added `@Nullable` to the four-argument overload's return value and covered
the explicit SQL `NULL` default path.
##########
pinot-common/src/test/java/org/apache/pinot/common/utils/JsonNumberUtilsTest.java:
##########
@@ -0,0 +1,105 @@
+/**
+ * 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.pinot.spi.utils;
+
+import org.testng.Assert;
Review Comment:
Updated the new test code to use static TestNG assertion imports.
##########
pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java:
##########
@@ -611,6 +634,409 @@ public static Object jsonExtractObject(@Nullable Object
object) {
return null;
}
+ /// Extract a scalar (or scalar-array) value from a JSON document and coerce
it to `resultsType`.
+ ///
+ /// Scalar-function counterpart of the `jsonExtractScalar` transform
(`JsonExtractScalarTransformFunction` in
+ /// pinot-core), so that `json_extract_scalar(...)` resolves in the
multi-stage engine and in ad-hoc scalar
+ /// contexts. `resultsType` is a Pinot [DataType] name, optionally suffixed
with `_ARRAY` for a multi-value
+ /// result. Supported types are
`INT/LONG/FLOAT/DOUBLE/BIG_DECIMAL/BOOLEAN/TIMESTAMP/STRING/JSON/BYTES` and
+ /// the `INT/LONG/FLOAT/DOUBLE/BIG_DECIMAL/BOOLEAN/TIMESTAMP/STRING` array
variants.
+ ///
+ /// The document may be a `String`, a UTF-8 encoded `byte[]` (BYTES columns)
or an already-parsed container.
+ /// Coercion mirrors the transform exactly: `BOOLEAN` is returned as its
stored `INT` (0/1), `TIMESTAMP` as
+ /// epoch millis (numeric values as-is, strings via ISO-8601), `BIG_DECIMAL`
/ `STRING` / `JSON` / `LONG`
+ /// use a BigDecimal-preserving parser. The 3-argument form throws on an
unresolved single-value path. The
+ /// 4-argument form returns `defaultValue` (including SQL `NULL`). A
multi-value path yields an empty
+ /// array when unresolved or when the resolved value is not a JSON array,
but a `null` element inside a
+ /// resolved array still throws unless a default is supplied. A malformed
JSON document is treated as
+ /// unresolved. An illegal JSONPath (for example `$[`) is rejected, matching
transform init.
+ @ScalarFunction
+ public static Object jsonExtractScalar(Object jsonInput, String jsonPath,
String resultsType) {
+ return jsonExtractScalarInternal(jsonInput, jsonPath, resultsType, null,
false);
+ }
+
+ /// See [#jsonExtractScalar(Object, String, String)]. `defaultValue` is
returned (coerced to `resultsType`)
+ /// when the path resolves to `null` or the document is malformed. An
explicit SQL `NULL` default returns
+ /// Java `null` rather than throwing.
+ @ScalarFunction(nullableParameters = true)
+ public static Object jsonExtractScalar(@Nullable Object jsonInput, String
jsonPath, String resultsType,
+ @Nullable Object defaultValue) {
+ return jsonExtractScalarInternal(jsonInput, jsonPath, resultsType,
defaultValue, true);
+ }
+
+ @Nullable
+ private static Object jsonExtractScalarInternal(@Nullable Object jsonInput,
String jsonPath, String resultsType,
+ @Nullable Object defaultValue, boolean hasDefault) {
+ String type = resultsType.toUpperCase();
+ boolean isSingleValue = !type.endsWith("_ARRAY");
+ DataType dataType;
+ try {
+ dataType = DataType.valueOf(isSingleValue ? type : type.substring(0,
type.length() - 6));
+ } catch (IllegalArgumentException e) {
+ throw new
IllegalArgumentException(unsupportedResultsTypeMessage(resultsType));
+ }
+ // BIG_DECIMAL / STRING / JSON / LONG must read floats as BigDecimal so
values above 2^53 stay exact.
+ boolean useBigDecimal = dataType == DataType.BIG_DECIMAL || dataType ==
DataType.STRING
+ || dataType == DataType.JSON || dataType == DataType.LONG;
+ // Compile the path before touching the document. `$[` must fail the
query, not become a default,
+ // matching JsonExtractScalarTransformFunction#init.
+ JsonPath compiledPath = JsonPathCache.INSTANCE.getOrCompute(jsonPath);
+ if (isSingleValue) {
+ Object value = readJsonPathValue(jsonInput, compiledPath, useBigDecimal);
+ if (value == null) {
+ if (!hasDefault) {
+ throw new IllegalArgumentException(
+ "Cannot resolve JSON path on some records. Consider setting a
default value.");
+ }
+ if (defaultValue == null) {
+ return null;
+ }
+ return coerceScalar(defaultValue, dataType, true);
+ }
+ return coerceScalar(value, dataType, false);
+ }
+ return coerceScalarArray(readJsonPathArray(jsonInput, compiledPath,
useBigDecimal), dataType, defaultValue,
+ hasDefault);
+ }
+
+ private static DocumentContext parseJsonDocument(Object jsonInput,
ParseContext parseContext) {
+ if (jsonInput instanceof String) {
+ return parseContext.parse((String) jsonInput);
+ }
+ if (jsonInput instanceof byte[]) {
+ // BYTES columns carry the raw UTF-8 document; parse(Object) would treat
the array as already parsed.
+ return parseContext.parseUtf8((byte[]) jsonInput);
+ }
+ return parseContext.parse(jsonInput);
+ }
+
+ @Nullable
+ private static Object readJsonPathValue(@Nullable Object jsonInput, JsonPath
jsonPath, boolean useBigDecimal) {
+ DocumentContext document = parseJsonDocumentOrNull(jsonInput,
useBigDecimal);
+ if (document == null) {
+ return null;
+ }
+ return document.read(jsonPath);
+ }
+
+ @Nullable
+ private static Object[] readJsonPathArray(@Nullable Object jsonInput,
JsonPath jsonPath, boolean useBigDecimal) {
+ DocumentContext document = parseJsonDocumentOrNull(jsonInput,
useBigDecimal);
+ if (document == null) {
+ return null;
+ }
+ return jsonArrayOrUnresolved(document.read(jsonPath));
+ }
+
+ /// Parses the document only. A bad document is unresolved (null). Path
compile already happened
+ /// in [jsonExtractScalarInternal], so a bad path is not swallowed here.
+ @Nullable
+ private static DocumentContext parseJsonDocumentOrNull(@Nullable Object
jsonInput, boolean useBigDecimal) {
+ if (jsonInput == null) {
+ return null;
+ }
+ try {
+ return parseJsonDocument(jsonInput, useBigDecimal ?
PARSE_CONTEXT_WITH_BIG_DECIMAL : PARSE_CONTEXT);
+ } catch (Exception e) {
+ // Malformed JSON (e.g. a plain-text row) is treated as unresolved,
mirroring the transform which swallows
+ // per-row parse errors; the caller then applies the default or throws.
+ return null;
+ }
+ }
+
+ /// Transform casts the JsonPath result to `List`. A scalar or object is
therefore unresolved and
+ /// becomes an empty array. Do not wrap a non-list in a one-element array.
+ @Nullable
+ private static Object[] jsonArrayOrUnresolved(@Nullable Object value) {
+ if (value instanceof List) {
+ return ((List<?>) value).toArray();
+ }
+ return null;
+ }
+
+ private static Object coerceScalar(Object value, DataType dataType, boolean
isDefault) {
+ switch (dataType) {
+ case INT:
+ return coerceToInt(value, false);
+ case BOOLEAN:
+ return coerceToInt(value, true);
+ case LONG:
+ return coerceToLong(value, false);
+ case TIMESTAMP:
+ return coerceToLong(value, true);
+ case FLOAT:
+ return coerceToFloat(value);
+ case DOUBLE:
+ return coerceToDouble(value);
+ case BIG_DECIMAL:
+ return coerceToBigDecimal(value);
+ case STRING:
+ case JSON:
+ return coerceToString(value);
+ case BYTES:
+ return isDefault ? coerceDefaultToBytes(value) :
coerceExtractedToBytes(value);
+ default:
+ throw new
IllegalArgumentException(unsupportedResultsTypeMessage(dataType.name()));
+ }
+ }
+
+ private static Object coerceScalarArray(@Nullable Object[] array, DataType
dataType, @Nullable Object defaultValue,
+ boolean hasDefault) {
+ switch (dataType) {
+ case INT:
+ return toIntArray(array, defaultValue, hasDefault, false);
+ case BOOLEAN:
+ return toIntArray(array, defaultValue, hasDefault, true);
+ case LONG:
+ return toLongArray(array, defaultValue, hasDefault, false);
+ case TIMESTAMP:
+ return toLongArray(array, defaultValue, hasDefault, true);
+ case FLOAT:
+ return toFloatArray(array, defaultValue, hasDefault);
+ case DOUBLE:
+ return toDoubleArray(array, defaultValue, hasDefault);
+ case BIG_DECIMAL:
+ return toBigDecimalArray(array, defaultValue, hasDefault);
+ case STRING:
+ return toStringArray(array, defaultValue, hasDefault);
+ default:
+ throw new
IllegalArgumentException(unsupportedResultsTypeMessage(dataType.name() +
"_ARRAY"));
+ }
+ }
+
+ /// Resolve a single array element: pass through a non-null element,
substitute the default when the
+ /// element is `null` and a default was supplied, or throw when a null
element has no default.
+ /// An explicit SQL `NULL` default returns `null` so the caller can write a
type placeholder.
+ @Nullable
+ private static Object resolveArrayElement(@Nullable Object element,
@Nullable Object defaultValue,
+ boolean hasDefault) {
+ if (element != null) {
+ return element;
+ }
+ if (!hasDefault) {
+ throw new IllegalArgumentException(
+ "At least one of the resolved JSON arrays include nulls, which is
not supported in Pinot. "
+ + "Consider setting a default value as the fourth argument of
json_extract_scalar.");
+ }
+ return defaultValue;
+ }
+
+ private static int[] toIntArray(@Nullable Object[] array, @Nullable Object
defaultValue, boolean hasDefault,
+ boolean isBoolean) {
+ if (array == null) {
+ return new int[0];
+ }
+ int[] values = new int[array.length];
+ for (int i = 0; i < array.length; i++) {
+ Object resolved = resolveArrayElement(array[i], defaultValue,
hasDefault);
+ values[i] = resolved == null ? 0 : coerceToInt(resolved, isBoolean);
+ }
+ return values;
+ }
+
+ private static long[] toLongArray(@Nullable Object[] array, @Nullable Object
defaultValue, boolean hasDefault,
+ boolean isTimestamp) {
+ if (array == null) {
+ return new long[0];
+ }
+ long[] values = new long[array.length];
+ for (int i = 0; i < array.length; i++) {
+ Object resolved = resolveArrayElement(array[i], defaultValue,
hasDefault);
+ values[i] = resolved == null ? 0L : coerceToLong(resolved, isTimestamp);
+ }
+ return values;
+ }
+
+ private static float[] toFloatArray(@Nullable Object[] array, @Nullable
Object defaultValue, boolean hasDefault) {
+ if (array == null) {
+ return new float[0];
+ }
+ float[] values = new float[array.length];
+ for (int i = 0; i < array.length; i++) {
+ Object resolved = resolveArrayElement(array[i], defaultValue,
hasDefault);
+ values[i] = resolved == null ? 0f : coerceToFloat(resolved);
+ }
+ return values;
+ }
+
+ private static double[] toDoubleArray(@Nullable Object[] array, @Nullable
Object defaultValue, boolean hasDefault) {
+ if (array == null) {
+ return new double[0];
+ }
+ double[] values = new double[array.length];
+ for (int i = 0; i < array.length; i++) {
+ Object resolved = resolveArrayElement(array[i], defaultValue,
hasDefault);
+ values[i] = resolved == null ? 0d : coerceToDouble(resolved);
+ }
+ return values;
+ }
+
+ private static BigDecimal[] toBigDecimalArray(@Nullable Object[] array,
@Nullable Object defaultValue,
+ boolean hasDefault) {
+ if (array == null) {
+ return new BigDecimal[0];
+ }
+ BigDecimal[] values = new BigDecimal[array.length];
+ for (int i = 0; i < array.length; i++) {
+ Object resolved = resolveArrayElement(array[i], defaultValue,
hasDefault);
+ values[i] = resolved == null ? BigDecimal.ZERO :
coerceToBigDecimal(resolved);
+ }
+ return values;
+ }
+
+ private static String[] toStringArray(@Nullable Object[] array, @Nullable
Object defaultValue, boolean hasDefault) {
+ if (array == null) {
+ return new String[0];
+ }
+ String[] values = new String[array.length];
+ for (int i = 0; i < array.length; i++) {
+ Object resolved = resolveArrayElement(array[i], defaultValue,
hasDefault);
+ values[i] = resolved == null ? "" : coerceToString(resolved);
+ }
+ return values;
+ }
+
+ /// SQL BYTES defaults are hex literals or raw `byte[]`. Do not
Base64-decode them.
+ private static byte[] coerceDefaultToBytes(Object value) {
+ if (value instanceof byte[]) {
+ return (byte[]) value;
+ }
+ return BytesUtils.toBytes(value.toString());
+ }
+
+ /// Coerces a JsonPath result to stored `INT`. When `isBoolean` is true,
follows Pinot's numeric
+ /// BOOLEAN convention (any non-zero `Number` is true; `"true"` / `"TRUE"` /
`"1"` via
+ /// [BooleanUtils#toInt(String)]).
+ public static int coerceToInt(Object value, boolean isBoolean) {
+ if (isBoolean) {
+ if (value instanceof Boolean) {
+ return (Boolean) value ? 1 : 0;
+ }
+ // For BOOLEAN result, follow Pinot's numeric convention: any non-zero
number is true.
+ if (value instanceof Number) {
+ return ((Number) value).doubleValue() != 0 ? 1 : 0;
+ }
+ // String fallback: BooleanUtils.toInt accepts "true" / "TRUE" / "1".
+ return BooleanUtils.toInt(value.toString());
+ }
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ if (value instanceof Boolean) {
+ return (Boolean) value ? 1 : 0;
+ }
+ return Integer.parseInt(value.toString());
+ }
+
+ /// Coerces a JsonPath result to stored `LONG`. When `isTimestamp` is true,
numeric values are
+ /// epoch millis and strings go through [TimestampUtils#toMillisSinceEpoch].
Otherwise string
+ /// numbers use [JsonNumberUtils#parseJsonLong] (exact decimal, truncate
toward zero, reject
+ /// overflow). Unquoted JSON numbers arrive as [Number]; floats should
already be [BigDecimal]
+ /// from the LONG parse context so values above `2^53` stay exact.
+ public static long coerceToLong(Object value, boolean isTimestamp) {
+ if (value instanceof Number) {
+ return longFromJsonNumber((Number) value);
+ }
+ if (isTimestamp) {
+ return TimestampUtils.toMillisSinceEpoch(value.toString());
+ }
+ if (value instanceof Boolean) {
+ return (Boolean) value ? 1L : 0L;
+ }
+ try {
+ return JsonNumberUtils.parseJsonLong(value.toString());
+ } catch (NumberFormatException e) {
+ throw new NumberFormatException("For input string: \"" + value + "\"");
+ }
+ }
+
+ /// Converts a JsonPath [Number] to long without saturating.
`Double.longValue()` and
+ /// `BigInteger.longValue()` wrap or clamp values outside the long range.
+ private static long longFromJsonNumber(Number number) {
+ if (number instanceof BigInteger) {
+ try {
+ return ((BigInteger) number).longValueExact();
+ } catch (ArithmeticException e) {
+ throw new NumberFormatException("For input string: \"" + number +
"\"");
+ }
+ }
+ if (number instanceof BigDecimal) {
+ try {
+ return ((BigDecimal) number).setScale(0,
RoundingMode.DOWN).longValueExact();
+ } catch (ArithmeticException e) {
+ throw new NumberFormatException("For input string: \"" + number +
"\"");
+ }
+ }
+ if (number instanceof Double || number instanceof Float) {
+ try {
+ return BigDecimal.valueOf(number.doubleValue()).setScale(0,
RoundingMode.DOWN).longValueExact();
Review Comment:
Fixed. Materialized `Double` and `Float` values now use a finite,
range-checked cast, so `100000000000000032d` and `(double) Long.MIN_VALUE` keep
their represented values. NaN, infinities, and values at `2^63` are rejected. I
added regression coverage for each case.
##########
pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java:
##########
@@ -611,6 +634,409 @@ public static Object jsonExtractObject(@Nullable Object
object) {
return null;
}
+ /// Extract a scalar (or scalar-array) value from a JSON document and coerce
it to `resultsType`.
+ ///
+ /// Scalar-function counterpart of the `jsonExtractScalar` transform
(`JsonExtractScalarTransformFunction` in
+ /// pinot-core), so that `json_extract_scalar(...)` resolves in the
multi-stage engine and in ad-hoc scalar
+ /// contexts. `resultsType` is a Pinot [DataType] name, optionally suffixed
with `_ARRAY` for a multi-value
+ /// result. Supported types are
`INT/LONG/FLOAT/DOUBLE/BIG_DECIMAL/BOOLEAN/TIMESTAMP/STRING/JSON/BYTES` and
+ /// the `INT/LONG/FLOAT/DOUBLE/BIG_DECIMAL/BOOLEAN/TIMESTAMP/STRING` array
variants.
+ ///
+ /// The document may be a `String`, a UTF-8 encoded `byte[]` (BYTES columns)
or an already-parsed container.
+ /// Coercion mirrors the transform exactly: `BOOLEAN` is returned as its
stored `INT` (0/1), `TIMESTAMP` as
+ /// epoch millis (numeric values as-is, strings via ISO-8601), `BIG_DECIMAL`
/ `STRING` / `JSON` / `LONG`
+ /// use a BigDecimal-preserving parser. The 3-argument form throws on an
unresolved single-value path. The
+ /// 4-argument form returns `defaultValue` (including SQL `NULL`). A
multi-value path yields an empty
+ /// array when unresolved or when the resolved value is not a JSON array,
but a `null` element inside a
+ /// resolved array still throws unless a default is supplied. A malformed
JSON document is treated as
+ /// unresolved. An illegal JSONPath (for example `$[`) is rejected, matching
transform init.
+ @ScalarFunction
+ public static Object jsonExtractScalar(Object jsonInput, String jsonPath,
String resultsType) {
+ return jsonExtractScalarInternal(jsonInput, jsonPath, resultsType, null,
false);
+ }
+
+ /// See [#jsonExtractScalar(Object, String, String)]. `defaultValue` is
returned (coerced to `resultsType`)
+ /// when the path resolves to `null` or the document is malformed. An
explicit SQL `NULL` default returns
+ /// Java `null` rather than throwing.
+ @ScalarFunction(nullableParameters = true)
+ public static Object jsonExtractScalar(@Nullable Object jsonInput, String
jsonPath, String resultsType,
+ @Nullable Object defaultValue) {
+ return jsonExtractScalarInternal(jsonInput, jsonPath, resultsType,
defaultValue, true);
+ }
+
+ @Nullable
+ private static Object jsonExtractScalarInternal(@Nullable Object jsonInput,
String jsonPath, String resultsType,
+ @Nullable Object defaultValue, boolean hasDefault) {
+ String type = resultsType.toUpperCase();
+ boolean isSingleValue = !type.endsWith("_ARRAY");
+ DataType dataType;
+ try {
+ dataType = DataType.valueOf(isSingleValue ? type : type.substring(0,
type.length() - 6));
+ } catch (IllegalArgumentException e) {
+ throw new
IllegalArgumentException(unsupportedResultsTypeMessage(resultsType));
+ }
+ // BIG_DECIMAL / STRING / JSON / LONG must read floats as BigDecimal so
values above 2^53 stay exact.
+ boolean useBigDecimal = dataType == DataType.BIG_DECIMAL || dataType ==
DataType.STRING
+ || dataType == DataType.JSON || dataType == DataType.LONG;
+ // Compile the path before touching the document. `$[` must fail the
query, not become a default,
+ // matching JsonExtractScalarTransformFunction#init.
+ JsonPath compiledPath = JsonPathCache.INSTANCE.getOrCompute(jsonPath);
+ if (isSingleValue) {
+ Object value = readJsonPathValue(jsonInput, compiledPath, useBigDecimal);
+ if (value == null) {
+ if (!hasDefault) {
+ throw new IllegalArgumentException(
+ "Cannot resolve JSON path on some records. Consider setting a
default value.");
+ }
+ if (defaultValue == null) {
+ return null;
+ }
+ return coerceScalar(defaultValue, dataType, true);
+ }
+ return coerceScalar(value, dataType, false);
+ }
+ return coerceScalarArray(readJsonPathArray(jsonInput, compiledPath,
useBigDecimal), dataType, defaultValue,
+ hasDefault);
+ }
+
+ private static DocumentContext parseJsonDocument(Object jsonInput,
ParseContext parseContext) {
+ if (jsonInput instanceof String) {
+ return parseContext.parse((String) jsonInput);
+ }
+ if (jsonInput instanceof byte[]) {
+ // BYTES columns carry the raw UTF-8 document; parse(Object) would treat
the array as already parsed.
+ return parseContext.parseUtf8((byte[]) jsonInput);
+ }
+ return parseContext.parse(jsonInput);
+ }
+
+ @Nullable
+ private static Object readJsonPathValue(@Nullable Object jsonInput, JsonPath
jsonPath, boolean useBigDecimal) {
+ DocumentContext document = parseJsonDocumentOrNull(jsonInput,
useBigDecimal);
+ if (document == null) {
+ return null;
+ }
+ return document.read(jsonPath);
+ }
+
+ @Nullable
+ private static Object[] readJsonPathArray(@Nullable Object jsonInput,
JsonPath jsonPath, boolean useBigDecimal) {
+ DocumentContext document = parseJsonDocumentOrNull(jsonInput,
useBigDecimal);
+ if (document == null) {
+ return null;
+ }
+ return jsonArrayOrUnresolved(document.read(jsonPath));
+ }
+
+ /// Parses the document only. A bad document is unresolved (null). Path
compile already happened
+ /// in [jsonExtractScalarInternal], so a bad path is not swallowed here.
+ @Nullable
+ private static DocumentContext parseJsonDocumentOrNull(@Nullable Object
jsonInput, boolean useBigDecimal) {
+ if (jsonInput == null) {
+ return null;
+ }
+ try {
+ return parseJsonDocument(jsonInput, useBigDecimal ?
PARSE_CONTEXT_WITH_BIG_DECIMAL : PARSE_CONTEXT);
+ } catch (Exception e) {
+ // Malformed JSON (e.g. a plain-text row) is treated as unresolved,
mirroring the transform which swallows
+ // per-row parse errors; the caller then applies the default or throws.
+ return null;
+ }
+ }
+
+ /// Transform casts the JsonPath result to `List`. A scalar or object is
therefore unresolved and
+ /// becomes an empty array. Do not wrap a non-list in a one-element array.
+ @Nullable
+ private static Object[] jsonArrayOrUnresolved(@Nullable Object value) {
+ if (value instanceof List) {
Review Comment:
Fixed. Both JSONPath contexts now use the `Object[]`-aware provider, and
array result handling accepts `Object[]` as well as `List`. Tests cover root
arrays, nested LONG traversal, and the `jsonPathArray` ingestion chain while
still rejecting scalar and object results.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]