From 0bacbc48bf7549812050f0577693ace3f36d60c8 Mon Sep 17 00:00:00 2001 From: Norman Jordan Date: Fri, 21 Jun 2024 15:35:33 -0700 Subject: [PATCH] [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 +++++ .../apache/calcite/sql/SqlBasicFunction.java | 18 ++++++ .../java/org/apache/calcite/sql/SqlKind.java | 6 ++ .../calcite/sql/fun/SqlLibraryOperators.java | 18 +++++- .../sql2rel/StandardConvertletTable.java | 56 +++++++++++++++++++ site/_docs/reference.md | 4 +- .../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 30baac65b70b..492c6a4d51a0 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 44ca45744dca..05c1f80c0b6b 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 @@ protected SqlBasicFunction(String name, SqlKind kind, SqlSyntax syntax, 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 b5eaec86c95c..2d010c2091c2 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, @@ -445,6 +448,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 773569e4160d..08e2161ad248 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 @@ -446,19 +446,33 @@ static RelDataType deriveTypeSplit(SqlOperatorBinding operatorBinding, 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 CEIL 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 e04f2e7f3cc0..68f412dd6179 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 @@ private StandardConvertletTable() { 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 @@ private static class GreatestConvertlet implements SqlRexConvertlet { } } + /** 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 exprs = + convertOperands(cx, call, SqlOperandTypeChecker.Consistency.NONE); + final List list = new ArrayList<>(); + for (int i = 0; i < exprs.size(); i++) { + RexNode expr = exprs.get(i); + final List 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 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 1a78470c5b81..961d1f796d34 100644 --- a/site/_docs/reference.md +++ b/site/_docs/reference.md @@ -2781,7 +2781,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`) @@ -2800,7 +2800,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 1554083d215c..e391eb31407a 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -10747,8 +10747,29 @@ void assertSubFunReturns(boolean binary, String s, int start, "VARCHAR(5) NOT NULL"); }; final List 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 + * [CALCITE-5634] Enable GREATEST, LEAST functions in PostgreSQL library. */ + @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\\(, \\)", + false); + final Consumer 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 libraries = list(SqlLibrary.POSTGRESQL, SqlLibrary.REDSHIFT); f0.forEachLibrary(libraries, consumer); } @@ -10772,8 +10793,29 @@ void assertSubFunReturns(boolean binary, String s, int start, "VARCHAR(5) NOT NULL"); }; final List 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 + * [CALCITE-5634] Enable GREATEST, LEAST functions in PostgreSQL library. */ + @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\\(, \\)", + false); + final Consumer 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 libraries = list(SqlLibrary.POSTGRESQL, SqlLibrary.REDSHIFT); f0.forEachLibrary(libraries, consumer); }