Skip to content

Commit

Permalink
[CALCITE-6397] Add NVL2 function (enabled in Oracle, Spark library)
Browse files Browse the repository at this point in the history
  • Loading branch information
caicancai authored and mihaibudiu committed Jun 1, 2024
1 parent d1ebe16 commit eb92f96
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 2 deletions.
7 changes: 5 additions & 2 deletions core/src/main/java/org/apache/calcite/sql/SqlKind.java
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,13 @@ public enum SqlKind {
/** {@code DECODE} function (Oracle). */
DECODE,

/** {@code NVL} function (Oracle). */
/** {@code NVL} function (Oracle, Spark). */
NVL,

/** {@code GREATEST} function (Oracle). */
/** {@code NVL2} function (Oracle, Spark). */
NVL2,

/** {@code GREATEST} function (Oracle, Spark). */
GREATEST,

/** The two-argument {@code CONCAT} function (Oracle). */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,14 @@ private static SqlCall transformConvert(SqlValidator validator, SqlCall call) {
.andThen(SqlTypeTransforms.TO_NULLABLE_ALL),
OperandTypes.SAME_SAME);

/** The "NVL2(value, value, value)" function. */
@LibraryOperator(libraries = {ORACLE, SPARK})
public static final SqlBasicFunction NVL2 =
SqlBasicFunction.create(SqlKind.NVL2,
ReturnTypes.NVL2_RESTRICTIVE
.andThen(SqlTypeTransforms.TO_NULLABLE_ALL),
OperandTypes.SECOND_THIRD_SAME);

/** The "IFNULL(value, value)" function. */
@LibraryOperator(libraries = {BIG_QUERY, SPARK})
public static final SqlFunction IFNULL = NVL.withName("IFNULL");
Expand Down
13 changes: 13 additions & 0 deletions core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -986,6 +986,19 @@ public static SqlSingleOperandTypeChecker same(int operandCount,

public static final SqlSingleOperandTypeChecker ANY_ANY =
family(SqlTypeFamily.ANY, SqlTypeFamily.ANY);

/**
* Operand type-checking strategy where the second and third operands must be comparable.
* This is used when the operator has three operands and only the
* second and third operands need to be comparable.
*/
public static final SqlSingleOperandTypeChecker SECOND_THIRD_SAME =
new SameOperandTypeChecker(3) {
@Override protected List<Integer> getOperandList(int operandCount) {
// Only check the second and third operands
return ImmutableList.of(1, 2);
}
};
public static final SqlSingleOperandTypeChecker ANY_IGNORE =
family(SqlTypeFamily.ANY, SqlTypeFamily.IGNORE);
public static final SqlSingleOperandTypeChecker IGNORE_ANY =
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/java/org/apache/calcite/sql/type/ReturnTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.apache.calcite.util.Util;

import java.util.AbstractList;
import java.util.Arrays;
import java.util.List;
import java.util.function.UnaryOperator;

Expand Down Expand Up @@ -551,6 +552,15 @@ public static SqlCall stripSeparator(SqlCall call) {
opBinding -> opBinding.getTypeFactory().leastRestrictive(
opBinding.collectOperandTypes());

/**
* Type-inference strategy for NVL2 function. It returns the least restrictive type
* between the second and third operands.
*/
public static final SqlReturnTypeInference NVL2_RESTRICTIVE = opBinding -> {
return opBinding.getTypeFactory().leastRestrictive(
Arrays.asList(opBinding.getOperandType(1), opBinding.getOperandType(2)));
};

/**
* Type-inference strategy that returns the type of the first operand, unless it
* is an integer type, in which case the return type is DOUBLE.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ private StandardConvertletTable() {
registerOp(operator, StandardConvertletTable::convertQuantifyOperator));

registerOp(SqlLibraryOperators.NVL, StandardConvertletTable::convertNvl);
registerOp(SqlLibraryOperators.NVL2, StandardConvertletTable::convertNvl2);
registerOp(SqlLibraryOperators.DECODE,
StandardConvertletTable::convertDecode);
registerOp(SqlLibraryOperators.IF, StandardConvertletTable::convertIf);
Expand Down Expand Up @@ -421,6 +422,29 @@ private static RexNode convertNvl(SqlRexContext cx, SqlCall call) {
operand1)));
}

/** Converts a call to the {@code NVL2} function. */
private static RexNode convertNvl2(SqlRexContext cx, SqlCall call) {
final RexBuilder rexBuilder = cx.getRexBuilder();
final List<RexNode> operands =
convertOperands(cx, call, call.getOperandList(), SqlOperandTypeChecker.Consistency.NONE);
final RelDataType type = cx.getValidator().getValidatedNodeType(call);

// Create a CASE expression equivalent to the NVL2 function
// NVL2(x, y, z) is equivalent to CASE WHEN x IS NOT NULL THEN y ELSE z END
return rexBuilder.makeCall(type, SqlStdOperatorTable.CASE,
ImmutableList.of(
rexBuilder.makeCall(SqlStdOperatorTable.IS_NOT_NULL,
operands.get(0)),
rexBuilder.makeCast(
cx.getTypeFactory()
.createTypeWithNullability(type, operands.get(1).getType().isNullable()),
operands.get(1)),
rexBuilder.makeCast(
cx.getTypeFactory()
.createTypeWithNullability(type, operands.get(2).getType().isNullable()),
operands.get(2))));
}

/** Converts a call to the INSTR function.
* INSTR(string, substring, position, occurrence) is equivalent to
* POSITION(substring, string, position, occurrence) */
Expand Down
1 change: 1 addition & 0 deletions site/_docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2812,6 +2812,7 @@ In the following:
| b m p s | MD5(string) | Calculates an MD5 128-bit checksum of *string* and returns it as a hex string
| m | MONTHNAME(date) | Returns the name, in the connection's locale, of the month in *datetime*; for example, it returns '二月' for both DATE '2020-02-10' and TIMESTAMP '2020-02-10 10:10:10'
| o s | NVL(value1, value2) | Returns *value1* if *value1* is not null, otherwise *value2*
| o s | NVL2(value1, value2, value3) | Returns *value2* if *value1* is not null, otherwise *value3*
| b | OFFSET(index) | When indexing an array, wrapping *index* in `OFFSET` returns the value at the 0-based *index*; throws error if *index* is out of bounds
| b | ORDINAL(index) | Similar to `OFFSET` except *index* begins at 1
| b | PARSE_DATE(format, string) | Uses format specified by *format* to convert *string* representation of date to a DATE value
Expand Down
41 changes: 41 additions & 0 deletions testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10705,6 +10705,47 @@ void assertSubFunReturns(boolean binary, String s, int start,
checkNvl(f, FunctionAlias.of(SqlLibraryOperators.NVL));
}

/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-6397">[CALCITE-6397]
* Add NVL2 function (enabled in Oracle, Spark library) </a>.
*/
@Test void testNvl2Func() {
final SqlOperatorFixture f = fixture();
f.setFor(SqlLibraryOperators.NVL2, VmName.EXPAND);
f.checkFails("^nvl2(NULL, 2, 1)^",
"No match found for function signature "
+ "NVL2\\(<NULL>, <NUMERIC>, <NUMERIC>\\)", false);

final Consumer<SqlOperatorFixture> consumer = f12 -> {
f12.checkScalar("nvl2(NULL, 2, 1)", "1", "INTEGER NOT NULL");
f12.checkScalar("nvl2(true, true, false)", true, "BOOLEAN NOT NULL");
f12.checkScalar("nvl2(false, true, false)", true, "BOOLEAN NOT NULL");
f12.checkScalar("nvl2(NULL, true, false)", false, "BOOLEAN NOT NULL");
f12.checkScalar("nvl2(3, 2, 1)", "2", "INTEGER NOT NULL");
f12.checkScalar("nvl2(3, 'a', 'b')", "a", "CHAR(1) NOT NULL");
f12.checkScalar("nvl2(NULL, 'a', 'b')", "b", "CHAR(1) NOT NULL");
f12.checkScalar("nvl2(NULL, 'ab', 'de')", "de", "CHAR(2) NOT NULL");
f12.checkScalar("nvl2('ab', 'abc', 'def')", "abc", "CHAR(3) NOT NULL");
f12.checkScalar("nvl2('a', 3, 2)", "3", "INTEGER NOT NULL");
f12.checkScalar("NVL2(NULL, 3.0, 4.0)", "4.0", "DECIMAL(2, 1) NOT NULL");
f12.checkScalar("NVL2('abc', 3.0, 4.0)", "3.0", "DECIMAL(2, 1) NOT NULL");
f12.checkScalar("NVL2(1, 3.0, 2.111)", "3.0", "DECIMAL(4, 3) NOT NULL");
f12.checkScalar("NVL2(NULL, 3.0, 2.111)", "2.111", "DECIMAL(4, 3) NOT NULL");
f12.checkScalar("NVL2(3.111, 3.1415926, 2.111)", "3.1415926", "DECIMAL(8, 7) NOT NULL");

f12.checkNull("nvl2('ab', CAST(NULL AS VARCHAR(6)), 'def')");
f12.checkNull("nvl2(NULL, 'abc', NULL)");
f12.checkNull("nvl2(NULL, NULL, NULL)");

f12.checkFails("^NVL2(2.0, 2.0, true)^", "Parameters must be of the same type", false);
f12.checkFails("^NVL2(NULL, 2.0, true)^", "Parameters must be of the same type", false);
f12.checkFails("^NVL2(2.0, 1, true)^", "Parameters must be of the same type", false);
f12.checkFails("^NVL2(NULL, 1, true)^", "Parameters must be of the same type", false);
};
f.forEachLibrary(list(SqlLibrary.ORACLE, SqlLibrary.SPARK), consumer);

}

/** Tests the {@code NVL} and {@code IFNULL} operators. */
void checkNvl(SqlOperatorFixture f0, FunctionAlias functionAlias) {
final SqlFunction function = functionAlias.function;
Expand Down

0 comments on commit eb92f96

Please sign in to comment.