You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by xi...@apache.org on 2026/04/13 00:38:17 UTC

(pinot) branch master updated: Add polymorphic arithmetic scalar functions (#18171)

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

xiangfu 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 598b628da7f Add polymorphic arithmetic scalar functions (#18171)
598b628da7f is described below

commit 598b628da7fd5ccdb20b1929fe4225aee52a8bf2
Author: Xiang Fu <xi...@apache.org>
AuthorDate: Sun Apr 12 17:38:11 2026 -0700

    Add polymorphic arithmetic scalar functions (#18171)
---
 .../function/scalar/ArithmeticFunctions.java       |  49 ----
 .../scalar/arithmetic/AbsScalarFunction.java       |  94 +++++++
 .../scalar/arithmetic/ArithmeticFunctionUtils.java | 113 ++++++++
 ...ava => BaseBinaryArithmeticScalarFunction.java} |  31 ++-
 ...java => BaseUnaryArithmeticScalarFunction.java} |  31 +--
 .../scalar/arithmetic/GreatestScalarFunction.java  |  97 +++++++
 .../scalar/arithmetic/LeastScalarFunction.java     |  97 +++++++
 .../scalar/arithmetic/MinusScalarFunction.java     |   2 +-
 ...sScalarFunction.java => ModScalarFunction.java} |  53 +++-
 .../arithmetic/ModuloOrZeroScalarFunction.java     |  97 +++++++
 .../scalar/arithmetic/MultScalarFunction.java      |   2 +-
 .../scalar/arithmetic/NegateScalarFunction.java    |  92 +++++++
 .../scalar/arithmetic/PlusScalarFunction.java      |   2 +-
 .../arithmetic/PositiveModuloScalarFunction.java   | 114 ++++++++
 .../arithmetic/ArithmeticScalarFunctionTest.java   |  42 +++
 .../pinot/sql/parsers/CalciteSqlCompilerTest.java  |  15 ++
 .../pinot/core/function/FunctionRegistryTest.java  |  80 ++++++
 .../custom/ArithmeticFunctionsIntegrationTest.java | 292 +++++++++++++++++++++
 .../resources/udf-test-results/all-functions.yaml  |  18 +-
 .../apache/pinot/query/QueryCompilationTest.java   |   8 +
 .../pinot/query/runtime/function/AbsUdf.java       |  17 +-
 21 files changed, 1241 insertions(+), 105 deletions(-)

diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArithmeticFunctions.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArithmeticFunctions.java
index 70ff0dca5f6..2939aaa9acb 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArithmeticFunctions.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/ArithmeticFunctions.java
@@ -74,55 +74,6 @@ public class ArithmeticFunctions {
     return Double.isNaN(value) ? 1 : 0;
   }
 
-  @ScalarFunction
-  public static double mod(double a, double b) {
-    return a % b;
-  }
-
-  @ScalarFunction
-  public static double moduloOrZero(double a, double b) {
-    //Same as mod but returns zero when dividing by zero or when dividing a minimal negative number by minus one.
-    return (b == 0 || (a == Long.MIN_VALUE && b == -1)) ? 0 : mod(a, b);
-  }
-
-  @ScalarFunction
-  public static double positiveModulo(double a, double b) {
-    double result = a % b;
-    return result >= 0 ? result : result + Math.abs(b);
-  }
-
-  @ScalarFunction
-  public static double negate(double a) {
-    return -a;
-  }
-
-  @ScalarFunction
-  public static double least(double a, double b) {
-    return Double.min(a, b);
-  }
-
-  @ScalarFunction
-  public static double greatest(double a, double b) {
-    return Double.max(a, b);
-  }
-
-  @Deprecated
-  @ScalarFunction
-  public static double min(double a, double b) {
-    return least(a, b);
-  }
-
-  @Deprecated
-  @ScalarFunction
-  public static double max(double a, double b) {
-    return greatest(a, b);
-  }
-
-  @ScalarFunction
-  public static double abs(double a) {
-    return Math.abs(a);
-  }
-
   @ScalarFunction(names = {"ceil", "ceiling"})
   public static double ceil(double a) {
     return Math.ceil(a);
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/AbsScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/AbsScalarFunction.java
new file mode 100644
index 00000000000..196d31819f3
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/AbsScalarFunction.java
@@ -0,0 +1,94 @@
+/**
+ * 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.scalar.arithmetic;
+
+import java.math.BigDecimal;
+import java.util.EnumMap;
+import java.util.Map;
+import org.apache.pinot.common.function.FunctionInfo;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+
+
+/**
+ * Polymorphic absolute-value scalar function implementation.
+ *
+ * <p>Instances are immutable and thread-safe.
+ */
+@ScalarFunction
+public class AbsScalarFunction extends BaseUnaryArithmeticScalarFunction {
+
+  private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
+
+  static {
+    try {
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.INT,
+          new FunctionInfo(AbsScalarFunction.class.getMethod("intAbs", int.class), AbsScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.LONG,
+          new FunctionInfo(AbsScalarFunction.class.getMethod("longAbs", long.class), AbsScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.FLOAT,
+          new FunctionInfo(AbsScalarFunction.class.getMethod("floatAbs", float.class), AbsScalarFunction.class,
+              false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.DOUBLE,
+          new FunctionInfo(AbsScalarFunction.class.getMethod("doubleAbs", double.class), AbsScalarFunction.class,
+              false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.BIG_DECIMAL,
+          new FunctionInfo(AbsScalarFunction.class.getMethod("bigDecimalAbs", BigDecimal.class),
+              AbsScalarFunction.class, false));
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  protected FunctionInfo functionInfoForType(ColumnDataType argumentType) {
+    return TYPE_FUNCTION_INFO_MAP.get(argumentType);
+  }
+
+  @Override
+  public String getName() {
+    return "abs";
+  }
+
+  public static int intAbs(int value) {
+    if (value == Integer.MIN_VALUE) {
+      throw new ArithmeticException("integer overflow");
+    }
+    return Math.abs(value);
+  }
+
+  public static long longAbs(long value) {
+    if (value == Long.MIN_VALUE) {
+      throw new ArithmeticException("long overflow");
+    }
+    return Math.abs(value);
+  }
+
+  public static float floatAbs(float value) {
+    return Math.abs(value);
+  }
+
+  public static double doubleAbs(double value) {
+    return Math.abs(value);
+  }
+
+  public static BigDecimal bigDecimalAbs(BigDecimal value) {
+    return value.abs();
+  }
+}
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/ArithmeticFunctionUtils.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/ArithmeticFunctionUtils.java
new file mode 100644
index 00000000000..10c35f7fb7b
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/ArithmeticFunctionUtils.java
@@ -0,0 +1,113 @@
+/**
+ * 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.scalar.arithmetic;
+
+import java.util.List;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.SqlOperatorBinding;
+import org.apache.calcite.sql.type.OperandTypes;
+import org.apache.calcite.sql.type.SqlTypeFamily;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeUtil;
+import org.apache.pinot.common.function.sql.PinotSqlFunction;
+
+
+/**
+ * Shared Calcite registration utilities for arithmetic scalar functions.
+ *
+ * <p>This helper is stateless and thread-safe.
+ */
+final class ArithmeticFunctionUtils {
+  private ArithmeticFunctionUtils() {
+  }
+
+  static PinotSqlFunction unaryArithmeticSqlFunction(String name) {
+    return new PinotSqlFunction(name, ArithmeticFunctionUtils::inferUnaryReturnType,
+        OperandTypes.family(List.of(SqlTypeFamily.ANY)));
+  }
+
+  static PinotSqlFunction binaryArithmeticSqlFunction(String name) {
+    return new PinotSqlFunction(name, ArithmeticFunctionUtils::inferBinaryReturnType,
+        OperandTypes.family(List.of(SqlTypeFamily.ANY, SqlTypeFamily.ANY)));
+  }
+
+  private static RelDataType inferUnaryReturnType(SqlOperatorBinding opBinding) {
+    SqlTypeName returnType =
+        SqlTypeUtil.isNumeric(opBinding.getOperandType(0)) ? normalizeNumericType(opBinding.getOperandType(0))
+            : SqlTypeName.DOUBLE;
+    return createTypeWithOperandNullability(opBinding, returnType);
+  }
+
+  private static RelDataType inferBinaryReturnType(SqlOperatorBinding opBinding) {
+    RelDataType leftType = opBinding.getOperandType(0);
+    RelDataType rightType = opBinding.getOperandType(1);
+    SqlTypeName returnType;
+    if (SqlTypeUtil.isNumeric(leftType) && SqlTypeUtil.isNumeric(rightType)) {
+      SqlTypeName leftSqlType = normalizeNumericType(leftType);
+      SqlTypeName rightSqlType = normalizeNumericType(rightType);
+      if (leftSqlType == rightSqlType) {
+        returnType = leftSqlType;
+      } else if (isIntegral(leftSqlType) && isIntegral(rightSqlType)) {
+        returnType = SqlTypeName.BIGINT;
+      } else if (leftSqlType == SqlTypeName.DECIMAL || rightSqlType == SqlTypeName.DECIMAL) {
+        returnType = SqlTypeName.DECIMAL;
+      } else if (leftSqlType == SqlTypeName.DOUBLE || rightSqlType == SqlTypeName.DOUBLE) {
+        returnType = SqlTypeName.DOUBLE;
+      } else {
+        returnType = SqlTypeName.REAL;
+      }
+    } else {
+      // Preserve legacy coercions (for example, STRING inputs) by validating these functions as DOUBLE-returning.
+      returnType = SqlTypeName.DOUBLE;
+    }
+    return createTypeWithOperandNullability(opBinding, returnType);
+  }
+
+  private static SqlTypeName normalizeNumericType(RelDataType relDataType) {
+    switch (relDataType.getSqlTypeName()) {
+      case TINYINT:
+      case SMALLINT:
+      case INTEGER:
+        return SqlTypeName.INTEGER;
+      case BIGINT:
+        return SqlTypeName.BIGINT;
+      case REAL:
+      case FLOAT:
+        return SqlTypeName.REAL;
+      case DOUBLE:
+        return SqlTypeName.DOUBLE;
+      case DECIMAL:
+        return SqlTypeName.DECIMAL;
+      default:
+        return SqlTypeName.DOUBLE;
+    }
+  }
+
+  private static boolean isIntegral(SqlTypeName sqlTypeName) {
+    return sqlTypeName == SqlTypeName.INTEGER || sqlTypeName == SqlTypeName.BIGINT;
+  }
+
+  private static RelDataType createTypeWithOperandNullability(SqlOperatorBinding opBinding, SqlTypeName sqlTypeName) {
+    RelDataTypeFactory typeFactory = opBinding.getTypeFactory();
+    RelDataType type = typeFactory.createSqlType(sqlTypeName);
+    boolean nullable = opBinding.collectOperandTypes().stream().anyMatch(RelDataType::isNullable);
+    return typeFactory.createTypeWithNullability(type, nullable);
+  }
+}
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PolymorphicBinaryArithmeticScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/BaseBinaryArithmeticScalarFunction.java
similarity index 65%
copy from pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PolymorphicBinaryArithmeticScalarFunction.java
copy to pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/BaseBinaryArithmeticScalarFunction.java
index 10167161f92..3b1bb8447e8 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PolymorphicBinaryArithmeticScalarFunction.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/BaseBinaryArithmeticScalarFunction.java
@@ -25,9 +25,11 @@ import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
 
 
 /**
- * Base class for polymorphic binary arithmetic scalar functions
+ * Base class for binary arithmetic scalar functions.
+ *
+ * <p>Instances are immutable and thread-safe.
  */
-public abstract class PolymorphicBinaryArithmeticScalarFunction implements PinotScalarFunction {
+public abstract class BaseBinaryArithmeticScalarFunction implements PinotScalarFunction {
 
   @Nullable
   @Override
@@ -51,17 +53,32 @@ public abstract class PolymorphicBinaryArithmeticScalarFunction implements Pinot
   }
 
   private FunctionInfo functionInfoForTypes(ColumnDataType argumentType1, ColumnDataType argumentType2) {
-    if ((argumentType1 == ColumnDataType.LONG || argumentType1 == ColumnDataType.INT) && (
-        argumentType2 == ColumnDataType.LONG || argumentType2 == ColumnDataType.INT)) {
-      return functionInfoForType(ColumnDataType.LONG);
+    if (argumentType1.isNumber() && argumentType2.isNumber()) {
+      if (argumentType1 == argumentType2) {
+        return functionInfoForType(argumentType1);
+      }
+      if (argumentType1.isWholeNumber() && argumentType2.isWholeNumber()) {
+        return functionInfoForType(ColumnDataType.LONG);
+      }
+      if (argumentType1 == ColumnDataType.BIG_DECIMAL || argumentType2 == ColumnDataType.BIG_DECIMAL) {
+        return functionInfoForType(ColumnDataType.BIG_DECIMAL);
+      }
+      if (argumentType1.ordinal() > argumentType2.ordinal()) {
+        return functionInfoForType(argumentType1);
+      }
+      return functionInfoForType(argumentType2);
     }
 
-    // Fall back to double based comparison by default
-    return functionInfoForType(ColumnDataType.DOUBLE);
+    // Fall back to double based arithmetic by default so legacy coercions (e.g. STRING -> DOUBLE) still work.
+    return defaultFunctionInfo();
   }
 
   /**
    * Get the binary arithmetic scalar function's {@link FunctionInfo} for the given argument type.
    */
   protected abstract FunctionInfo functionInfoForType(ColumnDataType argumentType);
+
+  protected FunctionInfo defaultFunctionInfo() {
+    return functionInfoForType(ColumnDataType.DOUBLE);
+  }
 }
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PolymorphicBinaryArithmeticScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/BaseUnaryArithmeticScalarFunction.java
similarity index 63%
rename from pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PolymorphicBinaryArithmeticScalarFunction.java
rename to pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/BaseUnaryArithmeticScalarFunction.java
index 10167161f92..c0e67335a31 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PolymorphicBinaryArithmeticScalarFunction.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/BaseUnaryArithmeticScalarFunction.java
@@ -25,43 +25,40 @@ import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
 
 
 /**
- * Base class for polymorphic binary arithmetic scalar functions
+ * Base class for unary arithmetic scalar functions.
+ *
+ * <p>Instances are immutable and thread-safe.
  */
-public abstract class PolymorphicBinaryArithmeticScalarFunction implements PinotScalarFunction {
+public abstract class BaseUnaryArithmeticScalarFunction implements PinotScalarFunction {
 
   @Nullable
   @Override
   public FunctionInfo getFunctionInfo(ColumnDataType[] argumentTypes) {
-    if (argumentTypes.length != 2) {
+    if (argumentTypes.length != 1) {
       return null;
     }
 
-    return functionInfoForTypes(argumentTypes[0].getStoredType(), argumentTypes[1].getStoredType());
+    FunctionInfo functionInfo = functionInfoForType(argumentTypes[0].getStoredType());
+    return functionInfo != null ? functionInfo : defaultFunctionInfo();
   }
 
   @Nullable
   @Override
   public FunctionInfo getFunctionInfo(int numArguments) {
-    if (numArguments != 2) {
+    if (numArguments != 1) {
       return null;
     }
 
     // For backward compatibility
-    return functionInfoForType(ColumnDataType.DOUBLE);
-  }
-
-  private FunctionInfo functionInfoForTypes(ColumnDataType argumentType1, ColumnDataType argumentType2) {
-    if ((argumentType1 == ColumnDataType.LONG || argumentType1 == ColumnDataType.INT) && (
-        argumentType2 == ColumnDataType.LONG || argumentType2 == ColumnDataType.INT)) {
-      return functionInfoForType(ColumnDataType.LONG);
-    }
-
-    // Fall back to double based comparison by default
-    return functionInfoForType(ColumnDataType.DOUBLE);
+    return defaultFunctionInfo();
   }
 
   /**
-   * Get the binary arithmetic scalar function's {@link FunctionInfo} for the given argument type.
+   * Get the unary arithmetic scalar function's {@link FunctionInfo} for the given argument type.
    */
   protected abstract FunctionInfo functionInfoForType(ColumnDataType argumentType);
+
+  protected FunctionInfo defaultFunctionInfo() {
+    return functionInfoForType(ColumnDataType.DOUBLE);
+  }
 }
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/GreatestScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/GreatestScalarFunction.java
new file mode 100644
index 00000000000..58d76b25b3a
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/GreatestScalarFunction.java
@@ -0,0 +1,97 @@
+/**
+ * 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.scalar.arithmetic;
+
+import java.math.BigDecimal;
+import java.util.EnumMap;
+import java.util.Map;
+import org.apache.pinot.common.function.FunctionInfo;
+import org.apache.pinot.common.function.sql.PinotSqlFunction;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+
+
+/**
+ * Polymorphic greatest scalar function implementation.
+ *
+ * <p>Instances are immutable and thread-safe.
+ */
+@ScalarFunction
+public class GreatestScalarFunction extends BaseBinaryArithmeticScalarFunction {
+
+  private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
+
+  static {
+    try {
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.INT,
+          new FunctionInfo(GreatestScalarFunction.class.getMethod("intGreatest", int.class, int.class),
+              GreatestScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.LONG,
+          new FunctionInfo(GreatestScalarFunction.class.getMethod("longGreatest", long.class, long.class),
+              GreatestScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.FLOAT,
+          new FunctionInfo(GreatestScalarFunction.class.getMethod("floatGreatest", float.class, float.class),
+              GreatestScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.DOUBLE,
+          new FunctionInfo(GreatestScalarFunction.class.getMethod("doubleGreatest", double.class, double.class),
+              GreatestScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.BIG_DECIMAL, new FunctionInfo(
+          GreatestScalarFunction.class.getMethod("bigDecimalGreatest", BigDecimal.class, BigDecimal.class),
+          GreatestScalarFunction.class, false));
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  protected FunctionInfo functionInfoForType(ColumnDataType argumentType) {
+    FunctionInfo functionInfo = TYPE_FUNCTION_INFO_MAP.get(argumentType);
+    return functionInfo != null ? functionInfo : defaultFunctionInfo();
+  }
+
+  @Override
+  public String getName() {
+    return "greatest";
+  }
+
+  @Override
+  public PinotSqlFunction toPinotSqlFunction() {
+    return ArithmeticFunctionUtils.binaryArithmeticSqlFunction(getName());
+  }
+
+  public static int intGreatest(int a, int b) {
+    return Math.max(a, b);
+  }
+
+  public static long longGreatest(long a, long b) {
+    return Math.max(a, b);
+  }
+
+  public static float floatGreatest(float a, float b) {
+    return Math.max(a, b);
+  }
+
+  public static double doubleGreatest(double a, double b) {
+    return Double.max(a, b);
+  }
+
+  public static BigDecimal bigDecimalGreatest(BigDecimal a, BigDecimal b) {
+    return a.compareTo(b) >= 0 ? a : b;
+  }
+}
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/LeastScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/LeastScalarFunction.java
new file mode 100644
index 00000000000..db0705f88be
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/LeastScalarFunction.java
@@ -0,0 +1,97 @@
+/**
+ * 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.scalar.arithmetic;
+
+import java.math.BigDecimal;
+import java.util.EnumMap;
+import java.util.Map;
+import org.apache.pinot.common.function.FunctionInfo;
+import org.apache.pinot.common.function.sql.PinotSqlFunction;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+
+
+/**
+ * Polymorphic least scalar function implementation.
+ *
+ * <p>Instances are immutable and thread-safe.
+ */
+@ScalarFunction
+public class LeastScalarFunction extends BaseBinaryArithmeticScalarFunction {
+
+  private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
+
+  static {
+    try {
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.INT,
+          new FunctionInfo(LeastScalarFunction.class.getMethod("intLeast", int.class, int.class),
+              LeastScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.LONG,
+          new FunctionInfo(LeastScalarFunction.class.getMethod("longLeast", long.class, long.class),
+              LeastScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.FLOAT,
+          new FunctionInfo(LeastScalarFunction.class.getMethod("floatLeast", float.class, float.class),
+              LeastScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.DOUBLE,
+          new FunctionInfo(LeastScalarFunction.class.getMethod("doubleLeast", double.class, double.class),
+              LeastScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.BIG_DECIMAL,
+          new FunctionInfo(LeastScalarFunction.class.getMethod("bigDecimalLeast", BigDecimal.class, BigDecimal.class),
+              LeastScalarFunction.class, false));
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  protected FunctionInfo functionInfoForType(ColumnDataType argumentType) {
+    FunctionInfo functionInfo = TYPE_FUNCTION_INFO_MAP.get(argumentType);
+    return functionInfo != null ? functionInfo : defaultFunctionInfo();
+  }
+
+  @Override
+  public String getName() {
+    return "least";
+  }
+
+  @Override
+  public PinotSqlFunction toPinotSqlFunction() {
+    return ArithmeticFunctionUtils.binaryArithmeticSqlFunction(getName());
+  }
+
+  public static int intLeast(int a, int b) {
+    return Math.min(a, b);
+  }
+
+  public static long longLeast(long a, long b) {
+    return Math.min(a, b);
+  }
+
+  public static float floatLeast(float a, float b) {
+    return Math.min(a, b);
+  }
+
+  public static double doubleLeast(double a, double b) {
+    return Double.min(a, b);
+  }
+
+  public static BigDecimal bigDecimalLeast(BigDecimal a, BigDecimal b) {
+    return a.compareTo(b) <= 0 ? a : b;
+  }
+}
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MinusScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MinusScalarFunction.java
index 61488e58e7c..6772a665d5b 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MinusScalarFunction.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MinusScalarFunction.java
@@ -26,7 +26,7 @@ import org.apache.pinot.spi.annotations.ScalarFunction;
 
 
 @ScalarFunction(names = {"sub", "minus"})
-public class MinusScalarFunction extends PolymorphicBinaryArithmeticScalarFunction {
+public class MinusScalarFunction extends BaseBinaryArithmeticScalarFunction {
 
   private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
 
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MinusScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/ModScalarFunction.java
similarity index 50%
copy from pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MinusScalarFunction.java
copy to pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/ModScalarFunction.java
index 61488e58e7c..54a9634a395 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MinusScalarFunction.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/ModScalarFunction.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.common.function.scalar.arithmetic;
 
+import java.math.BigDecimal;
 import java.util.EnumMap;
 import java.util.Map;
 import org.apache.pinot.common.function.FunctionInfo;
@@ -25,19 +26,33 @@ import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
 import org.apache.pinot.spi.annotations.ScalarFunction;
 
 
-@ScalarFunction(names = {"sub", "minus"})
-public class MinusScalarFunction extends PolymorphicBinaryArithmeticScalarFunction {
+/**
+ * Polymorphic modulo scalar function implementation.
+ *
+ * <p>Instances are immutable and thread-safe.
+ */
+@ScalarFunction
+public class ModScalarFunction extends BaseBinaryArithmeticScalarFunction {
 
   private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
 
   static {
     try {
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.INT,
+          new FunctionInfo(ModScalarFunction.class.getMethod("intMod", int.class, int.class), ModScalarFunction.class,
+              false));
       TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.LONG,
-          new FunctionInfo(MinusScalarFunction.class.getMethod("longMinus", long.class, long.class),
-              MinusScalarFunction.class, false));
+          new FunctionInfo(ModScalarFunction.class.getMethod("longMod", long.class, long.class),
+              ModScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.FLOAT,
+          new FunctionInfo(ModScalarFunction.class.getMethod("floatMod", float.class, float.class),
+              ModScalarFunction.class, false));
       TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.DOUBLE,
-          new FunctionInfo(MinusScalarFunction.class.getMethod("doubleMinus", double.class, double.class),
-              MinusScalarFunction.class, false));
+          new FunctionInfo(ModScalarFunction.class.getMethod("doubleMod", double.class, double.class),
+              ModScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.BIG_DECIMAL,
+          new FunctionInfo(ModScalarFunction.class.getMethod("bigDecimalMod", BigDecimal.class, BigDecimal.class),
+              ModScalarFunction.class, false));
     } catch (NoSuchMethodException e) {
       throw new RuntimeException(e);
     }
@@ -46,21 +61,31 @@ public class MinusScalarFunction extends PolymorphicBinaryArithmeticScalarFuncti
   @Override
   protected FunctionInfo functionInfoForType(ColumnDataType argumentType) {
     FunctionInfo functionInfo = TYPE_FUNCTION_INFO_MAP.get(argumentType);
-
-    // Fall back to double based comparison by default
-    return functionInfo != null ? functionInfo : TYPE_FUNCTION_INFO_MAP.get(ColumnDataType.DOUBLE);
+    return functionInfo != null ? functionInfo : defaultFunctionInfo();
   }
 
   @Override
   public String getName() {
-    return "minus";
+    return "mod";
+  }
+
+  public static int intMod(int a, int b) {
+    return a % b;
+  }
+
+  public static long longMod(long a, long b) {
+    return a % b;
+  }
+
+  public static float floatMod(float a, float b) {
+    return a % b;
   }
 
-  public static long longMinus(long a, long b) {
-    return a - b;
+  public static double doubleMod(double a, double b) {
+    return a % b;
   }
 
-  public static double doubleMinus(double a, double b) {
-    return a - b;
+  public static BigDecimal bigDecimalMod(BigDecimal a, BigDecimal b) {
+    return a.remainder(b);
   }
 }
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/ModuloOrZeroScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/ModuloOrZeroScalarFunction.java
new file mode 100644
index 00000000000..13a818b1714
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/ModuloOrZeroScalarFunction.java
@@ -0,0 +1,97 @@
+/**
+ * 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.scalar.arithmetic;
+
+import java.math.BigDecimal;
+import java.util.EnumMap;
+import java.util.Map;
+import org.apache.pinot.common.function.FunctionInfo;
+import org.apache.pinot.common.function.sql.PinotSqlFunction;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+
+
+/**
+ * Polymorphic modulo-or-zero scalar function implementation.
+ *
+ * <p>Instances are immutable and thread-safe.
+ */
+@ScalarFunction
+public class ModuloOrZeroScalarFunction extends BaseBinaryArithmeticScalarFunction {
+
+  private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
+
+  static {
+    try {
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.INT, new FunctionInfo(
+          ModuloOrZeroScalarFunction.class.getMethod("intModuloOrZero", int.class, int.class),
+          ModuloOrZeroScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.LONG, new FunctionInfo(
+          ModuloOrZeroScalarFunction.class.getMethod("longModuloOrZero", long.class, long.class),
+          ModuloOrZeroScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.FLOAT, new FunctionInfo(
+          ModuloOrZeroScalarFunction.class.getMethod("floatModuloOrZero", float.class, float.class),
+          ModuloOrZeroScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.DOUBLE, new FunctionInfo(
+          ModuloOrZeroScalarFunction.class.getMethod("doubleModuloOrZero", double.class, double.class),
+          ModuloOrZeroScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.BIG_DECIMAL, new FunctionInfo(
+          ModuloOrZeroScalarFunction.class.getMethod("bigDecimalModuloOrZero", BigDecimal.class, BigDecimal.class),
+          ModuloOrZeroScalarFunction.class, false));
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  protected FunctionInfo functionInfoForType(ColumnDataType argumentType) {
+    FunctionInfo functionInfo = TYPE_FUNCTION_INFO_MAP.get(argumentType);
+    return functionInfo != null ? functionInfo : defaultFunctionInfo();
+  }
+
+  @Override
+  public String getName() {
+    return "moduloOrZero";
+  }
+
+  @Override
+  public PinotSqlFunction toPinotSqlFunction() {
+    return ArithmeticFunctionUtils.binaryArithmeticSqlFunction(getName());
+  }
+
+  public static int intModuloOrZero(int a, int b) {
+    return b == 0 ? 0 : a % b;
+  }
+
+  public static long longModuloOrZero(long a, long b) {
+    return b == 0 ? 0L : a % b;
+  }
+
+  public static float floatModuloOrZero(float a, float b) {
+    return b == 0 ? 0F : a % b;
+  }
+
+  public static double doubleModuloOrZero(double a, double b) {
+    return b == 0 ? 0D : a % b;
+  }
+
+  public static BigDecimal bigDecimalModuloOrZero(BigDecimal a, BigDecimal b) {
+    return b.signum() == 0 ? BigDecimal.ZERO : a.remainder(b);
+  }
+}
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MultScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MultScalarFunction.java
index a7370453939..bf0dacfe809 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MultScalarFunction.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/MultScalarFunction.java
@@ -26,7 +26,7 @@ import org.apache.pinot.spi.annotations.ScalarFunction;
 
 
 @ScalarFunction(names = {"mult", "times"})
-public class MultScalarFunction extends PolymorphicBinaryArithmeticScalarFunction {
+public class MultScalarFunction extends BaseBinaryArithmeticScalarFunction {
 
   private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
 
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/NegateScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/NegateScalarFunction.java
new file mode 100644
index 00000000000..8dca5edbb99
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/NegateScalarFunction.java
@@ -0,0 +1,92 @@
+/**
+ * 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.scalar.arithmetic;
+
+import java.math.BigDecimal;
+import java.util.EnumMap;
+import java.util.Map;
+import org.apache.pinot.common.function.FunctionInfo;
+import org.apache.pinot.common.function.sql.PinotSqlFunction;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+
+
+/**
+ * Polymorphic negation scalar function implementation.
+ *
+ * <p>Instances are immutable and thread-safe.
+ */
+@ScalarFunction
+public class NegateScalarFunction extends BaseUnaryArithmeticScalarFunction {
+
+  private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
+
+  static {
+    try {
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.INT, new FunctionInfo(
+          NegateScalarFunction.class.getMethod("intNegate", int.class), NegateScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.LONG, new FunctionInfo(
+          NegateScalarFunction.class.getMethod("longNegate", long.class), NegateScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.FLOAT, new FunctionInfo(
+          NegateScalarFunction.class.getMethod("floatNegate", float.class), NegateScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.DOUBLE, new FunctionInfo(
+          NegateScalarFunction.class.getMethod("doubleNegate", double.class), NegateScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.BIG_DECIMAL, new FunctionInfo(
+          NegateScalarFunction.class.getMethod("bigDecimalNegate", BigDecimal.class), NegateScalarFunction.class,
+          false));
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  protected FunctionInfo functionInfoForType(ColumnDataType argumentType) {
+    return TYPE_FUNCTION_INFO_MAP.get(argumentType);
+  }
+
+  @Override
+  public String getName() {
+    return "negate";
+  }
+
+  @Override
+  public PinotSqlFunction toPinotSqlFunction() {
+    return ArithmeticFunctionUtils.unaryArithmeticSqlFunction(getName());
+  }
+
+  public static int intNegate(int value) {
+    return Math.negateExact(value);
+  }
+
+  public static long longNegate(long value) {
+    return Math.negateExact(value);
+  }
+
+  public static float floatNegate(float value) {
+    return -value;
+  }
+
+  public static double doubleNegate(double value) {
+    return -value;
+  }
+
+  public static BigDecimal bigDecimalNegate(BigDecimal value) {
+    return value.negate();
+  }
+}
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PlusScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PlusScalarFunction.java
index 5951afa5271..ab16bf590c3 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PlusScalarFunction.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PlusScalarFunction.java
@@ -26,7 +26,7 @@ import org.apache.pinot.spi.annotations.ScalarFunction;
 
 
 @ScalarFunction(names = {"add", "plus"})
-public class PlusScalarFunction extends PolymorphicBinaryArithmeticScalarFunction {
+public class PlusScalarFunction extends BaseBinaryArithmeticScalarFunction {
 
   private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
 
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PositiveModuloScalarFunction.java b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PositiveModuloScalarFunction.java
new file mode 100644
index 00000000000..d8363470e35
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/function/scalar/arithmetic/PositiveModuloScalarFunction.java
@@ -0,0 +1,114 @@
+/**
+ * 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.scalar.arithmetic;
+
+import java.math.BigDecimal;
+import java.util.EnumMap;
+import java.util.Map;
+import org.apache.pinot.common.function.FunctionInfo;
+import org.apache.pinot.common.function.sql.PinotSqlFunction;
+import org.apache.pinot.common.utils.DataSchema.ColumnDataType;
+import org.apache.pinot.spi.annotations.ScalarFunction;
+
+
+/**
+ * Polymorphic positive-modulo scalar function implementation.
+ *
+ * <p>Instances are immutable and thread-safe.
+ */
+@ScalarFunction
+public class PositiveModuloScalarFunction extends BaseBinaryArithmeticScalarFunction {
+
+  private static final Map<ColumnDataType, FunctionInfo> TYPE_FUNCTION_INFO_MAP = new EnumMap<>(ColumnDataType.class);
+
+  static {
+    try {
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.INT, new FunctionInfo(
+          PositiveModuloScalarFunction.class.getMethod("intPositiveModulo", int.class, int.class),
+          PositiveModuloScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.LONG, new FunctionInfo(
+          PositiveModuloScalarFunction.class.getMethod("longPositiveModulo", long.class, long.class),
+          PositiveModuloScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.FLOAT, new FunctionInfo(
+          PositiveModuloScalarFunction.class.getMethod("floatPositiveModulo", float.class, float.class),
+          PositiveModuloScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.DOUBLE, new FunctionInfo(
+          PositiveModuloScalarFunction.class.getMethod("doublePositiveModulo", double.class, double.class),
+          PositiveModuloScalarFunction.class, false));
+      TYPE_FUNCTION_INFO_MAP.put(ColumnDataType.BIG_DECIMAL, new FunctionInfo(
+          PositiveModuloScalarFunction.class.getMethod("bigDecimalPositiveModulo", BigDecimal.class, BigDecimal.class),
+          PositiveModuloScalarFunction.class, false));
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  protected FunctionInfo functionInfoForType(ColumnDataType argumentType) {
+    FunctionInfo functionInfo = TYPE_FUNCTION_INFO_MAP.get(argumentType);
+    return functionInfo != null ? functionInfo : defaultFunctionInfo();
+  }
+
+  @Override
+  public String getName() {
+    return "positiveModulo";
+  }
+
+  @Override
+  public PinotSqlFunction toPinotSqlFunction() {
+    return ArithmeticFunctionUtils.binaryArithmeticSqlFunction(getName());
+  }
+
+  public static int intPositiveModulo(int a, int b) {
+    int result = a % b;
+    if (result >= 0) {
+      return result;
+    }
+    if (b == Integer.MIN_VALUE) {
+      return Integer.MAX_VALUE + result + 1;
+    }
+    return result + Math.abs(b);
+  }
+
+  public static long longPositiveModulo(long a, long b) {
+    long result = a % b;
+    if (result >= 0) {
+      return result;
+    }
+    if (b == Long.MIN_VALUE) {
+      return Long.MAX_VALUE + result + 1;
+    }
+    return result + Math.abs(b);
+  }
+
+  public static float floatPositiveModulo(float a, float b) {
+    float result = a % b;
+    return result >= 0 ? result : result + Math.abs(b);
+  }
+
+  public static double doublePositiveModulo(double a, double b) {
+    double result = a % b;
+    return result >= 0 ? result : result + Math.abs(b);
+  }
+
+  public static BigDecimal bigDecimalPositiveModulo(BigDecimal a, BigDecimal b) {
+    BigDecimal result = a.remainder(b);
+    return result.signum() >= 0 ? result : result.add(b.abs());
+  }
+}
diff --git a/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/arithmetic/ArithmeticScalarFunctionTest.java b/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/arithmetic/ArithmeticScalarFunctionTest.java
new file mode 100644
index 00000000000..6f9fedef585
--- /dev/null
+++ b/pinot-common/src/test/java/org/apache/pinot/common/function/scalar/arithmetic/ArithmeticScalarFunctionTest.java
@@ -0,0 +1,42 @@
+/**
+ * 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.scalar.arithmetic;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertThrows;
+
+
+/**
+ * Tests for arithmetic scalar function edge cases.
+ */
+public class ArithmeticScalarFunctionTest {
+
+  @Test
+  public void testAbsIntegralOverflowFailsFast() {
+    assertThrows(ArithmeticException.class, () -> AbsScalarFunction.intAbs(Integer.MIN_VALUE));
+    assertThrows(ArithmeticException.class, () -> AbsScalarFunction.longAbs(Long.MIN_VALUE));
+  }
+
+  @Test
+  public void testNegateIntegralOverflowFailsFast() {
+    assertThrows(ArithmeticException.class, () -> NegateScalarFunction.intNegate(Integer.MIN_VALUE));
+    assertThrows(ArithmeticException.class, () -> NegateScalarFunction.longNegate(Long.MIN_VALUE));
+  }
+}
diff --git a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
index f3a2bf5d6bb..46bb7b09408 100644
--- a/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
+++ b/pinot-common/src/test/java/org/apache/pinot/sql/parsers/CalciteSqlCompilerTest.java
@@ -1086,6 +1086,21 @@ public class CalciteSqlCompilerTest {
         pinotQuery.getFilterExpression().getFunctionCall().getOperands().get(1).getLiteral().getIntValue(), 1394323200);
   }
 
+  @Test
+  public void testPolymorphicArithmeticScalarFunctionsCompile() {
+    PinotQuery pinotQuery = compileToPinotQuery("SELECT least(col1, col2), greatest(col3, col4), negate(col5), "
+        + "positiveModulo(col6, col7), moduloOrZero(col8, col9) FROM myTable WHERE least(col1, col2) = 1 "
+        + "AND greatest(col3, col4) = 2 AND negate(col5) = -1 AND positiveModulo(col6, col7) = 0 "
+        + "AND moduloOrZero(col8, col9) = 0");
+
+    List<Expression> selectList = pinotQuery.getSelectList();
+    Assert.assertEquals(selectList.get(0).getFunctionCall().getOperator(), "least");
+    Assert.assertEquals(selectList.get(1).getFunctionCall().getOperator(), "greatest");
+    Assert.assertEquals(selectList.get(2).getFunctionCall().getOperator(), "negate");
+    Assert.assertEquals(selectList.get(3).getFunctionCall().getOperator(), "positivemodulo");
+    Assert.assertEquals(selectList.get(4).getFunctionCall().getOperator(), "moduloorzero");
+  }
+
   @Test
   public void testSelectionTransformFunction() {
     PinotQuery pinotQuery =
diff --git a/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java b/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
index 5071d9dca05..52be2353a5a 100644
--- a/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
+++ b/pinot-core/src/test/java/org/apache/pinot/core/function/FunctionRegistryTest.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.core.function;
 
+import java.math.BigDecimal;
 import java.util.EnumSet;
 import org.apache.pinot.common.function.FunctionInfo;
 import org.apache.pinot.common.function.FunctionRegistry;
@@ -144,4 +145,83 @@ public class FunctionRegistryTest {
     assertNotNull(bitextractByArity);
     assertEquals(bitextractByArity.getMethod().getName(), "longBitExtract");
   }
+
+  @Test
+  public void testArithmeticFunctionPolymorphism() {
+    FunctionInfo intAbs = FunctionRegistry.lookupFunctionInfo("abs", new ColumnDataType[]{ColumnDataType.INT});
+    assertNotNull(intAbs);
+    assertEquals(intAbs.getMethod().getName(), "intAbs");
+    assertEquals(intAbs.getMethod().getReturnType(), int.class);
+
+    FunctionInfo longAbs = FunctionRegistry.lookupFunctionInfo("abs", new ColumnDataType[]{ColumnDataType.LONG});
+    assertNotNull(longAbs);
+    assertEquals(longAbs.getMethod().getName(), "longAbs");
+    assertEquals(longAbs.getMethod().getReturnType(), long.class);
+
+    FunctionInfo floatAbs = FunctionRegistry.lookupFunctionInfo("abs", new ColumnDataType[]{ColumnDataType.FLOAT});
+    assertNotNull(floatAbs);
+    assertEquals(floatAbs.getMethod().getName(), "floatAbs");
+    assertEquals(floatAbs.getMethod().getReturnType(), float.class);
+
+    FunctionInfo bigDecimalAbs =
+        FunctionRegistry.lookupFunctionInfo("abs", new ColumnDataType[]{ColumnDataType.BIG_DECIMAL});
+    assertNotNull(bigDecimalAbs);
+    assertEquals(bigDecimalAbs.getMethod().getName(), "bigDecimalAbs");
+    assertEquals(bigDecimalAbs.getMethod().getReturnType(), BigDecimal.class);
+
+    FunctionInfo stringAbs = FunctionRegistry.lookupFunctionInfo("abs", new ColumnDataType[]{ColumnDataType.STRING});
+    assertNotNull(stringAbs);
+    assertEquals(stringAbs.getMethod().getName(), "doubleAbs");
+    assertEquals(stringAbs.getMethod().getReturnType(), double.class);
+
+    FunctionInfo absByArity = FunctionRegistry.lookupFunctionInfo("abs", 1);
+    assertNotNull(absByArity);
+    assertEquals(absByArity.getMethod().getName(), "doubleAbs");
+
+    FunctionInfo intMod =
+        FunctionRegistry.lookupFunctionInfo("mod", new ColumnDataType[]{ColumnDataType.INT, ColumnDataType.INT});
+    assertNotNull(intMod);
+    assertEquals(intMod.getMethod().getName(), "intMod");
+    assertEquals(intMod.getMethod().getReturnType(), int.class);
+
+    FunctionInfo longMod =
+        FunctionRegistry.lookupFunctionInfo("mod", new ColumnDataType[]{ColumnDataType.LONG, ColumnDataType.INT});
+    assertNotNull(longMod);
+    assertEquals(longMod.getMethod().getName(), "longMod");
+    assertEquals(longMod.getMethod().getReturnType(), long.class);
+
+    FunctionInfo floatMod =
+        FunctionRegistry.lookupFunctionInfo("mod", new ColumnDataType[]{ColumnDataType.FLOAT, ColumnDataType.INT});
+    assertNotNull(floatMod);
+    assertEquals(floatMod.getMethod().getName(), "floatMod");
+    assertEquals(floatMod.getMethod().getReturnType(), float.class);
+
+    FunctionInfo bigDecimalMod = FunctionRegistry.lookupFunctionInfo("mod",
+        new ColumnDataType[]{ColumnDataType.BIG_DECIMAL, ColumnDataType.INT});
+    assertNotNull(bigDecimalMod);
+    assertEquals(bigDecimalMod.getMethod().getName(), "bigDecimalMod");
+    assertEquals(bigDecimalMod.getMethod().getReturnType(), BigDecimal.class);
+
+    FunctionInfo stringMod =
+        FunctionRegistry.lookupFunctionInfo("mod", new ColumnDataType[]{ColumnDataType.STRING, ColumnDataType.INT});
+    assertNotNull(stringMod);
+    assertEquals(stringMod.getMethod().getName(), "doubleMod");
+    assertEquals(stringMod.getMethod().getReturnType(), double.class);
+
+    FunctionInfo modByArity = FunctionRegistry.lookupFunctionInfo("mod", 2);
+    assertNotNull(modByArity);
+    assertEquals(modByArity.getMethod().getName(), "doubleMod");
+
+    FunctionInfo negateFloat =
+        FunctionRegistry.lookupFunctionInfo("negate", new ColumnDataType[]{ColumnDataType.FLOAT});
+    assertNotNull(negateFloat);
+    assertEquals(negateFloat.getMethod().getName(), "floatNegate");
+    assertEquals(negateFloat.getMethod().getReturnType(), float.class);
+
+    FunctionInfo leastBigDecimal = FunctionRegistry.lookupFunctionInfo("least",
+        new ColumnDataType[]{ColumnDataType.BIG_DECIMAL, ColumnDataType.DOUBLE});
+    assertNotNull(leastBigDecimal);
+    assertEquals(leastBigDecimal.getMethod().getName(), "bigDecimalLeast");
+    assertEquals(leastBigDecimal.getMethod().getReturnType(), BigDecimal.class);
+  }
 }
diff --git a/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/ArithmeticFunctionsIntegrationTest.java b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/ArithmeticFunctionsIntegrationTest.java
new file mode 100644
index 00000000000..08808abbaad
--- /dev/null
+++ b/pinot-integration-tests/src/test/java/org/apache/pinot/integration/tests/custom/ArithmeticFunctionsIntegrationTest.java
@@ -0,0 +1,292 @@
+/**
+ * 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.integration.tests.custom;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.File;
+import java.math.BigDecimal;
+import java.util.List;
+import org.apache.avro.file.DataFileWriter;
+import org.apache.avro.generic.GenericData;
+import org.apache.pinot.spi.data.FieldSpec;
+import org.apache.pinot.spi.data.Schema;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+
+/**
+ * End-to-end integration tests for polymorphic arithmetic scalar functions over a custom Pinot cluster.
+ *
+ * <p>This test validates broker/server query execution and result typing for arithmetic scalar functions that now
+ * support multiple numeric types, including BIG_DECIMAL. Test methods mutate query-engine mode and are not
+ * thread-safe.
+ */
+@Test(suiteName = "CustomClusterIntegrationTest")
+public class ArithmeticFunctionsIntegrationTest extends CustomDataQueryClusterIntegrationTest {
+  private static final String DEFAULT_TABLE_NAME = "ArithmeticFunctionsIntegrationTest";
+  private static final String ID_COLUMN = "id";
+  private static final String INT_VALUE_COLUMN = "intValue";
+  private static final String INT_DIVISOR_COLUMN = "intDivisor";
+  private static final String NEGATIVE_INT_DIVISOR_COLUMN = "negativeIntDivisor";
+  private static final String ZERO_INT_DIVISOR_COLUMN = "zeroIntDivisor";
+  private static final String LONG_VALUE_COLUMN = "longValue";
+  private static final String FLOAT_VALUE_COLUMN = "floatValue";
+  private static final String FLOAT_DIVISOR_COLUMN = "floatDivisor";
+  private static final String DOUBLE_VALUE_COLUMN = "doubleValue";
+  private static final String DOUBLE_DIVISOR_COLUMN = "doubleDivisor";
+  private static final String BIG_DECIMAL_VALUE_COLUMN = "bigDecimalValue";
+  private static final String BIG_DECIMAL_DIVISOR_COLUMN = "bigDecimalDivisor";
+  private static final String NEGATIVE_BIG_DECIMAL_DIVISOR_COLUMN = "negativeBigDecimalDivisor";
+  private static final String ZERO_BIG_DECIMAL_DIVISOR_COLUMN = "zeroBigDecimalDivisor";
+
+  private static final List<RowData> ROWS = List.of(
+      new RowData(0, -9, 5, -5, 0, -9L, -9.5F, 5F, -9.5D, -5D, new BigDecimal("-9.0"), new BigDecimal("5.0"),
+          new BigDecimal("-5.0"), BigDecimal.ZERO),
+      new RowData(1, 9, 5, -5, 0, 9L, 9.5F, 5F, 9.5D, 5D, new BigDecimal("9.0"), new BigDecimal("5.0"),
+          new BigDecimal("-5.0"), BigDecimal.ZERO)
+  );
+
+  @Override
+  public String getTableName() {
+    return DEFAULT_TABLE_NAME;
+  }
+
+  @Override
+  protected long getCountStarResult() {
+    return ROWS.size();
+  }
+
+  @Override
+  public Schema createSchema() {
+    return new Schema.SchemaBuilder().setSchemaName(getTableName())
+        .addSingleValueDimension(ID_COLUMN, FieldSpec.DataType.INT)
+        .addMetric(INT_VALUE_COLUMN, FieldSpec.DataType.INT)
+        .addMetric(INT_DIVISOR_COLUMN, FieldSpec.DataType.INT)
+        .addMetric(NEGATIVE_INT_DIVISOR_COLUMN, FieldSpec.DataType.INT)
+        .addMetric(ZERO_INT_DIVISOR_COLUMN, FieldSpec.DataType.INT)
+        .addMetric(LONG_VALUE_COLUMN, FieldSpec.DataType.LONG)
+        .addMetric(FLOAT_VALUE_COLUMN, FieldSpec.DataType.FLOAT)
+        .addMetric(FLOAT_DIVISOR_COLUMN, FieldSpec.DataType.FLOAT)
+        .addMetric(DOUBLE_VALUE_COLUMN, FieldSpec.DataType.DOUBLE)
+        .addMetric(DOUBLE_DIVISOR_COLUMN, FieldSpec.DataType.DOUBLE)
+        .addMetric(BIG_DECIMAL_VALUE_COLUMN, FieldSpec.DataType.BIG_DECIMAL)
+        .addMetric(BIG_DECIMAL_DIVISOR_COLUMN, FieldSpec.DataType.BIG_DECIMAL)
+        .addMetric(NEGATIVE_BIG_DECIMAL_DIVISOR_COLUMN, FieldSpec.DataType.BIG_DECIMAL)
+        .addMetric(ZERO_BIG_DECIMAL_DIVISOR_COLUMN, FieldSpec.DataType.BIG_DECIMAL)
+        .build();
+  }
+
+  @Override
+  public List<File> createAvroFiles()
+      throws Exception {
+    org.apache.avro.Schema avroSchema = org.apache.avro.Schema.createRecord("arithmeticRecord", null, null, false);
+    avroSchema.setFields(List.of(
+        new org.apache.avro.Schema.Field(ID_COLUMN, org.apache.avro.Schema.create(org.apache.avro.Schema.Type.INT),
+            null, null),
+        new org.apache.avro.Schema.Field(INT_VALUE_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.INT), null, null),
+        new org.apache.avro.Schema.Field(INT_DIVISOR_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.INT), null, null),
+        new org.apache.avro.Schema.Field(NEGATIVE_INT_DIVISOR_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.INT), null, null),
+        new org.apache.avro.Schema.Field(ZERO_INT_DIVISOR_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.INT), null, null),
+        new org.apache.avro.Schema.Field(LONG_VALUE_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.LONG), null, null),
+        // Use DOUBLE in Avro for FLOAT Pinot columns to avoid Avro float precision loss.
+        new org.apache.avro.Schema.Field(FLOAT_VALUE_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.DOUBLE), null, null),
+        new org.apache.avro.Schema.Field(FLOAT_DIVISOR_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.DOUBLE), null, null),
+        new org.apache.avro.Schema.Field(DOUBLE_VALUE_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.DOUBLE), null, null),
+        new org.apache.avro.Schema.Field(DOUBLE_DIVISOR_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.DOUBLE), null, null),
+        new org.apache.avro.Schema.Field(BIG_DECIMAL_VALUE_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.STRING), null, null),
+        new org.apache.avro.Schema.Field(BIG_DECIMAL_DIVISOR_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.STRING), null, null),
+        new org.apache.avro.Schema.Field(NEGATIVE_BIG_DECIMAL_DIVISOR_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.STRING), null, null),
+        new org.apache.avro.Schema.Field(ZERO_BIG_DECIMAL_DIVISOR_COLUMN,
+            org.apache.avro.Schema.create(org.apache.avro.Schema.Type.STRING), null, null)
+    ));
+
+    try (AvroFilesAndWriters avroFilesAndWriters = createAvroFilesAndWriters(avroSchema)) {
+      List<DataFileWriter<GenericData.Record>> writers = avroFilesAndWriters.getWriters();
+      for (int i = 0; i < ROWS.size(); i++) {
+        RowData rowData = ROWS.get(i);
+        GenericData.Record record = new GenericData.Record(avroSchema);
+        record.put(ID_COLUMN, rowData._id);
+        record.put(INT_VALUE_COLUMN, rowData._intValue);
+        record.put(INT_DIVISOR_COLUMN, rowData._intDivisor);
+        record.put(NEGATIVE_INT_DIVISOR_COLUMN, rowData._negativeIntDivisor);
+        record.put(ZERO_INT_DIVISOR_COLUMN, rowData._zeroIntDivisor);
+        record.put(LONG_VALUE_COLUMN, rowData._longValue);
+        record.put(FLOAT_VALUE_COLUMN, (double) rowData._floatValue);
+        record.put(FLOAT_DIVISOR_COLUMN, (double) rowData._floatDivisor);
+        record.put(DOUBLE_VALUE_COLUMN, rowData._doubleValue);
+        record.put(DOUBLE_DIVISOR_COLUMN, rowData._doubleDivisor);
+        record.put(BIG_DECIMAL_VALUE_COLUMN, rowData._bigDecimalValue.toPlainString());
+        record.put(BIG_DECIMAL_DIVISOR_COLUMN, rowData._bigDecimalDivisor.toPlainString());
+        record.put(NEGATIVE_BIG_DECIMAL_DIVISOR_COLUMN, rowData._negativeBigDecimalDivisor.toPlainString());
+        record.put(ZERO_BIG_DECIMAL_DIVISOR_COLUMN, rowData._zeroBigDecimalDivisor.toPlainString());
+        writers.get(i % getNumAvroFiles()).append(record);
+      }
+      return avroFilesAndWriters.getAvroFiles();
+    }
+  }
+
+  @Test(dataProvider = "useBothQueryEngines")
+  public void testUnaryArithmeticFunctions(boolean useMultiStageQueryEngine)
+      throws Exception {
+    setUseMultiStageQueryEngine(useMultiStageQueryEngine);
+    String query = String.format("SELECT COUNT(*) FROM %s WHERE %s = 0 "
+            + "AND abs(%s) = 9 "
+            + "AND abs(%s) = 9 "
+            + "AND abs(%s) = 9.5 "
+            + "AND abs(%s) = 9.5 "
+            + "AND abs(%s) = 9.0 "
+            + "AND negate(%s) = 9 "
+            + "AND negate(%s) = 9 "
+            + "AND negate(%s) = 9.5 "
+            + "AND negate(%s) = 9.5 "
+            + "AND negate(%s) = 9.0",
+        getTableName(), ID_COLUMN,
+        INT_VALUE_COLUMN,
+        LONG_VALUE_COLUMN,
+        FLOAT_VALUE_COLUMN,
+        DOUBLE_VALUE_COLUMN,
+        BIG_DECIMAL_VALUE_COLUMN,
+        INT_VALUE_COLUMN,
+        LONG_VALUE_COLUMN,
+        FLOAT_VALUE_COLUMN,
+        DOUBLE_VALUE_COLUMN,
+        BIG_DECIMAL_VALUE_COLUMN);
+    JsonNode response = postQuery(query);
+    assertCount(response, 1L);
+  }
+
+  @Test(dataProvider = "useBothQueryEngines")
+  public void testModuloAndPositiveModuloFunctions(boolean useMultiStageQueryEngine)
+      throws Exception {
+    setUseMultiStageQueryEngine(useMultiStageQueryEngine);
+    String query = String.format("SELECT COUNT(*) FROM %s WHERE %s = 0 "
+            + "AND mod(%s, %s) = -4 "
+            + "AND mod(%s, %s) = -4 "
+            + "AND mod(%s, %s) = -4.5 "
+            + "AND mod(%s, %s) = -4.5 "
+            + "AND mod(%s, %s) = -4.0 "
+            + "AND positiveModulo(%s, %s) = 1 "
+            + "AND positiveModulo(%s, %s) = 1 "
+            + "AND positiveModulo(%s, %s) = 0.5 "
+            + "AND positiveModulo(%s, %s) = 1.0",
+        getTableName(), ID_COLUMN,
+        INT_VALUE_COLUMN, INT_DIVISOR_COLUMN,
+        LONG_VALUE_COLUMN, INT_DIVISOR_COLUMN,
+        FLOAT_VALUE_COLUMN, FLOAT_DIVISOR_COLUMN,
+        DOUBLE_VALUE_COLUMN, DOUBLE_DIVISOR_COLUMN,
+        BIG_DECIMAL_VALUE_COLUMN, BIG_DECIMAL_DIVISOR_COLUMN,
+        INT_VALUE_COLUMN, INT_DIVISOR_COLUMN,
+        INT_VALUE_COLUMN, NEGATIVE_INT_DIVISOR_COLUMN,
+        FLOAT_VALUE_COLUMN, FLOAT_DIVISOR_COLUMN,
+        BIG_DECIMAL_VALUE_COLUMN, NEGATIVE_BIG_DECIMAL_DIVISOR_COLUMN);
+    JsonNode response = postQuery(query);
+    assertCount(response, 1L);
+  }
+
+  @Test(dataProvider = "useBothQueryEngines")
+  public void testModuloOrZeroFunctions(boolean useMultiStageQueryEngine)
+      throws Exception {
+    setUseMultiStageQueryEngine(useMultiStageQueryEngine);
+    String query = String.format("SELECT COUNT(*) FROM %s WHERE %s = 0 "
+            + "AND moduloOrZero(%s, %s) = 0 "
+            + "AND moduloOrZero(%s, %s) = 0",
+        getTableName(), ID_COLUMN,
+        INT_VALUE_COLUMN, ZERO_INT_DIVISOR_COLUMN,
+        BIG_DECIMAL_VALUE_COLUMN, ZERO_BIG_DECIMAL_DIVISOR_COLUMN);
+    JsonNode response = postQuery(query);
+    assertCount(response, 1L);
+  }
+
+  @Test(dataProvider = "useBothQueryEngines")
+  public void testLeastGreatestFunctions(boolean useMultiStageQueryEngine)
+      throws Exception {
+    setUseMultiStageQueryEngine(useMultiStageQueryEngine);
+    String query = String.format("SELECT COUNT(*) FROM %s WHERE %s = 0 "
+            + "AND least(%s, %s) = %s "
+            + "AND greatest(%s, %s) = %s "
+            + "AND least(%s, %s) = %s "
+            + "AND greatest(%s, %s) = %s "
+            + "AND least(%s, %s) = %s "
+            + "AND greatest(%s, %s) = %s",
+        getTableName(), ID_COLUMN,
+        LONG_VALUE_COLUMN, INT_DIVISOR_COLUMN, LONG_VALUE_COLUMN,
+        FLOAT_VALUE_COLUMN, INT_DIVISOR_COLUMN, INT_DIVISOR_COLUMN,
+        BIG_DECIMAL_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, DOUBLE_VALUE_COLUMN,
+        BIG_DECIMAL_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BIG_DECIMAL_VALUE_COLUMN,
+        BIG_DECIMAL_VALUE_COLUMN, BIG_DECIMAL_DIVISOR_COLUMN, BIG_DECIMAL_VALUE_COLUMN,
+        BIG_DECIMAL_VALUE_COLUMN, BIG_DECIMAL_DIVISOR_COLUMN, BIG_DECIMAL_DIVISOR_COLUMN);
+    JsonNode response = postQuery(query);
+    assertCount(response, 1L);
+  }
+
+  private void assertCount(JsonNode response, long expectedCount) {
+    assertEquals(response.path("exceptions").size(), 0, response.toPrettyString());
+    assertEquals(getType(response, 0), "LONG");
+    assertEquals(getLongCellValue(response, 0, 0), expectedCount);
+  }
+
+  private static final class RowData {
+    private final int _id;
+    private final int _intValue;
+    private final int _intDivisor;
+    private final int _negativeIntDivisor;
+    private final int _zeroIntDivisor;
+    private final long _longValue;
+    private final float _floatValue;
+    private final float _floatDivisor;
+    private final double _doubleValue;
+    private final double _doubleDivisor;
+    private final BigDecimal _bigDecimalValue;
+    private final BigDecimal _bigDecimalDivisor;
+    private final BigDecimal _negativeBigDecimalDivisor;
+    private final BigDecimal _zeroBigDecimalDivisor;
+
+    private RowData(int id, int intValue, int intDivisor, int negativeIntDivisor, int zeroIntDivisor, long longValue,
+        float floatValue, float floatDivisor, double doubleValue, double doubleDivisor, BigDecimal bigDecimalValue,
+        BigDecimal bigDecimalDivisor, BigDecimal negativeBigDecimalDivisor, BigDecimal zeroBigDecimalDivisor) {
+      _id = id;
+      _intValue = intValue;
+      _intDivisor = intDivisor;
+      _negativeIntDivisor = negativeIntDivisor;
+      _zeroIntDivisor = zeroIntDivisor;
+      _longValue = longValue;
+      _floatValue = floatValue;
+      _floatDivisor = floatDivisor;
+      _doubleValue = doubleValue;
+      _doubleDivisor = doubleDivisor;
+      _bigDecimalValue = bigDecimalValue;
+      _bigDecimalDivisor = bigDecimalDivisor;
+      _negativeBigDecimalDivisor = negativeBigDecimalDivisor;
+      _zeroBigDecimalDivisor = zeroBigDecimalDivisor;
+    }
+  }
+}
diff --git a/pinot-integration-tests/src/test/resources/udf-test-results/all-functions.yaml b/pinot-integration-tests/src/test/resources/udf-test-results/all-functions.yaml
index 0d8b950ef74..27d9e7226d1 100644
--- a/pinot-integration-tests/src/test/resources/udf-test-results/all-functions.yaml
+++ b/pinot-integration-tests/src/test/resources/udf-test-results/all-functions.yaml
@@ -27,7 +27,7 @@
 
 ---
 abs:
-  scalar: "ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.ArithmeticFunctions.abs}"
+  scalar: "org.apache.pinot.common.function.scalar.arithmetic.AbsScalarFunction"
   transform: "org.apache.pinot.core.operator.transform.function.SingleParamMathTransformFunction.AbsTransformFunction"
   udf: "org.apache.pinot.query.runtime.function.AbsUdf"
 acos:
@@ -808,7 +808,7 @@ greaterthanorequal:
   transform: "org.apache.pinot.core.operator.transform.function.GreaterThanOrEqualTransformFunction"
   udf: null
 greatest:
-  scalar: "ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.ArithmeticFunctions.greatest}"
+  scalar: "org.apache.pinot.common.function.scalar.arithmetic.GreatestScalarFunction"
   transform: "org.apache.pinot.core.operator.transform.function.GreatestTransformFunction"
   udf: null
 griddisk:
@@ -1042,7 +1042,7 @@ lcm:
   transform: null
   udf: null
 least:
-  scalar: "ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.ArithmeticFunctions.least}"
+  scalar: "org.apache.pinot.common.function.scalar.arithmetic.LeastScalarFunction"
   transform: "org.apache.pinot.core.operator.transform.function.LeastTransformFunction"
   udf: null
 left:
@@ -1114,7 +1114,7 @@ mapvalue:
   transform: "org.apache.pinot.core.operator.transform.function.MapValueTransformFunction"
   udf: null
 max:
-  scalar: "ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.ArithmeticFunctions.max}"
+  scalar: null
   transform: null
   udf: null
 md2:
@@ -1136,7 +1136,7 @@ millisecondmv:
   transform: null
   udf: null
 min:
-  scalar: "ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.ArithmeticFunctions.min}"
+  scalar: null
   transform: null
   udf: null
 minus:
@@ -1154,11 +1154,11 @@ minutemv:
   transform: null
   udf: null
 mod:
-  scalar: "ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.ArithmeticFunctions.mod}"
+  scalar: "org.apache.pinot.common.function.scalar.arithmetic.ModScalarFunction"
   transform: "org.apache.pinot.core.operator.transform.function.ModuloTransformFunction"
   udf: null
 moduloorzero:
-  scalar: "ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.ArithmeticFunctions.moduloOrZero}"
+  scalar: "org.apache.pinot.common.function.scalar.arithmetic.ModuloOrZeroScalarFunction"
   transform: null
   udf: null
 month:
@@ -1223,7 +1223,7 @@ murmurhash3x64bit64:
   transform: null
   udf: null
 negate:
-  scalar: "ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.ArithmeticFunctions.negate}"
+  scalar: "org.apache.pinot.common.function.scalar.arithmetic.NegateScalarFunction"
   transform: null
   udf: null
 normalize:
@@ -1260,7 +1260,7 @@ plus:
   transform: "org.apache.pinot.core.operator.transform.function.AdditionTransformFunction"
   udf: "org.apache.pinot.query.runtime.function.PlusUdf"
 positivemodulo:
-  scalar: "ArgumentCountBasedScalarFunction{org.apache.pinot.common.function.scalar.ArithmeticFunctions.positiveModulo}"
+  scalar: "org.apache.pinot.common.function.scalar.arithmetic.PositiveModuloScalarFunction"
   transform: null
   udf: null
 pow:
diff --git a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
index 38aa9ec9556..842bbf97c15 100644
--- a/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
+++ b/pinot-query-planner/src/test/java/org/apache/pinot/query/QueryCompilationTest.java
@@ -65,6 +65,14 @@ public class QueryCompilationTest extends QueryEnvironmentTestBase {
     assertNotNull(dispatchableSubPlan);
   }
 
+  @Test
+  public void testPolymorphicArithmeticScalarFunctionsPlanQuery() {
+    DispatchableSubPlan dispatchableSubPlan = _queryEnvironment.planQuery(
+        "SELECT abs(col3), negate(col7), least(col4, col4), greatest(col4, col4), positiveModulo(col3, col6), "
+            + "moduloOrZero(col4, col4) FROM a");
+    assertNotNull(dispatchableSubPlan);
+  }
+
   @Test(dataProvider = "testQueryExceptionDataProvider")
   public void testQueryWithException(String query, String exceptionSnippet) {
     try {
diff --git a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/function/AbsUdf.java b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/function/AbsUdf.java
index ddf09a68c8d..acfe32fb8de 100644
--- a/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/function/AbsUdf.java
+++ b/pinot-query-runtime/src/main/java/org/apache/pinot/query/runtime/function/AbsUdf.java
@@ -22,8 +22,9 @@ import com.google.auto.service.AutoService;
 import java.util.Map;
 import java.util.Set;
 import org.apache.commons.lang3.tuple.Pair;
+import org.apache.pinot.common.function.PinotScalarFunction;
 import org.apache.pinot.common.function.TransformFunctionType;
-import org.apache.pinot.common.function.scalar.ArithmeticFunctions;
+import org.apache.pinot.common.function.scalar.arithmetic.AbsScalarFunction;
 import org.apache.pinot.core.operator.transform.function.SingleParamMathTransformFunction;
 import org.apache.pinot.core.operator.transform.function.TransformFunction;
 import org.apache.pinot.core.udf.Udf;
@@ -33,11 +34,10 @@ import org.apache.pinot.core.udf.UdfSignature;
 
 
 @AutoService(Udf.class)
-public class AbsUdf extends Udf.FromAnnotatedMethod {
-
-  public AbsUdf()
-      throws NoSuchMethodException {
-    super(ArithmeticFunctions.class.getMethod("abs", double.class));
+public class AbsUdf extends Udf {
+  @Override
+  public String getMainName() {
+    return "abs";
   }
 
   @Override
@@ -60,4 +60,9 @@ public class AbsUdf extends Udf.FromAnnotatedMethod {
   public Pair<TransformFunctionType, Class<? extends TransformFunction>> getTransformFunction() {
     return Pair.of(TransformFunctionType.ABS, SingleParamMathTransformFunction.AbsTransformFunction.class);
   }
+
+  @Override
+  public PinotScalarFunction getScalarFunction() {
+    return new AbsScalarFunction();
+  }
 }


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