Skip to content

Commit

Permalink
[CALCITE-5634] Enable GREATEST, LEAST functions in PostgreSQL library
Browse files Browse the repository at this point in the history
* Add new GREATEST and LEAST functions for PostgreSQL
* The new functions only return null if all arguments are null
  • Loading branch information
normanj-bitquill committed Jul 8, 2024
1 parent 8ab0b03 commit 0bacbc4
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 8 deletions.
15 changes: 15 additions & 0 deletions babel/src/test/resources/sql/postgresql.iq
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions core/src/main/java/org/apache/calcite/sql/SqlBasicFunction.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions core/src/main/java/org/apache/calcite/sql/SqlKind.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand All @@ -445,6 +448,9 @@ public enum SqlKind {
/** {@code LEAST} function (Oracle). */
LEAST,

/** {@code LEAST} function (PostgreSQL). */
LEAST_PG,

/** {@code LOG} function. (Mysql, Spark). */
LOG,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 <code>CEIL</code> function
* except the return type should be a double if the operand is an integer. */
@LibraryOperator(libraries = {BIG_QUERY})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<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) {
Expand Down
4 changes: 2 additions & 2 deletions site/_docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand All @@ -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)`
Expand Down
50 changes: 46 additions & 4 deletions testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10747,8 +10747,29 @@ void assertSubFunReturns(boolean binary, String s, int start,
"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);
}

Expand All @@ -10772,8 +10793,29 @@ void assertSubFunReturns(boolean binary, String s, int start,
"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);
}

Expand Down

0 comments on commit 0bacbc4

Please sign in to comment.