This is an automated email from the ASF dual-hosted git repository.
Jackie-Jiang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 5cb954d5786 Improve JsonExtractScalarTransformFunction: type coercion,
BIG_DECIMAL_ARRAY, guards (#18429)
5cb954d5786 is described below
commit 5cb954d5786e018f185fae8513eedf07a228706e
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Tue May 5 17:38:04 2026 -0700
Improve JsonExtractScalarTransformFunction: type coercion,
BIG_DECIMAL_ARRAY, guards (#18429)
---
.../JsonExtractScalarTransformFunction.java | 451 +++++++++++++--------
.../JsonExtractScalarTransformFunctionTest.java | 314 ++++++++++++++
2 files changed, 596 insertions(+), 169 deletions(-)
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java
index 5c76ed9e01c..dee336e2dc9 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunction.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.core.operator.transform.function;
+import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.Configuration;
@@ -38,24 +39,41 @@ import
org.apache.pinot.core.operator.transform.TransformResultMetadata;
import org.apache.pinot.core.util.NumberUtils;
import org.apache.pinot.core.util.NumericException;
import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.utils.BooleanUtils;
import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.TimestampUtils;
import org.roaringbitmap.RoaringBitmap;
-/**
- * The <code>JsonExtractScalarTransformFunction</code> class implements the
json path transformation based on
- * <a href="https://goessner.net/articles/JsonPath/">Stefan Goessner JsonPath
implementation.</a>.
- *
- * Please note, currently this method only works with String field. The values
in this field should be Json String.
- *
- * Usage:
- * jsonExtractScalar(jsonFieldName, 'jsonPath', 'resultsType')
- * <code>jsonFieldName</code> is the Json String field/expression.
- * <code>jsonPath</code> is a JsonPath expression which used to read from JSON
document
- * <code>results_type</code> refers to the results data type, could be INT,
LONG, FLOAT, DOUBLE, BIG_DECIMAL, STRING,
- * INT_ARRAY, LONG_ARRAY, FLOAT_ARRAY, DOUBLE_ARRAY, STRING_ARRAY.
- *
- */
+/// Implements the `jsonExtractScalar(jsonField, jsonPath, resultsType[,
defaultValue])` transform.
+/// Reads a JSON document from `jsonField` for each row, resolves the
+/// [Stefan Goessner JsonPath](https://goessner.net/articles/JsonPath/)
expression against it, and
+/// converts the resolved value to `resultsType`.
+///
+/// **Arguments:**
+/// - `jsonField` — single-value `STRING` or `BYTES` column / transform
expression containing JSON.
+/// - `jsonPath` — JsonPath expression used to read the value.
+/// - `resultsType` — Pinot data type for the output. Append `_ARRAY` for
multi-value results.
+/// - `defaultValue` (optional) — used when the path resolves to `null` or
fails. Without it, unresolved
+/// SV rows throw `IllegalArgumentException`; MV rows surface as empty
arrays, but null elements within
+/// a resolved array still throw.
+///
+/// **Supported `resultsType`:** `INT`, `LONG`, `FLOAT`, `DOUBLE`,
`BIG_DECIMAL`, `BOOLEAN`, `TIMESTAMP`,
+/// `STRING`, `JSON`, `BYTES`, plus `_ARRAY` variants of `INT` / `LONG` /
`FLOAT` / `DOUBLE` /
+/// `BIG_DECIMAL` / `STRING`.
+///
+/// **Per-row coercion** of the JsonPath result to `resultsType`:
+/// - `BOOLEAN` (stored as `INT`) follows Pinot's numeric convention — any
non-zero `Number` is true;
+/// `Boolean` and `"true"` / `"TRUE"` / `"1"` strings (via
[BooleanUtils#toInt(String)]) are also true.
+/// - `TIMESTAMP` (stored as `LONG`) accepts numeric epoch millis directly;
strings go through
+/// [TimestampUtils#toMillisSinceEpoch] (ISO-8601 and numeric millis
strings).
+/// - `STRING` returns `String` values as-is; other JSON values are serialized
via
+/// [JsonUtils#objectToString].
+/// - `BIG_DECIMAL` and `STRING` paths use a BigDecimal-preserving JSON parser
+/// (`JSON_PARSER_CONTEXT_WITH_BIG_DECIMAL`) to avoid precision loss on
numeric values; other paths use
+/// the default parser.
+/// - Other types coerce via `Number` cast (preserved as the canonical
primitive form) or
+/// `parse*(toString())`.
public class JsonExtractScalarTransformFunction extends BaseTransformFunction {
public static final String FUNCTION_NAME = "jsonExtractScalar";
@@ -73,6 +91,8 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
private TransformFunction _jsonFieldTransformFunction;
private JsonPath _jsonPath;
+ private DataType _dataType;
+ private DataType _storedType;
private Object _defaultValue;
private boolean _defaultIsNull;
private TransformResultMetadata _resultMetadata;
@@ -104,19 +124,19 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
_jsonPath = JsonPathCache.INSTANCE.getOrCompute(jsonPathString);
String resultsType = ((LiteralTransformFunction)
arguments.get(2)).getStringLiteral().toUpperCase();
boolean isSingleValue = !resultsType.endsWith("_ARRAY");
- DataType dataType;
try {
- dataType = DataType.valueOf(isSingleValue ? resultsType :
resultsType.substring(0, resultsType.length() - 6));
+ _dataType = DataType.valueOf(isSingleValue ? resultsType :
resultsType.substring(0, resultsType.length() - 6));
} catch (Exception e) {
throw new IllegalArgumentException(String.format(
"Unsupported results type: %s for jsonExtractScalar function.
Supported types are: "
- +
"INT/LONG/FLOAT/DOUBLE/BOOLEAN/BIG_DECIMAL/TIMESTAMP/STRING/INT_ARRAY/LONG_ARRAY/FLOAT_ARRAY"
- + "/DOUBLE_ARRAY/STRING_ARRAY", resultsType));
+ +
"INT/LONG/FLOAT/DOUBLE/BIG_DECIMAL/BOOLEAN/TIMESTAMP/STRING/JSON/BYTES/"
+ +
"INT_ARRAY/LONG_ARRAY/FLOAT_ARRAY/DOUBLE_ARRAY/BIG_DECIMAL_ARRAY/STRING_ARRAY",
resultsType));
}
+ _storedType = _dataType.getStoredType();
if (arguments.size() == 4) {
LiteralTransformFunction literalTransformFun =
(LiteralTransformFunction) arguments.get(3);
_defaultIsNull = literalTransformFun.isNull() && _nullHandlingEnabled;
- switch (dataType) {
+ switch (_dataType) {
case INT:
_defaultValue = literalTransformFun.getIntLiteral();
break;
@@ -129,15 +149,17 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
case DOUBLE:
_defaultValue = literalTransformFun.getDoubleLiteral();
break;
- case TIMESTAMP:
- // Use long literal so numeric millis stay exact and string
timestamps use LiteralContext parsing.
- _defaultValue = literalTransformFun.getLongLiteral();
+ case BIG_DECIMAL:
+ _defaultValue = literalTransformFun.getBigDecimalLiteral();
break;
case BOOLEAN:
- _defaultValue = literalTransformFun.getBooleanLiteral();
+ // Stored as Integer 0 / 1 to match BOOLEAN's storedType (INT) so
per-row consumers can
+ // unbox directly without a Boolean → Integer conversion.
+ _defaultValue = literalTransformFun.getBooleanLiteral() ? 1 : 0;
break;
- case BIG_DECIMAL:
- _defaultValue = literalTransformFun.getBigDecimalLiteral();
+ case TIMESTAMP:
+ // Use long literal so numeric millis stay exact and string
timestamps use LiteralContext parsing.
+ _defaultValue = literalTransformFun.getLongLiteral();
break;
case STRING:
case JSON:
@@ -148,12 +170,12 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
break;
default:
throw new IllegalArgumentException(
- "Unsupported results type: " + dataType + " for
jsonExtractScalar function. Supported types are: "
- +
"INT/LONG/FLOAT/DOUBLE/BOOLEAN/BIG_DECIMAL/TIMESTAMP/STRING/JSON/BYTES"
+ "Unsupported results type: " + _dataType + " for
jsonExtractScalar function. Supported types are: "
+ +
"INT/LONG/FLOAT/DOUBLE/BIG_DECIMAL/BOOLEAN/TIMESTAMP/STRING/JSON/BYTES"
);
}
}
- _resultMetadata = new TransformResultMetadata(dataType, isSingleValue,
false);
+ _resultMetadata = new TransformResultMetadata(_dataType, isSingleValue,
false);
}
@Override
@@ -198,20 +220,13 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
@Override
public int[] transformToIntValuesSV(ValueBlock valueBlock) {
- if (_resultMetadata.getDataType().getStoredType() != DataType.INT) {
+ if (_storedType != DataType.INT) {
return super.transformToIntValuesSV(valueBlock);
}
-
initIntValuesSV(valueBlock.getNumDocs());
IntFunction<Object> resultExtractor = getResultExtractor(valueBlock);
- int defaultValue = 0;
- if (_defaultValue != null) {
- if (_defaultValue instanceof Number) {
- defaultValue = ((Number) _defaultValue).intValue();
- } else {
- defaultValue = Integer.parseInt(_defaultValue.toString());
- }
- }
+ int defaultValue = _defaultValue != null ? (Integer) _defaultValue : 0;
+ boolean isBoolean = _dataType == DataType.BOOLEAN;
int numDocs = valueBlock.getNumDocs();
for (int i = 0; i < numDocs; i++) {
Object result = null;
@@ -227,30 +242,20 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a
default value.");
}
- if (result instanceof Number) {
- _intValuesSV[i] = ((Number) result).intValue();
- } else {
- _intValuesSV[i] = Integer.parseInt(result.toString());
- }
+ _intValuesSV[i] = toInt(result, isBoolean);
}
return _intValuesSV;
}
@Override
public long[] transformToLongValuesSV(ValueBlock valueBlock) {
- if (_resultMetadata.getDataType().getStoredType() != DataType.LONG) {
+ if (_storedType != DataType.LONG) {
return super.transformToLongValuesSV(valueBlock);
}
initLongValuesSV(valueBlock.getNumDocs());
IntFunction<Object> resultExtractor = getResultExtractor(valueBlock);
- long defaultValue = 0;
- if (_defaultValue != null) {
- if (_defaultValue instanceof Number) {
- defaultValue = ((Number) _defaultValue).longValue();
- } else {
- defaultValue = Long.parseLong(_defaultValue.toString());
- }
- }
+ long defaultValue = _defaultValue != null ? (Long) _defaultValue : 0L;
+ boolean isTimestamp = _dataType == DataType.TIMESTAMP;
int numDocs = valueBlock.getNumDocs();
for (int i = 0; i < numDocs; i++) {
Object result = null;
@@ -266,31 +271,19 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a
default value.");
}
- if (result instanceof Number) {
- _longValuesSV[i] = ((Number) result).longValue();
- } else {
- try {
- _longValuesSV[i] = NumberUtils.parseJsonLong(result.toString());
- } catch (NumericException nfe) {
- throw new NumberFormatException("For input string: \"" + result +
"\"");
- }
- }
+ _longValuesSV[i] = toLong(result, isTimestamp);
}
return _longValuesSV;
}
@Override
public float[] transformToFloatValuesSV(ValueBlock valueBlock) {
+ if (_storedType != DataType.FLOAT) {
+ return super.transformToFloatValuesSV(valueBlock);
+ }
initFloatValuesSV(valueBlock.getNumDocs());
IntFunction<Object> resultExtractor = getResultExtractor(valueBlock);
- float defaultValue = 0;
- if (_defaultValue != null) {
- if (_defaultValue instanceof Number) {
- defaultValue = ((Number) _defaultValue).floatValue();
- } else {
- defaultValue = Float.parseFloat(_defaultValue.toString());
- }
- }
+ float defaultValue = _defaultValue != null ? (Float) _defaultValue : 0f;
int numDocs = valueBlock.getNumDocs();
for (int i = 0; i < numDocs; i++) {
Object result = null;
@@ -306,27 +299,19 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a
default value.");
}
- if (result instanceof Number) {
- _floatValuesSV[i] = ((Number) result).floatValue();
- } else {
- _floatValuesSV[i] = Float.parseFloat(result.toString());
- }
+ _floatValuesSV[i] = toFloat(result);
}
return _floatValuesSV;
}
@Override
public double[] transformToDoubleValuesSV(ValueBlock valueBlock) {
+ if (_storedType != DataType.DOUBLE) {
+ return super.transformToDoubleValuesSV(valueBlock);
+ }
initDoubleValuesSV(valueBlock.getNumDocs());
IntFunction<Object> resultExtractor = getResultExtractor(valueBlock);
- double defaultValue = 0;
- if (_defaultValue != null) {
- if (_defaultValue instanceof Number) {
- defaultValue = ((Number) _defaultValue).doubleValue();
- } else {
- defaultValue = Double.parseDouble(_defaultValue.toString());
- }
- }
+ double defaultValue = _defaultValue != null ? (Double) _defaultValue : 0d;
int numDocs = valueBlock.getNumDocs();
for (int i = 0; i < numDocs; i++) {
Object result = null;
@@ -342,27 +327,19 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a
default value.");
}
- if (result instanceof Number) {
- _doubleValuesSV[i] = ((Number) result).doubleValue();
- } else {
- _doubleValuesSV[i] = Double.parseDouble(result.toString());
- }
+ _doubleValuesSV[i] = toDouble(result);
}
return _doubleValuesSV;
}
@Override
public BigDecimal[] transformToBigDecimalValuesSV(ValueBlock valueBlock) {
- initBigDecimalValuesSV(valueBlock.getNumDocs());
- IntFunction<Object> resultExtractor = getResultExtractor(valueBlock,
JSON_PARSER_CONTEXT_WITH_BIG_DECIMAL);
- BigDecimal defaultValue = null;
- if (_defaultValue != null) {
- if (_defaultValue instanceof BigDecimal) {
- defaultValue = (BigDecimal) _defaultValue;
- } else {
- defaultValue = new BigDecimal(_defaultValue.toString());
- }
+ if (_storedType != DataType.BIG_DECIMAL) {
+ return super.transformToBigDecimalValuesSV(valueBlock);
}
+ initBigDecimalValuesSV(valueBlock.getNumDocs());
+ IntFunction<Object> resultExtractor =
getResultExtractorWithBigDecimal(valueBlock);
+ BigDecimal defaultValue = (BigDecimal) _defaultValue;
int numDocs = valueBlock.getNumDocs();
for (int i = 0; i < numDocs; i++) {
Object result = null;
@@ -378,23 +355,19 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a
default value.");
}
- if (result instanceof BigDecimal) {
- _bigDecimalValuesSV[i] = (BigDecimal) result;
- } else {
- _bigDecimalValuesSV[i] = new BigDecimal(result.toString());
- }
+ _bigDecimalValuesSV[i] = toBigDecimal(result);
}
return _bigDecimalValuesSV;
}
@Override
public String[] transformToStringValuesSV(ValueBlock valueBlock) {
- initStringValuesSV(valueBlock.getNumDocs());
- IntFunction<Object> resultExtractor = getResultExtractor(valueBlock,
JSON_PARSER_CONTEXT_WITH_BIG_DECIMAL);
- String defaultValue = null;
- if (_defaultValue != null) {
- defaultValue = _defaultValue.toString();
+ if (_storedType != DataType.STRING) {
+ return super.transformToStringValuesSV(valueBlock);
}
+ initStringValuesSV(valueBlock.getNumDocs());
+ IntFunction<Object> resultExtractor =
getResultExtractorWithBigDecimal(valueBlock);
+ String defaultValue = (String) _defaultValue;
int numDocs = valueBlock.getNumDocs();
for (int i = 0; i < numDocs; i++) {
Object result = null;
@@ -410,22 +383,23 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a
default value.");
}
- if (result instanceof String) {
- _stringValuesSV[i] = (String) result;
- } else {
- _stringValuesSV[i] = JsonUtils.objectToJsonNode(result).toString();
- }
+ _stringValuesSV[i] = toString(result);
}
return _stringValuesSV;
}
@Override
public int[][] transformToIntValuesMV(ValueBlock valueBlock) {
+ if (_storedType != DataType.INT) {
+ return super.transformToIntValuesMV(valueBlock);
+ }
initIntValuesMV(valueBlock.getNumDocs());
- IntFunction<List<Integer>> resultExtractor =
getResultExtractor(valueBlock);
+ IntFunction<List<Object>> resultExtractor = getResultExtractor(valueBlock);
+ int defaultValue = _defaultValue != null ? (Integer) _defaultValue : 0;
+ boolean isBoolean = _dataType == DataType.BOOLEAN;
int numDocs = valueBlock.getNumDocs();
for (int i = 0; i < numDocs; i++) {
- List<Integer> result = null;
+ List<Object> result = null;
try {
result = resultExtractor.apply(i);
} catch (Exception ignored) {
@@ -437,17 +411,17 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
int numValues = result.size();
int[] values = new int[numValues];
for (int j = 0; j < numValues; j++) {
- Integer value = result.get(j);
- if (value == null) {
+ Object element = result.get(j);
+ if (element == null) {
if (_defaultValue != null) {
- value = ((Number) _defaultValue).intValue();
- } else {
- 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.");
+ values[j] = defaultValue;
+ continue;
}
+ 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.");
}
- values[j] = value;
+ values[j] = toInt(element, isBoolean);
}
_intValuesMV[i] = values;
}
@@ -456,11 +430,16 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
@Override
public long[][] transformToLongValuesMV(ValueBlock valueBlock) {
+ if (_storedType != DataType.LONG) {
+ return super.transformToLongValuesMV(valueBlock);
+ }
initLongValuesMV(valueBlock.getNumDocs());
- IntFunction<List<Long>> resultExtractor = getResultExtractor(valueBlock);
- int length = valueBlock.getNumDocs();
- for (int i = 0; i < length; i++) {
- List<Long> result = null;
+ IntFunction<List<Object>> resultExtractor = getResultExtractor(valueBlock);
+ long defaultValue = _defaultValue != null ? (Long) _defaultValue : 0L;
+ boolean isTimestamp = _dataType == DataType.TIMESTAMP;
+ int numDocs = valueBlock.getNumDocs();
+ for (int i = 0; i < numDocs; i++) {
+ List<Object> result = null;
try {
result = resultExtractor.apply(i);
} catch (Exception ignored) {
@@ -472,17 +451,17 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
int numValues = result.size();
long[] values = new long[numValues];
for (int j = 0; j < numValues; j++) {
- Long value = result.get(j);
- if (value == null) {
+ Object element = result.get(j);
+ if (element == null) {
if (_defaultValue != null) {
- value = ((Number) _defaultValue).longValue();
- } else {
- 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.");
+ values[j] = defaultValue;
+ continue;
}
+ 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.");
}
- values[j] = value;
+ values[j] = toLong(element, isTimestamp);
}
_longValuesMV[i] = values;
}
@@ -491,11 +470,15 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
@Override
public float[][] transformToFloatValuesMV(ValueBlock valueBlock) {
+ if (_storedType != DataType.FLOAT) {
+ return super.transformToFloatValuesMV(valueBlock);
+ }
initFloatValuesMV(valueBlock.getNumDocs());
- IntFunction<List<Float>> resultExtractor = getResultExtractor(valueBlock);
- int length = valueBlock.getNumDocs();
- for (int i = 0; i < length; i++) {
- List<Float> result = null;
+ IntFunction<List<Object>> resultExtractor = getResultExtractor(valueBlock);
+ float defaultValue = _defaultValue != null ? (Float) _defaultValue : 0f;
+ int numDocs = valueBlock.getNumDocs();
+ for (int i = 0; i < numDocs; i++) {
+ List<Object> result = null;
try {
result = resultExtractor.apply(i);
} catch (Exception ignored) {
@@ -507,17 +490,17 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
int numValues = result.size();
float[] values = new float[numValues];
for (int j = 0; j < numValues; j++) {
- Float value = result.get(j);
- if (value == null) {
+ Object element = result.get(j);
+ if (element == null) {
if (_defaultValue != null) {
- value = ((Number) _defaultValue).floatValue();
- } else {
- 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.");
+ values[j] = defaultValue;
+ continue;
}
+ 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.");
}
- values[j] = value;
+ values[j] = toFloat(element);
}
_floatValuesMV[i] = values;
}
@@ -526,11 +509,15 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
@Override
public double[][] transformToDoubleValuesMV(ValueBlock valueBlock) {
+ if (_storedType != DataType.DOUBLE) {
+ return super.transformToDoubleValuesMV(valueBlock);
+ }
initDoubleValuesMV(valueBlock.getNumDocs());
- IntFunction<List<Double>> resultExtractor = getResultExtractor(valueBlock);
- int length = valueBlock.getNumDocs();
- for (int i = 0; i < length; i++) {
- List<Double> result = null;
+ IntFunction<List<Object>> resultExtractor = getResultExtractor(valueBlock);
+ double defaultValue = _defaultValue != null ? (Double) _defaultValue : 0d;
+ int numDocs = valueBlock.getNumDocs();
+ for (int i = 0; i < numDocs; i++) {
+ List<Object> result = null;
try {
result = resultExtractor.apply(i);
} catch (Exception ignored) {
@@ -542,30 +529,73 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
int numValues = result.size();
double[] values = new double[numValues];
for (int j = 0; j < numValues; j++) {
- Double value = result.get(j);
- if (value == null) {
+ Object element = result.get(j);
+ if (element == null) {
if (_defaultValue != null) {
- value = ((Number) _defaultValue).doubleValue();
- } else {
- 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.");
+ values[j] = defaultValue;
+ continue;
}
+ 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.");
}
- values[j] = value;
+ values[j] = toDouble(element);
}
_doubleValuesMV[i] = values;
}
return _doubleValuesMV;
}
+ @Override
+ public BigDecimal[][] transformToBigDecimalValuesMV(ValueBlock valueBlock) {
+ if (_storedType != DataType.BIG_DECIMAL) {
+ return super.transformToBigDecimalValuesMV(valueBlock);
+ }
+ initBigDecimalValuesMV(valueBlock.getNumDocs());
+ IntFunction<List<Object>> resultExtractor =
getResultExtractorWithBigDecimal(valueBlock);
+ BigDecimal defaultValue = (BigDecimal) _defaultValue;
+ int numDocs = valueBlock.getNumDocs();
+ for (int i = 0; i < numDocs; i++) {
+ List<Object> result = null;
+ try {
+ result = resultExtractor.apply(i);
+ } catch (Exception ignored) {
+ }
+ if (result == null) {
+ _bigDecimalValuesMV[i] = new BigDecimal[0];
+ continue;
+ }
+ int numValues = result.size();
+ BigDecimal[] values = new BigDecimal[numValues];
+ for (int j = 0; j < numValues; j++) {
+ Object element = result.get(j);
+ if (element == null) {
+ if (_defaultValue != null) {
+ values[j] = defaultValue;
+ continue;
+ }
+ 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.");
+ }
+ values[j] = toBigDecimal(element);
+ }
+ _bigDecimalValuesMV[i] = values;
+ }
+ return _bigDecimalValuesMV;
+ }
+
@Override
public String[][] transformToStringValuesMV(ValueBlock valueBlock) {
+ if (_storedType != DataType.STRING) {
+ return super.transformToStringValuesMV(valueBlock);
+ }
initStringValuesMV(valueBlock.getNumDocs());
- IntFunction<List<String>> resultExtractor = getResultExtractor(valueBlock);
- int length = valueBlock.getNumDocs();
- for (int i = 0; i < length; i++) {
- List<String> result = null;
+ IntFunction<List<Object>> resultExtractor =
getResultExtractorWithBigDecimal(valueBlock);
+ String defaultValue = (String) _defaultValue;
+ int numDocs = valueBlock.getNumDocs();
+ for (int i = 0; i < numDocs; i++) {
+ List<Object> result = null;
try {
result = resultExtractor.apply(i);
} catch (Exception ignored) {
@@ -577,23 +607,102 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
int numValues = result.size();
String[] values = new String[numValues];
for (int j = 0; j < numValues; j++) {
- String value = result.get(j);
- if (value == null) {
+ Object element = result.get(j);
+ if (element == null) {
if (_defaultValue != null) {
- value = _defaultValue.toString();
- } else {
- 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.");
+ values[j] = defaultValue;
+ continue;
}
+ 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.");
}
- values[j] = value;
+ values[j] = toString(element);
}
_stringValuesMV[i] = values;
}
return _stringValuesMV;
}
+ private static int toInt(Object value, boolean isBoolean) {
+ if (isBoolean) {
+ if (value instanceof Boolean) {
+ return (Boolean) value ? 1 : 0;
+ }
+ // For BOOLEAN result, follow PinotDataType numeric convention: non-zero
number → 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());
+ }
+
+ private static long toLong(Object value, boolean isTimestamp) {
+ if (value instanceof Number) {
+ return ((Number) value).longValue();
+ }
+ if (isTimestamp) {
+ return TimestampUtils.toMillisSinceEpoch(value.toString());
+ }
+ if (value instanceof Boolean) {
+ return (Boolean) value ? 1L : 0L;
+ }
+ try {
+ return NumberUtils.parseJsonLong(value.toString());
+ } catch (NumericException nfe) {
+ throw new NumberFormatException("For input string: \"" + value + "\"");
+ }
+ }
+
+ private static float toFloat(Object value) {
+ if (value instanceof Number) {
+ return ((Number) value).floatValue();
+ }
+ if (value instanceof Boolean) {
+ return (Boolean) value ? 1f : 0f;
+ }
+ return Float.parseFloat(value.toString());
+ }
+
+ private static double toDouble(Object value) {
+ if (value instanceof Number) {
+ return ((Number) value).doubleValue();
+ }
+ if (value instanceof Boolean) {
+ return (Boolean) value ? 1d : 0d;
+ }
+ return Double.parseDouble(value.toString());
+ }
+
+ private static BigDecimal toBigDecimal(Object value) {
+ if (value instanceof BigDecimal) {
+ return (BigDecimal) value;
+ }
+ if (value instanceof Boolean) {
+ return (Boolean) value ? BigDecimal.ONE : BigDecimal.ZERO;
+ }
+ return new BigDecimal(value.toString());
+ }
+
+ private static String toString(Object value) {
+ if (value instanceof String) {
+ return (String) value;
+ }
+ try {
+ return JsonUtils.objectToString(value);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException("Caught exception while serializing JSON
value: " + value, e);
+ }
+ }
+
private <T> IntFunction<T> getResultExtractor(ValueBlock valueBlock,
ParseContext parseContext) {
if (_jsonFieldTransformFunction.getResultMetadata().getDataType() ==
DataType.BYTES) {
byte[][] jsonBytes =
_jsonFieldTransformFunction.transformToBytesValuesSV(valueBlock);
@@ -607,4 +716,8 @@ public class JsonExtractScalarTransformFunction extends
BaseTransformFunction {
private <T> IntFunction<T> getResultExtractor(ValueBlock valueBlock) {
return getResultExtractor(valueBlock, JSON_PARSER_CONTEXT);
}
+
+ private <T> IntFunction<T> getResultExtractorWithBigDecimal(ValueBlock
valueBlock) {
+ return getResultExtractor(valueBlock,
JSON_PARSER_CONTEXT_WITH_BIG_DECIMAL);
+ }
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunctionTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunctionTest.java
index 0eaaf52b259..458d549a941 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunctionTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/JsonExtractScalarTransformFunctionTest.java
@@ -653,4 +653,318 @@ public class JsonExtractScalarTransformFunctionTest
extends BaseTransformFunctio
// TODO: Change the framework to do not duplicate segments when only
one segment is used
.thenResultIs(expectedRow, expectedRow); // 2 rows because of segment
duplication
}
+
+ // === Per-stored-type coercion tests for the value-handling helpers. Tests
are grouped first by SV
+ // vs MV result type, then ordered within each group by canonical Pinot
type order
+ // (INT/LONG/FLOAT/DOUBLE → BIG_DECIMAL → BOOLEAN → TIMESTAMP → STRING).
===
+
+ // -- Single-value (SV) tests --
+
+ /// Runs `SELECT jsonExtractScalar(json, '$.v', resultsType) FROM testTable`
against a single-row table
+ /// containing the given JSON document, and asserts the result for the
(always-duplicated) two
+ /// expected rows.
+ private void assertJsonExtractScalar(String json, String resultsType, Object
expectedValue) {
+ Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .setEnableColumnBasedNullHandling(true)
+ .addDimensionField("json", DataType.JSON)
+ .build();
+ TableConfig tableConfig = new TableConfigBuilder(TableType.OFFLINE)
+ .setTableName("testTable")
+ .build();
+ Object[] expectedRow = new Object[]{expectedValue};
+ FluentQueryTest.withBaseDir(_baseDir)
+ .withNullHandling(false)
+ .givenTable(schema, tableConfig)
+ .onFirstInstance(new Object[]{json})
+ .whenQuery("SELECT jsonExtractScalar(json, '$.v', '" + resultsType +
"') FROM testTable")
+ .thenResultIs(expectedRow, expectedRow);
+ }
+
+ @Test
+ public void testExtractBooleanAsNumeric() {
+ // JSON true / false coerces to 1 / 0 across all numeric result types —
matches PinotDataType's
+ // BOOLEAN.toInt / toLong / toFloat / toDouble / toBigDecimal convention.
+ assertJsonExtractScalar("{\"v\": true}", "INT", 1);
+ assertJsonExtractScalar("{\"v\": false}", "INT", 0);
+ assertJsonExtractScalar("{\"v\": true}", "LONG", 1L);
+ assertJsonExtractScalar("{\"v\": false}", "LONG", 0L);
+ assertJsonExtractScalar("{\"v\": true}", "FLOAT", 1f);
+ assertJsonExtractScalar("{\"v\": false}", "FLOAT", 0f);
+ assertJsonExtractScalar("{\"v\": true}", "DOUBLE", 1d);
+ assertJsonExtractScalar("{\"v\": false}", "DOUBLE", 0d);
+ // BIG_DECIMAL formatted as String via toPlainString.
+ assertJsonExtractScalar("{\"v\": true}", "BIG_DECIMAL", "1");
+ assertJsonExtractScalar("{\"v\": false}", "BIG_DECIMAL", "0");
+ }
+
+ @Test
+ public void testExtractBigDecimalPreservesPrecision() {
+ // The BIG_DECIMAL parser preserves full numeric precision — beyond what
Double can represent.
+ // Broker formats BIG_DECIMAL as String via BigDecimal.toPlainString().
+ assertJsonExtractScalar("{\"v\": 12345678901234567890.123456789}",
"BIG_DECIMAL",
+ "12345678901234567890.123456789");
+ }
+
+ @DataProvider(name = "booleanCoercion")
+ public Object[][] booleanCoercion() {
+ return new Object[][]{
+ // JSON boolean — direct map.
+ {"true", 1},
+ {"false", 0},
+ // JSON number — Pinot's numeric BOOLEAN convention: any non-zero → 1.
+ {"1", 1},
+ {"0", 0},
+ {"5", 1},
+ {"-1", 1},
+ {"0.5", 1},
+ {"0.0", 0},
+ // JSON string — only "true" / "TRUE" / "1" → 1; anything else
(including "yes", "TRUE ") → 0.
+ {"\"true\"", 1},
+ {"\"TRUE\"", 1},
+ {"\"True\"", 1},
+ {"\"1\"", 1},
+ {"\"false\"", 0},
+ {"\"0\"", 0},
+ {"\"yes\"", 0},
+ {"\"\"", 0}
+ };
+ }
+
+ @Test(dataProvider = "booleanCoercion")
+ public void testExtractBoolean(String jsonValueLiteral, int expected) {
+ // The SELECT projection for a BOOLEAN result surfaces as Boolean true /
false in the broker rows.
+ Object expectedBoolean = expected == 1;
+ assertJsonExtractScalar("{\"v\": " + jsonValueLiteral + "}", "BOOLEAN",
expectedBoolean);
+ }
+
+ @DataProvider(name = "timestampCoercion")
+ public Object[][] timestampCoercion() {
+ long epochMillis = 1700000000000L;
+ return new Object[][]{
+ // Numeric epoch millis — straight longValue path.
+ {String.valueOf(epochMillis), epochMillis},
+ // Numeric epoch millis as string — TimestampUtils accepts numeric
strings.
+ {"\"" + epochMillis + "\"", epochMillis},
+ // ISO-8601 string — TimestampUtils parses to epoch millis.
+ {"\"2023-11-14T22:13:20Z\"", epochMillis},
+ {"\"2023-11-14 22:13:20\"", epochMillis -
java.util.TimeZone.getDefault().getOffset(epochMillis)}
+ };
+ }
+
+ @Test(dataProvider = "timestampCoercion")
+ public void testExtractTimestamp(String jsonValueLiteral, long
expectedMillis) {
+ // Broker formats TIMESTAMP via Timestamp.toString() (local-TZ wall-clock
representation).
+ assertJsonExtractScalar("{\"v\": " + jsonValueLiteral + "}", "TIMESTAMP",
+ new java.sql.Timestamp(expectedMillis).toString());
+ }
+
+ @Test
+ public void testExtractTimestampRejectsBoolean() {
+ // TIMESTAMP doesn't accept Boolean — Boolean → epoch millis is
semantically nonsensical, matching
+ // PinotDataType.TIMESTAMP.toBoolean throwing
UnsupportedOperationException. JSON true is routed
+ // through TimestampUtils.toMillisSinceEpoch("true") which throws
IllegalArgumentException; the
+ // broker surfaces the failure as a query error.
+ Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .setEnableColumnBasedNullHandling(true)
+ .addDimensionField("json", DataType.JSON)
+ .build();
+ TableConfig tableConfig = new TableConfigBuilder(TableType.OFFLINE)
+ .setTableName("testTable")
+ .build();
+ try {
+ FluentQueryTest.withBaseDir(_baseDir)
+ .withNullHandling(false)
+ .givenTable(schema, tableConfig)
+ .onFirstInstance(new Object[]{"{\"v\": true}"})
+ .whenQuery("SELECT jsonExtractScalar(json, '$.v', 'TIMESTAMP') FROM
testTable")
+ .thenResultIs(new Object[]{"unused"});
+ Assert.fail("Expected query to fail when extracting JSON boolean as
TIMESTAMP");
+ } catch (AssertionError e) {
+ // Expected — broker surfaces the parse failure as a query error.
+ }
+ }
+
+ @Test
+ public void testExtractStringFromNonStringJson() {
+ // A String JSON value passes through as-is.
+ assertJsonExtractScalar("{\"v\": \"hello\"}", "STRING", "hello");
+ // Numbers, booleans, arrays, and objects are JSON-serialized via
JsonUtils.objectToString.
+ assertJsonExtractScalar("{\"v\": 42}", "STRING", "42");
+ assertJsonExtractScalar("{\"v\": 3.14}", "STRING", "3.14");
+ assertJsonExtractScalar("{\"v\": true}", "STRING", "true");
+ assertJsonExtractScalar("{\"v\": [1,2,3]}", "STRING", "[1,2,3]");
+ assertJsonExtractScalar("{\"v\": {\"a\":1}}", "STRING", "{\"a\":1}");
+ }
+
+ @Test
+ public void testExtractStringPreservesNumericPrecision() {
+ // STRING uses JSON_PARSER_CONTEXT_WITH_BIG_DECIMAL so floats that exceed
Double precision survive
+ // the JSON → Java → JSON round-trip without truncation. Symmetric with
BIG_DECIMAL precision.
+ assertJsonExtractScalar(
+ "{\"v\": 12345678901234567890.123456789}", "STRING",
+ "12345678901234567890.123456789");
+ }
+
+ // -- Multi-value (MV) tests --
+
+ /// Asserts that `SELECT jsonExtractScalar(json, '$.v', resultsType)` over a
single-row table with the
+ /// given JSON document produces the given primitive-array result.
+ private void assertJsonExtractMv(String json, String resultsType, Object
expectedArray) {
+ Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .setEnableColumnBasedNullHandling(true)
+ .addDimensionField("json", DataType.JSON)
+ .build();
+ TableConfig tableConfig = new TableConfigBuilder(TableType.OFFLINE)
+ .setTableName("testTable")
+ .build();
+ Object[] expectedRow = new Object[]{expectedArray};
+ FluentQueryTest.withBaseDir(_baseDir)
+ .withNullHandling(false)
+ .givenTable(schema, tableConfig)
+ .onFirstInstance(new Object[]{json})
+ .whenQuery("SELECT jsonExtractScalar(json, '$.v', '" + resultsType +
"') FROM testTable")
+ .thenResultIs(expectedRow, expectedRow);
+ }
+
+ @Test
+ public void testIntArrayFromHeterogeneousJsonElements() {
+ // JSON numbers — direct intValue path.
+ assertJsonExtractMv("{\"v\": [1, 2, 3]}", "INT_ARRAY", new int[]{1, 2, 3});
+ // JSON string-form numbers — Integer.parseInt(toString()) fallback.
+ assertJsonExtractMv("{\"v\": [\"1\", \"2\", \"3\"]}", "INT_ARRAY", new
int[]{1, 2, 3});
+ // Mixed numeric and string-numeric — both forms coerce.
+ assertJsonExtractMv("{\"v\": [1, \"2\", 3]}", "INT_ARRAY", new int[]{1, 2,
3});
+ }
+
+ @Test
+ public void testNumericArrayWithBooleanElements() {
+ // JSON true / false elements coerce to 1 / 0 in numeric MV paths.
+ assertJsonExtractMv("{\"v\": [true, false, true]}", "INT_ARRAY", new
int[]{1, 0, 1});
+ assertJsonExtractMv("{\"v\": [true, false]}", "LONG_ARRAY", new long[]{1L,
0L});
+ assertJsonExtractMv("{\"v\": [true, false]}", "FLOAT_ARRAY", new
float[]{1f, 0f});
+ assertJsonExtractMv("{\"v\": [true, false]}", "DOUBLE_ARRAY", new
double[]{1d, 0d});
+ assertJsonExtractMv("{\"v\": [true, false]}", "BIG_DECIMAL_ARRAY", new
String[]{"1", "0"});
+ // Mixed numeric / boolean elements — Boolean coerces to 1/0, Number
values pass through.
+ assertJsonExtractMv("{\"v\": [1, true, 3, false]}", "INT_ARRAY", new
int[]{1, 1, 3, 0});
+ }
+
+ @Test
+ public void testBigDecimalArrayPreservesPrecision() {
+ // Broker formats BIG_DECIMAL_ARRAY as String[] via per-element
BigDecimal.toPlainString().
+ String[] expected = {
+ "12345678901234567890.123456789",
+ "0.0000000000000001",
+ "3.14"
+ };
+ assertJsonExtractMv(
+ "{\"v\": [12345678901234567890.123456789, 0.0000000000000001, 3.14]}",
+ "BIG_DECIMAL_ARRAY", expected);
+ }
+
+ @Test
+ public void testBooleanArrayResultType() {
+ // BOOLEAN_ARRAY uses storedType INT and the BOOLEAN per-element
convention (non-zero Number → 1),
+ // distinct from INT_ARRAY which uses intValue() directly. JSON [1, 5, 0,
2.5] therefore yields
+ // [true, true, false, true] for BOOLEAN_ARRAY but [1, 5, 0, 2] for
INT_ARRAY.
+ assertJsonExtractMv("{\"v\": [1, 5, 0, 2.5]}", "BOOLEAN_ARRAY",
+ new boolean[]{true, true, false, true});
+ // Mixed Boolean / Number / String — each element coerced via
PinotDataType BOOLEAN convention.
+ assertJsonExtractMv("{\"v\": [true, 0, \"true\", false]}", "BOOLEAN_ARRAY",
+ new boolean[]{true, false, true, false});
+ }
+
+ @Test
+ public void testTimestampArrayResultType() {
+ // TIMESTAMP_ARRAY: storedType LONG, transformToLongValuesMV with
isTimestamp=true. JSON numeric
+ // millis pass through Number.longValue(); JSON ISO strings parse via
TimestampUtils.
+ long epochMillis = 1700000000000L;
+ String tsString = new java.sql.Timestamp(epochMillis).toString();
+ // Broker formats TIMESTAMP_ARRAY as String[] (per-element
Timestamp.toString()).
+ assertJsonExtractMv(
+ "{\"v\": [" + epochMillis + ", \"2023-11-14T22:13:20Z\"]}",
+ "TIMESTAMP_ARRAY",
+ new String[]{tsString, tsString});
+ }
+
+ @Test
+ public void testStringArrayFromNonStringJsonElements() {
+ // JSON strings pass through.
+ assertJsonExtractMv("{\"v\": [\"a\", \"b\"]}", "STRING_ARRAY", new
String[]{"a", "b"});
+ // JSON numbers, booleans, and nested structures get JSON-serialized.
+ assertJsonExtractMv("{\"v\": [1, 2, 3]}", "STRING_ARRAY", new
String[]{"1", "2", "3"});
+ assertJsonExtractMv("{\"v\": [true, false]}", "STRING_ARRAY", new
String[]{"true", "false"});
+ assertJsonExtractMv("{\"v\": [[1, 2], [3, 4]]}", "STRING_ARRAY", new
String[]{"[1,2]", "[3,4]"});
+ }
+
+ // -- Default values for newly-handled result types --
+
+ @Test
+ public void testDefaultValueForBoolean() {
+ // BOOLEAN default literal stored as Integer 0/1 in init() to match INT
storedType. When the JSON
+ // path doesn't resolve, the default surfaces correctly through
transformToIntValuesSV.
+ Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .setEnableColumnBasedNullHandling(true)
+ .addDimensionField("json", DataType.JSON)
+ .build();
+ TableConfig tableConfig = new TableConfigBuilder(TableType.OFFLINE)
+ .setTableName("testTable")
+ .build();
+ FluentQueryTest.withBaseDir(_baseDir)
+ .withNullHandling(false)
+ .givenTable(schema, tableConfig)
+ .onFirstInstance(new Object[]{"{\"other\": 1}"})
+ .whenQuery("SELECT jsonExtractScalar(json, '$.missing', 'BOOLEAN',
true) FROM testTable")
+ .thenResultIs(new Object[]{true}, new Object[]{true});
+ }
+
+ @Test
+ public void testDefaultValueForTimestamp() {
+ // TIMESTAMP default literal stored as Long in init() to match LONG
storedType. When the JSON path
+ // doesn't resolve, the default surfaces through transformToLongValuesSV.
+ long defaultMillis = 1234567890000L;
+ String expected = new java.sql.Timestamp(defaultMillis).toString();
+ Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .setEnableColumnBasedNullHandling(true)
+ .addDimensionField("json", DataType.JSON)
+ .build();
+ TableConfig tableConfig = new TableConfigBuilder(TableType.OFFLINE)
+ .setTableName("testTable")
+ .build();
+ FluentQueryTest.withBaseDir(_baseDir)
+ .withNullHandling(false)
+ .givenTable(schema, tableConfig)
+ .onFirstInstance(new Object[]{"{\"other\": 1}"})
+ .whenQuery("SELECT jsonExtractScalar(json, '$.missing', 'TIMESTAMP', "
+ defaultMillis + ") "
+ + "FROM testTable")
+ .thenResultIs(new Object[]{expected}, new Object[]{expected});
+ }
+
+ // -- Cross-type guard: requesting a type other than the function's declared
result type should
+ // route through the base class's cross-type conversion path. --
+
+ @Test
+ public void testCrossTypeConversionFromStringResult() {
+ // The function's declared result type is STRING, but the caller requests
INT — base class should
+ // handle the STRING→INT conversion via parsing.
+ Schema schema = new Schema.SchemaBuilder()
+ .setSchemaName("testTable")
+ .setEnableColumnBasedNullHandling(true)
+ .addDimensionField("json", DataType.JSON)
+ .build();
+ TableConfig tableConfig = new TableConfigBuilder(TableType.OFFLINE)
+ .setTableName("testTable")
+ .build();
+ FluentQueryTest.withBaseDir(_baseDir)
+ .withNullHandling(false)
+ .givenTable(schema, tableConfig)
+ .onFirstInstance(new Object[]{"{\"v\": \"42\"}"})
+ // Cast STRING-result to LONG triggers the base-class cross-type path:
STRING → parseLong.
+ .whenQuery("SELECT CAST(jsonExtractScalar(json, '$.v', 'STRING') AS
LONG) FROM testTable")
+ .thenResultIs(new Object[]{42L}, new Object[]{42L});
+ }
}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]