diff --git a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractIndexTransformFunction.java b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractIndexTransformFunction.java
index 97b34e05e2..2386750ae8 100644
--- a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractIndexTransformFunction.java
+++ b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/JsonExtractIndexTransformFunction.java
@@ -24,14 +24,17 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import javax.annotation.Nullable;
import org.apache.pinot.common.function.JsonPathCache;
import org.apache.pinot.core.operator.ColumnContext;
import org.apache.pinot.core.operator.blocks.ValueBlock;
import org.apache.pinot.core.operator.transform.TransformResultMetadata;
+import org.apache.pinot.core.query.request.context.QueryContext;
import org.apache.pinot.segment.spi.index.IndexService;
import org.apache.pinot.segment.spi.index.IndexType;
import org.apache.pinot.segment.spi.index.reader.JsonIndexReader;
import org.apache.pinot.spi.data.FieldSpec.DataType;
+import org.apache.pinot.spi.utils.CommonConstants.NullValuePlaceHolder;
import org.apache.pinot.spi.utils.JsonUtils;
import org.roaringbitmap.RoaringBitmap;
@@ -41,6 +44,12 @@
* implementation changed to read values from the JSON index. For large JSON blobs this can be faster than parsing
* GBs of JSON at query time. For small JSON blobs/highly filtered input this is generally slower than the *scalar
* implementation. The inflection point is highly dependent on the number of docs remaining post filter.
+ *
+ *
Null handling: when query-level null handling is enabled (e.g. {@code SET enableNullHandling = true})
+ * and no default literal is supplied, an unresolved JSON path on a row surfaces as SQL {@code NULL} via the
+ * {@link #getNullBitmap(ValueBlock)} bitmap rather than throwing. With null handling disabled, the legacy throw
+ * is preserved. A user-supplied non-null default literal takes precedence and is written for unresolved rows
+ * regardless of null-handling state.
*/
public class JsonExtractIndexTransformFunction extends BaseTransformFunction {
public static final String FUNCTION_NAME = "jsonExtractIndex";
@@ -54,14 +63,22 @@ public class JsonExtractIndexTransformFunction extends BaseTransformFunction {
private boolean _isSingleValue;
private String _filterJsonPath;
+ // Cache for the per-ValueBlock SV scan result. Both the SV transforms and getNullBitmap need
+ // the same `String[]` for the same ValueBlock; without caching, we'd hit the JSON index twice
+ // per block on the null-handling path. Identity comparison is safe — the server constructs a
+ // fresh ValueBlock per batch and never mutates a previously returned one.
+ private ValueBlock _cachedValueBlock;
+ private String[] _cachedValuesSV;
+
@Override
public String getName() {
return FUNCTION_NAME;
}
@Override
- public void init(List arguments, Map columnContextMap) {
- super.init(arguments, columnContextMap);
+ public void init(List arguments, Map columnContextMap,
+ QueryContext queryContext) {
+ super.init(arguments, columnContextMap, queryContext);
// Check that there are exactly 3 or 4 or 5 arguments
if (arguments.size() < 3 || arguments.size() > 5) {
throw new IllegalArgumentException(
@@ -118,22 +135,27 @@ public void init(List arguments, Map c
if (!(fourthArgument instanceof LiteralTransformFunction)) {
throw new IllegalArgumentException("Default value must be a literal");
}
-
- if (_isSingleValue) {
- _defaultValue = dataType.convert(((LiteralTransformFunction) fourthArgument).getStringLiteral());
- } else {
- try {
- JsonNode mvArray = JsonUtils.stringToJsonNode(((LiteralTransformFunction) fourthArgument).getStringLiteral());
- if (!mvArray.isArray()) {
+ LiteralTransformFunction literalFn = (LiteralTransformFunction) fourthArgument;
+ // SQL NULL literal as the default ⇒ behave like the 3-arg form: leave `_defaultValue` null
+ // so unresolved rows surface as SQL NULL (NH on) or throw (NH off). This mirrors the scalar
+ // function's `_defaultIsNull` handling without needing a separate flag.
+ if (!literalFn.isNull()) {
+ if (_isSingleValue) {
+ _defaultValue = dataType.convert(literalFn.getStringLiteral());
+ } else {
+ try {
+ JsonNode mvArray = JsonUtils.stringToJsonNode(literalFn.getStringLiteral());
+ if (!mvArray.isArray()) {
+ throw new IllegalArgumentException("Default value must be a valid JSON array");
+ }
+ Object[] defaultValues = new Object[mvArray.size()];
+ for (int i = 0; i < mvArray.size(); i++) {
+ defaultValues[i] = dataType.convert(mvArray.get(i).asText());
+ }
+ _defaultValue = defaultValues;
+ } catch (IOException e) {
throw new IllegalArgumentException("Default value must be a valid JSON array");
}
- Object[] defaultValues = new Object[mvArray.size()];
- for (int i = 0; i < mvArray.size(); i++) {
- defaultValues[i] = dataType.convert(mvArray.get(i).asText());
- }
- _defaultValue = defaultValues;
- } catch (IOException e) {
- throw new IllegalArgumentException("Default value must be a valid JSON array");
}
}
}
@@ -159,8 +181,7 @@ public int[] transformToIntValuesSV(ValueBlock valueBlock) {
int numDocs = valueBlock.getNumDocs();
int[] inputDocIds = valueBlock.getDocIds();
initIntValuesSV(numDocs);
- String[] valuesFromIndex = _jsonIndexReader.getValuesSV(valueBlock.getDocIds(), valueBlock.getNumDocs(),
- getValueToMatchingDocsMap(), false);
+ String[] valuesFromIndex = getCachedValuesFromIndex(valueBlock);
for (int i = 0; i < numDocs; i++) {
String value = valuesFromIndex[i];
if (value == null) {
@@ -168,6 +189,10 @@ public int[] transformToIntValuesSV(ValueBlock valueBlock) {
_intValuesSV[i] = (int) _defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _intValuesSV[i] = NullValuePlaceHolder.INT;
+ continue;
+ }
throw new RuntimeException(
String.format("Illegal Json Path: [%s], for docId [%s]", _jsonPathString, inputDocIds[i]));
}
@@ -181,8 +206,7 @@ public long[] transformToLongValuesSV(ValueBlock valueBlock) {
int numDocs = valueBlock.getNumDocs();
int[] inputDocIds = valueBlock.getDocIds();
initLongValuesSV(numDocs);
- String[] valuesFromIndex = _jsonIndexReader.getValuesSV(valueBlock.getDocIds(), valueBlock.getNumDocs(),
- getValueToMatchingDocsMap(), false);
+ String[] valuesFromIndex = getCachedValuesFromIndex(valueBlock);
for (int i = 0; i < numDocs; i++) {
String value = valuesFromIndex[i];
if (value == null) {
@@ -190,6 +214,10 @@ public long[] transformToLongValuesSV(ValueBlock valueBlock) {
_longValuesSV[i] = (long) _defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _longValuesSV[i] = NullValuePlaceHolder.LONG;
+ continue;
+ }
throw new RuntimeException(
String.format("Illegal Json Path: [%s], for docId [%s]", _jsonPathString, inputDocIds[i]));
}
@@ -203,8 +231,7 @@ public float[] transformToFloatValuesSV(ValueBlock valueBlock) {
int numDocs = valueBlock.getNumDocs();
int[] inputDocIds = valueBlock.getDocIds();
initFloatValuesSV(numDocs);
- String[] valuesFromIndex = _jsonIndexReader.getValuesSV(valueBlock.getDocIds(), valueBlock.getNumDocs(),
- getValueToMatchingDocsMap(), false);
+ String[] valuesFromIndex = getCachedValuesFromIndex(valueBlock);
for (int i = 0; i < numDocs; i++) {
String value = valuesFromIndex[i];
if (value == null) {
@@ -212,6 +239,10 @@ public float[] transformToFloatValuesSV(ValueBlock valueBlock) {
_floatValuesSV[i] = (float) _defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _floatValuesSV[i] = NullValuePlaceHolder.FLOAT;
+ continue;
+ }
throw new RuntimeException(
String.format("Illegal Json Path: [%s], for docId [%s]", _jsonPathString, inputDocIds[i]));
}
@@ -225,8 +256,7 @@ public double[] transformToDoubleValuesSV(ValueBlock valueBlock) {
int numDocs = valueBlock.getNumDocs();
int[] inputDocIds = valueBlock.getDocIds();
initDoubleValuesSV(numDocs);
- String[] valuesFromIndex = _jsonIndexReader.getValuesSV(valueBlock.getDocIds(), valueBlock.getNumDocs(),
- getValueToMatchingDocsMap(), false);
+ String[] valuesFromIndex = getCachedValuesFromIndex(valueBlock);
for (int i = 0; i < numDocs; i++) {
String value = valuesFromIndex[i];
if (value == null) {
@@ -234,6 +264,10 @@ public double[] transformToDoubleValuesSV(ValueBlock valueBlock) {
_doubleValuesSV[i] = (double) _defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _doubleValuesSV[i] = NullValuePlaceHolder.DOUBLE;
+ continue;
+ }
throw new RuntimeException(
String.format("Illegal Json Path: [%s], for docId [%s]", _jsonPathString, inputDocIds[i]));
}
@@ -247,8 +281,7 @@ public BigDecimal[] transformToBigDecimalValuesSV(ValueBlock valueBlock) {
int numDocs = valueBlock.getNumDocs();
int[] inputDocIds = valueBlock.getDocIds();
initBigDecimalValuesSV(numDocs);
- String[] valuesFromIndex = _jsonIndexReader.getValuesSV(valueBlock.getDocIds(), valueBlock.getNumDocs(),
- getValueToMatchingDocsMap(), false);
+ String[] valuesFromIndex = getCachedValuesFromIndex(valueBlock);
for (int i = 0; i < numDocs; i++) {
String value = valuesFromIndex[i];
if (value == null) {
@@ -256,6 +289,10 @@ public BigDecimal[] transformToBigDecimalValuesSV(ValueBlock valueBlock) {
_bigDecimalValuesSV[i] = (BigDecimal) _defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _bigDecimalValuesSV[i] = NullValuePlaceHolder.BIG_DECIMAL;
+ continue;
+ }
throw new RuntimeException(
String.format("Illegal Json Path: [%s], for docId [%s]", _jsonPathString, inputDocIds[i]));
}
@@ -269,8 +306,7 @@ public String[] transformToStringValuesSV(ValueBlock valueBlock) {
int numDocs = valueBlock.getNumDocs();
int[] inputDocIds = valueBlock.getDocIds();
initStringValuesSV(numDocs);
- String[] valuesFromIndex = _jsonIndexReader.getValuesSV(valueBlock.getDocIds(), valueBlock.getNumDocs(),
- getValueToMatchingDocsMap(), false);
+ String[] valuesFromIndex = getCachedValuesFromIndex(valueBlock);
for (int i = 0; i < numDocs; i++) {
String value = valuesFromIndex[i];
if (value == null) {
@@ -278,6 +314,10 @@ public String[] transformToStringValuesSV(ValueBlock valueBlock) {
_stringValuesSV[i] = (String) _defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _stringValuesSV[i] = NullValuePlaceHolder.STRING;
+ continue;
+ }
throw new RuntimeException(
String.format("Illegal Json Path: [%s], for docId [%s]", _jsonPathString, inputDocIds[i]));
}
@@ -286,6 +326,28 @@ public String[] transformToStringValuesSV(ValueBlock valueBlock) {
return _stringValuesSV;
}
+ @Nullable
+ @Override
+ public RoaringBitmap getNullBitmap(ValueBlock valueBlock) {
+ // Short-circuit to argument-bitmap propagation when this function isn't introducing nulls of
+ // its own: non-SV output, null handling disabled, or a non-null default literal supplied (the
+ // SV transform writes it for unresolved rows). The SQL NULL literal case is handled in `init`
+ // by leaving `_defaultValue` null so it falls through to the scan below — same as the 3-arg
+ // form.
+ if (!_isSingleValue || !_nullHandlingEnabled || _defaultValue != null) {
+ return super.getNullBitmap(valueBlock);
+ }
+ String[] valuesFromIndex = getCachedValuesFromIndex(valueBlock);
+ int numDocs = valueBlock.getNumDocs();
+ RoaringBitmap nullBitmap = new RoaringBitmap();
+ for (int i = 0; i < numDocs; i++) {
+ if (valuesFromIndex[i] == null) {
+ nullBitmap.add(i);
+ }
+ }
+ return nullBitmap.isEmpty() ? null : nullBitmap;
+ }
+
@Override
public int[][] transformToIntValuesMV(ValueBlock valueBlock) {
int numDocs = valueBlock.getNumDocs();
@@ -433,4 +495,17 @@ private Map getValueToMatchingDocsMap() {
}
return _valueToMatchingDocsMap;
}
+
+ /**
+ * Returns the JSON-index SV scan result for the given block, caching it so that the SV
+ * transform and {@link #getNullBitmap} don't each pay the cost on the null-handling path.
+ */
+ private String[] getCachedValuesFromIndex(ValueBlock valueBlock) {
+ if (valueBlock != _cachedValueBlock) {
+ _cachedValuesSV = _jsonIndexReader.getValuesSV(valueBlock.getDocIds(), valueBlock.getNumDocs(),
+ getValueToMatchingDocsMap(), false);
+ _cachedValueBlock = valueBlock;
+ }
+ return _cachedValuesSV;
+ }
}
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 94da2934f3..54fd5cbee1 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
@@ -22,6 +22,7 @@
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.Configuration;
+import com.jayway.jsonpath.InvalidJsonException;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.ParseContext;
@@ -41,6 +42,7 @@
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.CommonConstants.NullValuePlaceHolder;
import org.apache.pinot.spi.utils.JsonUtils;
import org.apache.pinot.spi.utils.TimestampUtils;
import org.roaringbitmap.RoaringBitmap;
@@ -55,9 +57,11 @@
/// - `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.
+/// - `defaultValue` (optional) — used when the path resolves to `null` or fails. Without it, the
+/// behavior depends on query-level null handling: when enabled, unresolved SV rows surface as SQL
+/// `NULL` (typed zero/empty sentinel + a bit in [#getNullBitmap]); when disabled, unresolved SV
+/// rows throw `IllegalArgumentException`. MV rows always surface as empty arrays when the path
+/// doesn't resolve; null elements within a resolved array still throw if no default is set.
///
/// **Supported `resultsType`:** `INT`, `LONG`, `FLOAT`, `DOUBLE`, `BIG_DECIMAL`, `BOOLEAN`, `TIMESTAMP`,
/// `STRING`, `JSON`, `BYTES`, plus `_ARRAY` variants of `INT` / `LONG` / `FLOAT` / `DOUBLE` /
@@ -189,7 +193,14 @@ public TransformResultMetadata getResultMetadata() {
@Override
@Nullable
public RoaringBitmap getNullBitmap(ValueBlock valueBlock) {
- if (!_defaultIsNull) {
+ // Short-circuit to argument-bitmap propagation when this function isn't introducing nulls of
+ // its own. Two cases qualify:
+ // - null handling is disabled (the SV transform either fills a default or throws), or
+ // - a real (non-SQL-NULL) default was supplied (the SV transform writes the default for
+ // unresolved rows).
+ // The 4-arg SQL-NULL case has _defaultIsNull=true and falls through to the per-row scan
+ // below, so unresolved rows surface as SQL NULL via the bitmap.
+ if (!_nullHandlingEnabled || (_defaultValue != null && !_defaultIsNull)) {
return super.getNullBitmap(valueBlock);
}
RoaringBitmap bitmap = new RoaringBitmap();
@@ -206,7 +217,16 @@ public RoaringBitmap getNullBitmap(ValueBlock valueBlock) {
Object result = null;
try {
result = resultExtractor.apply(i);
+ } catch (InvalidJsonException e) {
+ // Input that is not valid JSON at all (as opposed to valid JSON with a missing path) is a
+ // caller / schema modeling error — e.g. jsonExtractScalar invoked on a non-JSON column.
+ // Surface it regardless of _nullHandlingEnabled so the bug isn't silently masked as SQL NULL.
+ throw new IllegalArgumentException(
+ "jsonExtractScalar received input that is not valid JSON. "
+ + "This function requires a JSON-typed column or a JSON-formatted string.", e);
} catch (Exception ignored) {
+ // Null / empty input or a missing path: result stays null and the cascade below applies
+ // null-handling or the default value (preserves PR #205's behavior).
}
if (result == null) {
nullBitmap.add(i);
@@ -235,13 +255,28 @@ public int[] transformToIntValuesSV(ValueBlock valueBlock) {
Object result = null;
try {
result = resultExtractor.apply(i);
+ } catch (InvalidJsonException e) {
+ // Input that is not valid JSON at all (as opposed to valid JSON with a missing path) is a
+ // caller / schema modeling error — e.g. jsonExtractScalar invoked on a non-JSON column.
+ // Surface it regardless of _nullHandlingEnabled so the bug isn't silently masked as SQL NULL.
+ throw new IllegalArgumentException(
+ "jsonExtractScalar received input that is not valid JSON. "
+ + "This function requires a JSON-typed column or a JSON-formatted string.", e);
} catch (Exception ignored) {
+ // Null / empty input or a missing path: result stays null and the cascade below applies
+ // null-handling or the default value (preserves PR #205's behavior).
}
if (result == null) {
if (_defaultValue != null) {
_intValuesSV[i] = defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ // Write the null placeholder so stale data from a reused buffer can't surface.
+ // getNullBitmap marks the row null; consumers should read the bitmap, not this value.
+ _intValuesSV[i] = NullValuePlaceHolder.INT;
+ continue;
+ }
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a default value.");
}
@@ -264,13 +299,26 @@ public long[] transformToLongValuesSV(ValueBlock valueBlock) {
Object result = null;
try {
result = resultExtractor.apply(i);
+ } catch (InvalidJsonException e) {
+ // Input that is not valid JSON at all (as opposed to valid JSON with a missing path) is a
+ // caller / schema modeling error — e.g. jsonExtractScalar invoked on a non-JSON column.
+ // Surface it regardless of _nullHandlingEnabled so the bug isn't silently masked as SQL NULL.
+ throw new IllegalArgumentException(
+ "jsonExtractScalar received input that is not valid JSON. "
+ + "This function requires a JSON-typed column or a JSON-formatted string.", e);
} catch (Exception ignored) {
+ // Null / empty input or a missing path: result stays null and the cascade below applies
+ // null-handling or the default value (preserves PR #205's behavior).
}
if (result == null) {
if (_defaultValue != null) {
_longValuesSV[i] = defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _longValuesSV[i] = NullValuePlaceHolder.LONG;
+ continue;
+ }
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a default value.");
}
@@ -292,13 +340,26 @@ public float[] transformToFloatValuesSV(ValueBlock valueBlock) {
Object result = null;
try {
result = resultExtractor.apply(i);
+ } catch (InvalidJsonException e) {
+ // Input that is not valid JSON at all (as opposed to valid JSON with a missing path) is a
+ // caller / schema modeling error — e.g. jsonExtractScalar invoked on a non-JSON column.
+ // Surface it regardless of _nullHandlingEnabled so the bug isn't silently masked as SQL NULL.
+ throw new IllegalArgumentException(
+ "jsonExtractScalar received input that is not valid JSON. "
+ + "This function requires a JSON-typed column or a JSON-formatted string.", e);
} catch (Exception ignored) {
+ // Null / empty input or a missing path: result stays null and the cascade below applies
+ // null-handling or the default value (preserves PR #205's behavior).
}
if (result == null) {
if (_defaultValue != null) {
_floatValuesSV[i] = defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _floatValuesSV[i] = NullValuePlaceHolder.FLOAT;
+ continue;
+ }
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a default value.");
}
@@ -320,13 +381,26 @@ public double[] transformToDoubleValuesSV(ValueBlock valueBlock) {
Object result = null;
try {
result = resultExtractor.apply(i);
+ } catch (InvalidJsonException e) {
+ // Input that is not valid JSON at all (as opposed to valid JSON with a missing path) is a
+ // caller / schema modeling error — e.g. jsonExtractScalar invoked on a non-JSON column.
+ // Surface it regardless of _nullHandlingEnabled so the bug isn't silently masked as SQL NULL.
+ throw new IllegalArgumentException(
+ "jsonExtractScalar received input that is not valid JSON. "
+ + "This function requires a JSON-typed column or a JSON-formatted string.", e);
} catch (Exception ignored) {
+ // Null / empty input or a missing path: result stays null and the cascade below applies
+ // null-handling or the default value (preserves PR #205's behavior).
}
if (result == null) {
if (_defaultValue != null) {
_doubleValuesSV[i] = defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _doubleValuesSV[i] = NullValuePlaceHolder.DOUBLE;
+ continue;
+ }
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a default value.");
}
@@ -348,13 +422,26 @@ public BigDecimal[] transformToBigDecimalValuesSV(ValueBlock valueBlock) {
Object result = null;
try {
result = resultExtractor.apply(i);
+ } catch (InvalidJsonException e) {
+ // Input that is not valid JSON at all (as opposed to valid JSON with a missing path) is a
+ // caller / schema modeling error — e.g. jsonExtractScalar invoked on a non-JSON column.
+ // Surface it regardless of _nullHandlingEnabled so the bug isn't silently masked as SQL NULL.
+ throw new IllegalArgumentException(
+ "jsonExtractScalar received input that is not valid JSON. "
+ + "This function requires a JSON-typed column or a JSON-formatted string.", e);
} catch (Exception ignored) {
+ // Null / empty input or a missing path: result stays null and the cascade below applies
+ // null-handling or the default value (preserves PR #205's behavior).
}
if (result == null) {
if (_defaultValue != null) {
_bigDecimalValuesSV[i] = defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _bigDecimalValuesSV[i] = NullValuePlaceHolder.BIG_DECIMAL;
+ continue;
+ }
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a default value.");
}
@@ -376,13 +463,26 @@ public String[] transformToStringValuesSV(ValueBlock valueBlock) {
Object result = null;
try {
result = resultExtractor.apply(i);
+ } catch (InvalidJsonException e) {
+ // Input that is not valid JSON at all (as opposed to valid JSON with a missing path) is a
+ // caller / schema modeling error — e.g. jsonExtractScalar invoked on a non-JSON column.
+ // Surface it regardless of _nullHandlingEnabled so the bug isn't silently masked as SQL NULL.
+ throw new IllegalArgumentException(
+ "jsonExtractScalar received input that is not valid JSON. "
+ + "This function requires a JSON-typed column or a JSON-formatted string.", e);
} catch (Exception ignored) {
+ // Null / empty input or a missing path: result stays null and the cascade below applies
+ // null-handling or the default value (preserves PR #205's behavior).
}
if (result == null) {
if (_defaultValue != null) {
_stringValuesSV[i] = defaultValue;
continue;
}
+ if (_nullHandlingEnabled) {
+ _stringValuesSV[i] = NullValuePlaceHolder.STRING;
+ continue;
+ }
throw new IllegalArgumentException(
"Cannot resolve JSON path on some records. Consider setting a default value.");
}
@@ -405,7 +505,16 @@ public int[][] transformToIntValuesMV(ValueBlock valueBlock) {
List