This is an automated email from the ASF dual-hosted git repository.

jackie 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 54db091  builtin datetime functions to avoid throwing 
NullPointerException when input value is null (#8382)
54db091 is described below

commit 54db0915c9ccb744639b2730b32a540d17ab9cd3
Author: nizarhejazi <[email protected]>
AuthorDate: Fri Mar 25 12:05:24 2022 -0700

    builtin datetime functions to avoid throwing NullPointerException when 
input value is null (#8382)
---
 .../apache/pinot/common/function/FunctionInfo.java |  8 +++++-
 .../pinot/common/function/FunctionRegistry.java    | 13 +++++----
 .../common/function/scalar/JsonFunctions.java      | 27 +++++++++---------
 .../pinot/common/function/JsonFunctionsTest.java   |  2 +-
 .../core/data/function/DateTimeFunctionsTest.java  | 19 ++++++++++++-
 .../function/InbuiltFunctionEvaluatorTest.java     | 33 +++++++++++-----------
 .../local/function/InbuiltFunctionEvaluator.java   | 20 +++++++++++++
 .../pinot/spi/annotations/ScalarFunction.java      |  3 ++
 8 files changed, 87 insertions(+), 38 deletions(-)

diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/FunctionInfo.java 
b/pinot-common/src/main/java/org/apache/pinot/common/function/FunctionInfo.java
index 0169823..7b8fcff 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/FunctionInfo.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/FunctionInfo.java
@@ -24,11 +24,13 @@ import java.lang.reflect.Method;
 public class FunctionInfo {
   private final Method _method;
   private final Class<?> _clazz;
+  private final boolean _nullableParameters;
 
-  public FunctionInfo(Method method, Class<?> clazz) {
+  public FunctionInfo(Method method, Class<?> clazz, boolean 
nullableParameters) {
     method.setAccessible(true);
     _method = method;
     _clazz = clazz;
+    _nullableParameters = nullableParameters;
   }
 
   public Method getMethod() {
@@ -38,4 +40,8 @@ public class FunctionInfo {
   public Class<?> getClazz() {
     return _clazz;
   }
+
+  public boolean hasNullableParameters() {
+    return _nullableParameters;
+  }
 }
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java
index 831ece4..368acf9 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java
@@ -63,12 +63,13 @@ public class FunctionRegistry {
       if (scalarFunction.enabled()) {
         // Annotated function names
         String[] scalarFunctionNames = scalarFunction.names();
+        boolean nullableParameters = scalarFunction.nullableParameters();
         if (scalarFunctionNames.length > 0) {
           for (String name : scalarFunctionNames) {
-            FunctionRegistry.registerFunction(name, method);
+            FunctionRegistry.registerFunction(name, method, 
nullableParameters);
           }
         } else {
-          FunctionRegistry.registerFunction(method);
+          FunctionRegistry.registerFunction(method, nullableParameters);
         }
       }
     }
@@ -87,15 +88,15 @@ public class FunctionRegistry {
   /**
    * Registers a method with the name of the method.
    */
-  public static void registerFunction(Method method) {
-    registerFunction(method.getName(), method);
+  public static void registerFunction(Method method, boolean 
nullableParameters) {
+    registerFunction(method.getName(), method, nullableParameters);
   }
 
   /**
    * Registers a method with the given function name.
    */
-  public static void registerFunction(String functionName, Method method) {
-    FunctionInfo functionInfo = new FunctionInfo(method, 
method.getDeclaringClass());
+  public static void registerFunction(String functionName, Method method, 
boolean nullableParameters) {
+    FunctionInfo functionInfo = new FunctionInfo(method, 
method.getDeclaringClass(), nullableParameters);
     String canonicalName = canonicalize(functionName);
     Map<Integer, FunctionInfo> functionInfoMap = 
FUNCTION_INFO_MAP.computeIfAbsent(canonicalName, k -> new HashMap<>());
     Preconditions.checkState(functionInfoMap.put(method.getParameterCount(), 
functionInfo) == null,
diff --git 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java
 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java
index c109241..a81ec37 100644
--- 
a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java
+++ 
b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/JsonFunctions.java
@@ -31,6 +31,7 @@ import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
+import javax.annotation.Nullable;
 import org.apache.pinot.common.function.JsonPathCache;
 import org.apache.pinot.spi.annotations.ScalarFunction;
 import org.apache.pinot.spi.utils.JsonUtils;
@@ -66,8 +67,8 @@ public class JsonFunctions {
   /**
    * Convert Map to Json String
    */
-  @ScalarFunction
-  public static String toJsonMapStr(Map map)
+  @ScalarFunction(nullableParameters = true)
+  public static String toJsonMapStr(@Nullable Map map)
       throws JsonProcessingException {
     return JsonUtils.objectToString(map);
   }
@@ -75,7 +76,7 @@ public class JsonFunctions {
   /**
    * Convert object to Json String
    */
-  @ScalarFunction
+  @ScalarFunction(nullableParameters = true)
   public static String jsonFormat(Object object)
       throws JsonProcessingException {
     return JsonUtils.objectToString(object);
@@ -89,7 +90,7 @@ public class JsonFunctions {
     if (object instanceof String) {
       return PARSE_CONTEXT.parse((String) object).read(jsonPath, 
NO_PREDICATES);
     }
-    return object == null ? null : PARSE_CONTEXT.parse(object).read(jsonPath, 
NO_PREDICATES);
+    return PARSE_CONTEXT.parse(object).read(jsonPath, NO_PREDICATES);
   }
 
   /**
@@ -103,8 +104,8 @@ public class JsonFunctions {
     return convertObjectToArray(PARSE_CONTEXT.parse(object).read(jsonPath, 
NO_PREDICATES));
   }
 
-  @ScalarFunction
-  public static Object[] jsonPathArrayDefaultEmpty(Object object, String 
jsonPath) {
+  @ScalarFunction(nullableParameters = true)
+  public static Object[] jsonPathArrayDefaultEmpty(@Nullable Object object, 
String jsonPath) {
     try {
       Object[] result = object == null ? null : jsonPathArray(object, 
jsonPath);
       return result == null ? EMPTY : result;
@@ -134,14 +135,14 @@ public class JsonFunctions {
     if (jsonValue instanceof String) {
       return (String) jsonValue;
     }
-    return jsonValue == null ? null : JsonUtils.objectToString(jsonValue);
+    return JsonUtils.objectToString(jsonValue);
   }
 
   /**
    * Extract from Json with path to String
    */
-  @ScalarFunction
-  public static String jsonPathString(Object object, String jsonPath, String 
defaultValue) {
+  @ScalarFunction(nullableParameters = true)
+  public static String jsonPathString(@Nullable Object object, String 
jsonPath, String defaultValue) {
     try {
       Object jsonValue = jsonPath(object, jsonPath);
       if (jsonValue instanceof String) {
@@ -164,8 +165,8 @@ public class JsonFunctions {
   /**
    * Extract from Json with path to Long
    */
-  @ScalarFunction
-  public static long jsonPathLong(Object object, String jsonPath, long 
defaultValue) {
+  @ScalarFunction(nullableParameters = true)
+  public static long jsonPathLong(@Nullable Object object, String jsonPath, 
long defaultValue) {
     try {
       Object jsonValue = jsonPath(object, jsonPath);
       if (jsonValue == null) {
@@ -191,8 +192,8 @@ public class JsonFunctions {
   /**
    * Extract from Json with path to Double
    */
-  @ScalarFunction
-  public static double jsonPathDouble(Object object, String jsonPath, double 
defaultValue) {
+  @ScalarFunction(nullableParameters = true)
+  public static double jsonPathDouble(@Nullable Object object, String 
jsonPath, double defaultValue) {
     try {
       Object jsonValue = jsonPath(object, jsonPath);
       if (jsonValue == null) {
diff --git 
a/pinot-common/src/test/java/org/apache/pinot/common/function/JsonFunctionsTest.java
 
b/pinot-common/src/test/java/org/apache/pinot/common/function/JsonFunctionsTest.java
index 375e186..585afc9 100644
--- 
a/pinot-common/src/test/java/org/apache/pinot/common/function/JsonFunctionsTest.java
+++ 
b/pinot-common/src/test/java/org/apache/pinot/common/function/JsonFunctionsTest.java
@@ -260,7 +260,7 @@ public class JsonFunctionsTest {
   public static Object[][] jsonPathStringTestCases() {
     return new Object[][]{
         {ImmutableMap.of("foo", "x", "bar", ImmutableMap.of("foo", "y")), 
"$.foo", "x"},
-        {ImmutableMap.of("foo", "x", "bar", ImmutableMap.of("foo", "y")), 
"$.qux", null},
+        {ImmutableMap.of("foo", "x", "bar", ImmutableMap.of("foo", "y")), 
"$.qux", "null"},
         {ImmutableMap.of("foo", "x", "bar", ImmutableMap.of("foo", "y")), 
"$.bar", "{\"foo\":\"y\"}"},
     };
   }
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/data/function/DateTimeFunctionsTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/data/function/DateTimeFunctionsTest.java
index e291c7f..343dabc 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/data/function/DateTimeFunctionsTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/data/function/DateTimeFunctionsTest.java
@@ -251,6 +251,13 @@ public class DateTimeFunctionsTest {
         "fromDateTime(dateTime, 'EEE MMM dd HH:mm:ss ZZZ yyyy')", 
Lists.newArrayList("dateTime"), row112, 1251142606000L
     });
 
+    // fromDateTime with null
+    GenericRow row113 = new GenericRow();
+    row113.putValue("dateTime", null);
+    inputs.add(new Object[]{
+        "fromDateTime(dateTime, 'yyyy-MM-dd''T''HH:mm:ss.SSS''Z''')", 
Lists.newArrayList("dateTime"), row113, null
+    });
+
     // timezone_hour and timezone_minute
     List<String> expectedArguments = Collections.singletonList("tz");
     GenericRow row120 = new GenericRow();
@@ -326,6 +333,14 @@ public class DateTimeFunctionsTest {
     inputs.add(new Object[]{"second(millis, tz)", expectedArguments, row131, 
13});
     inputs.add(new Object[]{"millisecond(millis, tz)", expectedArguments, 
row131, 123});
 
+    GenericRow row140 = new GenericRow();
+    row140.putValue("duration", null);
+    inputs.add(new Object[]{"ago(duration)", Lists.newArrayList("duration"), 
row140, null});
+
+    GenericRow row141 = new GenericRow();
+    row141.putValue("timezoneId", null);
+    inputs.add(new Object[]{"timezoneHour(timezoneId)", 
Lists.newArrayList("timezoneId"), row141, null});
+
     return inputs.toArray(new Object[0][]);
   }
 
@@ -426,6 +441,8 @@ public class DateTimeFunctionsTest {
     testDateTimeConvert(5019675L/* 20170920T03:15:00 */, "5:MINUTES:EPOCH", 
"1:MILLISECONDS:EPOCH", "1:HOURS",
         1505901600000L/* 20170920T03:00:00 */);
 
+    testDateTimeConvert(null, "5:MINUTES:EPOCH", "1:MILLISECONDS:EPOCH", 
"1:HOURS", null);
+
     // EPOCH to SDF
     // Test conversion from millis since epoch to simple date format (UTC)
     testDateTimeConvert(1505985360000L/* 20170921T02:16:00 */, 
"1:MILLISECONDS:EPOCH",
@@ -524,6 +541,6 @@ public class DateTimeFunctionsTest {
     row.putValue("timeCol", timeValue);
     List<String> arguments = Collections.singletonList("timeCol");
     testFunction(String.format("dateTimeConvert(timeCol, '%s', '%s', '%s')", 
inputFormatStr, outputFormatStr,
-        outputGranularityStr), arguments, row, expectedResult.toString());
+        outputGranularityStr), arguments, row, expectedResult == null ? null : 
expectedResult.toString());
   }
 }
diff --git 
a/pinot-core/src/test/java/org/apache/pinot/core/data/function/InbuiltFunctionEvaluatorTest.java
 
b/pinot-core/src/test/java/org/apache/pinot/core/data/function/InbuiltFunctionEvaluatorTest.java
index 6e1da8e..3663118 100644
--- 
a/pinot-core/src/test/java/org/apache/pinot/core/data/function/InbuiltFunctionEvaluatorTest.java
+++ 
b/pinot-core/src/test/java/org/apache/pinot/core/data/function/InbuiltFunctionEvaluatorTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.core.data.function;
 
+import java.lang.reflect.Method;
 import java.util.Collections;
 import org.apache.pinot.common.function.FunctionRegistry;
 import org.apache.pinot.segment.local.function.InbuiltFunctionEvaluator;
@@ -25,8 +26,8 @@ import org.apache.pinot.spi.data.readers.GenericRow;
 import org.testng.annotations.Test;
 
 import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNull;
 import static org.testng.Assert.assertTrue;
-import static org.testng.Assert.fail;
 
 
 public class InbuiltFunctionEvaluatorTest {
@@ -111,7 +112,8 @@ public class InbuiltFunctionEvaluatorTest {
   public void testStateSharedBetweenRowsForExecution()
       throws Exception {
     MyFunc myFunc = new MyFunc();
-    
FunctionRegistry.registerFunction(myFunc.getClass().getDeclaredMethod("appendToStringAndReturn",
 String.class));
+    Method method = 
myFunc.getClass().getDeclaredMethod("appendToStringAndReturn", String.class);
+    FunctionRegistry.registerFunction(method, false);
     String expression = "appendToStringAndReturn('test ')";
     InbuiltFunctionEvaluator evaluator = new 
InbuiltFunctionEvaluator(expression);
     assertTrue(evaluator.getArguments().isEmpty());
@@ -122,20 +124,19 @@ public class InbuiltFunctionEvaluatorTest {
   }
 
   @Test
-  public void testExceptionDuringInbuiltFunctionEvaluator()
-      throws Exception {
-    String expression = "fromDateTime(reverse('2020-01-01T00:00:00Z'), 
\"invalid_identifier\")";
-    InbuiltFunctionEvaluator evaluator = new 
InbuiltFunctionEvaluator(expression);
-    assertEquals(evaluator.getArguments().size(), 1);
-    GenericRow row = new GenericRow();
-    try {
-      evaluator.evaluate(row);
-      fail();
-    } catch (Exception e) {
-      // assert that exception contains the full function call signature
-      
assertTrue(e.toString().contains("fromDateTime(reverse('2020-01-01T00:00:00Z'),invalid_identifier)"));
-      // assert that FunctionInvoker ISE is captured correctly.
-      assertTrue(e.getCause() instanceof IllegalStateException);
+  public void testNullReturnedByInbuiltFunctionEvaluatorThatCannotTakeNull() {
+    String[] expressions = {
+        "fromDateTime(\"NULL\", 'yyyy-MM-dd''T''HH:mm:ss.SSS''Z''')",
+        "fromDateTime(\"invalid_identifier\", 
'yyyy-MM-dd''T''HH:mm:ss.SSS''Z''')",
+        "toDateTime(1648010797, \"invalid_identifier\", 
\"invalid_identifier\")",
+        "toDateTime(\"invalid_identifier\", \"invalid_identifier\", 
\"invalid_identifier\")",
+        "toDateTime(\"NULL\", \"invalid_identifier\", \"invalid_identifier\")",
+        "toDateTime(\"invalid_identifier\", \"NULL\", \"invalid_identifier\")"
+    };
+    for (String expression : expressions) {
+      InbuiltFunctionEvaluator evaluator = new 
InbuiltFunctionEvaluator(expression);
+      GenericRow row = new GenericRow();
+      assertNull(evaluator.evaluate(row));
     }
   }
 
diff --git 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/function/InbuiltFunctionEvaluator.java
 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/function/InbuiltFunctionEvaluator.java
index 9ec6f6e..be17cb7 100644
--- 
a/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/function/InbuiltFunctionEvaluator.java
+++ 
b/pinot-segment-local/src/main/java/org/apache/pinot/segment/local/function/InbuiltFunctionEvaluator.java
@@ -108,11 +108,13 @@ public class InbuiltFunctionEvaluator implements 
FunctionEvaluator {
 
   private static class FunctionExecutionNode implements ExecutableNode {
     final FunctionInvoker _functionInvoker;
+    final FunctionInfo _functionInfo;
     final ExecutableNode[] _argumentNodes;
     final Object[] _arguments;
 
     FunctionExecutionNode(FunctionInfo functionInfo, ExecutableNode[] 
argumentNodes) {
       _functionInvoker = new FunctionInvoker(functionInfo);
+      _functionInfo = functionInfo;
       _argumentNodes = argumentNodes;
       _arguments = new Object[_argumentNodes.length];
     }
@@ -124,6 +126,15 @@ public class InbuiltFunctionEvaluator implements 
FunctionEvaluator {
         for (int i = 0; i < numArguments; i++) {
           _arguments[i] = _argumentNodes[i].execute(row);
         }
+        if (!_functionInfo.hasNullableParameters()) {
+          // Preserve null values during ingestion transformation if function 
is an inbuilt
+          // scalar function that cannot handle nulls, and invoked with null 
parameter(s).
+          for (Object argument : _arguments) {
+            if (argument == null) {
+              return null;
+            }
+          }
+        }
         _functionInvoker.convertTypes(_arguments);
         return _functionInvoker.invoke(_arguments);
       } catch (Exception e) {
@@ -138,6 +149,15 @@ public class InbuiltFunctionEvaluator implements 
FunctionEvaluator {
         for (int i = 0; i < numArguments; i++) {
           _arguments[i] = _argumentNodes[i].execute(values);
         }
+        if (!_functionInfo.hasNullableParameters()) {
+          // Preserve null values during ingestion transformation if function 
is an inbuilt
+          // scalar function that cannot handle nulls, and invoked with null 
parameter(s).
+          for (Object argument : _arguments) {
+            if (argument == null) {
+              return null;
+            }
+          }
+        }
         _functionInvoker.convertTypes(_arguments);
         return _functionInvoker.invoke(_arguments);
       } catch (Exception e) {
diff --git 
a/pinot-spi/src/main/java/org/apache/pinot/spi/annotations/ScalarFunction.java 
b/pinot-spi/src/main/java/org/apache/pinot/spi/annotations/ScalarFunction.java
index 8cc1e71..6963c21 100644
--- 
a/pinot-spi/src/main/java/org/apache/pinot/spi/annotations/ScalarFunction.java
+++ 
b/pinot-spi/src/main/java/org/apache/pinot/spi/annotations/ScalarFunction.java
@@ -49,4 +49,7 @@ public @interface ScalarFunction {
   // If empty, FunctionsRegistry registers the method name as function name;
   // If not empty, FunctionsRegistry only registers the function names 
specified here, the method name is ignored.
   String[] names() default {};
+
+  // Whether the scalar function expects and can handle null arguments.
+  boolean nullableParameters() default false;
 }

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to