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]


Reply via email to