yashmayya commented on code in PR #13573:
URL: https://github.com/apache/pinot/pull/13573#discussion_r1671753878


##########
pinot-spi/src/main/java/org/apache/pinot/spi/annotations/ScalarFunction.java:
##########
@@ -54,14 +54,13 @@
 
   /**
    * Whether the scalar function expects and can handle null arguments.
-   *
    */
   boolean nullableParameters() default false;
 
-  boolean isPlaceholder() default false;
-
   /**
    * Whether the scalar function takes various number of arguments.
    */
   boolean isVarArg() default false;
+
+  @Deprecated boolean isPlaceholder() default false;

Review Comment:
   I didn't quite get what the purpose of this method was earlier? Also, any 
particular reason for keeping it around as deprecated rather than removing it 
(since there are no usages in this codebase currently)? Do we support 
externally created scalar functions as plugins?



##########
pinot-common/src/main/java/org/apache/pinot/common/function/PinotScalarFunction.java:
##########
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.common.function;
+
+import javax.annotation.Nullable;
+import org.apache.pinot.common.function.sql.PinotSqlFunction;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+
+
+/**
+ * Provides finer control to the scalar functions annotated with {@link 
ScalarFunction}.
+ */
+public interface PinotScalarFunction {
+
+  /**
+   * Returns the name of the function.
+   */
+  String getName();
+
+  /**
+   * Returns the corresponding {@link PinotSqlFunction} to be registered into 
the OperatorTable, or {@link null} if

Review Comment:
   ```suggestion
      * Returns the corresponding {@link PinotSqlFunction} to be registered 
into the OperatorTable, or {@code null} if
   ```
   nit: `{@link null}` is invalid Javadoc (similarly elsewhere)



##########
pinot-common/src/main/java/org/apache/pinot/common/function/PinotScalarFunction.java:
##########
@@ -0,0 +1,57 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.common.function;
+
+import javax.annotation.Nullable;
+import org.apache.pinot.common.function.sql.PinotSqlFunction;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+
+
+/**
+ * Provides finer control to the scalar functions annotated with {@link 
ScalarFunction}.
+ */
+public interface PinotScalarFunction {
+
+  /**
+   * Returns the name of the function.
+   */
+  String getName();
+
+  /**
+   * Returns the corresponding {@link PinotSqlFunction} to be registered into 
the OperatorTable, or {@link null} if
+   * it doesn't need to be registered (e.g. standard SqlFunction).
+   */
+  @Nullable
+  PinotSqlFunction toPinotSqlFunction();
+
+  /**
+   * Returns the {@link FunctionInfo} for the given argument types, or {@link 
null} if there is no matching.
+   */
+  @Nullable
+  default FunctionInfo getFunctionInfo(ColumnDataType[] argumentTypes) {

Review Comment:
   Is the idea to implement this method in something like a 
`ArgumentTypeBasedScalarFunction` in the future to support polymorphic scalar 
functions?



##########
pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java:
##########
@@ -46,44 +56,92 @@ private FunctionRegistry() {
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(FunctionRegistry.class);
 
-  // TODO: consolidate the following 2
-  // This FUNCTION_INFO_MAP is used by Pinot server to look up function by # 
of arguments
-  private static final Map<String, Map<Integer, FunctionInfo>> 
FUNCTION_INFO_MAP = new HashMap<>();
-  // This FUNCTION_MAP is used by Calcite function catalog to look up function 
by function signature.
-  private static final NameMultimap<Function> FUNCTION_MAP = new 
NameMultimap<>();
+  // Key is canonical name
+  public static final Map<String, PinotScalarFunction> FUNCTION_MAP;
 
   private static final int VAR_ARG_KEY = -1;
 
   /**
    * Registers the scalar functions via reflection.
-   * NOTE: In order to plugin methods using reflection, the methods should be 
inside a class that includes ".function."
-   *       in its class path. This convention can significantly reduce the 
time of class scanning.
+   * NOTE: In order to plugin functions or methods using reflection, they 
should be inside a class that includes

Review Comment:
   Is "functions" here referring to classes that are implementations of the 
`PinotScalarFunction` interface? 



##########
pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java:
##########
@@ -46,44 +56,92 @@ private FunctionRegistry() {
 
   private static final Logger LOGGER = 
LoggerFactory.getLogger(FunctionRegistry.class);
 
-  // TODO: consolidate the following 2
-  // This FUNCTION_INFO_MAP is used by Pinot server to look up function by # 
of arguments
-  private static final Map<String, Map<Integer, FunctionInfo>> 
FUNCTION_INFO_MAP = new HashMap<>();
-  // This FUNCTION_MAP is used by Calcite function catalog to look up function 
by function signature.
-  private static final NameMultimap<Function> FUNCTION_MAP = new 
NameMultimap<>();
+  // Key is canonical name
+  public static final Map<String, PinotScalarFunction> FUNCTION_MAP;
 
   private static final int VAR_ARG_KEY = -1;
 
   /**
    * Registers the scalar functions via reflection.
-   * NOTE: In order to plugin methods using reflection, the methods should be 
inside a class that includes ".function."
-   *       in its class path. This convention can significantly reduce the 
time of class scanning.
+   * NOTE: In order to plugin functions or methods using reflection, they 
should be inside a class that includes
+   *       ".function." in its class path. This convention can significantly 
reduce the time of class scanning.
    */
   static {
     long startTimeMs = System.currentTimeMillis();
+
+    // Register ScalarFunction classes

Review Comment:
   Is this also being introduced here for future use with implementations of 
the `PinotScalarFunction` interface to support polymorphic scalar functions?



##########
pinot-common/src/main/java/org/apache/pinot/common/function/TransformFunctionType.java:
##########
@@ -302,36 +263,22 @@ public enum TransformFunctionType {
 
   private final String _name;
   private final List<String> _alternativeNames;
-  private final SqlKind _sqlKind;
   private final SqlReturnTypeInference _returnTypeInference;
   private final SqlOperandTypeChecker _operandTypeChecker;
-  private final SqlFunctionCategory _sqlFunctionCategory;
 
   TransformFunctionType(String name, String... alternativeNames) {
-    this(name, null, null, null, null, alternativeNames);
-  }
-
-  TransformFunctionType(String name, SqlReturnTypeInference 
returnTypeInference,
-      SqlOperandTypeChecker operandTypeChecker, String... alternativeNames) {
-    this(name, SqlKind.OTHER_FUNCTION, returnTypeInference, operandTypeChecker,
-        SqlFunctionCategory.USER_DEFINED_FUNCTION, alternativeNames);
+    this(name, null, null, alternativeNames);
   }
 
-  /**
-   * Constructor to use for transform functions which are supported in both v1 
and multistage engines
-   */
-  TransformFunctionType(String name, SqlKind sqlKind, SqlReturnTypeInference 
returnTypeInference,
-      SqlOperandTypeChecker operandTypeChecker, SqlFunctionCategory 
sqlFunctionCategory, String... alternativeNames) {
+  TransformFunctionType(String name, @Nullable SqlReturnTypeInference 
returnTypeInference,
+      @Nullable SqlOperandTypeChecker operandTypeChecker, String... 
alternativeNames) {
     _name = name;
-    List<String> all = new ArrayList<>(alternativeNames.length + 2);
-    all.add(name);
-    all.add(name());
-    all.addAll(Arrays.asList(alternativeNames));
-    _alternativeNames = Collections.unmodifiableList(all);
-    _sqlKind = sqlKind;
+    List<String> names = new ArrayList<>(alternativeNames.length + 1);
+    names.add(name);
+    names.addAll(Arrays.asList(alternativeNames));
+    _alternativeNames = List.copyOf(names);

Review Comment:
   nit: this member variable and its getter method should probably be renamed 
considering that its not just the alternative names but all the names for the 
function?



##########
pinot-core/src/test/java/org/apache/pinot/core/function/FunctionDefinitionRegistryTest.java:
##########
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.function;
+
+import java.util.EnumSet;
+import org.apache.pinot.common.function.FunctionRegistry;
+import org.apache.pinot.common.function.TransformFunctionType;
+import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+import org.apache.pinot.sql.FilterKind;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+
+// NOTE: Keep this test in pinot-core to include all built-in scalar functions.
+// TODO: Consider breaking this test into multiple tests.
+public class FunctionDefinitionRegistryTest {
+  // TODO: Support these functions
+  private static final EnumSet<TransformFunctionType> 
IGNORED_TRANSFORM_FUNCTION_TYPES = EnumSet.of(
+      // Special placeholder functions without implementation
+      TransformFunctionType.ARRAY_TO_MV, 
TransformFunctionType.ARRAY_VALUE_CONSTRUCTOR, TransformFunctionType.SCALAR,
+      // Special functions that requires index
+      TransformFunctionType.JSON_EXTRACT_INDEX, 
TransformFunctionType.MAP_VALUE, TransformFunctionType.LOOKUP,
+      // TODO: Support these functions
+      TransformFunctionType.IN, TransformFunctionType.NOT_IN, 
TransformFunctionType.IS_TRUE,
+      TransformFunctionType.IS_NOT_TRUE, TransformFunctionType.IS_FALSE, 
TransformFunctionType.IS_NOT_FALSE,
+      TransformFunctionType.AND, TransformFunctionType.OR, 
TransformFunctionType.JSON_EXTRACT_SCALAR,
+      TransformFunctionType.JSON_EXTRACT_KEY, 
TransformFunctionType.TIME_CONVERT,
+      TransformFunctionType.DATE_TIME_CONVERT_WINDOW_HOP, 
TransformFunctionType.ARRAY_LENGTH,
+      TransformFunctionType.ARRAY_AVERAGE, TransformFunctionType.ARRAY_MIN, 
TransformFunctionType.ARRAY_MAX,
+      TransformFunctionType.ARRAY_SUM, TransformFunctionType.VALUE_IN, 
TransformFunctionType.IN_ID_SET,
+      TransformFunctionType.GROOVY, TransformFunctionType.CLP_DECODE, 
TransformFunctionType.CLP_ENCODED_VARS_MATCH,
+      TransformFunctionType.ST_POLYGON, TransformFunctionType.ST_AREA);
+  private static final EnumSet<FilterKind> IGNORED_FILTER_KINDS = EnumSet.of(
+      // Special filter functions without implementation
+      FilterKind.TEXT_MATCH, FilterKind.TEXT_CONTAINS, FilterKind.JSON_MATCH, 
FilterKind.VECTOR_SIMILARITY,
+      // TODO: Support these functions
+      FilterKind.AND, FilterKind.OR, FilterKind.RANGE, FilterKind.IN, 
FilterKind.NOT_IN);
+
+  @Test
+  public void testCalciteFunctionMapAllRegistered() {

Review Comment:
   Should this test's name be updated?



##########
pinot-core/src/test/java/org/apache/pinot/core/function/FunctionDefinitionRegistryTest.java:
##########
@@ -0,0 +1,106 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.core.function;
+
+import java.util.EnumSet;
+import org.apache.pinot.common.function.FunctionRegistry;
+import org.apache.pinot.common.function.TransformFunctionType;
+import org.apache.pinot.segment.spi.AggregationFunctionType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+import org.apache.pinot.sql.FilterKind;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertTrue;
+
+
+// NOTE: Keep this test in pinot-core to include all built-in scalar functions.
+// TODO: Consider breaking this test into multiple tests.
+public class FunctionDefinitionRegistryTest {
+  // TODO: Support these functions
+  private static final EnumSet<TransformFunctionType> 
IGNORED_TRANSFORM_FUNCTION_TYPES = EnumSet.of(
+      // Special placeholder functions without implementation
+      TransformFunctionType.ARRAY_TO_MV, 
TransformFunctionType.ARRAY_VALUE_CONSTRUCTOR, TransformFunctionType.SCALAR,
+      // Special functions that requires index
+      TransformFunctionType.JSON_EXTRACT_INDEX, 
TransformFunctionType.MAP_VALUE, TransformFunctionType.LOOKUP,
+      // TODO: Support these functions
+      TransformFunctionType.IN, TransformFunctionType.NOT_IN, 
TransformFunctionType.IS_TRUE,
+      TransformFunctionType.IS_NOT_TRUE, TransformFunctionType.IS_FALSE, 
TransformFunctionType.IS_NOT_FALSE,
+      TransformFunctionType.AND, TransformFunctionType.OR, 
TransformFunctionType.JSON_EXTRACT_SCALAR,
+      TransformFunctionType.JSON_EXTRACT_KEY, 
TransformFunctionType.TIME_CONVERT,
+      TransformFunctionType.DATE_TIME_CONVERT_WINDOW_HOP, 
TransformFunctionType.ARRAY_LENGTH,
+      TransformFunctionType.ARRAY_AVERAGE, TransformFunctionType.ARRAY_MIN, 
TransformFunctionType.ARRAY_MAX,
+      TransformFunctionType.ARRAY_SUM, TransformFunctionType.VALUE_IN, 
TransformFunctionType.IN_ID_SET,
+      TransformFunctionType.GROOVY, TransformFunctionType.CLP_DECODE, 
TransformFunctionType.CLP_ENCODED_VARS_MATCH,
+      TransformFunctionType.ST_POLYGON, TransformFunctionType.ST_AREA);

Review Comment:
   I don't get why some of these functions are considered as not supported?



##########
pinot-core/src/main/java/org/apache/pinot/core/operator/transform/function/TransformFunctionFactory.java:
##########
@@ -354,8 +354,8 @@ public static TransformFunction get(ExpressionContext 
expression, Map<String, Co
   public static TransformFunction get(ExpressionContext expression, 
Map<String, DataSource> dataSourceMap) {
     Map<String, ColumnContext> columnContextMap = new 
HashMap<>(HashUtil.getHashMapCapacity(dataSourceMap.size()));
     dataSourceMap.forEach((k, v) -> columnContextMap.put(k, 
ColumnContext.fromDataSource(v)));
-    QueryContext dummy = QueryContextConverterUtils.getQueryContext(
-        CalciteSqlParser.compileToPinotQuery("SELECT * from testTable;"));
+    QueryContext dummy =

Review Comment:
   Shouldn't this method ideally be in some test utils class rather than in 
this class?



##########
pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java:
##########
@@ -95,108 +153,152 @@ public static void init() {
   }
 
   /**
-   * Registers a method with the name of the method.
+   * Registers a {@link PinotScalarFunction} under the given canonical name.
    */
-  public static void registerFunction(Method method, boolean 
nullableParameters, boolean isPlaceholder,
-      boolean isVarArg) {
-    registerFunction(method.getName(), method, nullableParameters, 
isPlaceholder, isVarArg);
+  private static void register(String canonicalName, PinotScalarFunction 
function,
+      Map<String, PinotScalarFunction> functionMap) {
+    Preconditions.checkState(functionMap.put(canonicalName, function) == null, 
"Function: %s is already registered",
+        canonicalName);
   }
 
   /**
-   * Registers a method with the given function name.
+   * Registers a {@link FunctionInfo} under the given canonical name.
    */
-  public static void registerFunction(String functionName, Method method, 
boolean nullableParameters,
-      boolean isPlaceholder, boolean isVarArg) {
-    if (!isPlaceholder) {
-      registerFunctionInfoMap(functionName, method, nullableParameters, 
isVarArg);
-    }
-    registerCalciteNamedFunctionMap(functionName, method, nullableParameters, 
isVarArg);
+  private static void register(String canonicalName, FunctionInfo 
functionInfo, int numArguments,
+      Map<String, Map<Integer, FunctionInfo>> functionInfoMap) {
+    Preconditions.checkState(
+        functionInfoMap.computeIfAbsent(canonicalName, k -> new 
HashMap<>()).put(numArguments, functionInfo) == null,
+        "Function: %s with %s arguments is already registered", canonicalName,
+        numArguments == VAR_ARG_KEY ? "variable" : numArguments);
   }
 
-  private static void registerFunctionInfoMap(String functionName, Method 
method, boolean nullableParameters,
-      boolean isVarArg) {
-    FunctionInfo functionInfo = new FunctionInfo(method, 
method.getDeclaringClass(), nullableParameters);
-    String canonicalName = canonicalize(functionName);
-    Map<Integer, FunctionInfo> functionInfoMap = 
FUNCTION_INFO_MAP.computeIfAbsent(canonicalName, k -> new HashMap<>());
-    if (isVarArg) {
-      FunctionInfo existFunctionInfo = functionInfoMap.put(VAR_ARG_KEY, 
functionInfo);
-      Preconditions.checkState(existFunctionInfo == null || 
existFunctionInfo.getMethod() == functionInfo.getMethod(),
-          "Function: %s with variable number of parameters is already 
registered", functionName);
-    } else {
-      FunctionInfo existFunctionInfo = 
functionInfoMap.put(method.getParameterCount(), functionInfo);
-      Preconditions.checkState(existFunctionInfo == null || 
existFunctionInfo.getMethod() == functionInfo.getMethod(),
-          "Function: %s with %s parameters is already registered", 
functionName, method.getParameterCount());
-    }
-  }
-
-  private static void registerCalciteNamedFunctionMap(String functionName, 
Method method, boolean nullableParameters,
-      boolean isVarArg) {
-    if (method.getAnnotation(Deprecated.class) == null) {
-      FUNCTION_MAP.put(functionName, ScalarFunctionImpl.create(method));
-    }
-  }
-
-  public static Map<String, List<Function>> getRegisteredCalciteFunctionMap() {
-    return FUNCTION_MAP.map();
+  /**
+   * Returns {@code true} if the given canonical name is registered, {@code 
false} otherwise.
+   */
+  public static boolean contains(String canonicalName) {
+    return FUNCTION_MAP.containsKey(canonicalName);
   }
 
-  public static Set<String> getRegisteredCalciteFunctionNames() {
-    return FUNCTION_MAP.map().keySet();
+  @Deprecated
+  public static boolean containsFunction(String name) {
+    return contains(canonicalize(name));
   }
 
   /**
-   * Returns {@code true} if the given function name is registered, {@code 
false} otherwise.
+   * Returns the {@link FunctionInfo} associated with the given canonical name 
and argument types, or {@code null} if
+   * there is no matching method. This method should be called after the 
FunctionRegistry is initialized and all methods
+   * are already registered.
    */
-  public static boolean containsFunction(String functionName) {
-    return FUNCTION_INFO_MAP.containsKey(canonicalize(functionName));
+  @Nullable
+  public static FunctionInfo lookupFunctionInfo(String canonicalName, 
ColumnDataType[] argumentTypes) {
+    PinotScalarFunction function = FUNCTION_MAP.get(canonicalName);
+    return function != null ? function.getFunctionInfo(argumentTypes) : null;
   }
 
   /**
-   * Returns the {@link FunctionInfo} associated with the given function name 
and number of parameters, or {@code null}
+   * Returns the {@link FunctionInfo} associated with the given canonical name 
and number of arguments, or {@code null}
    * if there is no matching method. This method should be called after the 
FunctionRegistry is initialized and all
    * methods are already registered.
+   * TODO: Move all usages to {@link #lookupFunctionInfo(String, 
ColumnDataType[])}.
    */
   @Nullable
-  public static FunctionInfo getFunctionInfo(String functionName, int 
numParameters) {
-    Map<Integer, FunctionInfo> functionInfoMap = 
FUNCTION_INFO_MAP.get(canonicalize(functionName));
-    if (functionInfoMap != null) {
-      FunctionInfo functionInfo = functionInfoMap.get(numParameters);
-      if (functionInfo != null) {
-        return functionInfo;
-      }
-      return functionInfoMap.get(VAR_ARG_KEY);
-    }
-    return null;
+  public static FunctionInfo lookupFunctionInfo(String canonicalName, int 
numArguments) {
+    PinotScalarFunction function = FUNCTION_MAP.get(canonicalName);
+    return function != null ? function.getFunctionInfo(numArguments) : null;
   }
 
-  private static String canonicalize(String functionName) {
-    return StringUtils.remove(functionName, '_').toLowerCase();
+  @Deprecated
+  @Nullable
+  public static FunctionInfo getFunctionInfo(String name, int numArguments) {
+    return lookupFunctionInfo(canonicalize(name), numArguments);

Review Comment:
   Why is this deprecated? Isn't it more convenient to canonicalize the name 
inside the function registry rather than making sure that the name is canonical 
at every call site?



##########
pinot-query-planner/src/main/java/org/apache/pinot/calcite/rel/rules/PinotAggregateReduceFunctionsRule.java:
##########
@@ -16,21 +16,27 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.pinot.calcite.sql.fun;
+package org.apache.pinot.calcite.rel.rules;
 
-import org.apache.calcite.sql.SqlCall;
-import org.apache.calcite.sql.SqlNode;
-import org.apache.calcite.sql.fun.SqlCoalesceFunction;
-import org.apache.calcite.sql.validate.SqlValidator;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.rules.AggregateReduceFunctionsRule;
+import org.apache.calcite.sql.SqlKind;
 
 
 /**
- * Pinot supports native COALESCE function, thus no need to create CASE WHEN 
conversion.
+ * Pinot customized version of {@link AggregateReduceFunctionsRule} which only 
reduce on SUM and AVG.
  */
-public class PinotSqlCoalesceFunction extends SqlCoalesceFunction {
+public class PinotAggregateReduceFunctionsRule extends 
AggregateReduceFunctionsRule {
+  public static final PinotAggregateReduceFunctionsRule INSTANCE =
+      new PinotAggregateReduceFunctionsRule(Config.DEFAULT);
+
+  private PinotAggregateReduceFunctionsRule(Config config) {
+    super(config);
+  }
 
   @Override
-  public SqlNode rewriteCall(SqlValidator validator, SqlCall call) {
-    return call;
+  public boolean canReduce(AggregateCall call) {
+    SqlKind kind = call.getAggregation().getKind();
+    return kind == SqlKind.SUM || kind == SqlKind.AVG;

Review Comment:
   Why do we need this customized rule? Which of the original Calcite rule's 
reductions don't work in Pinot?



##########
pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java:
##########
@@ -95,108 +153,152 @@ public static void init() {
   }
 
   /**
-   * Registers a method with the name of the method.
+   * Registers a {@link PinotScalarFunction} under the given canonical name.
    */
-  public static void registerFunction(Method method, boolean 
nullableParameters, boolean isPlaceholder,
-      boolean isVarArg) {
-    registerFunction(method.getName(), method, nullableParameters, 
isPlaceholder, isVarArg);
+  private static void register(String canonicalName, PinotScalarFunction 
function,
+      Map<String, PinotScalarFunction> functionMap) {
+    Preconditions.checkState(functionMap.put(canonicalName, function) == null, 
"Function: %s is already registered",
+        canonicalName);
   }
 
   /**
-   * Registers a method with the given function name.
+   * Registers a {@link FunctionInfo} under the given canonical name.
    */
-  public static void registerFunction(String functionName, Method method, 
boolean nullableParameters,
-      boolean isPlaceholder, boolean isVarArg) {
-    if (!isPlaceholder) {
-      registerFunctionInfoMap(functionName, method, nullableParameters, 
isVarArg);
-    }
-    registerCalciteNamedFunctionMap(functionName, method, nullableParameters, 
isVarArg);
+  private static void register(String canonicalName, FunctionInfo 
functionInfo, int numArguments,
+      Map<String, Map<Integer, FunctionInfo>> functionInfoMap) {
+    Preconditions.checkState(
+        functionInfoMap.computeIfAbsent(canonicalName, k -> new 
HashMap<>()).put(numArguments, functionInfo) == null,
+        "Function: %s with %s arguments is already registered", canonicalName,
+        numArguments == VAR_ARG_KEY ? "variable" : numArguments);
   }
 
-  private static void registerFunctionInfoMap(String functionName, Method 
method, boolean nullableParameters,
-      boolean isVarArg) {
-    FunctionInfo functionInfo = new FunctionInfo(method, 
method.getDeclaringClass(), nullableParameters);
-    String canonicalName = canonicalize(functionName);
-    Map<Integer, FunctionInfo> functionInfoMap = 
FUNCTION_INFO_MAP.computeIfAbsent(canonicalName, k -> new HashMap<>());
-    if (isVarArg) {
-      FunctionInfo existFunctionInfo = functionInfoMap.put(VAR_ARG_KEY, 
functionInfo);
-      Preconditions.checkState(existFunctionInfo == null || 
existFunctionInfo.getMethod() == functionInfo.getMethod(),
-          "Function: %s with variable number of parameters is already 
registered", functionName);
-    } else {
-      FunctionInfo existFunctionInfo = 
functionInfoMap.put(method.getParameterCount(), functionInfo);
-      Preconditions.checkState(existFunctionInfo == null || 
existFunctionInfo.getMethod() == functionInfo.getMethod(),
-          "Function: %s with %s parameters is already registered", 
functionName, method.getParameterCount());
-    }
-  }
-
-  private static void registerCalciteNamedFunctionMap(String functionName, 
Method method, boolean nullableParameters,
-      boolean isVarArg) {
-    if (method.getAnnotation(Deprecated.class) == null) {
-      FUNCTION_MAP.put(functionName, ScalarFunctionImpl.create(method));
-    }
-  }
-
-  public static Map<String, List<Function>> getRegisteredCalciteFunctionMap() {
-    return FUNCTION_MAP.map();
+  /**
+   * Returns {@code true} if the given canonical name is registered, {@code 
false} otherwise.
+   */
+  public static boolean contains(String canonicalName) {
+    return FUNCTION_MAP.containsKey(canonicalName);
   }
 
-  public static Set<String> getRegisteredCalciteFunctionNames() {
-    return FUNCTION_MAP.map().keySet();
+  @Deprecated
+  public static boolean containsFunction(String name) {
+    return contains(canonicalize(name));
   }
 
   /**
-   * Returns {@code true} if the given function name is registered, {@code 
false} otherwise.
+   * Returns the {@link FunctionInfo} associated with the given canonical name 
and argument types, or {@code null} if
+   * there is no matching method. This method should be called after the 
FunctionRegistry is initialized and all methods
+   * are already registered.
    */
-  public static boolean containsFunction(String functionName) {
-    return FUNCTION_INFO_MAP.containsKey(canonicalize(functionName));
+  @Nullable
+  public static FunctionInfo lookupFunctionInfo(String canonicalName, 
ColumnDataType[] argumentTypes) {
+    PinotScalarFunction function = FUNCTION_MAP.get(canonicalName);
+    return function != null ? function.getFunctionInfo(argumentTypes) : null;
   }
 
   /**
-   * Returns the {@link FunctionInfo} associated with the given function name 
and number of parameters, or {@code null}
+   * Returns the {@link FunctionInfo} associated with the given canonical name 
and number of arguments, or {@code null}
    * if there is no matching method. This method should be called after the 
FunctionRegistry is initialized and all
    * methods are already registered.
+   * TODO: Move all usages to {@link #lookupFunctionInfo(String, 
ColumnDataType[])}.
    */
   @Nullable
-  public static FunctionInfo getFunctionInfo(String functionName, int 
numParameters) {
-    Map<Integer, FunctionInfo> functionInfoMap = 
FUNCTION_INFO_MAP.get(canonicalize(functionName));
-    if (functionInfoMap != null) {
-      FunctionInfo functionInfo = functionInfoMap.get(numParameters);
-      if (functionInfo != null) {
-        return functionInfo;
-      }
-      return functionInfoMap.get(VAR_ARG_KEY);
-    }
-    return null;
+  public static FunctionInfo lookupFunctionInfo(String canonicalName, int 
numArguments) {
+    PinotScalarFunction function = FUNCTION_MAP.get(canonicalName);
+    return function != null ? function.getFunctionInfo(numArguments) : null;
   }
 
-  private static String canonicalize(String functionName) {
-    return StringUtils.remove(functionName, '_').toLowerCase();
+  @Deprecated
+  @Nullable
+  public static FunctionInfo getFunctionInfo(String name, int numArguments) {
+    return lookupFunctionInfo(canonicalize(name), numArguments);
   }
 
-  /**
-   * Placeholders for scalar function, they register and represents the 
signature for transform and filter predicate
-   * so that v2 engine can understand and plan them correctly.
-   */
-  private static class PlaceholderScalarFunctions {
+  public static String canonicalize(String name) {
+    return StringUtils.remove(name, '_').toLowerCase();
+  }
+
+  public static class ArgumentCountBasedScalarFunction implements 
PinotScalarFunction {
+    private final String _name;
+    private final Map<Integer, FunctionInfo> _functionInfoMap;
+
+    private ArgumentCountBasedScalarFunction(String name, Map<Integer, 
FunctionInfo> functionInfoMap) {
+      _name = name.toUpperCase();

Review Comment:
   Why are the canonical names lower case while the name here is upper case?



##########
pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java:
##########
@@ -95,108 +153,152 @@ public static void init() {
   }
 
   /**
-   * Registers a method with the name of the method.
+   * Registers a {@link PinotScalarFunction} under the given canonical name.
    */
-  public static void registerFunction(Method method, boolean 
nullableParameters, boolean isPlaceholder,
-      boolean isVarArg) {
-    registerFunction(method.getName(), method, nullableParameters, 
isPlaceholder, isVarArg);
+  private static void register(String canonicalName, PinotScalarFunction 
function,
+      Map<String, PinotScalarFunction> functionMap) {
+    Preconditions.checkState(functionMap.put(canonicalName, function) == null, 
"Function: %s is already registered",
+        canonicalName);
   }
 
   /**
-   * Registers a method with the given function name.
+   * Registers a {@link FunctionInfo} under the given canonical name.
    */
-  public static void registerFunction(String functionName, Method method, 
boolean nullableParameters,
-      boolean isPlaceholder, boolean isVarArg) {
-    if (!isPlaceholder) {
-      registerFunctionInfoMap(functionName, method, nullableParameters, 
isVarArg);
-    }
-    registerCalciteNamedFunctionMap(functionName, method, nullableParameters, 
isVarArg);
+  private static void register(String canonicalName, FunctionInfo 
functionInfo, int numArguments,
+      Map<String, Map<Integer, FunctionInfo>> functionInfoMap) {
+    Preconditions.checkState(
+        functionInfoMap.computeIfAbsent(canonicalName, k -> new 
HashMap<>()).put(numArguments, functionInfo) == null,
+        "Function: %s with %s arguments is already registered", canonicalName,
+        numArguments == VAR_ARG_KEY ? "variable" : numArguments);
   }
 
-  private static void registerFunctionInfoMap(String functionName, Method 
method, boolean nullableParameters,
-      boolean isVarArg) {
-    FunctionInfo functionInfo = new FunctionInfo(method, 
method.getDeclaringClass(), nullableParameters);
-    String canonicalName = canonicalize(functionName);
-    Map<Integer, FunctionInfo> functionInfoMap = 
FUNCTION_INFO_MAP.computeIfAbsent(canonicalName, k -> new HashMap<>());
-    if (isVarArg) {
-      FunctionInfo existFunctionInfo = functionInfoMap.put(VAR_ARG_KEY, 
functionInfo);
-      Preconditions.checkState(existFunctionInfo == null || 
existFunctionInfo.getMethod() == functionInfo.getMethod(),
-          "Function: %s with variable number of parameters is already 
registered", functionName);
-    } else {
-      FunctionInfo existFunctionInfo = 
functionInfoMap.put(method.getParameterCount(), functionInfo);
-      Preconditions.checkState(existFunctionInfo == null || 
existFunctionInfo.getMethod() == functionInfo.getMethod(),
-          "Function: %s with %s parameters is already registered", 
functionName, method.getParameterCount());
-    }
-  }
-
-  private static void registerCalciteNamedFunctionMap(String functionName, 
Method method, boolean nullableParameters,
-      boolean isVarArg) {
-    if (method.getAnnotation(Deprecated.class) == null) {
-      FUNCTION_MAP.put(functionName, ScalarFunctionImpl.create(method));
-    }
-  }
-
-  public static Map<String, List<Function>> getRegisteredCalciteFunctionMap() {
-    return FUNCTION_MAP.map();
+  /**
+   * Returns {@code true} if the given canonical name is registered, {@code 
false} otherwise.
+   */
+  public static boolean contains(String canonicalName) {
+    return FUNCTION_MAP.containsKey(canonicalName);
   }
 
-  public static Set<String> getRegisteredCalciteFunctionNames() {
-    return FUNCTION_MAP.map().keySet();
+  @Deprecated
+  public static boolean containsFunction(String name) {
+    return contains(canonicalize(name));
   }
 
   /**
-   * Returns {@code true} if the given function name is registered, {@code 
false} otherwise.
+   * Returns the {@link FunctionInfo} associated with the given canonical name 
and argument types, or {@code null} if
+   * there is no matching method. This method should be called after the 
FunctionRegistry is initialized and all methods
+   * are already registered.
    */
-  public static boolean containsFunction(String functionName) {
-    return FUNCTION_INFO_MAP.containsKey(canonicalize(functionName));
+  @Nullable
+  public static FunctionInfo lookupFunctionInfo(String canonicalName, 
ColumnDataType[] argumentTypes) {
+    PinotScalarFunction function = FUNCTION_MAP.get(canonicalName);
+    return function != null ? function.getFunctionInfo(argumentTypes) : null;
   }
 
   /**
-   * Returns the {@link FunctionInfo} associated with the given function name 
and number of parameters, or {@code null}
+   * Returns the {@link FunctionInfo} associated with the given canonical name 
and number of arguments, or {@code null}
    * if there is no matching method. This method should be called after the 
FunctionRegistry is initialized and all
    * methods are already registered.
+   * TODO: Move all usages to {@link #lookupFunctionInfo(String, 
ColumnDataType[])}.
    */
   @Nullable
-  public static FunctionInfo getFunctionInfo(String functionName, int 
numParameters) {
-    Map<Integer, FunctionInfo> functionInfoMap = 
FUNCTION_INFO_MAP.get(canonicalize(functionName));
-    if (functionInfoMap != null) {
-      FunctionInfo functionInfo = functionInfoMap.get(numParameters);
-      if (functionInfo != null) {
-        return functionInfo;
-      }
-      return functionInfoMap.get(VAR_ARG_KEY);
-    }
-    return null;
+  public static FunctionInfo lookupFunctionInfo(String canonicalName, int 
numArguments) {
+    PinotScalarFunction function = FUNCTION_MAP.get(canonicalName);
+    return function != null ? function.getFunctionInfo(numArguments) : null;
   }
 
-  private static String canonicalize(String functionName) {
-    return StringUtils.remove(functionName, '_').toLowerCase();
+  @Deprecated
+  @Nullable
+  public static FunctionInfo getFunctionInfo(String name, int numArguments) {
+    return lookupFunctionInfo(canonicalize(name), numArguments);
   }
 
-  /**
-   * Placeholders for scalar function, they register and represents the 
signature for transform and filter predicate
-   * so that v2 engine can understand and plan them correctly.
-   */
-  private static class PlaceholderScalarFunctions {
+  public static String canonicalize(String name) {
+    return StringUtils.remove(name, '_').toLowerCase();
+  }
+
+  public static class ArgumentCountBasedScalarFunction implements 
PinotScalarFunction {
+    private final String _name;
+    private final Map<Integer, FunctionInfo> _functionInfoMap;
+
+    private ArgumentCountBasedScalarFunction(String name, Map<Integer, 
FunctionInfo> functionInfoMap) {
+      _name = name.toUpperCase();
+      _functionInfoMap = functionInfoMap;
+    }
 
-    @ScalarFunction(names = {"textContains", "text_contains"}, isPlaceholder = 
true)
-    public static boolean textContains(String text, String pattern) {
-      throw new UnsupportedOperationException("Placeholder scalar function, 
should not reach here");
+    @Override
+    public String getName() {
+      return _name;
     }
 
-    @ScalarFunction(names = {"textMatch", "text_match"}, isPlaceholder = true)
-    public static boolean textMatch(String text, String pattern) {
-      throw new UnsupportedOperationException("Placeholder scalar function, 
should not reach here");
+    @Override
+    public PinotSqlFunction toPinotSqlFunction() {
+      return new PinotSqlFunction(_name, getReturnTypeInference(), 
getOperandTypeChecker());
     }
 
-    @ScalarFunction(names = {"jsonMatch", "json_match"}, isPlaceholder = true)
-    public static boolean jsonMatch(String text, String pattern) {
-      throw new UnsupportedOperationException("Placeholder scalar function, 
should not reach here");
+    private SqlReturnTypeInference getReturnTypeInference() {
+      return opBinding -> {
+        int numArguments = opBinding.getOperandCount();
+        FunctionInfo functionInfo = getFunctionInfo(numArguments);
+        Preconditions.checkState(functionInfo != null, "Failed to find 
function: %s with %s arguments", _name,
+            numArguments);
+        Method method = functionInfo.getMethod();
+        Class<?> returnClass = method.getReturnType();
+        RelDataTypeFactory typeFactory = opBinding.getTypeFactory();
+        RelDataType returnType = returnClass == Object.class ? 
typeFactory.createSqlType(SqlTypeName.ANY)
+            : JavaTypeFactoryImpl.toSql(typeFactory, 
typeFactory.createJavaType(returnClass));
+
+        if (!functionInfo.hasNullableParameters()) {
+          // When any parameter is null, return is null
+          for (RelDataType type : opBinding.collectOperandTypes()) {
+            if (type.isNullable()) {
+              return typeFactory.createTypeWithNullability(returnType, true);
+            }
+          }
+        }
+
+        return method.isAnnotationPresent(Nullable.class) ? 
typeFactory.createTypeWithNullability(returnType, true)
+            : returnType;
+      };
+    }
+
+    private SqlOperandTypeChecker getOperandTypeChecker() {
+      if (_functionInfoMap.containsKey(VAR_ARG_KEY)) {
+        return OperandTypes.VARIADIC;
+      }
+      if (_functionInfoMap.size() == 1) {
+        return 
getOperandTypeChecker(_functionInfoMap.values().iterator().next().getMethod());
+      }
+      List<SqlOperandTypeChecker> operandTypeCheckers = new 
ArrayList<>(_functionInfoMap.size());
+      for (FunctionInfo functionInfo : _functionInfoMap.values()) {
+        
operandTypeCheckers.add(getOperandTypeChecker(functionInfo.getMethod()));
+      }
+      return OperandTypes.or(operandTypeCheckers.toArray(new 
SqlOperandTypeChecker[0]));
+    }
+
+    private static SqlTypeFamily getSqlTypeFamily(Class<?> clazz) {
+      // NOTE: Pinot allows some non-standard type conversions such as 
Timestamp <-> long, boolean <-> int etc. Do not
+      //       restrict the type family for now. We only restrict the type 
family for String so that cast can be added.
+      //       Explicit cast is required to correctly convert boolean and 
Timestamp to String.
+      // TODO: Revisit this.
+      return clazz == String.class ? SqlTypeFamily.CHARACTER : 
SqlTypeFamily.ANY;

Review Comment:
   I didn't quite follow why only `String` type is being handled as a special 
case here, could you please elaborate? Is the idea that either implicit casting 
or runtime function errors will handle other cases rather than imposing an 
overly restrictive type checker at compile time?



##########
pinot-common/src/main/java/org/apache/pinot/common/function/FunctionRegistry.java:
##########
@@ -95,108 +153,152 @@ public static void init() {
   }
 
   /**
-   * Registers a method with the name of the method.
+   * Registers a {@link PinotScalarFunction} under the given canonical name.
    */
-  public static void registerFunction(Method method, boolean 
nullableParameters, boolean isPlaceholder,
-      boolean isVarArg) {
-    registerFunction(method.getName(), method, nullableParameters, 
isPlaceholder, isVarArg);
+  private static void register(String canonicalName, PinotScalarFunction 
function,
+      Map<String, PinotScalarFunction> functionMap) {
+    Preconditions.checkState(functionMap.put(canonicalName, function) == null, 
"Function: %s is already registered",
+        canonicalName);
   }
 
   /**
-   * Registers a method with the given function name.
+   * Registers a {@link FunctionInfo} under the given canonical name.
    */
-  public static void registerFunction(String functionName, Method method, 
boolean nullableParameters,
-      boolean isPlaceholder, boolean isVarArg) {
-    if (!isPlaceholder) {
-      registerFunctionInfoMap(functionName, method, nullableParameters, 
isVarArg);
-    }
-    registerCalciteNamedFunctionMap(functionName, method, nullableParameters, 
isVarArg);
+  private static void register(String canonicalName, FunctionInfo 
functionInfo, int numArguments,
+      Map<String, Map<Integer, FunctionInfo>> functionInfoMap) {
+    Preconditions.checkState(
+        functionInfoMap.computeIfAbsent(canonicalName, k -> new 
HashMap<>()).put(numArguments, functionInfo) == null,
+        "Function: %s with %s arguments is already registered", canonicalName,
+        numArguments == VAR_ARG_KEY ? "variable" : numArguments);
   }
 
-  private static void registerFunctionInfoMap(String functionName, Method 
method, boolean nullableParameters,
-      boolean isVarArg) {
-    FunctionInfo functionInfo = new FunctionInfo(method, 
method.getDeclaringClass(), nullableParameters);
-    String canonicalName = canonicalize(functionName);
-    Map<Integer, FunctionInfo> functionInfoMap = 
FUNCTION_INFO_MAP.computeIfAbsent(canonicalName, k -> new HashMap<>());
-    if (isVarArg) {
-      FunctionInfo existFunctionInfo = functionInfoMap.put(VAR_ARG_KEY, 
functionInfo);
-      Preconditions.checkState(existFunctionInfo == null || 
existFunctionInfo.getMethod() == functionInfo.getMethod(),
-          "Function: %s with variable number of parameters is already 
registered", functionName);
-    } else {
-      FunctionInfo existFunctionInfo = 
functionInfoMap.put(method.getParameterCount(), functionInfo);
-      Preconditions.checkState(existFunctionInfo == null || 
existFunctionInfo.getMethod() == functionInfo.getMethod(),
-          "Function: %s with %s parameters is already registered", 
functionName, method.getParameterCount());
-    }
-  }
-
-  private static void registerCalciteNamedFunctionMap(String functionName, 
Method method, boolean nullableParameters,
-      boolean isVarArg) {
-    if (method.getAnnotation(Deprecated.class) == null) {
-      FUNCTION_MAP.put(functionName, ScalarFunctionImpl.create(method));
-    }
-  }
-
-  public static Map<String, List<Function>> getRegisteredCalciteFunctionMap() {
-    return FUNCTION_MAP.map();
+  /**
+   * Returns {@code true} if the given canonical name is registered, {@code 
false} otherwise.
+   */
+  public static boolean contains(String canonicalName) {
+    return FUNCTION_MAP.containsKey(canonicalName);
   }
 
-  public static Set<String> getRegisteredCalciteFunctionNames() {
-    return FUNCTION_MAP.map().keySet();
+  @Deprecated
+  public static boolean containsFunction(String name) {
+    return contains(canonicalize(name));
   }
 
   /**
-   * Returns {@code true} if the given function name is registered, {@code 
false} otherwise.
+   * Returns the {@link FunctionInfo} associated with the given canonical name 
and argument types, or {@code null} if
+   * there is no matching method. This method should be called after the 
FunctionRegistry is initialized and all methods
+   * are already registered.
    */
-  public static boolean containsFunction(String functionName) {
-    return FUNCTION_INFO_MAP.containsKey(canonicalize(functionName));
+  @Nullable
+  public static FunctionInfo lookupFunctionInfo(String canonicalName, 
ColumnDataType[] argumentTypes) {
+    PinotScalarFunction function = FUNCTION_MAP.get(canonicalName);
+    return function != null ? function.getFunctionInfo(argumentTypes) : null;
   }
 
   /**
-   * Returns the {@link FunctionInfo} associated with the given function name 
and number of parameters, or {@code null}
+   * Returns the {@link FunctionInfo} associated with the given canonical name 
and number of arguments, or {@code null}
    * if there is no matching method. This method should be called after the 
FunctionRegistry is initialized and all
    * methods are already registered.
+   * TODO: Move all usages to {@link #lookupFunctionInfo(String, 
ColumnDataType[])}.
    */
   @Nullable
-  public static FunctionInfo getFunctionInfo(String functionName, int 
numParameters) {
-    Map<Integer, FunctionInfo> functionInfoMap = 
FUNCTION_INFO_MAP.get(canonicalize(functionName));
-    if (functionInfoMap != null) {
-      FunctionInfo functionInfo = functionInfoMap.get(numParameters);
-      if (functionInfo != null) {
-        return functionInfo;
-      }
-      return functionInfoMap.get(VAR_ARG_KEY);
-    }
-    return null;
+  public static FunctionInfo lookupFunctionInfo(String canonicalName, int 
numArguments) {
+    PinotScalarFunction function = FUNCTION_MAP.get(canonicalName);
+    return function != null ? function.getFunctionInfo(numArguments) : null;
   }
 
-  private static String canonicalize(String functionName) {
-    return StringUtils.remove(functionName, '_').toLowerCase();
+  @Deprecated
+  @Nullable
+  public static FunctionInfo getFunctionInfo(String name, int numArguments) {
+    return lookupFunctionInfo(canonicalize(name), numArguments);
   }
 
-  /**
-   * Placeholders for scalar function, they register and represents the 
signature for transform and filter predicate
-   * so that v2 engine can understand and plan them correctly.
-   */
-  private static class PlaceholderScalarFunctions {
+  public static String canonicalize(String name) {
+    return StringUtils.remove(name, '_').toLowerCase();
+  }
+
+  public static class ArgumentCountBasedScalarFunction implements 
PinotScalarFunction {
+    private final String _name;
+    private final Map<Integer, FunctionInfo> _functionInfoMap;
+
+    private ArgumentCountBasedScalarFunction(String name, Map<Integer, 
FunctionInfo> functionInfoMap) {
+      _name = name.toUpperCase();
+      _functionInfoMap = functionInfoMap;
+    }
 
-    @ScalarFunction(names = {"textContains", "text_contains"}, isPlaceholder = 
true)
-    public static boolean textContains(String text, String pattern) {
-      throw new UnsupportedOperationException("Placeholder scalar function, 
should not reach here");
+    @Override
+    public String getName() {
+      return _name;
     }
 
-    @ScalarFunction(names = {"textMatch", "text_match"}, isPlaceholder = true)
-    public static boolean textMatch(String text, String pattern) {
-      throw new UnsupportedOperationException("Placeholder scalar function, 
should not reach here");
+    @Override
+    public PinotSqlFunction toPinotSqlFunction() {
+      return new PinotSqlFunction(_name, getReturnTypeInference(), 
getOperandTypeChecker());
     }
 
-    @ScalarFunction(names = {"jsonMatch", "json_match"}, isPlaceholder = true)
-    public static boolean jsonMatch(String text, String pattern) {
-      throw new UnsupportedOperationException("Placeholder scalar function, 
should not reach here");
+    private SqlReturnTypeInference getReturnTypeInference() {
+      return opBinding -> {
+        int numArguments = opBinding.getOperandCount();
+        FunctionInfo functionInfo = getFunctionInfo(numArguments);
+        Preconditions.checkState(functionInfo != null, "Failed to find 
function: %s with %s arguments", _name,
+            numArguments);
+        Method method = functionInfo.getMethod();
+        Class<?> returnClass = method.getReturnType();
+        RelDataTypeFactory typeFactory = opBinding.getTypeFactory();
+        RelDataType returnType = returnClass == Object.class ? 
typeFactory.createSqlType(SqlTypeName.ANY)
+            : JavaTypeFactoryImpl.toSql(typeFactory, 
typeFactory.createJavaType(returnClass));
+
+        if (!functionInfo.hasNullableParameters()) {
+          // When any parameter is null, return is null
+          for (RelDataType type : opBinding.collectOperandTypes()) {
+            if (type.isNullable()) {
+              return typeFactory.createTypeWithNullability(returnType, true);
+            }
+          }
+        }
+
+        return method.isAnnotationPresent(Nullable.class) ? 
typeFactory.createTypeWithNullability(returnType, true)
+            : returnType;
+      };
+    }
+
+    private SqlOperandTypeChecker getOperandTypeChecker() {
+      if (_functionInfoMap.containsKey(VAR_ARG_KEY)) {
+        return OperandTypes.VARIADIC;
+      }
+      if (_functionInfoMap.size() == 1) {
+        return 
getOperandTypeChecker(_functionInfoMap.values().iterator().next().getMethod());
+      }
+      List<SqlOperandTypeChecker> operandTypeCheckers = new 
ArrayList<>(_functionInfoMap.size());
+      for (FunctionInfo functionInfo : _functionInfoMap.values()) {
+        
operandTypeCheckers.add(getOperandTypeChecker(functionInfo.getMethod()));
+      }
+      return OperandTypes.or(operandTypeCheckers.toArray(new 
SqlOperandTypeChecker[0]));
+    }
+
+    private static SqlTypeFamily getSqlTypeFamily(Class<?> clazz) {
+      // NOTE: Pinot allows some non-standard type conversions such as 
Timestamp <-> long, boolean <-> int etc. Do not
+      //       restrict the type family for now. We only restrict the type 
family for String so that cast can be added.
+      //       Explicit cast is required to correctly convert boolean and 
Timestamp to String.
+      // TODO: Revisit this.
+      return clazz == String.class ? SqlTypeFamily.CHARACTER : 
SqlTypeFamily.ANY;
+    }
+
+    private static SqlOperandTypeChecker getOperandTypeChecker(Method method) {
+      Class<?>[] parameterTypes = method.getParameterTypes();
+      int length = parameterTypes.length;
+      SqlTypeFamily[] typeFamilies = new SqlTypeFamily[length];
+      for (int i = 0; i < length; i++) {
+        typeFamilies[i] = getSqlTypeFamily(parameterTypes[i]);
+      }
+      return OperandTypes.family(typeFamilies);
     }
 
-    @ScalarFunction(names = {"vectorSimilarity", "vector_similarity"}, 
isPlaceholder = true)
-    public static boolean vectorSimilarity(float[] vector1, float[] vector2, 
int topk) {
-      throw new UnsupportedOperationException("Placeholder scalar function, 
should not reach here");
+    @Nullable
+    @Override
+    public FunctionInfo getFunctionInfo(int numArguments) {
+      FunctionInfo functionInfo = _functionInfoMap.get(numArguments);
+      return functionInfo != null ? functionInfo : 
_functionInfoMap.get(VAR_ARG_KEY);

Review Comment:
   This means that any scalar function that is annotated as vararg must support 
any number of arguments including 0 - i.e., there's no concept of a minimum 
number of args here?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: commits-unsubscr...@pinot.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@pinot.apache.org
For additional commands, e-mail: commits-h...@pinot.apache.org

Reply via email to