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 881a1f74dc6 Honor nullableParameters for scalar functions in the
single-stage engine (#19983)
881a1f74dc6 is described below
commit 881a1f74dc660983a82080e4f78eca207c37cb17
Author: Xiaotian (Jackie) Jiang <[email protected]>
AuthorDate: Tue Oct 20 21:43:58 2026 -0700
Honor nullableParameters for scalar functions in the single-stage engine
(#19983)
---
.../common/function/scalar/StringFunctions.java | 18 +-
.../function/scalar/StringFunctionsTest.java | 6 +
.../function/ScalarTransformFunctionWrapper.java | 250 +++++++++++++++++++--
.../ScalarTransformFunctionWrapperTest.java | 65 ++++++
.../queries/NullHandlingEnabledQueriesTest.java | 24 ++
5 files changed, 341 insertions(+), 22 deletions(-)
diff --git
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
index 8482797325a..fcbac14c673 100644
---
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
+++
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/StringFunctions.java
@@ -449,9 +449,14 @@ public class StringFunctions {
/// @param input an input string for prefix strings generations.
/// @param maxlength the max length of the prefix strings for the string.
/// @param prefix the prefix to be prepended to prefix strings generated.
e.g. '^' for regex matching
- /// @return generate an array of prefix matchers of the string that are
shorter than the specified length.
+ /// @return generate an array of prefix matchers of the string that are
shorter than the specified length, or `null`
+ /// if the input is `null`.
+ @Nullable
@ScalarFunction(nullableParameters = true)
- public static String[] prefixesWithPrefix(String input, int maxlength,
@Nullable String prefix) {
+ public static String[] prefixesWithPrefix(@Nullable String input, int
maxlength, @Nullable String prefix) {
+ if (input == null) {
+ return null;
+ }
if (prefix == null) {
return prefixes(input, maxlength);
}
@@ -479,9 +484,14 @@ public class StringFunctions {
/// @param input an input string for suffix strings generations.
/// @param maxlength the max length of the suffix strings for the string.
/// @param suffix the suffix string to be appended for suffix strings
generated. e.g. '$' for regex matching.
- /// @return generate an array of suffix matchers of the string that are
shorter than the specified length.
+ /// @return generate an array of suffix matchers of the string that are
shorter than the specified length, or `null`
+ /// if the input is `null`.
+ @Nullable
@ScalarFunction(nullableParameters = true)
- public static String[] suffixesWithSuffix(String input, int maxlength,
@Nullable String suffix) {
+ public static String[] suffixesWithSuffix(@Nullable String input, int
maxlength, @Nullable String suffix) {
+ if (input == null) {
+ return null;
+ }
if (suffix == null) {
return suffixes(input, maxlength);
}
diff --git
a/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/StringFunctionsTest.java
b/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/StringFunctionsTest.java
index ed6f3359df1..d705c65559e 100644
---
a/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/StringFunctionsTest.java
+++
b/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/StringFunctionsTest.java
@@ -381,6 +381,12 @@ public class StringFunctionsTest {
assertEquals(StringFunctions.suffixesWithSuffix(input, length, "$"),
expectedSuffixWithRegexChar);
}
+ @Test
+ public void testPrefixAndSuffixWithNullInput() {
+ assertNull(StringFunctions.prefixesWithPrefix(null, 3, "^"));
+ assertNull(StringFunctions.suffixesWithSuffix(null, 3, "$"));
+ }
+
@Test(dataProvider = "initcapTestCases")
public void testInitcap(String input, String expected) {
assertEquals(StringFunctions.initcap(input), expected);
diff --git
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/ScalarTransformFunctionWrapper.java
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/ScalarTransformFunctionWrapper.java
index f55123a3295..e6636906663 100644
---
a/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/ScalarTransformFunctionWrapper.java
+++
b/pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/ScalarTransformFunctionWrapper.java
@@ -24,6 +24,7 @@ import java.sql.Timestamp;
import java.util.List;
import java.util.Map;
import java.util.UUID;
+import javax.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.pinot.common.function.FunctionInfo;
import org.apache.pinot.common.function.FunctionUtils;
@@ -37,12 +38,20 @@ import org.apache.pinot.spi.utils.ByteArray;
import org.apache.pinot.spi.utils.CommonConstants.NullValuePlaceHolder;
import org.apache.pinot.spi.utils.PinotDataType;
import org.apache.pinot.spi.utils.UuidUtils;
+import org.roaringbitmap.PeekableIntIterator;
+import org.roaringbitmap.RoaringBitmap;
/// Wrapper transform function on the annotated scalar function.
+///
+/// When null handling is enabled and the function accepts `null` arguments
(see `ScalarFunction#nullableParameters`),
+/// the function is invoked with `null` for the null rows of the arguments,
and the rows where it returns `null` are the
+/// null rows of the result. Otherwise, the function is invoked with the
values stored in the null rows (e.g. the
+/// default null value of a column), and a row of the result is null when any
argument is null in it.
public class ScalarTransformFunctionWrapper extends BaseTransformFunction {
private final String _name;
private final QueryFunctionInvoker _functionInvoker;
+ private final boolean _nullableParameters;
private final ColumnDataType _resultType;
private final TransformResultMetadata _resultMetadata;
@@ -53,9 +62,19 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
private PinotDataType[] _nonLiteralTypes;
private Object[][] _nonLiteralValues;
+ // Whether the function is invoked with null for null argument rows, and
decides the null rows of the result
+ private boolean _invokeWithNulls;
+ // When invoking with nulls, the value block evaluated last, whose values
are in the result buffer, and the rows of it
+ // where the function returns null. Value blocks are not reused across
batches, so these stay valid for the block.
+ @Nullable
+ private ValueBlock _evaluatedValueBlock;
+ @Nullable
+ private RoaringBitmap _resultNullBitmap;
+
public ScalarTransformFunctionWrapper(FunctionInfo functionInfo) {
_name = functionInfo.getMethod().getName();
_functionInvoker = new QueryFunctionInvoker(functionInfo);
+ _nullableParameters = functionInfo.hasNullableParameters();
Class<?>[] parameterClasses = _functionInvoker.getParameterClasses();
PinotDataType[] parameterTypes = _functionInvoker.getParameterTypes();
int numParameters = parameterClasses.length;
@@ -157,6 +176,13 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
_nonLiteralValues = new Object[_numNonLiteralArguments][];
}
+ @Override
+ public void init(List<TransformFunction> arguments, Map<String,
ColumnContext> columnContextMap,
+ boolean nullHandlingEnabled) {
+ super.init(arguments, columnContextMap, nullHandlingEnabled);
+ _invokeWithNulls = nullHandlingEnabled && _nullableParameters;
+ }
+
@Override
public TransformResultMetadata getResultMetadata() {
return _resultMetadata;
@@ -167,6 +193,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.INT) {
return super.transformToIntValuesSV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _intValuesSV;
+ }
int length = valueBlock.getNumDocs();
initIntValuesSV(length);
getNonLiteralValues(valueBlock);
@@ -175,7 +204,12 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
Object value = _functionInvoker.invoke(_scalarArguments);
- _intValuesSV[i] = value != null ? (int) _resultType.toInternal(value) :
NullValuePlaceHolder.INT;
+ if (value != null) {
+ _intValuesSV[i] = (int) _resultType.toInternal(value);
+ } else {
+ _intValuesSV[i] = NullValuePlaceHolder.INT;
+ addNullResult(i);
+ }
}
return _intValuesSV;
}
@@ -185,6 +219,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.LONG) {
return super.transformToLongValuesSV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _longValuesSV;
+ }
int length = valueBlock.getNumDocs();
initLongValuesSV(length);
getNonLiteralValues(valueBlock);
@@ -193,7 +230,12 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
Object value = _functionInvoker.invoke(_scalarArguments);
- _longValuesSV[i] = value != null ? (long) _resultType.toInternal(value)
: NullValuePlaceHolder.LONG;
+ if (value != null) {
+ _longValuesSV[i] = (long) _resultType.toInternal(value);
+ } else {
+ _longValuesSV[i] = NullValuePlaceHolder.LONG;
+ addNullResult(i);
+ }
}
return _longValuesSV;
}
@@ -203,6 +245,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.FLOAT) {
return super.transformToFloatValuesSV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _floatValuesSV;
+ }
int length = valueBlock.getNumDocs();
initFloatValuesSV(length);
getNonLiteralValues(valueBlock);
@@ -211,8 +256,12 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
Object value = _functionInvoker.invoke(_scalarArguments);
- _floatValuesSV[i] =
- value != null ? (float) _resultType.toInternal(value) :
NullValuePlaceHolder.FLOAT;
+ if (value != null) {
+ _floatValuesSV[i] = (float) _resultType.toInternal(value);
+ } else {
+ _floatValuesSV[i] = NullValuePlaceHolder.FLOAT;
+ addNullResult(i);
+ }
}
return _floatValuesSV;
}
@@ -222,6 +271,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.DOUBLE) {
return super.transformToDoubleValuesSV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _doubleValuesSV;
+ }
int length = valueBlock.getNumDocs();
initDoubleValuesSV(length);
getNonLiteralValues(valueBlock);
@@ -230,8 +282,12 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
Object value = _functionInvoker.invoke(_scalarArguments);
- _doubleValuesSV[i] =
- value != null ? (double) _resultType.toInternal(value) :
NullValuePlaceHolder.DOUBLE;
+ if (value != null) {
+ _doubleValuesSV[i] = (double) _resultType.toInternal(value);
+ } else {
+ _doubleValuesSV[i] = NullValuePlaceHolder.DOUBLE;
+ addNullResult(i);
+ }
}
return _doubleValuesSV;
}
@@ -241,6 +297,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.BIG_DECIMAL)
{
return super.transformToBigDecimalValuesSV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _bigDecimalValuesSV;
+ }
int length = valueBlock.getNumDocs();
initBigDecimalValuesSV(length);
getNonLiteralValues(valueBlock);
@@ -249,8 +308,12 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
Object value = _functionInvoker.invoke(_scalarArguments);
- _bigDecimalValuesSV[i] =
- value != null ? (BigDecimal) _resultType.toInternal(value) :
NullValuePlaceHolder.BIG_DECIMAL;
+ if (value != null) {
+ _bigDecimalValuesSV[i] = (BigDecimal) _resultType.toInternal(value);
+ } else {
+ _bigDecimalValuesSV[i] = NullValuePlaceHolder.BIG_DECIMAL;
+ addNullResult(i);
+ }
}
return _bigDecimalValuesSV;
}
@@ -260,6 +323,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.STRING) {
return super.transformToStringValuesSV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _stringValuesSV;
+ }
int length = valueBlock.getNumDocs();
initStringValuesSV(length);
getNonLiteralValues(valueBlock);
@@ -268,8 +334,12 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
Object value = _functionInvoker.invoke(_scalarArguments);
- _stringValuesSV[i] =
- value != null ? (String) _resultType.toInternal(value) :
NullValuePlaceHolder.STRING;
+ if (value != null) {
+ _stringValuesSV[i] = (String) _resultType.toInternal(value);
+ } else {
+ _stringValuesSV[i] = NullValuePlaceHolder.STRING;
+ addNullResult(i);
+ }
}
return _stringValuesSV;
}
@@ -279,6 +349,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.BYTES) {
return super.transformToBytesValuesSV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _bytesValuesSV;
+ }
int length = valueBlock.getNumDocs();
initBytesValuesSV(length);
getNonLiteralValues(valueBlock);
@@ -287,9 +360,12 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
Object value = _functionInvoker.invoke(_scalarArguments);
- byte[] bytes =
- value != null ? ((ByteArray)
_resultType.toInternal(value)).getBytes() : NullValuePlaceHolder.BYTES;
- _bytesValuesSV[i] = bytes;
+ if (value != null) {
+ _bytesValuesSV[i] = ((ByteArray)
_resultType.toInternal(value)).getBytes();
+ } else {
+ _bytesValuesSV[i] = NullValuePlaceHolder.BYTES;
+ addNullResult(i);
+ }
}
return _bytesValuesSV;
}
@@ -299,6 +375,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.INT) {
return super.transformToIntValuesMV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _intValuesMV;
+ }
int length = valueBlock.getNumDocs();
initIntValuesMV(length);
getNonLiteralValues(valueBlock);
@@ -306,7 +385,13 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
for (int j = 0; j < _numNonLiteralArguments; j++) {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
- _intValuesMV[i] = (int[])
_resultType.toInternal(_functionInvoker.invoke(_scalarArguments));
+ Object value = _functionInvoker.invoke(_scalarArguments);
+ if (value != null) {
+ _intValuesMV[i] = (int[]) _resultType.toInternal(value);
+ } else {
+ _intValuesMV[i] = NullValuePlaceHolder.INT_ARRAY;
+ addNullResult(i);
+ }
}
return _intValuesMV;
}
@@ -316,6 +401,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.LONG) {
return super.transformToLongValuesMV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _longValuesMV;
+ }
int length = valueBlock.getNumDocs();
initLongValuesMV(length);
getNonLiteralValues(valueBlock);
@@ -323,7 +411,13 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
for (int j = 0; j < _numNonLiteralArguments; j++) {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
- _longValuesMV[i] = (long[])
_resultType.toInternal(_functionInvoker.invoke(_scalarArguments));
+ Object value = _functionInvoker.invoke(_scalarArguments);
+ if (value != null) {
+ _longValuesMV[i] = (long[]) _resultType.toInternal(value);
+ } else {
+ _longValuesMV[i] = NullValuePlaceHolder.LONG_ARRAY;
+ addNullResult(i);
+ }
}
return _longValuesMV;
}
@@ -333,6 +427,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.FLOAT) {
return super.transformToFloatValuesMV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _floatValuesMV;
+ }
int length = valueBlock.getNumDocs();
initFloatValuesMV(length);
getNonLiteralValues(valueBlock);
@@ -340,7 +437,13 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
for (int j = 0; j < _numNonLiteralArguments; j++) {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
- _floatValuesMV[i] = (float[])
_resultType.toInternal(_functionInvoker.invoke(_scalarArguments));
+ Object value = _functionInvoker.invoke(_scalarArguments);
+ if (value != null) {
+ _floatValuesMV[i] = (float[]) _resultType.toInternal(value);
+ } else {
+ _floatValuesMV[i] = NullValuePlaceHolder.FLOAT_ARRAY;
+ addNullResult(i);
+ }
}
return _floatValuesMV;
}
@@ -350,6 +453,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.DOUBLE) {
return super.transformToDoubleValuesMV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _doubleValuesMV;
+ }
int length = valueBlock.getNumDocs();
initDoubleValuesMV(length);
getNonLiteralValues(valueBlock);
@@ -357,7 +463,13 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
for (int j = 0; j < _numNonLiteralArguments; j++) {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
- _doubleValuesMV[i] = (double[])
_resultType.toInternal(_functionInvoker.invoke(_scalarArguments));
+ Object value = _functionInvoker.invoke(_scalarArguments);
+ if (value != null) {
+ _doubleValuesMV[i] = (double[]) _resultType.toInternal(value);
+ } else {
+ _doubleValuesMV[i] = NullValuePlaceHolder.DOUBLE_ARRAY;
+ addNullResult(i);
+ }
}
return _doubleValuesMV;
}
@@ -367,6 +479,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
if (_resultMetadata.getDataType().getStoredType() != DataType.STRING) {
return super.transformToStringValuesMV(valueBlock);
}
+ if (valueBlock == _evaluatedValueBlock) {
+ return _stringValuesMV;
+ }
int length = valueBlock.getNumDocs();
initStringValuesMV(length);
getNonLiteralValues(valueBlock);
@@ -374,11 +489,91 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
for (int j = 0; j < _numNonLiteralArguments; j++) {
_scalarArguments[_nonLiteralIndices[j]] = _nonLiteralValues[j][i];
}
- _stringValuesMV[i] = (String[])
_resultType.toInternal(_functionInvoker.invoke(_scalarArguments));
+ Object value = _functionInvoker.invoke(_scalarArguments);
+ if (value != null) {
+ _stringValuesMV[i] = (String[]) _resultType.toInternal(value);
+ } else {
+ _stringValuesMV[i] = NullValuePlaceHolder.STRING_ARRAY;
+ addNullResult(i);
+ }
}
return _stringValuesMV;
}
+ @Nullable
+ @Override
+ public RoaringBitmap getNullBitmap(ValueBlock valueBlock) {
+ if (!_invokeWithNulls) {
+ return super.getNullBitmap(valueBlock);
+ }
+ if (valueBlock != _evaluatedValueBlock) {
+ evaluate(valueBlock);
+ }
+ return _resultNullBitmap;
+ }
+
+ /// Evaluates the function on the given value block with the transform
method of the result type, which also records
+ /// the null rows of the result.
+ private void evaluate(ValueBlock valueBlock) {
+ DataType storedType = _resultMetadata.getDataType().getStoredType();
+ if (_resultMetadata.isSingleValue()) {
+ switch (storedType) {
+ case INT:
+ transformToIntValuesSV(valueBlock);
+ break;
+ case LONG:
+ transformToLongValuesSV(valueBlock);
+ break;
+ case FLOAT:
+ transformToFloatValuesSV(valueBlock);
+ break;
+ case DOUBLE:
+ transformToDoubleValuesSV(valueBlock);
+ break;
+ case BIG_DECIMAL:
+ transformToBigDecimalValuesSV(valueBlock);
+ break;
+ case STRING:
+ transformToStringValuesSV(valueBlock);
+ break;
+ case BYTES:
+ transformToBytesValuesSV(valueBlock);
+ break;
+ default:
+ throw new IllegalStateException("Unsupported single-value result
type: " + storedType);
+ }
+ } else {
+ switch (storedType) {
+ case INT:
+ transformToIntValuesMV(valueBlock);
+ break;
+ case LONG:
+ transformToLongValuesMV(valueBlock);
+ break;
+ case FLOAT:
+ transformToFloatValuesMV(valueBlock);
+ break;
+ case DOUBLE:
+ transformToDoubleValuesMV(valueBlock);
+ break;
+ case STRING:
+ transformToStringValuesMV(valueBlock);
+ break;
+ default:
+ throw new IllegalStateException("Unsupported multi-value result
type: " + storedType);
+ }
+ }
+ }
+
+ private void addNullResult(int row) {
+ if (_invokeWithNulls) {
+ if (_resultNullBitmap == null) {
+ _resultNullBitmap = new RoaringBitmap();
+ }
+ _resultNullBitmap.add(row);
+ }
+ }
+
/// Returns the type to read an argument for an `Object` parameter, so that
the function gets the external Java value
/// of the argument result type, the same as `ColumnDataType.toExternal()`
in the multi-stage engine `FunctionOperand`
/// for that type. Complex types such as MAP are not supported.
@@ -416,6 +611,9 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
/// Helper method to fetch values for the non-literal transform functions
based on the parameter types. For an
/// `Object` parameter, the values are read based on the argument result
type (see [#getObjectArgumentType]).
+ ///
+ /// When invoking with nulls, also replaces the values of the null rows with
`null`, and starts recording the null
+ /// rows of the result for the value block.
private void getNonLiteralValues(ValueBlock valueBlock) {
for (int i = 0; i < _numNonLiteralArguments; i++) {
PinotDataType parameterType = _nonLiteralTypes[i];
@@ -545,5 +743,21 @@ public class ScalarTransformFunctionWrapper extends
BaseTransformFunction {
throw new IllegalStateException("Unsupported parameter type: " +
parameterType);
}
}
+ if (_invokeWithNulls) {
+ for (int i = 0; i < _numNonLiteralArguments; i++) {
+ RoaringBitmap nullBitmap =
_nonLiteralFunctions[i].getNullBitmap(valueBlock);
+ if (nullBitmap != null && !nullBitmap.isEmpty()) {
+ // The values can be the buffer of the argument, so replace the null
rows in a copy
+ Object[] values = _nonLiteralValues[i].clone();
+ PeekableIntIterator iterator = nullBitmap.getIntIterator();
+ while (iterator.hasNext()) {
+ values[iterator.next()] = null;
+ }
+ _nonLiteralValues[i] = values;
+ }
+ }
+ _evaluatedValueBlock = valueBlock;
+ _resultNullBitmap = null;
+ }
}
}
diff --git
a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/ScalarTransformFunctionWrapperTest.java
b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/ScalarTransformFunctionWrapperTest.java
index 90ab19eea37..24e07be6525 100644
---
a/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/ScalarTransformFunctionWrapperTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/core/operator/transform/function/ScalarTransformFunctionWrapperTest.java
@@ -18,6 +18,7 @@
*/
package org.apache.pinot.core.operator.transform.function;
+import com.google.common.collect.Maps;
import it.unimi.dsi.fastutil.ints.IntLinkedOpenHashSet;
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
import java.nio.charset.StandardCharsets;
@@ -40,8 +41,11 @@ import
org.apache.pinot.common.function.scalar.StringFunctions;
import org.apache.pinot.common.function.scalar.uuid.UuidToStringScalarFunction;
import org.apache.pinot.common.request.context.ExpressionContext;
import org.apache.pinot.common.request.context.RequestContextUtils;
+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.core.query.request.context.utils.QueryContextConverterUtils;
import org.apache.pinot.spi.annotations.ScalarFunction;
import org.apache.pinot.spi.data.FieldSpec.DataType;
import org.apache.pinot.spi.utils.ArrayCopyUtils;
@@ -54,6 +58,7 @@ import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
@@ -1305,6 +1310,66 @@ public class ScalarTransformFunctionWrapperTest extends
BaseTransformFunctionTes
testTransformFunctionWithNull(transformFunction, expectedValues, bitmap);
}
+ @Test
+ public void testNullArgumentsWithNullHandling() {
+ String expression = String.format("prefixesWithPrefix(%s, 3, %s)",
STRING_SV_COLUMN, STRING_SV_NULL_COLUMN);
+ TransformFunction transformFunction =
getTransformFunctionWithNullHandling(expression);
+ assertTrue(transformFunction instanceof ScalarTransformFunctionWrapper);
+ assertNull(transformFunction.getNullBitmap(_projectionBlock));
+ String[][] values =
transformFunction.transformToStringValuesMV(_projectionBlock);
+ for (int i = 0; i < NUM_ROWS; i++) {
+ String prefix = isNullRow(i) ? null : _stringSVValues[i];
+ assertEquals(values[i],
StringFunctions.prefixesWithPrefix(_stringSVValues[i], 3, prefix));
+ }
+
+ // Without null handling, a null argument makes the row null
+ transformFunction =
TransformFunctionFactory.get(RequestContextUtils.getExpression(expression),
_dataSourceMap);
+ assertEquals(transformFunction.getNullBitmap(_projectionBlock),
getNullRows());
+ }
+
+ @Test
+ public void testNullLiteralArgumentWithNullHandling() {
+ TransformFunction transformFunction =
+
getTransformFunctionWithNullHandling(String.format("prefixesWithPrefix(%s, 3,
null)", STRING_SV_COLUMN));
+ assertNull(transformFunction.getNullBitmap(_projectionBlock));
+ String[][] values =
transformFunction.transformToStringValuesMV(_projectionBlock);
+ for (int i = 0; i < NUM_ROWS; i++) {
+ assertEquals(values[i], StringFunctions.prefixes(_stringSVValues[i], 3));
+ }
+ }
+
+ @Test
+ public void testNullResultsWithNullHandling() {
+ TransformFunction transformFunction =
+
getTransformFunctionWithNullHandling(String.format("prefixesWithPrefix(%s, 3,
'^')", STRING_SV_NULL_COLUMN));
+ String[][] values =
transformFunction.transformToStringValuesMV(_projectionBlock);
+ for (int i = 0; i < NUM_ROWS; i++) {
+ String[] expectedValues = isNullRow(i) ?
NullValuePlaceHolder.STRING_ARRAY
+ : StringFunctions.prefixesWithPrefix(_stringSVValues[i], 3, "^");
+ assertEquals(values[i], expectedValues);
+ }
+ assertEquals(transformFunction.getNullBitmap(_projectionBlock),
getNullRows());
+ }
+
+ private TransformFunction getTransformFunctionWithNullHandling(String
expression) {
+ Map<String, ColumnContext> columnContextMap =
Maps.newHashMapWithExpectedSize(_dataSourceMap.size());
+ _dataSourceMap.forEach(
+ (column, dataSource) -> columnContextMap.put(column,
ColumnContext.fromDataSource(dataSource)));
+ QueryContext queryContext =
+ QueryContextConverterUtils.getQueryContext("SET
enableNullHandling=true; SELECT * FROM testTable");
+ return
TransformFunctionFactory.get(RequestContextUtils.getExpression(expression),
columnContextMap, queryContext);
+ }
+
+ private RoaringBitmap getNullRows() {
+ RoaringBitmap nullRows = new RoaringBitmap();
+ for (int i = 0; i < NUM_ROWS; i++) {
+ if (isNullRow(i)) {
+ nullRows.add(i);
+ }
+ }
+ return nullRows;
+ }
+
@Test
public void testCbrtTransformFunction() {
ExpressionContext expression =
diff --git
a/pinot-core/src/test/java/org/apache/pinot/queries/NullHandlingEnabledQueriesTest.java
b/pinot-core/src/test/java/org/apache/pinot/queries/NullHandlingEnabledQueriesTest.java
index cb48a2e910a..b9e1cff051e 100644
---
a/pinot-core/src/test/java/org/apache/pinot/queries/NullHandlingEnabledQueriesTest.java
+++
b/pinot-core/src/test/java/org/apache/pinot/queries/NullHandlingEnabledQueriesTest.java
@@ -333,6 +333,30 @@ public class NullHandlingEnabledQueriesTest extends
BaseQueriesTest {
COLUMN1, COLUMN2)).get(0)[0], (long) NUM_OF_SEGMENT_COPIES);
}
+ /// A scalar function that accepts null arguments gets null for the null
row, and returns its default value there,
+ /// also when nested inside another function.
+ @Test
+ public void testScalarFunctionWithNullableParameters()
+ throws Exception {
+ initializeRows();
+ insertRow("{\"a\":\"x\",\"b\":1}");
+ insertRow(null);
+ Schema schema = schemaBuilder().addSingleValueDimension(COLUMN1,
DataType.STRING).build();
+ setUpSegments(TABLE_CONFIG, schema);
+
+ List<Object[]> rows = getRows(String.format(
+ "SELECT jsonPathString(%s, '$.a', 'none'), COUNT(*) FROM testTable
GROUP BY 1 ORDER BY 1", COLUMN1));
+ assertEquals(rows.size(), 2);
+ assertEquals(rows.get(0), new Object[]{"none", (long)
NUM_OF_SEGMENT_COPIES});
+ assertEquals(rows.get(1), new Object[]{"x", (long) NUM_OF_SEGMENT_COPIES});
+
+ rows = getRows(String.format(
+ "SELECT jsonPathLong(%s, '$.b', 5) + 0, COUNT(*) FROM testTable GROUP
BY 1 ORDER BY 1", COLUMN1));
+ assertEquals(rows.size(), 2);
+ assertEquals(rows.get(0), new Object[]{1.0, (long) NUM_OF_SEGMENT_COPIES});
+ assertEquals(rows.get(1), new Object[]{5.0, (long) NUM_OF_SEGMENT_COPIES});
+ }
+
/// A multi-value column's null row is stored as a single default value, and
its posting lists overlap across values,
/// so the count is taken over their union.
@Test
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]