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

mbudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new dad90736b0 [CALCITE-5634] Enable GREATEST, LEAST functions in 
PostgreSQL library
dad90736b0 is described below

commit dad90736b0dbf30eae0d8f215112e22f64c90235
Author: Norman Jordan <[email protected]>
AuthorDate: Fri Jun 21 15:35:33 2024 -0700

    [CALCITE-5634] Enable GREATEST, LEAST functions in PostgreSQL library
    
    * Add new GREATEST and LEAST functions for PostgreSQL
    * The new functions only return null if all arguments are null
---
 babel/src/test/resources/sql/postgresql.iq         | 15 ++++++
 .../org/apache/calcite/sql/SqlBasicFunction.java   | 18 +++++++
 .../main/java/org/apache/calcite/sql/SqlKind.java  |  6 +++
 .../calcite/sql/fun/SqlLibraryOperators.java       | 18 ++++++-
 .../calcite/sql2rel/StandardConvertletTable.java   | 56 ++++++++++++++++++++++
 site/_docs/reference.md                            |  4 +-
 .../org/apache/calcite/test/SqlOperatorTest.java   | 50 +++++++++++++++++--
 7 files changed, 159 insertions(+), 8 deletions(-)

diff --git a/babel/src/test/resources/sql/postgresql.iq 
b/babel/src/test/resources/sql/postgresql.iq
index 30baac65b7..492c6a4d51 100644
--- a/babel/src/test/resources/sql/postgresql.iq
+++ b/babel/src/test/resources/sql/postgresql.iq
@@ -506,4 +506,19 @@ X
 true
 !ok
 
+SELECT greatest(1, 2, 3) AS x;
+X
+3
+!ok
+
+SELECT greatest(1, null, 3) AS x;
+X
+3
+!ok
+
+SELECT least(1, 2, 3) AS x;
+X
+1
+!ok
+
 # End postgresql.iq
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlBasicFunction.java 
b/core/src/main/java/org/apache/calcite/sql/SqlBasicFunction.java
index 44ca45744d..05c1f80c0b 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlBasicFunction.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlBasicFunction.java
@@ -89,6 +89,24 @@ public class SqlBasicFunction extends SqlFunction {
     this.dynamic = dynamic;
   }
 
+  /**
+   * Creates a {@code SqlBasicFunction}.
+   *
+   * @param name function name
+   * @param kind function kind
+   * @param returnTypeInference Strategy to use for return type inference
+   * @param operandTypeChecker Strategy to use for parameter type checking
+   * @return a {@code SqlBasicFunction}
+   */
+  public static SqlBasicFunction create(String name, SqlKind kind,
+      SqlReturnTypeInference returnTypeInference,
+      SqlOperandTypeChecker operandTypeChecker) {
+    return new SqlBasicFunction(name, kind,
+        SqlSyntax.FUNCTION, true, returnTypeInference, null,
+        OperandHandlers.DEFAULT, operandTypeChecker, 0,
+        SqlFunctionCategory.SYSTEM, call -> SqlMonotonicity.NOT_MONOTONIC, 
false);
+  }
+
   /** Creates a {@code SqlBasicFunction} whose name is the same as its kind
    * and whose category {@link SqlFunctionCategory#SYSTEM}. */
   public static SqlBasicFunction create(SqlKind kind,
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlKind.java 
b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
index 29089a6ebe..9f64e13c5a 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
@@ -430,6 +430,9 @@ public enum SqlKind {
   /** {@code GREATEST} function (Oracle, Spark). */
   GREATEST,
 
+  /** {@code GREATEST} function (PostgreSQL). */
+  GREATEST_PG,
+
   /** The two-argument {@code CONCAT} function (Oracle). */
   CONCAT2,
 
@@ -449,6 +452,9 @@ public enum SqlKind {
   /** {@code LEAST} function (Oracle). */
   LEAST,
 
+  /** {@code LEAST} function (PostgreSQL). */
+  LEAST_PG,
+
   /** {@code LOG} function. (Mysql, Spark). */
   LOG,
 
diff --git 
a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
index 671282383f..642b74690a 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlLibraryOperators.java
@@ -447,19 +447,33 @@ public abstract class SqlLibraryOperators {
           SqlFunctionCategory.STRING);
 
   /** The "GREATEST(value, value)" function. */
-  @LibraryOperator(libraries = {BIG_QUERY, ORACLE, REDSHIFT, SPARK})
+  @LibraryOperator(libraries = {BIG_QUERY, ORACLE, SPARK})
   public static final SqlFunction GREATEST =
       SqlBasicFunction.create(SqlKind.GREATEST,
           ReturnTypes.LEAST_RESTRICTIVE.andThen(SqlTypeTransforms.TO_NULLABLE),
           OperandTypes.SAME_VARIADIC);
 
+  /** The "GREATEST(value, value)" function. */
+  @LibraryOperator(libraries = {POSTGRESQL})
+  public static final SqlFunction GREATEST_PG =
+      SqlBasicFunction.create("GREATEST", SqlKind.GREATEST_PG,
+          ReturnTypes.LEAST_RESTRICTIVE.andThen(SqlTypeTransforms.TO_NULLABLE),
+          OperandTypes.SAME_VARIADIC);
+
   /** The "LEAST(value, value)" function. */
-  @LibraryOperator(libraries = {BIG_QUERY, ORACLE, REDSHIFT, SPARK})
+  @LibraryOperator(libraries = {BIG_QUERY, ORACLE, SPARK})
   public static final SqlFunction LEAST =
       SqlBasicFunction.create(SqlKind.LEAST,
           ReturnTypes.LEAST_RESTRICTIVE.andThen(SqlTypeTransforms.TO_NULLABLE),
           OperandTypes.SAME_VARIADIC);
 
+  /** The "GREATEST(value, value)" function. */
+  @LibraryOperator(libraries = {POSTGRESQL})
+  public static final SqlFunction LEAST_PG =
+      SqlBasicFunction.create("LEAST", SqlKind.LEAST_PG,
+          ReturnTypes.LEAST_RESTRICTIVE.andThen(SqlTypeTransforms.TO_NULLABLE),
+          OperandTypes.SAME_VARIADIC);
+
   /** The "CEIL(value)" function. Identical to the standard <code>CEIL</code> 
function
    * except the return type should be a double if the operand is an integer. */
   @LibraryOperator(libraries = {BIG_QUERY})
diff --git 
a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java 
b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
index e04f2e7f3c..68f412dd61 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/StandardConvertletTable.java
@@ -193,7 +193,9 @@ public class StandardConvertletTable extends 
ReflectiveConvertletTable {
         new TrimConvertlet(SqlTrimFunction.Flag.TRAILING));
 
     registerOp(SqlLibraryOperators.GREATEST, new GreatestConvertlet());
+    registerOp(SqlLibraryOperators.GREATEST_PG, new GreatestPgConvertlet());
     registerOp(SqlLibraryOperators.LEAST, new GreatestConvertlet());
+    registerOp(SqlLibraryOperators.LEAST_PG, new GreatestPgConvertlet());
     registerOp(SqlLibraryOperators.SUBSTR_BIG_QUERY,
         new SubstrConvertlet(SqlLibrary.BIG_QUERY));
     registerOp(SqlLibraryOperators.SUBSTR_MYSQL,
@@ -1900,6 +1902,60 @@ public class StandardConvertletTable extends 
ReflectiveConvertletTable {
     }
   }
 
+  /** Convertlet that converts {@code GREATEST} and {@code LEAST}. */
+  private static class GreatestPgConvertlet implements SqlRexConvertlet {
+    @Override public RexNode convertCall(SqlRexContext cx, SqlCall call) {
+      // Translate
+      //   GREATEST(a, b, c, d)
+      // to
+      //   CASE
+      //   WHEN a IS NOT NULL AND (b IS NULL OR a > b) AND (c IS NULL OR a > 
c) AND
+      //        (d IS NULL OR a > d)
+      //   THEN a
+      //   WHEN b IS NOT NULL AND (c IS NULL OR b > c) AND (d IS NULL OR b > d)
+      //   THEN b
+      //   WHEN C IS NOT NULL AND (d IS NULL OR c > d)
+      //   THEN c
+      //   WHEN d IS NOT NULL
+      //   THEN d
+      //   ELSE NULL
+      //   END
+      final RexBuilder rexBuilder = cx.getRexBuilder();
+      final RelDataType type =
+          cx.getValidator().getValidatedNodeType(call);
+      final SqlBinaryOperator op;
+      switch (call.getKind()) {
+      case GREATEST_PG:
+        op = SqlStdOperatorTable.GREATER_THAN;
+        break;
+      case LEAST_PG:
+        op = SqlStdOperatorTable.LESS_THAN;
+        break;
+      default:
+        throw new AssertionError();
+      }
+      final List<RexNode> exprs =
+          convertOperands(cx, call, SqlOperandTypeChecker.Consistency.NONE);
+      final List<RexNode> list = new ArrayList<>();
+      for (int i = 0; i < exprs.size(); i++) {
+        RexNode expr = exprs.get(i);
+        final List<RexNode> andList = new ArrayList<>();
+        andList.add(rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL, 
expr));
+        for (int j = i + 1; j < exprs.size(); j++) {
+          final RexNode expr2 = exprs.get(j);
+          final List<RexNode> orList = new ArrayList<>();
+          orList.add(rexBuilder.makeCall(SqlStdOperatorTable.IS_NULL, expr2));
+          orList.add(rexBuilder.makeCall(op, expr, expr2));
+          andList.add(RexUtil.composeDisjunction(rexBuilder, orList));
+        }
+        list.add(RexUtil.composeConjunction(rexBuilder, andList));
+        list.add(expr);
+      }
+      list.add(rexBuilder.makeNullLiteral(type));
+      return rexBuilder.makeCall(type, SqlStdOperatorTable.CASE, list);
+    }
+  }
+
   /** Convertlet that handles {@code FLOOR} and {@code CEIL} functions. */
   private class FloorCeilConvertlet implements SqlRexConvertlet {
     @Override public RexNode convertCall(SqlRexContext cx, SqlCall call) {
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 2a3090ad26..dc80e67ed9 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2782,7 +2782,7 @@ In the following:
 | b | FORMAT_TIME(string, time)                      | Formats *time* 
according to the specified format *string*
 | b | FORMAT_TIMESTAMP(string timestamp)             | Formats *timestamp* 
according to the specified format *string*
 | s | GETBIT(value, position)                        | Equivalent to 
`BIT_GET(value, position)`
-| b o r s | GREATEST(expr [, expr ]*)                | Returns the greatest of 
the expressions
+| b o p r s | GREATEST(expr [, expr ]*)              | Returns the greatest of 
the expressions
 | b h s | IF(condition, value1, value2)              | Returns *value1* if 
*condition* is TRUE, *value2* otherwise
 | b s | IFNULL(value1, value2)                       | Equivalent to 
`NVL(value1, value2)`
 | p | string1 ILIKE string2 [ ESCAPE string3 ]       | Whether *string1* 
matches pattern *string2*, ignoring case (similar to `LIKE`)
@@ -2801,7 +2801,7 @@ In the following:
 | m | JSON_REPLACE(jsonValue, path, val [, path, val ]*)  | Returns a JSON 
document replace a data of *jsonValue*, *path*, *val*
 | m | JSON_SET(jsonValue, path, val [, path, val ]*) | Returns a JSON document 
set a data of *jsonValue*, *path*, *val*
 | m | JSON_STORAGE_SIZE(jsonValue)                   | Returns the number of 
bytes used to store the binary representation of *jsonValue*
-| b o r s | LEAST(expr [, expr ]* )                  | Returns the least of 
the expressions
+| b o p r s | LEAST(expr [, expr ]* )                | Returns the least of 
the expressions
 | b m p r s | LEFT(string, length)                   | Returns the leftmost 
*length* characters from the *string*
 | f s | LEN(string)                                  | Equivalent to 
`CHAR_LENGTH(string)`
 | b f s | LENGTH(string)                             | Equivalent to 
`CHAR_LENGTH(string)`
diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java 
b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
index 0cad83d55a..1c0b2cae2b 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -10789,8 +10789,29 @@ public class SqlOperatorTest {
           "VARCHAR(5) NOT NULL");
     };
     final List<SqlLibrary> libraries =
-        list(SqlLibrary.BIG_QUERY, SqlLibrary.ORACLE, SqlLibrary.REDSHIFT,
-            SqlLibrary.SPARK);
+        list(SqlLibrary.BIG_QUERY, SqlLibrary.ORACLE, SqlLibrary.SPARK);
+    f0.forEachLibrary(libraries, consumer);
+  }
+
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-5634";>
+   * [CALCITE-5634] Enable GREATEST, LEAST functions in PostgreSQL 
library</a>. */
+  @Test void testGreatestPgFunc() {
+    final SqlOperatorFixture f0 =
+        fixture().setFor(SqlLibraryOperators.GREATEST_PG, VmName.EXPAND);
+    f0.checkFails("^greatest('on', 'earth')^",
+        "No match found for function signature GREATEST\\(<CHARACTER>, 
<CHARACTER>\\)",
+        false);
+    final Consumer<SqlOperatorFixture> consumer = f -> {
+      f.checkString("greatest('on', 'earth')", "on   ", "CHAR(5) NOT NULL");
+      f.checkString("greatest('show', 'on', 'earth')", "show ",
+          "CHAR(5) NOT NULL");
+      f.checkScalar("greatest(12, CAST(NULL AS INTEGER), 3)", "12",
+          "INTEGER");
+      f.checkScalar("greatest(false, true)", true, "BOOLEAN NOT NULL");
+      f.checkScalar("greatest(CAST(NULL AS INTEGER), CAST(NULL AS INTEGER))", 
isNullValue(),
+          "INTEGER");
+    };
+    final List<SqlLibrary> libraries = list(SqlLibrary.POSTGRESQL, 
SqlLibrary.REDSHIFT);
     f0.forEachLibrary(libraries, consumer);
   }
 
@@ -10814,8 +10835,29 @@ public class SqlOperatorTest {
           "VARCHAR(5) NOT NULL");
     };
     final List<SqlLibrary> libraries =
-        list(SqlLibrary.BIG_QUERY, SqlLibrary.ORACLE, SqlLibrary.REDSHIFT,
-            SqlLibrary.SPARK);
+        list(SqlLibrary.BIG_QUERY, SqlLibrary.ORACLE, SqlLibrary.SPARK);
+    f0.forEachLibrary(libraries, consumer);
+  }
+
+  /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-5634";>
+   * [CALCITE-5634] Enable GREATEST, LEAST functions in PostgreSQL 
library</a>. */
+  @Test void testLeastPgFunc() {
+    final SqlOperatorFixture f0 = fixture()
+        .setFor(SqlLibraryOperators.LEAST_PG, VmName.EXPAND);
+    f0.checkFails("^least('on', 'earth')^",
+        "No match found for function signature LEAST\\(<CHARACTER>, 
<CHARACTER>\\)",
+        false);
+    final Consumer<SqlOperatorFixture> consumer = f -> {
+      f.checkString("least('on', 'earth')", "earth", "CHAR(5) NOT NULL");
+      f.checkString("least('show', 'on', 'earth')", "earth",
+          "CHAR(5) NOT NULL");
+      f.checkScalar("least(12, CAST(NULL AS INTEGER), 3)", "3",
+          "INTEGER");
+      f.checkScalar("least(false, true)", false, "BOOLEAN NOT NULL");
+      f.checkScalar("least(CAST(NULL AS INTEGER), CAST(NULL AS INTEGER))", 
isNullValue(),
+          "INTEGER");
+    };
+    final List<SqlLibrary> libraries = list(SqlLibrary.POSTGRESQL, 
SqlLibrary.REDSHIFT);
     f0.forEachLibrary(libraries, consumer);
   }
 

Reply via email to