This is an automated email from the ASF dual-hosted git repository.
guohongyu 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 e877885ed9 [CALCITE-6116] Add EXISTS function (enabled in Spark
library)
e877885ed9 is described below
commit e877885ed90127a4cadb25f1b718f91375fe6164
Author: Hongyu Guo <[email protected]>
AuthorDate: Wed Dec 13 14:27:10 2023 +0800
[CALCITE-6116] Add EXISTS function (enabled in Spark library)
Following [CALCITE-3679] Allow lambda expressions in SQL queries
* Refactoring LambdaOperandTypeChecker into an abstract class
* Add LambdaRelOperandTypeChecker and LambdaFamilyOperandTypeChecker
* Incorrect validation when lambda expression is null
---
.../calcite/adapter/enumerable/RexImpTable.java | 2 +
.../org/apache/calcite/runtime/SqlFunctions.java | 17 +++
.../calcite/sql/fun/SqlLibraryOperators.java | 8 +
.../org/apache/calcite/sql/type/OperandTypes.java | 168 +++++++++++++++++----
.../org/apache/calcite/util/BuiltInMethod.java | 1 +
.../calcite/rel/rel2sql/RelToSqlConverterTest.java | 39 ++++-
.../apache/calcite/test/SqlToRelConverterTest.java | 12 ++
.../org/apache/calcite/test/SqlValidatorTest.java | 2 +-
.../apache/calcite/test/SqlToRelConverterTest.xml | 11 ++
core/src/test/resources/sql/lambda.iq | 104 +++++++++++++
site/_docs/reference.md | 8 +-
.../java/org/apache/calcite/test/QuidemTest.java | 6 +
.../org/apache/calcite/test/SqlOperatorTest.java | 72 +++++++++
13 files changed, 421 insertions(+), 29 deletions(-)
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index 77cd73d24f..c1288c730d 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -174,6 +174,7 @@ import static
org.apache.calcite.sql.fun.SqlLibraryOperators.DATE_TRUNC;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.DAYNAME;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.DIFFERENCE;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.ENDS_WITH;
+import static org.apache.calcite.sql.fun.SqlLibraryOperators.EXISTS;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.EXISTS_NODE;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.EXTRACT_VALUE;
import static org.apache.calcite.sql.fun.SqlLibraryOperators.EXTRACT_XML;
@@ -839,6 +840,7 @@ public class RexImpTable {
defineMethod(ARRAY_UNION, BuiltInMethod.ARRAY_UNION.method,
NullPolicy.ANY);
defineMethod(ARRAYS_OVERLAP, BuiltInMethod.ARRAYS_OVERLAP.method,
NullPolicy.ANY);
defineMethod(ARRAYS_ZIP, BuiltInMethod.ARRAYS_ZIP.method,
NullPolicy.ANY);
+ defineMethod(EXISTS, BuiltInMethod.EXISTS.method, NullPolicy.ANY);
defineMethod(MAP_CONCAT, BuiltInMethod.MAP_CONCAT.method,
NullPolicy.ANY);
defineMethod(MAP_ENTRIES, BuiltInMethod.MAP_ENTRIES.method,
NullPolicy.STRICT);
defineMethod(MAP_KEYS, BuiltInMethod.MAP_KEYS.method, NullPolicy.STRICT);
diff --git a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
index adaf232add..abc2b7e3cf 100644
--- a/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
+++ b/core/src/main/java/org/apache/calcite/runtime/SqlFunctions.java
@@ -32,6 +32,7 @@ import org.apache.calcite.linq4j.function.Deterministic;
import org.apache.calcite.linq4j.function.Experimental;
import org.apache.calcite.linq4j.function.Function1;
import org.apache.calcite.linq4j.function.NonDeterministic;
+import org.apache.calcite.linq4j.function.Predicate1;
import org.apache.calcite.linq4j.tree.Primitive;
import org.apache.calcite.rel.type.TimeFrame;
import org.apache.calcite.rel.type.TimeFrameSet;
@@ -5403,6 +5404,22 @@ public class SqlFunctions {
return list;
}
+ /** Support the EXISTS(list, function1) function. */
+ public static @Nullable Boolean exists(List list, Function1<Object, Boolean>
function1) {
+ return nullableExists(list, function1);
+ }
+
+ /** Support the EXISTS(list, predicate1) function. */
+ public static Boolean exists(List list, Predicate1 predicate1) {
+ for (Object element : list) {
+ boolean ret = predicate1.apply(element);
+ if (ret) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/** Support the MAP_CONCAT function. */
public static Map mapConcat(Map... maps) {
final Map result = new LinkedHashMap();
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 7f113cc96d..878e7e0ab3 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
@@ -1193,6 +1193,14 @@ public abstract class SqlLibraryOperators {
SqlLibraryOperators::arrayAppendPrependReturnType,
OperandTypes.ARRAY_ELEMENT);
+ /** The "EXISTS(array, lambda)" function (Spark); returns whether a
predicate holds
+ * for one or more elements in the array. */
+ @LibraryOperator(libraries = {SPARK})
+ public static final SqlFunction EXISTS =
+ SqlBasicFunction.create("EXISTS",
+ ReturnTypes.BOOLEAN_NULLABLE,
+ OperandTypes.EXISTS);
+
@SuppressWarnings("argument.type.incompatible")
private static RelDataType arrayCompactReturnType(SqlOperatorBinding
opBinding) {
final RelDataType arrayType = opBinding.collectOperandTypes().get(0);
diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index 0b56e1db7b..0c68f0eacd 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -115,7 +115,7 @@ public abstract class OperandTypes {
*/
public static SqlSingleOperandTypeChecker function(SqlTypeFamily
returnTypeFamily,
SqlTypeFamily... paramTypeFamilies) {
- return new LambdaOperandTypeChecker(
+ return new LambdaFamilyOperandTypeChecker(
returnTypeFamily, ImmutableList.copyOf(paramTypeFamilies));
}
@@ -126,7 +126,7 @@ public abstract class OperandTypes {
*/
public static SqlSingleOperandTypeChecker function(SqlTypeFamily
returnTypeFamily,
List<SqlTypeFamily> paramTypeFamilies) {
- return new LambdaOperandTypeChecker(returnTypeFamily, paramTypeFamilies);
+ return new LambdaFamilyOperandTypeChecker(returnTypeFamily,
paramTypeFamilies);
}
/**
@@ -1135,6 +1135,39 @@ public abstract class OperandTypes {
}
};
+ public static final SqlOperandTypeChecker EXISTS =
+ new SqlOperandTypeChecker() {
+ @Override public boolean checkOperandTypes(
+ SqlCallBinding callBinding,
+ boolean throwOnFailure) {
+ // The first operand must be an array type
+ ARRAY.checkSingleOperandType(callBinding, callBinding.operand(0), 0,
throwOnFailure);
+ final RelDataType arrayType =
+ SqlTypeUtil.deriveType(callBinding, callBinding.operand(0));
+ final RelDataType componentType =
+ requireNonNull(arrayType.getComponentType(), "componentType");
+
+ // The second operand is a function(array_element_type)->boolean type
+ LambdaRelOperandTypeChecker lambdaChecker =
+ new LambdaRelOperandTypeChecker(
+ SqlTypeFamily.BOOLEAN,
+ ImmutableList.of(componentType));
+ return lambdaChecker.checkSingleOperandType(
+ callBinding,
+ callBinding.operand(1),
+ 1,
+ throwOnFailure);
+ }
+
+ @Override public SqlOperandCountRange getOperandCountRange() {
+ return SqlOperandCountRanges.of(2);
+ }
+
+ @Override public String getAllowedSignatures(SqlOperator op, String
opName) {
+ return "EXISTS(<ARRAY>, <FUNCTION(ARRAY_ELEMENT_TYPE)->BOOLEAN>)";
+ }
+ };
+
/**
* Checker for record just has one field.
*/
@@ -1442,18 +1475,17 @@ public abstract class OperandTypes {
/**
* Operand type-checking strategy where the type of the operand is a lambda
- * expression with a given return type and argument types.
+ * expression with a given return type and argument {@link SqlTypeFamily}s.
*/
- private static class LambdaOperandTypeChecker
- implements SqlSingleOperandTypeChecker {
+ private static class LambdaFamilyOperandTypeChecker
+ extends LambdaOperandTypeChecker {
- private final SqlTypeFamily returnTypeFamily;
private final List<SqlTypeFamily> argFamilies;
- LambdaOperandTypeChecker(
+ LambdaFamilyOperandTypeChecker(
SqlTypeFamily returnTypeFamily,
List<SqlTypeFamily> argFamilies) {
- this.returnTypeFamily = requireNonNull(returnTypeFamily,
"returnTypeFamily");
+ super(returnTypeFamily);
this.argFamilies = ImmutableList.copyOf(argFamilies);
}
@@ -1481,40 +1513,125 @@ public abstract class OperandTypes {
}
final SqlLambda lambdaExpr = (SqlLambda) operand;
- if (lambdaExpr.getParameters().isEmpty()
- || argFamilies.stream().allMatch(f -> f == SqlTypeFamily.ANY)
- || returnTypeFamily == SqlTypeFamily.ANY) {
- return true;
+ if (SqlUtil.isNullLiteral(lambdaExpr.getExpression(), false)) {
+ checkNull(callBinding, lambdaExpr, throwOnFailure);
}
- if (SqlUtil.isNullLiteral(lambdaExpr.getExpression(), false)) {
- if (callBinding.isTypeCoercionEnabled()) {
- return true;
+ final SqlValidator validator = callBinding.getValidator();
+ if (!lambdaExpr.getParameters().isEmpty()
+ && !argFamilies.stream().allMatch(f -> f == SqlTypeFamily.ANY)) {
+ // Replace the parameter types in the lambda expression.
+ final SqlLambdaScope scope =
+ (SqlLambdaScope) validator.getLambdaScope(lambdaExpr);
+ for (int i = 0; i < argFamilies.size(); i++) {
+ final SqlNode param = lambdaExpr.getParameters().get(i);
+ final RelDataType type =
+
argFamilies.get(i).getDefaultConcreteType(callBinding.getTypeFactory());
+ if (type != null) {
+ scope.getParameterTypes().put(param.toString(), type);
+ }
}
+ lambdaExpr.accept(new TypeRemover(validator));
+ // Given the new relDataType, re-validate the lambda expression.
+ validator.validateLambda(lambdaExpr);
+ }
+ return checkReturnType(validator, callBinding, lambdaExpr,
throwOnFailure);
+ }
+ }
+
+ /**
+ * Operand type-checking strategy where the type of the operand is a lambda
+ * expression with a given return type and argument {@link RelDataType}s.
+ */
+ private static class LambdaRelOperandTypeChecker
+ extends LambdaOperandTypeChecker {
+ private final List<RelDataType> argTypes;
+
+ LambdaRelOperandTypeChecker(
+ SqlTypeFamily returnTypeFamily,
+ List<RelDataType> argTypes) {
+ super(returnTypeFamily);
+ this.argTypes = argTypes;
+ }
+
+ @Override public String getAllowedSignatures(SqlOperator op, String
opName) {
+ ImmutableList.Builder<SqlTypeFamily> builder = ImmutableList.builder();
+ argTypes.stream()
+ .map(t -> requireNonNull(t.getSqlTypeName().getFamily()))
+ .forEach(builder::add);
+ builder.add(returnTypeFamily);
+
+ return SqlUtil.getAliasedSignature(op, opName, builder.build());
+ }
+
+ @Override public boolean checkSingleOperandType(SqlCallBinding
callBinding, SqlNode operand,
+ int iFormalOperand,
+ boolean throwOnFailure) {
+ if (!(operand instanceof SqlLambda)
+ || ((SqlLambda) operand).getParameters().size() != argTypes.size()) {
if (throwOnFailure) {
- throw
callBinding.getValidator().newValidationError(lambdaExpr.getExpression(),
- RESOURCE.nullIllegal());
+ throw callBinding.newValidationSignatureError();
}
return false;
}
+ final SqlLambda lambdaExpr = (SqlLambda) operand;
+ if (SqlUtil.isNullLiteral(lambdaExpr.getExpression(), false)) {
+ checkNull(callBinding, lambdaExpr, throwOnFailure);
+ }
+
// Replace the parameter types in the lambda expression.
final SqlValidator validator = callBinding.getValidator();
final SqlLambdaScope scope =
(SqlLambdaScope) validator.getLambdaScope(lambdaExpr);
- for (int i = 0; i < argFamilies.size(); i++) {
+ for (int i = 0; i < argTypes.size(); i++) {
final SqlNode param = lambdaExpr.getParameters().get(i);
- final RelDataType type =
-
argFamilies.get(i).getDefaultConcreteType(callBinding.getTypeFactory());
+ final RelDataType type = argTypes.get(i);
if (type != null) {
scope.getParameterTypes().put(param.toString(), type);
}
}
lambdaExpr.accept(new TypeRemover(validator));
-
// Given the new relDataType, re-validate the lambda expression.
validator.validateLambda(lambdaExpr);
+
+ return checkReturnType(validator, callBinding, lambdaExpr,
throwOnFailure);
+ }
+ }
+
+ /**
+ * Abstract base class for type-checking strategies involving lambda
expressions.
+ * This class provides common functionality for checking the type of lambda
expression.
+ */
+ private abstract static class LambdaOperandTypeChecker
+ implements SqlSingleOperandTypeChecker {
+ protected final SqlTypeFamily returnTypeFamily;
+
+ LambdaOperandTypeChecker(SqlTypeFamily returnTypeFamily) {
+ this.returnTypeFamily = requireNonNull(returnTypeFamily,
"returnTypeFamily");
+ }
+
+ protected boolean checkNull(
+ SqlCallBinding callBinding,
+ SqlLambda lambdaExpr,
+ boolean throwOnFailure) {
+ if (callBinding.isTypeCoercionEnabled()) {
+ return true;
+ }
+
+ if (throwOnFailure) {
+ throw
callBinding.getValidator().newValidationError(lambdaExpr.getExpression(),
+ RESOURCE.nullIllegal());
+ }
+ return false;
+ }
+
+ protected boolean checkReturnType(
+ SqlValidator validator,
+ SqlCallBinding callBinding,
+ SqlLambda lambdaExpr,
+ boolean throwOnFailure) {
final RelDataType newType = validator.getValidatedNodeType(lambdaExpr);
assert newType instanceof FunctionSqlType;
final SqlTypeName returnTypeName =
@@ -1533,14 +1650,14 @@ public abstract class OperandTypes {
/**
* Visitor that removes the relDataType of a sqlNode and its children in
the
* validator. Now this visitor is only used for removing the relDataType
- * in {@code LambdaOperandTypeChecker}. Since lambda expressions
- * will be validated for the second time based on the given parameter type,
+ * when we check lambda operand. Since lambda expressions will be
+ * validated for the second time based on the given parameter type,
* the type cached during the first validation must be cleared.
*/
- private static class TypeRemover extends SqlBasicVisitor<Void> {
+ protected static class TypeRemover extends SqlBasicVisitor<Void> {
private final SqlValidator validator;
- TypeRemover(SqlValidator validator) {
+ protected TypeRemover(SqlValidator validator) {
this.validator = validator;
}
@@ -1553,7 +1670,6 @@ public abstract class OperandTypes {
validator.removeValidatedNodeType(call);
return super.visit(call);
}
-
}
}
}
diff --git a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
index 89f3318a28..142f1d30d2 100644
--- a/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
+++ b/core/src/main/java/org/apache/calcite/util/BuiltInMethod.java
@@ -773,6 +773,7 @@ public enum BuiltInMethod {
ARRAY_REVERSE(SqlFunctions.class, "reverse", List.class),
ARRAYS_OVERLAP(SqlFunctions.class, "arraysOverlap", List.class, List.class),
ARRAYS_ZIP(SqlFunctions.class, "arraysZip", List.class, List.class),
+ EXISTS(SqlFunctions.class, "exists", List.class, Function1.class),
SORT_ARRAY(SqlFunctions.class, "sortArray", List.class, boolean.class),
MAP(SqlFunctions.class, "map", Object[].class),
MAP_CONCAT(SqlFunctions.class, "mapConcat", Map[].class),
diff --git
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
index 79c340e2da..aedc4bdc37 100644
---
a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
+++
b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java
@@ -7430,7 +7430,7 @@ class RelToSqlConverterTest {
+ "FROM \"foodmart\".\"employee\"";
sql(sql3).ok(expected3);
- final String sql4 = "select higher_order_function2(1, () -> null)";
+ final String sql4 = "select higher_order_function2(1, () -> cast(null as
integer))";
final String expected4 = "SELECT HIGHER_ORDER_FUNCTION2("
+ "1, () -> NULL)\nFROM (VALUES (0)) AS \"t\" (\"ZERO\")";
sql(sql4).ok(expected4);
@@ -7452,6 +7452,43 @@ class RelToSqlConverterTest {
sql(sql6).ok(expected6);
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6116">[CALCITE-6116]
+ * Add EXISTS function (enabled in Spark library)</a>. */
+ @Test void testExistsFunctionInSpark() {
+ final String sql = "select \"EXISTS\"(array[1,2,3], x -> x > 2)";
+ final String expected = "SELECT EXISTS(ARRAY[1, 2, 3], \"X\" -> \"X\" >
2)\n"
+ + "FROM (VALUES (0)) AS \"t\" (\"ZERO\")";
+ sql(sql)
+ .withLibrary(SqlLibrary.SPARK)
+ .ok(expected);
+
+ final String sql2 = "select \"EXISTS\"(array[1,2,3], (x) -> false)";
+ final String expected2 = "SELECT EXISTS(ARRAY[1, 2, 3], \"X\" -> FALSE)\n"
+ + "FROM (VALUES (0)) AS \"t\" (\"ZERO\")";
+ sql(sql2)
+ .withLibrary(SqlLibrary.SPARK)
+ .ok(expected2);
+
+ // empty array
+ final String sql3 = "select \"EXISTS\"(array(), (x) -> false)";
+ final String expected3 = "SELECT EXISTS(ARRAY(), \"X\" -> FALSE)\n"
+ + "FROM (VALUES (0)) AS \"t\" (\"ZERO\")";
+ sql(sql3)
+ .withLibrary(SqlLibrary.SPARK)
+ .ok(expected3);
+
+ final String sql4 = "select \"EXISTS\"('string', (x) -> false)";
+ final String error4 = "org.apache.calcite.runtime.CalciteContextException:
"
+ + "From line 1, column 8 to line 1, column 39: "
+ + "Cannot apply 'EXISTS' to arguments of type "
+ + "'EXISTS(<CHAR(6)>, <FUNCTION(ANY) -> BOOLEAN>)'. "
+ + "Supported form(s): EXISTS(<ARRAY>,
<FUNCTION(ARRAY_ELEMENT_TYPE)->BOOLEAN>)";
+ sql(sql4)
+ .withLibrary(SqlLibrary.SPARK)
+ .throws_(error4);
+ }
+
/** Test case for
* <a
href="https://issues.apache.org/jira/browse/CALCITE-5265">[CALCITE-5265]
* JDBC adapter sometimes adds unnecessary parentheses around SELECT in
INSERT</a>. */
diff --git
a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 6a43a487f7..cd95b88aef 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -137,6 +137,18 @@ class SqlToRelConverterTest extends SqlToRelTestBase {
.ok();
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6116">[CALCITE-6116]
+ * Add EXISTS function (enabled in Spark library)</a>. */
+ @Test void testExistsFunctionInSpark() {
+ final String sql = "select \"EXISTS\"(array(1,2,3), x -> false)";
+ fixture()
+ .withFactory(c ->
+ c.withOperatorTable(t ->
SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK)))
+ .withSql(sql)
+ .ok();
+ }
+
@Test void testDotLiteralAfterRow() {
final String sql = "select row(1,2).\"EXPR$1\" from emp";
sql(sql).ok();
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 3e9269b783..b790a81c84 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -6696,7 +6696,7 @@ public class SqlValidatorTest extends
SqlValidatorTestCase {
s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> x + 1)").ok();
s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> y)").ok();
s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> char_length(x) +
1)").ok();
- s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> null)").ok();
+ s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> cast(null as
integer))").ok();
s.withSql("select HIGHER_ORDER_FUNCTION2(1, () -> 0.1)").ok();
s.withSql("select emp.deptno, HIGHER_ORDER_FUNCTION(1, (x, deptno) ->
deptno) from emp").ok();
s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> char_length(x) + 1)")
diff --git
a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
index bf246b3a0b..9db58746c3 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -1976,6 +1976,17 @@ LogicalProject(EMPNO=[$0])
LogicalAggregate(group=[{0}], agg#0=[MIN($1)])
LogicalProject($f9=[CASE(IS NOT NULL($1), CAST($1):VARCHAR(20) NOT
NULL, 'M':VARCHAR(20))], $f0=[true])
LogicalTableScan(table=[[CATALOG, SALES, EMPNULLABLES]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testExistsFunctionInSpark">
+ <Resource name="sql">
+ <![CDATA[select "EXISTS"(array(1,2,3), x -> false)]]>
+ </Resource>
+ <Resource name="plan">
+ <![CDATA[
+LogicalProject(EXPR$0=[EXISTS(ARRAY(1, 2, 3), (X) -> false)])
+ LogicalValues(tuples=[[{ 0 }]])
]]>
</Resource>
</TestCase>
diff --git a/core/src/test/resources/sql/lambda.iq
b/core/src/test/resources/sql/lambda.iq
new file mode 100644
index 0000000000..207543ec77
--- /dev/null
+++ b/core/src/test/resources/sql/lambda.iq
@@ -0,0 +1,104 @@
+# lambda.iq - Queries involving functions with lambda as arguments
+#
+# 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.
+#
+!use sparkfunc
+!set outputformat mysql
+
+# EXISTS
+select "EXISTS"(array(1,2,3), x -> x = 2);
++--------+
+| EXPR$0 |
++--------+
+| true |
++--------+
+(1 row)
+
+!ok
+
+select "EXISTS"(array(1,2,3), x -> x = 4);
++--------+
+| EXPR$0 |
++--------+
+| false |
++--------+
+(1 row)
+
+!ok
+
+select "EXISTS"(array(1,2,3), x -> x > 2 and x < 4);
++--------+
+| EXPR$0 |
++--------+
+| true |
++--------+
+(1 row)
+
+!ok
+
+select "EXISTS"(array(1,2,3), x -> power(x, 2) = 9);
++--------+
+| EXPR$0 |
++--------+
+| true |
++--------+
+(1 row)
+
+!ok
+
+select "EXISTS"(array(-1,-2,-3), x -> abs(x) = 0);
++--------+
+| EXPR$0 |
++--------+
+| false |
++--------+
+(1 row)
+
+!ok
+
+select "EXISTS"(array(1,2,3), x -> cast(null as boolean));
++--------+
+| EXPR$0 |
++--------+
+| |
++--------+
+(1 row)
+
+!ok
+
+select "EXISTS"(cast(null as integer array), x -> cast(null as boolean));
++--------+
+| EXPR$0 |
++--------+
+| |
++--------+
+(1 row)
+
+!ok
+
+select "EXISTS"(array[1, 2, 3], 1);
+Cannot apply 'EXISTS' to arguments of type 'EXISTS(<INTEGER ARRAY>, <INTEGER>)'
+!error
+
+select "EXISTS"(array[array[1, 2], array[3, 4]], x -> x[1] = 1);
++--------+
+| EXPR$0 |
++--------+
+| true |
++--------+
+(1 row)
+
+!ok
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 1bcb076bf1..841c42650c 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -2651,6 +2651,9 @@ BigQuery's type system uses confusingly different names
for types and functions:
function, return a Calcite `TIMESTAMP WITH LOCAL TIME ZONE`;
* Similarly, `DATETIME(string)` returns a Calcite `TIMESTAMP`.
+In the following:
+* *func* is a lambda argument.
+
| C | Operator syntax | Description
|:- |:-----------------------------------------------|:-----------
| p | expr :: type | Casts *expr* to *type*
@@ -2731,8 +2734,9 @@ BigQuery's type system uses confusingly different names
for types and functions:
| p | DIFFERENCE(string, string) | Returns a measure of
the similarity of two strings, namely the number of character positions that
their `SOUNDEX` values have in common: 4 if the `SOUNDEX` values are same and 0
if the `SOUNDEX` values are totally different
| f | ENDSWITH(string1, string2) | Returns whether
*string2* is a suffix of *string1*
| b p | ENDS_WITH(string1, string2) | Equivalent to
`ENDSWITH(string1, string2)`
-| o | EXTRACT(xml, xpath, [, namespaces ]) | Returns the XML
fragment of the element or elements matched by the XPath expression. The
optional namespace value that specifies a default mapping or namespace mapping
for prefixes, which is used when evaluating the XPath expression
+| s | EXISTS(array, func) | Returns whether a
predicate *func* holds for one or more elements in the *array*
| o | EXISTSNODE(xml, xpath, [, namespaces ]) | Determines whether
traversal of a XML document using a specified xpath results in any nodes.
Returns 0 if no nodes remain after applying the XPath traversal on the document
fragment of the element or elements matched by the XPath expression. Returns 1
if any nodes remain. The optional namespace value that specifies a default
mapping or namespace mapping for prefixes, which is used when evaluating the
XPath expression.
+| o | EXTRACT(xml, xpath, [, namespaces ]) | Returns the XML
fragment of the element or elements matched by the XPath expression. The
optional namespace value that specifies a default mapping or namespace mapping
for prefixes, which is used when evaluating the XPath expression
| m | EXTRACTVALUE(xml, xpathExpr)) | Returns the text of the
first text node which is a child of the element or elements matched by the
XPath expression.
| h s | FACTORIAL(integer) | Returns the factorial
of *integer*, the range of *integer* is [0, 20]. Otherwise, returns NULL
| h s | FIND_IN_SET(matchStr, textStr) | Returns the index
(1-based) of the given *matchStr* in the comma-delimited *textStr*. Returns 0,
if the given *matchStr* is not found or if the *matchStr* contains a comma. For
example, FIND_IN_SET('bc', 'a,bc,def') returns 2
@@ -3139,6 +3143,8 @@ Higher-order functions are not included in the SQL
standard, so all the function
[Dialect-specific OperatorsPermalink]({{ site.baseurl
}}/docs/reference.html#dialect-specific-operators)
as well.
+Examples of functions with a lambda argument are *EXISTS*.
+
## User-defined functions
Calcite is extensible. You can define each kind of function using user code.
diff --git a/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
b/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
index aec668e469..9760536492 100644
--- a/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/QuidemTest.java
@@ -306,6 +306,12 @@ public abstract class QuidemTest {
.with(CalciteAssert.Config.REGULAR)
.with(CalciteAssert.SchemaSpec.POST)
.connect();
+ case "sparkfunc":
+ return CalciteAssert.that()
+ .with(CalciteConnectionProperty.FUN, "spark")
+ .with(CalciteAssert.Config.REGULAR)
+ .with(CalciteAssert.SchemaSpec.POST)
+ .connect();
case "oraclefunc":
return CalciteAssert.that()
.with(CalciteConnectionProperty.FUN, "oracle")
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 8853610ca4..583fd06882 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -6906,6 +6906,78 @@ public class SqlOperatorTest {
+ "'SORT_ARRAY\\(<ARRAY>, <BOOLEAN>\\)'", false);
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-6116">[CALCITE-6116]
+ * Add EXISTS function (enabled in Spark library)</a>. */
+ @Test void testExistsFunc() {
+ final SqlOperatorFixture f = fixture()
+ .setFor(SqlLibraryOperators.EXISTS)
+ .withLibrary(SqlLibrary.SPARK);
+ // wrong return type in function
+ f.withValidatorConfig(t -> t.withTypeCoercionEnabled(false))
+ .checkFails("^\"EXISTS\"(array[1, 2, 3], x -> x + 1)^",
+ "Cannot apply 'EXISTS' to arguments of type "
+ + "'EXISTS\\(<INTEGER ARRAY>, <FUNCTION\\(INTEGER\\) ->
INTEGER>\\)'. "
+ + "Supported form\\(s\\): "
+ + "EXISTS\\(<ARRAY>,
<FUNCTION\\(ARRAY_ELEMENT_TYPE\\)->BOOLEAN>\\)",
+ false);
+
+ // bad number of arguments
+ f.checkFails("^\"EXISTS\"(array[1, 2, 3])^",
+ "Invalid number of arguments to function 'EXISTS'\\. Was expecting 2
arguments",
+ false);
+ f.checkFails("^\"EXISTS\"(array[1, 2, 3], x -> x > 2, x -> x > 2)^",
+ "Invalid number of arguments to function 'EXISTS'\\. Was expecting 2
arguments",
+ false);
+
+ // function should not be null
+ f.checkFails("^\"EXISTS\"(array[1, 2, 3], null)^",
+ "Cannot apply 'EXISTS' to arguments of type 'EXISTS\\(<INTEGER ARRAY>,
<NULL>\\)'. "
+ + "Supported form\\(s\\): "
+ + "EXISTS\\(<ARRAY>,
<FUNCTION\\(ARRAY_ELEMENT_TYPE\\)->BOOLEAN>\\)",
+ false);
+
+ // bad type
+ f.checkFails("^\"EXISTS\"(1, x -> x > 2)^",
+ "Cannot apply 'EXISTS' to arguments of type 'EXISTS\\(<INTEGER>, "
+ + "<FUNCTION\\(ANY\\) -> BOOLEAN>\\)'. "
+ + "Supported form\\(s\\): "
+ + "EXISTS\\(<ARRAY>,
<FUNCTION\\(ARRAY_ELEMENT_TYPE\\)->BOOLEAN>\\)",
+ false);
+ f.checkFails("^\"EXISTS\"(array[1, 2, 3], 1)^",
+ "Cannot apply 'EXISTS' to arguments of type 'EXISTS\\(<INTEGER ARRAY>,
<INTEGER>\\)'. "
+ + "Supported form\\(s\\): "
+ + "EXISTS\\(<ARRAY>,
<FUNCTION\\(ARRAY_ELEMENT_TYPE\\)->BOOLEAN>\\)",
+ false);
+
+ // simple expression
+ f.checkScalar("\"EXISTS\"(array[1, 2, 3], x -> x > 2)", true, "BOOLEAN");
+ f.checkScalar("\"EXISTS\"(array[1, 2, 3], x -> x > 3)", false, "BOOLEAN");
+ f.checkScalar("\"EXISTS\"(array[1, 2, 3], x -> false)", false, "BOOLEAN");
+ f.checkScalar("\"EXISTS\"(array[1, 2, 3], x -> true)", true, "BOOLEAN");
+
+ // empty array
+ f.checkScalar("\"EXISTS\"(array(), x -> true)", false, "BOOLEAN");
+ f.checkScalar("\"EXISTS\"(array(), x -> false)", false, "BOOLEAN");
+ f.checkScalar("\"EXISTS\"(array(), x -> cast(x as int) = 1)", false,
"BOOLEAN");
+
+ // complex expression
+ f.checkScalar("\"EXISTS\"(array[-1, 2, 3], y -> abs(y) = 1)", true,
"BOOLEAN");
+ f.checkScalar("\"EXISTS\"(array[-1, 2, 3], y -> abs(y) = 4)", false,
"BOOLEAN");
+ f.checkScalar("\"EXISTS\"(array[1, 2, 3], x -> x > 2 and x < 4)", true,
"BOOLEAN");
+
+ // complex array
+ f.checkScalar("\"EXISTS\"(array[array[1, 2], array[3, 4]], x -> x[1] =
1)", true, "BOOLEAN");
+ f.checkScalar("\"EXISTS\"(array[array[1, 2], array[3, 4]], x -> x[1] =
5)", false, "BOOLEAN");
+
+ // test for null
+ f.checkScalar("\"EXISTS\"(array[null, 3], x -> x > 2 or x < 4)", true,
"BOOLEAN");
+ f.checkScalar("\"EXISTS\"(array[null, 3], x -> x is null)", true,
"BOOLEAN");
+ f.checkNull("\"EXISTS\"(array[null, 3], x -> cast(null as boolean))");
+ f.checkNull("\"EXISTS\"(array[null, 3], x -> x = null)");
+ f.checkNull("\"EXISTS\"(cast(null as integer array), x -> x > 2)");
+ }
+
/** Tests {@code MAP_CONCAT} function from Spark. */
@Test void testMapConcatFunc() {
// 1. check with std map constructor, map[k, v ...]