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 6f64865eb8 [CALCITE-3679] Allow lambda expressions in SQL queries
6f64865eb8 is described below
commit 6f64865eb8c71a65bde60a19de2de42775fae4ed
Author: Hongyu Guo <[email protected]>
AuthorDate: Fri Oct 20 13:06:16 2023 +0800
[CALCITE-3679] Allow lambda expressions in SQL queries
Co-authored-by: Ritesh Kapoor <[email protected]>
---
core/src/main/codegen/templates/Parser.jj | 45 +++++++
.../adapter/enumerable/RexToLixTranslator.java | 53 ++++++++
.../apache/calcite/rel/rel2sql/SqlImplementor.java | 16 +++
.../calcite/rel/type/RelDataTypeFactory.java | 11 ++
.../java/org/apache/calcite/rex/RexBiVisitor.java | 2 +
.../org/apache/calcite/rex/RexBiVisitorImpl.java | 4 +
.../java/org/apache/calcite/rex/RexBuilder.java | 11 ++
.../org/apache/calcite/rex/RexInterpreter.java | 8 ++
.../java/org/apache/calcite/rex/RexLambda.java | 100 ++++++++++++++
.../java/org/apache/calcite/rex/RexLambdaRef.java | 57 ++++++++
.../org/apache/calcite/rex/RexProgramBuilder.java | 5 +
.../java/org/apache/calcite/rex/RexShuttle.java | 9 ++
.../java/org/apache/calcite/rex/RexSimplify.java | 7 +
.../main/java/org/apache/calcite/rex/RexSlot.java | 2 +-
.../main/java/org/apache/calcite/rex/RexUtil.java | 8 ++
.../java/org/apache/calcite/rex/RexVisitor.java | 4 +
.../org/apache/calcite/rex/RexVisitorImpl.java | 8 ++
.../apache/calcite/runtime/CalciteResource.java | 3 +
.../main/java/org/apache/calcite/sql/SqlKind.java | 9 ++
.../java/org/apache/calcite/sql/SqlLambda.java | 125 ++++++++++++++++++
.../apache/calcite/sql/type/FunctionSqlType.java | 64 +++++++++
.../org/apache/calcite/sql/type/OperandTypes.java | 144 +++++++++++++++++++++
.../calcite/sql/type/SqlTypeFactoryImpl.java | 6 +
.../org/apache/calcite/sql/type/SqlTypeFamily.java | 7 +
.../org/apache/calcite/sql/type/SqlTypeName.java | 1 +
.../calcite/sql/validate/DelegatingScope.java | 3 +
.../calcite/sql/validate/LambdaNamespace.java | 51 ++++++++
.../calcite/sql/validate/SqlLambdaScope.java | 80 ++++++++++++
.../apache/calcite/sql/validate/SqlValidator.java | 19 +++
.../calcite/sql/validate/SqlValidatorImpl.java | 39 ++++++
.../apache/calcite/sql2rel/SqlToRelConverter.java | 37 ++++++
.../calcite/runtime/CalciteResource.properties | 1 +
.../calcite/rel/rel2sql/RelToSqlConverterTest.java | 43 ++++++
.../apache/calcite/test/SqlToRelConverterTest.java | 36 ++++++
.../org/apache/calcite/test/SqlValidatorTest.java | 42 ++++++
.../apache/calcite/test/SqlToRelConverterTest.xml | 33 +++++
site/_docs/reference.md | 19 +++
.../apache/calcite/sql/parser/SqlParserTest.java | 41 ++++++
.../apache/calcite/sql/test/AbstractSqlTester.java | 4 +
.../apache/calcite/test/MockSqlOperatorTable.java | 22 +++-
40 files changed, 1177 insertions(+), 2 deletions(-)
diff --git a/core/src/main/codegen/templates/Parser.jj
b/core/src/main/codegen/templates/Parser.jj
index 8dddb1a99c..b11d83e1a9 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -73,6 +73,7 @@ import org.apache.calcite.sql.SqlJsonQueryWrapperBehavior;
import org.apache.calcite.sql.SqlJsonValueEmptyOrErrorBehavior;
import org.apache.calcite.sql.SqlJsonValueReturning;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLambda;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlMatchRecognize;
import org.apache.calcite.sql.SqlMerge;
@@ -1033,6 +1034,9 @@ void AddArg0(List<SqlNode> list, ExprContext exprContext)
:
)
(
e = Default()
+ |
+ LOOKAHEAD((SimpleIdentifierOrList() | <LPAREN> <RPAREN>) <LAMBDA>)
+ e = LambdaExpression()
|
LOOKAHEAD(3)
e = TableParam()
@@ -1060,6 +1064,9 @@ void AddArg(List<SqlNode> list, ExprContext exprContext) :
)
(
e = Default()
+ |
+ LOOKAHEAD((SimpleIdentifierOrList() | <LPAREN> <RPAREN>) <LAMBDA>)
+ e = LambdaExpression()
|
e = Expression(exprContext)
|
@@ -3912,6 +3919,43 @@ SqlNode Expression3(ExprContext exprContext) :
}
}
+/**
+ * Parses a lambda expression.
+ */
+SqlNode LambdaExpression() :
+{
+ final SqlNodeList parameters;
+ final SqlNode expression;
+ final Span s;
+}
+{
+ parameters = SimpleIdentifierOrListOrEmpty()
+ <LAMBDA> { s = span(); }
+ expression = Expression(ExprContext.ACCEPT_NON_QUERY)
+ {
+ return new SqlLambda(s.end(this), parameters, expression);
+ }
+}
+
+/**
+ * List of simple identifiers in parentheses or empty parentheses or one
simple identifier.
+ * <ul>Examples:
+ * <li>{@code ()}
+ * <li>{@code DEPTNO}
+ * <li>{@code (EMPNO, DEPTNO)}
+ * </ul>
+ */
+SqlNodeList SimpleIdentifierOrListOrEmpty() :
+{
+ SqlNodeList list;
+}
+{
+ LOOKAHEAD(2)
+ <LPAREN> <RPAREN> { return SqlNodeList.EMPTY; }
+|
+ list = SimpleIdentifierOrList() { return list; }
+}
+
SqlOperator periodOperator() :
{
}
@@ -8787,6 +8831,7 @@ void NonReservedKeyWord2of3() :
| < NE2: "!=" >
| < PLUS: "+" >
| < MINUS: "-" >
+| < LAMBDA: "->" >
| < STAR: "*" >
| < SLASH: "/" >
| < PERCENT_REMAINDER: "%" >
diff --git
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
index bea44379b6..8f53929cb7 100644
---
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
+++
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java
@@ -39,6 +39,8 @@ import org.apache.calcite.rex.RexCorrelVariable;
import org.apache.calcite.rex.RexDynamicParam;
import org.apache.calcite.rex.RexFieldAccess;
import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLambda;
+import org.apache.calcite.rex.RexLambdaRef;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexLocalRef;
import org.apache.calcite.rex.RexNode;
@@ -1043,6 +1045,21 @@ public class RexToLixTranslator implements
RexVisitor<RexToLixTranslator.Result>
return new Result(isNullVariable, valueVariable);
}
+ @Override public Result visitLambdaRef(RexLambdaRef ref) {
+ final ParameterExpression valueVariable =
+ Expressions.parameter(
+ typeFactory.getJavaClass(ref.getType()), ref.getName());
+
+ // Generate one line of code to check whether lambdaRef is null, e.g.,
+ // "final boolean input_isNull = $0 == null;"
+ final Expression isNullExpression = checkNull(valueVariable);
+ final ParameterExpression isNullVariable =
+ Expressions.parameter(
+ Boolean.TYPE, list.newName("input_isNull"));
+ list.add(Expressions.declare(Modifier.FINAL, isNullVariable,
isNullExpression));
+ return new Result(isNullVariable, valueVariable);
+ }
+
@Override public Result visitLocalRef(RexLocalRef localRef) {
return deref(localRef).accept(this);
}
@@ -1436,6 +1453,42 @@ public class RexToLixTranslator implements
RexVisitor<RexToLixTranslator.Result>
return visitInputRef(fieldRef);
}
+ @Override public Result visitLambda(RexLambda lambda) {
+ final RexNode expression = lambda.getExpression();
+ final List<RexLambdaRef> rexLambdaRefs = lambda.getParameters();
+
+ // Prepare parameter expressions for lambda expression
+ final ParameterExpression[] parameterExpressions =
+ new ParameterExpression[rexLambdaRefs.size()];
+ for (int i = 0; i < rexLambdaRefs.size(); i++) {
+ final RexLambdaRef rexLambdaRef = rexLambdaRefs.get(i);
+ parameterExpressions[i] =
+ Expressions.parameter(
+ typeFactory.getJavaClass(rexLambdaRef.getType()),
rexLambdaRef.getName());
+ }
+
+ // Generate code for lambda expression body
+ final RexToLixTranslator exprTranslator = this.setBlock(new
BlockBuilder());
+ final Result exprResult = expression.accept(exprTranslator);
+ exprTranslator.list.add(
+ Expressions.return_(null, exprResult.valueVariable));
+
+ // Generate code for lambda expression
+ final Expression functionExpression =
+ Expressions.lambda(exprTranslator.list.toBlock(),
parameterExpressions);
+ final ParameterExpression valueVariable =
+ Expressions.parameter(functionExpression.getType(),
list.newName("function_value"));
+ list.add(Expressions.declare(Modifier.FINAL, valueVariable,
functionExpression));
+
+ // Generate code for checking whether lambda expression is null
+ final Expression isNullExpression = checkNull(valueVariable);
+ final ParameterExpression isNullVariable =
+ Expressions.parameter(Boolean.TYPE, list.newName("function_isNull"));
+ list.add(Expressions.declare(Modifier.FINAL, isNullVariable,
isNullExpression));
+
+ return new Result(isNullVariable, valueVariable);
+ }
+
Expression checkNull(Expression expr) {
if (Primitive.flavor(expr.getType())
== Primitive.Flavor.PRIMITIVE) {
diff --git
a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index c9b3c4eb09..242b526f89 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -44,6 +44,8 @@ import org.apache.calcite.rex.RexDynamicParam;
import org.apache.calcite.rex.RexFieldAccess;
import org.apache.calcite.rex.RexFieldCollation;
import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLambda;
+import org.apache.calcite.rex.RexLambdaRef;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexLocalRef;
import org.apache.calcite.rex.RexNode;
@@ -66,6 +68,7 @@ import org.apache.calcite.sql.SqlDynamicParam;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlJoin;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLambda;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlMatchRecognize;
import org.apache.calcite.sql.SqlNode;
@@ -779,6 +782,19 @@ public abstract class SqlImplementor {
return SqlStdOperatorTable.NOT.createCall(POS, node);
}
+ case LAMBDA:
+ final RexLambda lambda = (RexLambda) rex;
+ final SqlNodeList parameters = new SqlNodeList(POS);
+ for (RexLambdaRef parameter : lambda.getParameters()) {
+ parameters.add(toSql(program, parameter));
+ }
+ final SqlNode expression = toSql(program, lambda.getExpression());
+ return new SqlLambda(POS, parameters, expression);
+
+ case LAMBDA_REF:
+ final RexLambdaRef lambdaRef = (RexLambdaRef) rex;
+ return new SqlIdentifier(lambdaRef.getName(), POS);
+
default:
if (rex instanceof RexOver) {
return toSql(program, (RexOver) rex);
diff --git
a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
index da1b3c399a..ecbde3db8d 100644
--- a/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
+++ b/core/src/main/java/org/apache/calcite/rel/type/RelDataTypeFactory.java
@@ -134,6 +134,17 @@ public interface RelDataTypeFactory {
RelDataType keyType,
RelDataType valueType);
+ /**
+ * Creates a function type.
+ *
+ * @param parameterType type of parameters
+ * @param returnType type of lambda expression return type
+ * @return function type descriptor
+ */
+ RelDataType createFunctionSqlType(
+ RelDataType parameterType,
+ RelDataType returnType);
+
/**
* Creates a measure type.
*
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBiVisitor.java
b/core/src/main/java/org/apache/calcite/rex/RexBiVisitor.java
index 65bef924f6..11d93c9864 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBiVisitor.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBiVisitor.java
@@ -57,6 +57,8 @@ public interface RexBiVisitor<R, P> {
R visitPatternFieldRef(RexPatternFieldRef ref, P arg);
+ R visitLambda(RexLambda lambda, P arg);
+
/** Visits a list and writes the results to another list. */
default void visitList(Iterable<? extends RexNode> exprs, P arg,
List<R> out) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBiVisitorImpl.java
b/core/src/main/java/org/apache/calcite/rex/RexBiVisitorImpl.java
index afdd5b5bc5..b5a4a2a86f 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBiVisitorImpl.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBiVisitorImpl.java
@@ -118,4 +118,8 @@ public class RexBiVisitorImpl<@Nullable R, P> implements
RexBiVisitor<R, P> {
@Override public R visitPatternFieldRef(RexPatternFieldRef fieldRef, P arg) {
return null;
}
+
+ @Override public R visitLambda(RexLambda lambda, P arg) {
+ return null;
+ }
}
diff --git a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
index 23463f75e9..69149c1679 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -1745,6 +1745,17 @@ public class RexBuilder {
}
}
+ /**
+ * Creates a lambda expression.
+ *
+ * @param expr expression of the lambda
+ * @param parameters parameters of the lambda
+ * @return RexNode representing the lambda
+ */
+ public RexNode makeLambdaCall(RexNode expr, List<RexLambdaRef> parameters) {
+ return new RexLambda(parameters, expr);
+ }
+
/** Converts the type of a value to comply with
* {@link org.apache.calcite.rex.RexLiteral#valueMatchesType}.
*
diff --git a/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
b/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
index c495dddd35..9cb706043a 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexInterpreter.java
@@ -151,6 +151,14 @@ public class RexInterpreter implements
RexVisitor<Comparable> {
throw unbound(fieldRef);
}
+ @Override public Comparable visitLambda(RexLambda lambda) {
+ throw unbound(lambda);
+ }
+
+ @Override public Comparable visitLambdaRef(RexLambdaRef lambdaRef) {
+ throw unbound(lambdaRef);
+ }
+
@Override public Comparable visitCall(RexCall call) {
final List<Comparable> values = visitList(call.operands);
switch (call.getKind()) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexLambda.java
b/core/src/main/java/org/apache/calcite/rex/RexLambda.java
new file mode 100644
index 0000000000..8b43a7720e
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rex/RexLambda.java
@@ -0,0 +1,100 @@
+/*
+ * 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.
+ */
+package org.apache.calcite.rex;
+
+import org.apache.calcite.linq4j.Ord;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlKind;
+
+import com.google.common.collect.ImmutableList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a lambda expression.
+ */
+public class RexLambda extends RexNode {
+ //~ Instance fields --------------------------------------------------------
+
+ private final List<RexLambdaRef> parameters;
+ private final RexNode expression;
+
+ //~ Constructors -----------------------------------------------------------
+
+ RexLambda(List<RexLambdaRef> parameters, RexNode expression) {
+ this.parameters = ImmutableList.copyOf(parameters);
+ this.expression = Objects.requireNonNull(expression, "expression");
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ @Override public RelDataType getType() {
+ return expression.getType();
+ }
+
+ @Override public SqlKind getKind() {
+ return SqlKind.LAMBDA;
+ }
+
+ @Override public <R> R accept(RexVisitor<R> visitor) {
+ return visitor.visitLambda(this);
+ }
+
+ @Override public <R, P> R accept(RexBiVisitor<R, P> visitor, P arg) {
+ return visitor.visitLambda(this, arg);
+ }
+
+ public RexNode getExpression() {
+ return expression;
+ }
+
+ public List<RexLambdaRef> getParameters() {
+ return parameters;
+ }
+
+ @Override public boolean equals(@Nullable Object o) {
+ return this == o
+ || o instanceof RexLambda
+ && expression.equals(((RexLambda) o).expression)
+ && parameters.equals(((RexLambda) o).parameters);
+ }
+
+ @Override public int hashCode() {
+ return Objects.hash(expression, parameters);
+ }
+
+ @Override public String toString() {
+ if (digest == null) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("(");
+ for (Ord<RexLambdaRef> ord : Ord.zip(parameters)) {
+ final RexLambdaRef parameter = ord.e;
+ if (ord.i != 0) {
+ sb.append(", ");
+ }
+ sb.append(parameter.getName());
+ }
+ sb.append(") -> ");
+ sb.append(expression);
+ digest = sb.toString();
+ }
+ return digest;
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/rex/RexLambdaRef.java
b/core/src/main/java/org/apache/calcite/rex/RexLambdaRef.java
new file mode 100644
index 0000000000..8a87846094
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rex/RexLambdaRef.java
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+package org.apache.calcite.rex;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlKind;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Objects;
+
+/**
+ * Variable that references a field of a lambda expression.
+ */
+public class RexLambdaRef extends RexSlot {
+
+ public RexLambdaRef(int index, String name, RelDataType type) {
+ super(name, index, type);
+ }
+
+ @Override public SqlKind getKind() {
+ return SqlKind.LAMBDA_REF;
+ }
+
+ @Override public <R> R accept(RexVisitor<R> visitor) {
+ return visitor.visitLambdaRef(this);
+ }
+
+ @Override public <R, P> R accept(RexBiVisitor<R, P> visitor, P arg) {
+ return (R) null;
+ }
+
+ @Override public boolean equals(final @Nullable Object obj) {
+ return this == obj
+ || obj instanceof RexLambdaRef
+ && index == ((RexLambdaRef) obj).index
+ && type.equals(((RexLambdaRef) obj).type);
+ }
+
+ @Override public int hashCode() {
+ return Objects.hash(type, index);
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/rex/RexProgramBuilder.java
b/core/src/main/java/org/apache/calcite/rex/RexProgramBuilder.java
index 99bd7551eb..2064c4fb85 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexProgramBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexProgramBuilder.java
@@ -922,6 +922,11 @@ public class RexProgramBuilder {
final RexNode expr = super.visitCorrelVariable(variable);
return registerInternal(expr, false);
}
+
+ @Override public RexNode visitLambda(RexLambda lambda) {
+ super.visitLambda(lambda);
+ return registerInternal(lambda, false);
+ }
}
/**
diff --git a/core/src/main/java/org/apache/calcite/rex/RexShuttle.java
b/core/src/main/java/org/apache/calcite/rex/RexShuttle.java
index a300e9a8c7..0e14159fef 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexShuttle.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexShuttle.java
@@ -231,6 +231,15 @@ public class RexShuttle implements RexVisitor<RexNode> {
return rangeRef;
}
+ @Override public RexNode visitLambda(RexLambda lambda) {
+ lambda.getExpression().accept(this);
+ return lambda;
+ }
+
+ @Override public RexNode visitLambdaRef(RexLambdaRef lambdaRef) {
+ return lambdaRef;
+ }
+
/**
* Applies this shuttle to each expression in a list.
*
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
index 312e61b1dc..c81a72543f 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSimplify.java
@@ -1401,6 +1401,13 @@ public class RexSimplify {
return false;
}
+ @Override public Boolean visitLambda(RexLambda lambda) {
+ return lambda.getExpression().accept(this);
+ }
+
+ @Override public Boolean visitLambdaRef(RexLambdaRef lambdaRef) {
+ return true;
+ }
}
/** Analyzes a given {@link RexNode} and decides whenever it is safe to
diff --git a/core/src/main/java/org/apache/calcite/rex/RexSlot.java
b/core/src/main/java/org/apache/calcite/rex/RexSlot.java
index bf6dc1459c..0f73007bfb 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexSlot.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexSlot.java
@@ -22,7 +22,7 @@ import java.util.AbstractList;
import java.util.concurrent.CopyOnWriteArrayList;
/**
- * Abstract base class for {@link RexInputRef} and {@link RexLocalRef}.
+ * Abstract base class for {@link RexInputRef}, {@link RexLocalRef} and {@link
RexLambdaRef}.
*/
public abstract class RexSlot extends RexVariable {
//~ Instance fields --------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
index 0ae2fc31fe..f9390a5b3c 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexUtil.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexUtil.java
@@ -767,6 +767,14 @@ public class RexUtil {
// "<expr>.FIELD" is constant iff "<expr>" is constant.
return fieldAccess.getReferenceExpr().accept(this);
}
+
+ @Override public Boolean visitLambda(RexLambda lambda) {
+ return false;
+ }
+
+ @Override public Boolean visitLambdaRef(RexLambdaRef lambdaRef) {
+ return false;
+ }
}
/**
diff --git a/core/src/main/java/org/apache/calcite/rex/RexVisitor.java
b/core/src/main/java/org/apache/calcite/rex/RexVisitor.java
index b202ded573..f39e41e35d 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexVisitor.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexVisitor.java
@@ -57,6 +57,10 @@ public interface RexVisitor<R> {
R visitPatternFieldRef(RexPatternFieldRef fieldRef);
+ R visitLambda(RexLambda lambda);
+
+ R visitLambdaRef(RexLambdaRef lambdaRef);
+
/** Visits a list and writes the results to another list. */
default void visitList(Iterable<? extends RexNode> exprs, List<R> out) {
for (RexNode expr : exprs) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexVisitorImpl.java
b/core/src/main/java/org/apache/calcite/rex/RexVisitorImpl.java
index 4f1fd8d63e..a19f6b2cfd 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexVisitorImpl.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexVisitorImpl.java
@@ -118,6 +118,14 @@ public class RexVisitorImpl<@Nullable R> implements
RexVisitor<R> {
return null;
}
+ @Override public R visitLambda(RexLambda lambda) {
+ return null;
+ }
+
+ @Override public R visitLambdaRef(RexLambdaRef lambdaRef) {
+ return null;
+ }
+
/**
* Visits an array of expressions, returning the logical 'and' of their
* results.
diff --git a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
index 5277c1bf33..b32f752ad2 100644
--- a/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
+++ b/core/src/main/java/org/apache/calcite/runtime/CalciteResource.java
@@ -241,6 +241,9 @@ public interface CalciteResource {
ExInst<SqlValidatorException> paramNotFoundInFunctionDidYouMean(String a0,
String a1, String a2);
+ @BaseMessage("Param ''{0}'' not found in lambda expression ''{1}''")
+ ExInst<SqlValidatorException> paramNotFoundInLambdaExpression(String a0,
String a1);
+
@BaseMessage("Operand {0} must be a query")
ExInst<SqlValidatorException> needQueryOp(String a0);
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 bc898d3e5e..35034ca910 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlKind.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlKind.java
@@ -403,6 +403,9 @@ public enum SqlKind {
/** {@code CASE} expression. */
CASE,
+ /** {@code LAMBDA} expression. */
+ LAMBDA,
+
/** {@code INTERVAL} expression. */
INTERVAL,
@@ -625,6 +628,12 @@ public enum SqlKind {
*/
LOCAL_REF,
+ /** Reference to lambda expression parameter.
+ *
+ * <p>(Only used at the RexNode level.)
+ */
+ LAMBDA_REF,
+
/**
* Reference to correlation variable.
*
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlLambda.java
b/core/src/main/java/org/apache/calcite/sql/SqlLambda.java
new file mode 100644
index 0000000000..f0d7d5fd0a
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/SqlLambda.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+package org.apache.calcite.sql;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.validate.SqlLambdaScope;
+import org.apache.calcite.sql.validate.SqlValidator;
+import org.apache.calcite.sql.validate.SqlValidatorScope;
+import org.apache.calcite.util.UnmodifiableArrayList;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+
+import static com.google.common.collect.ImmutableList.toImmutableList;
+
+import static org.apache.calcite.sql.SqlPivot.stripList;
+
+/**
+ * A <code>SqlLambda</code> is a node of a parse tree which
+ * represents a lambda expression.
+ */
+public class SqlLambda extends SqlCall {
+
+ public static final SqlOperator OPERATOR = new SqlLambdaOperator();
+
+ SqlNodeList parameters;
+ SqlNode expression;
+
+ public SqlLambda(SqlParserPos pos, SqlNodeList parameters,
+ SqlNode expression) {
+ super(pos);
+ this.parameters = parameters;
+ this.expression = expression;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ @Override public SqlKind getKind() {
+ return SqlKind.LAMBDA;
+ }
+
+ @Override public SqlOperator getOperator() {
+ return OPERATOR;
+ }
+
+ @Override public List<SqlNode> getOperandList() {
+ return UnmodifiableArrayList.of(parameters, expression);
+ }
+
+ @Override public void setOperand(int i, @Nullable SqlNode operand) {
+ switch (i) {
+ case 0:
+ parameters = Objects.requireNonNull((SqlNodeList) operand, "parameters");
+ break;
+ case 1:
+ expression = Objects.requireNonNull(operand, "operand");
+ break;
+ default:
+ throw new AssertionError(i);
+ }
+ }
+
+ @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec)
{
+ if (parameters.size() != 1) {
+ writer.list(SqlWriter.FrameTypeEnum.PARENTHESES, SqlWriter.COMMA,
stripList(parameters));
+ } else {
+ parameters.unparse(writer, leftPrec, rightPrec);
+ }
+ writer.keyword(OPERATOR.getName());
+ expression.unparse(writer, leftPrec, rightPrec);
+ }
+
+ public SqlNodeList getParameters() {
+ return parameters;
+ }
+
+ public SqlNode getExpression() {
+ return expression;
+ }
+
+ /**
+ * The {@code SqlLambdaOperator} represents a lambda expression.
+ * The syntax :
+ * {@code IDENTIFIER -> EXPRESSION} or {@code (IDENTIFIER, IDENTIFIER, ...)
-> EXPRESSION}.
+ */
+ private static class SqlLambdaOperator extends SqlSpecialOperator {
+
+ SqlLambdaOperator() {
+ super("->", SqlKind.LAMBDA);
+ }
+
+ @Override public RelDataType deriveType(
+ SqlValidator validator, SqlValidatorScope scope, SqlCall call) {
+ final SqlLambda lambdaExpr = (SqlLambda) call;
+ final SqlLambdaScope lambdaScope = (SqlLambdaScope) scope;
+ final List<String> paramNames = lambdaExpr.getParameters().stream()
+ .map(SqlNode::toString)
+ .collect(toImmutableList());
+ final List<RelDataType> paramTypes =
lambdaScope.getParameterTypes().values()
+ .stream()
+ .collect(toImmutableList());
+ final RelDataType paramRowType =
+ validator.getTypeFactory().createStructType(paramTypes, paramNames);
+ final RelDataType returnType =
validator.getValidatedNodeType(lambdaExpr.getExpression());
+ return validator.getTypeFactory().createFunctionSqlType(paramRowType,
returnType);
+ }
+ }
+}
diff --git
a/core/src/main/java/org/apache/calcite/sql/type/FunctionSqlType.java
b/core/src/main/java/org/apache/calcite/sql/type/FunctionSqlType.java
new file mode 100644
index 0000000000..ac570a4587
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/type/FunctionSqlType.java
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+package org.apache.calcite.sql.type;
+
+import org.apache.calcite.linq4j.Ord;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFamily;
+import org.apache.calcite.rel.type.RelDataTypeField;
+
+import java.util.Objects;
+
+/**
+ * Function type.
+ * The type of lambda expression can be represented by a function type.
+ */
+public class FunctionSqlType extends AbstractSqlType {
+ private final RelDataType parameterType;
+ private final RelDataType returnType;
+
+ public FunctionSqlType(
+ RelDataType parameterType, RelDataType returnType) {
+ super(SqlTypeName.FUNCTION, true, null);
+ this.parameterType = Objects.requireNonNull(parameterType,
"parameterType");
+ this.returnType = Objects.requireNonNull(returnType, "returnType");
+ computeDigest();
+ }
+
+ @Override protected void generateTypeString(StringBuilder sb, boolean
withDetail) {
+ sb.append("Function");
+ sb.append("(");
+ for (Ord<RelDataTypeField> ord : Ord.zip(parameterType.getFieldList())) {
+ if (ord.i > 0) {
+ sb.append(", ");
+ }
+ RelDataTypeField field = ord.e;
+ sb.append(withDetail ? field.getType().getFullTypeString() :
field.getType().toString());
+ }
+ sb.append(")");
+ sb.append(" -> ");
+ sb.append(withDetail ? returnType.getFullTypeString() :
returnType.toString());
+ }
+
+ @Override public RelDataTypeFamily getFamily() {
+ return this;
+ }
+
+ public RelDataType getReturnType() {
+ return returnType;
+ }
+}
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 8abb7c8178..0b56e1db7b 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
@@ -20,14 +20,20 @@ import org.apache.calcite.avatica.util.TimeUnitRange;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeComparability;
import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlCallBinding;
+import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlIntervalQualifier;
+import org.apache.calcite.sql.SqlLambda;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperandCountRange;
import org.apache.calcite.sql.SqlOperator;
import org.apache.calcite.sql.SqlOperatorBinding;
import org.apache.calcite.sql.SqlUtil;
+import org.apache.calcite.sql.util.SqlBasicVisitor;
+import org.apache.calcite.sql.validate.SqlLambdaScope;
+import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.util.ImmutableIntList;
import org.apache.calcite.util.Pair;
@@ -102,6 +108,27 @@ public abstract class OperandTypes {
return family(families, i -> false);
}
+ /**
+ * Creates a checker that passes if the operand is a function with
+ * a given return type and parameter types. This method can be used
+ * to check a lambda expression.
+ */
+ public static SqlSingleOperandTypeChecker function(SqlTypeFamily
returnTypeFamily,
+ SqlTypeFamily... paramTypeFamilies) {
+ return new LambdaOperandTypeChecker(
+ returnTypeFamily, ImmutableList.copyOf(paramTypeFamilies));
+ }
+
+ /**
+ * Creates a checker that passes if the operand is a function with
+ * a given return type and parameter types. This method can be used
+ * to check a lambda expression.
+ */
+ public static SqlSingleOperandTypeChecker function(SqlTypeFamily
returnTypeFamily,
+ List<SqlTypeFamily> paramTypeFamilies) {
+ return new LambdaOperandTypeChecker(returnTypeFamily, paramTypeFamilies);
+ }
+
/**
* Creates a single-operand checker that passes if the operand's type has a
* particular {@link SqlTypeName}.
@@ -1412,4 +1439,121 @@ public abstract class OperandTypes {
return opName + "(" + typeName.getSpaceName() + ")";
}
}
+
+ /**
+ * Operand type-checking strategy where the type of the operand is a lambda
+ * expression with a given return type and argument types.
+ */
+ private static class LambdaOperandTypeChecker
+ implements SqlSingleOperandTypeChecker {
+
+ private final SqlTypeFamily returnTypeFamily;
+ private final List<SqlTypeFamily> argFamilies;
+
+ LambdaOperandTypeChecker(
+ SqlTypeFamily returnTypeFamily,
+ List<SqlTypeFamily> argFamilies) {
+ this.returnTypeFamily = requireNonNull(returnTypeFamily,
"returnTypeFamily");
+ this.argFamilies = ImmutableList.copyOf(argFamilies);
+ }
+
+ @Override public String getAllowedSignatures(SqlOperator op, String
opName) {
+ ImmutableList.Builder<SqlTypeFamily> builder = ImmutableList.builder();
+ builder.addAll(argFamilies);
+ builder.add(returnTypeFamily);
+
+ return SqlUtil.getAliasedSignature(op, opName, builder.build());
+ }
+
+ @Override public boolean checkOperandTypes(SqlCallBinding callBinding,
+ boolean throwOnFailure) {
+ return false;
+ }
+
+ @Override public boolean checkSingleOperandType(
+ SqlCallBinding callBinding, SqlNode operand, int iFormalOperand,
boolean throwOnFailure) {
+ if (!(operand instanceof SqlLambda)
+ || ((SqlLambda) operand).getParameters().size() !=
argFamilies.size()) {
+ if (throwOnFailure) {
+ throw callBinding.newValidationSignatureError();
+ }
+ return false;
+ }
+
+ 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)) {
+ if (callBinding.isTypeCoercionEnabled()) {
+ return true;
+ }
+
+ if (throwOnFailure) {
+ throw
callBinding.getValidator().newValidationError(lambdaExpr.getExpression(),
+ RESOURCE.nullIllegal());
+ }
+ return false;
+ }
+
+ // 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++) {
+ 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);
+ final RelDataType newType = validator.getValidatedNodeType(lambdaExpr);
+ assert newType instanceof FunctionSqlType;
+ final SqlTypeName returnTypeName =
+ ((FunctionSqlType) newType).getReturnType().getSqlTypeName();
+ if (returnTypeName == SqlTypeName.ANY
+ || returnTypeFamily.getTypeNames().contains(returnTypeName)) {
+ return true;
+ }
+
+ if (throwOnFailure) {
+ throw callBinding.newValidationSignatureError();
+ }
+ return false;
+ }
+
+ /**
+ * 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,
+ * the type cached during the first validation must be cleared.
+ */
+ private static class TypeRemover extends SqlBasicVisitor<Void> {
+ private final SqlValidator validator;
+
+ TypeRemover(SqlValidator validator) {
+ this.validator = validator;
+ }
+
+ @Override public Void visit(SqlIdentifier id) {
+ validator.removeValidatedNodeType(id);
+ return super.visit(id);
+ }
+
+ @Override public Void visit(SqlCall call) {
+ validator.removeValidatedNodeType(call);
+ return super.visit(call);
+ }
+
+ }
+ }
}
diff --git
a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
index 12a560402d..fcb1d65f6d 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFactoryImpl.java
@@ -121,6 +121,12 @@ public class SqlTypeFactoryImpl extends
RelDataTypeFactoryImpl {
return canonize(newType);
}
+ @Override public RelDataType createFunctionSqlType(
+ RelDataType parameterType,
+ RelDataType returnType) {
+ return canonize(new FunctionSqlType(parameterType, returnType));
+ }
+
@Override public RelDataType createMeasureType(RelDataType valueType) {
MeasureSqlType newType = MeasureSqlType.create(valueType);
return canonize(newType);
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
index 7c45e017bd..58b616798e 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeFamily.java
@@ -77,6 +77,7 @@ public enum SqlTypeFamily implements RelDataTypeFamily {
CURSOR,
COLUMN_LIST,
GEO,
+ FUNCTION,
/** Like ANY, but do not even validate the operand. It may not be an
* expression. */
IGNORE;
@@ -215,6 +216,8 @@ public enum SqlTypeFamily implements RelDataTypeFamily {
return ImmutableList.of(SqlTypeName.CURSOR);
case COLUMN_LIST:
return ImmutableList.of(SqlTypeName.COLUMN_LIST);
+ case FUNCTION:
+ return ImmutableList.of(SqlTypeName.FUNCTION);
default:
throw new IllegalArgumentException();
}
@@ -270,6 +273,10 @@ public enum SqlTypeFamily implements RelDataTypeFamily {
return factory.createSqlType(SqlTypeName.CURSOR);
case COLUMN_LIST:
return factory.createSqlType(SqlTypeName.COLUMN_LIST);
+ case FUNCTION:
+ return factory.createFunctionSqlType(
+ factory.createStructType(ImmutableList.of(), ImmutableList.of()),
+ factory.createSqlType(SqlTypeName.ANY));
default:
return null;
}
diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
index 1b1110a24e..442e5ce773 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeName.java
@@ -127,6 +127,7 @@ public enum SqlTypeName {
* do not flag it 'special' (internal). */
GEOMETRY(PrecScale.NO_NO, false, ExtraSqlTypes.GEOMETRY, SqlTypeFamily.GEO),
MEASURE(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY),
+ FUNCTION(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.FUNCTION),
SARG(PrecScale.NO_NO, true, Types.OTHER, SqlTypeFamily.ANY);
public static final int MAX_DATETIME_PRECISION = 3;
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java
b/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java
index d1e1970efd..935e9bc14b 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/DelegatingScope.java
@@ -25,6 +25,7 @@ import org.apache.calcite.schema.CustomColumnResolvingTable;
import org.apache.calcite.schema.Table;
import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlLambda;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
import org.apache.calcite.sql.SqlSelect;
@@ -233,6 +234,8 @@ public abstract class DelegatingScope implements
SqlValidatorScope {
@Override public SqlValidatorScope getOperandScope(SqlCall call) {
if (call instanceof SqlSelect) {
return validator.getSelectScope((SqlSelect) call);
+ } else if (call instanceof SqlLambda) {
+ return validator.getLambdaScope((SqlLambda) call);
}
return this;
}
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/LambdaNamespace.java
b/core/src/main/java/org/apache/calcite/sql/validate/LambdaNamespace.java
new file mode 100644
index 0000000000..216f77fe31
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/validate/LambdaNamespace.java
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+package org.apache.calcite.sql.validate;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlLambda;
+import org.apache.calcite.sql.SqlNode;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Namespace for {@code lambda expression}.
+ */
+public class LambdaNamespace extends AbstractNamespace {
+ private final SqlLambda lambdaExpression;
+
+ /**
+ * Creates a LambdaNamespace.
+ */
+ LambdaNamespace(SqlValidatorImpl validator, SqlLambda lambdaExpression,
+ SqlNode enclosingNode) {
+ super(validator, enclosingNode);
+ this.lambdaExpression = lambdaExpression;
+ }
+
+ @Override protected RelDataType validateImpl(RelDataType targetRowType) {
+ validator.validateLambda(lambdaExpression);
+ requireNonNull(rowType, "rowType");
+ return rowType;
+ }
+
+ @Override public @Nullable SqlNode getNode() {
+ return lambdaExpression;
+ }
+}
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/SqlLambdaScope.java
b/core/src/main/java/org/apache/calcite/sql/validate/SqlLambdaScope.java
new file mode 100644
index 0000000000..ac7e1eac54
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlLambdaScope.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+package org.apache.calcite.sql.validate;
+
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.sql.SqlIdentifier;
+import org.apache.calcite.sql.SqlLambda;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.util.Litmus;
+
+import com.google.common.base.Preconditions;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.calcite.util.Static.RESOURCE;
+
+/**
+ * Scope for a {@link SqlLambda LAMBDA EXPRESSION}.
+ */
+public class SqlLambdaScope extends ListScope {
+ private final SqlLambda lambdaExpr;
+ private final Map<String, RelDataType> parameterTypes;
+
+ public SqlLambdaScope(
+ SqlValidatorScope parent, SqlLambda lambdaExpr) {
+ super(parent);
+ this.lambdaExpr = lambdaExpr;
+
+ // default parameter type is ANY
+ final RelDataType any =
+ validator.typeFactory.createTypeWithNullability(
+ validator.typeFactory.createSqlType(SqlTypeName.ANY), true);
+ parameterTypes = new HashMap<>();
+ lambdaExpr.getParameters().forEach(param ->
parameterTypes.put(param.toString(), any));
+ }
+
+ @Override public SqlNode getNode() {
+ return lambdaExpr;
+ }
+
+ @Override public SqlQualified fullyQualify(SqlIdentifier identifier) {
+ boolean found = lambdaExpr.getParameters()
+ .stream()
+ .anyMatch(param -> param.equalsDeep(identifier, Litmus.IGNORE));
+ if (found) {
+ return SqlQualified.create(this, 1, null, identifier);
+ } else {
+ throw validator.newValidationError(identifier,
+ RESOURCE.paramNotFoundInLambdaExpression(identifier.toString(),
lambdaExpr.toString()));
+ }
+ }
+
+ @Override public @Nullable RelDataType resolveColumn(String columnName,
SqlNode ctx) {
+ Preconditions.checkArgument(parameterTypes.containsKey(columnName),
+ "column %s not found", columnName);
+ return parameterTypes.get(columnName);
+ }
+
+ public Map<String, RelDataType> getParameterTypes() {
+ return parameterTypes;
+ }
+}
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
index 71a5eebb23..80d1734d9c 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidator.java
@@ -33,6 +33,7 @@ import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlInsert;
import org.apache.calcite.sql.SqlIntervalQualifier;
+import org.apache.calcite.sql.SqlLambda;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlMatchRecognize;
import org.apache.calcite.sql.SqlMerge;
@@ -301,6 +302,16 @@ public interface SqlValidator {
*/
void validateMatchRecognize(SqlCall pattern);
+ /**
+ * Validates a lambda expression. lambda expression will be validated twice
+ * during the validation process. The first time is validate lambda
expression
+ * namespace, the second time is when validating higher order function
operands
+ * type check.
+ *
+ * @param lambdaExpr Lambda expression
+ */
+ void validateLambda(SqlLambda lambdaExpr);
+
/**
* Validates a call to an operator.
*
@@ -602,6 +613,14 @@ public interface SqlValidator {
*/
SqlValidatorScope getMatchRecognizeScope(SqlMatchRecognize node);
+ /**
+ * Returns the lambda expression scope.
+ *
+ * @param node Lambda expression
+ * @return naming scope for lambda expression
+ */
+ SqlValidatorScope getLambdaScope(SqlLambda node);
+
/**
* Returns a scope that cannot see anything.
*/
diff --git
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 36c59bc42f..81f2140b6a 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -62,6 +62,7 @@ import org.apache.calcite.sql.SqlIntervalLiteral;
import org.apache.calcite.sql.SqlIntervalQualifier;
import org.apache.calcite.sql.SqlJoin;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLambda;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlMatchRecognize;
import org.apache.calcite.sql.SqlMerge;
@@ -1216,6 +1217,10 @@ public class SqlValidatorImpl implements
SqlValidatorWithHints {
return getScopeOrThrow(node);
}
+ @Override public SqlValidatorScope getLambdaScope(SqlLambda node) {
+ return getScopeOrThrow(node);
+ }
+
@Override public SqlValidatorScope getJoinScope(SqlNode node) {
return requireNonNull(scopes.get(stripAs(node)),
() -> "scope for " + node);
@@ -2901,6 +2906,24 @@ public class SqlValidatorImpl implements
SqlValidatorWithHints {
forceNullable);
break;
+ case LAMBDA:
+ call = (SqlCall) node;
+ SqlLambdaScope lambdaScope =
+ new SqlLambdaScope(parentScope, (SqlLambda) call);
+ scopes.put(call, lambdaScope);
+ final LambdaNamespace lambdaNamespace =
+ new LambdaNamespace(this, (SqlLambda) call, node);
+ registerNamespace(
+ usingScope,
+ alias,
+ lambdaNamespace,
+ forceNullable);
+ operands = call.getOperandList();
+ for (int i = 0; i < operands.size(); i++) {
+ registerOperandSubQueries(parentScope, call, i);
+ }
+ break;
+
case WITH:
registerWith(parentScope, usingScope, (SqlWith) node, enclosingNode,
alias, forceNullable, checkUpdate);
@@ -3241,6 +3264,7 @@ public class SqlValidatorImpl implements
SqlValidatorWithHints {
return;
}
if (node.getKind().belongsTo(SqlKind.QUERY)
+ || node.getKind() == SqlKind.LAMBDA
|| node.getKind() == SqlKind.MULTISET_QUERY_CONSTRUCTOR
|| node.getKind() == SqlKind.MULTISET_VALUE_CONSTRUCTOR) {
registerQuery(parentScope, null, node, node, null, false);
@@ -3935,6 +3959,8 @@ public class SqlValidatorImpl implements
SqlValidatorWithHints {
// only need to check operand[0] for CONVERT or TRANSLATE
SqlNode child = ((SqlCall) stripDot).getOperandList().get(0);
checkRollUp(parent, current, child, scope, contextClause);
+ } else if (stripDot.getKind() == SqlKind.LAMBDA) {
+ // do not need to check lambda
} else {
List<? extends @Nullable SqlNode> children =
((SqlCall) stripDot).getOperandList();
@@ -5614,6 +5640,18 @@ public class SqlValidatorImpl implements
SqlValidatorWithHints {
inWindow = false;
}
+ @Override public void validateLambda(SqlLambda lambdaExpr) {
+ final SqlLambdaScope scope = (SqlLambdaScope) scopes.get(lambdaExpr);
+ requireNonNull(scope, "scope");
+ final LambdaNamespace ns =
+ getNamespaceOrThrow(lambdaExpr).unwrap(LambdaNamespace.class);
+
+ deriveType(scope, lambdaExpr.getExpression());
+ RelDataType type = deriveTypeImpl(scope, lambdaExpr);
+ setValidatedNodeType(lambdaExpr, type);
+ ns.setType(type);
+ }
+
@Override public void validateMatchRecognize(SqlCall call) {
final SqlMatchRecognize matchRecognize = (SqlMatchRecognize) call;
final MatchRecognizeScope scope =
@@ -6728,6 +6766,7 @@ public class SqlValidatorImpl implements
SqlValidatorWithHints {
case CURRENT_VALUE:
case NEXT_VALUE:
case WITH:
+ case LAMBDA:
return call;
default:
break;
diff --git
a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
index bdf337b3d8..e5afc62468 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -83,6 +83,7 @@ import org.apache.calcite.rex.RexDynamicParam;
import org.apache.calcite.rex.RexFieldAccess;
import org.apache.calcite.rex.RexFieldCollation;
import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.rex.RexLambdaRef;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.rex.RexOver;
@@ -118,6 +119,7 @@ import org.apache.calcite.sql.SqlInsert;
import org.apache.calcite.sql.SqlIntervalQualifier;
import org.apache.calcite.sql.SqlJoin;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLambda;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlMatchRecognize;
import org.apache.calcite.sql.SqlMerge;
@@ -160,6 +162,7 @@ import org.apache.calcite.sql.validate.ListScope;
import org.apache.calcite.sql.validate.MatchRecognizeScope;
import org.apache.calcite.sql.validate.ParameterScope;
import org.apache.calcite.sql.validate.SelectScope;
+import org.apache.calcite.sql.validate.SqlLambdaScope;
import org.apache.calcite.sql.validate.SqlMonotonicity;
import org.apache.calcite.sql.validate.SqlNameMatcher;
import org.apache.calcite.sql.validate.SqlQualified;
@@ -2185,6 +2188,37 @@ public class SqlToRelConverter {
return null;
}
+ /**
+ * Converts a lambda expression to a RexNode.
+ *
+ * @param bb Blackboard
+ * @param node Lambda expression
+ * @return Relational expression
+ */
+ private RexNode convertLambda(Blackboard bb, SqlNode node) {
+ final SqlLambda call = (SqlLambda) node;
+ final SqlLambdaScope scope = (SqlLambdaScope)
validator().getLambdaScope(call);
+
+ final Map<String, RexNode> nameToNodeMap = new HashMap<>();
+ final List<RexLambdaRef> parameters = new
ArrayList<>(scope.getParameterTypes().size());
+ final Map<String, RelDataType> parameterTypes = scope.getParameterTypes();
+
+ int i = 0;
+ for (SqlNode p : call.getParameters()) {
+ final String name = p.toString();
+ final RexLambdaRef parameter =
+ new RexLambdaRef(i, name, requireNonNull(parameterTypes.get(name)));
+ parameters.add(parameter);
+ nameToNodeMap.put(name, parameter);
+ i++;
+ }
+
+ final Blackboard lambdaBb = createBlackboard(scope, nameToNodeMap, false);
+ lambdaBb.setRoot(castNonNull(bb.inputs));
+ final RexNode expr = lambdaBb.convertExpression(call.getExpression());
+ return rexBuilder.makeLambdaCall(expr, parameters);
+ }
+
private RexNode convertOver(Blackboard bb, SqlNode node) {
SqlCall call = (SqlCall) node;
SqlCall aggCall = call.operand(0);
@@ -5585,6 +5619,9 @@ public class SqlToRelConverter {
case OVER:
return convertOver(this, expr);
+ case LAMBDA:
+ return convertLambda(this, expr);
+
default:
// fall through
}
diff --git
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
index 66cd5861d2..ba13eabd2c 100644
---
a/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
+++
b/core/src/main/resources/org/apache/calcite/runtime/CalciteResource.properties
@@ -86,6 +86,7 @@ ColumnNotFoundInTable=Column ''{0}'' not found in table
''{1}''
ColumnNotFoundInTableDidYouMean=Column ''{0}'' not found in table ''{1}''; did
you mean ''{2}''?
ColumnAmbiguous=Column ''{0}'' is ambiguous
ParamNotFoundInFunctionDidYouMean = Param ''{0}'' not found in function
''{1}''; did you mean ''{2}''?
+ParamNotFoundInLambdaExpression = Param ''{0}'' not found in lambda expression
''{1}''
NeedQueryOp=Operand {0} must be a query
NeedSameTypeParameter=Parameters must be of the same type
CanNotApplyOp2Type=Cannot apply ''{0}'' to arguments of type {1}. Supported
form(s): {2}
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 709e83e698..9eeabcceae 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
@@ -7339,6 +7339,49 @@ class RelToSqlConverterTest {
.ok(expected7);
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-3679">[CALCITE-3679]
+ * Allow lambda expressions in SQL queries</a>. */
+ @Test void testHigherOrderFunction() {
+ final String sql1 = "select higher_order_function(1, (x, y) ->
char_length(x) + 1)";
+ final String expected1 = "SELECT HIGHER_ORDER_FUNCTION("
+ + "1, (\"X\", \"Y\") -> CHAR_LENGTH(\"X\") + 1)\nFROM (VALUES (0)) AS
\"t\" (\"ZERO\")";
+ sql(sql1).ok(expected1);
+
+ final String sql2 = "select higher_order_function2(1, () -> abs(-1))";
+ final String expected2 = "SELECT HIGHER_ORDER_FUNCTION2("
+ + "1, () -> ABS(-1))\nFROM (VALUES (0)) AS \"t\" (\"ZERO\")";
+ sql(sql2).ok(expected2);
+
+ final String sql3 = "select \"department_id\", "
+ + "higher_order_function(1, (department_id, y) -> department_id + 1)
from \"employee\"";
+ final String expected3 = "SELECT \"department_id\",
HIGHER_ORDER_FUNCTION(1, "
+ + "(\"DEPARTMENT_ID\", \"Y\") -> CAST(\"DEPARTMENT_ID\" AS INTEGER) +
1)\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(sql3).ok(expected3);
+
+ final String sql4 = "select higher_order_function2(1, () -> null)";
+ final String expected4 = "SELECT HIGHER_ORDER_FUNCTION2("
+ + "1, () -> NULL)\nFROM (VALUES (0)) AS \"t\" (\"ZERO\")";
+ sql(sql4).ok(expected4);
+
+ final String sql5 = "select \"employee_id\", "
+ + "higher_order_function("
+ + "\"employee_id\", (product_id, employee_id) ->
char_length(product_id) + employee_id"
+ + ") from \"employee\"";
+ final String expected5 = "SELECT \"employee_id\", HIGHER_ORDER_FUNCTION("
+ + "\"employee_id\", (\"PRODUCT_ID\", \"EMPLOYEE_ID\") -> "
+ + "CHAR_LENGTH(\"PRODUCT_ID\") + \"EMPLOYEE_ID\")\n"
+ + "FROM \"foodmart\".\"employee\"";
+ sql(sql5).ok(expected5);
+
+ final String sql6 = "select higher_order_function(1, (y, x) -> x +
char_length(y) + 1)";
+ final String expected6 = "SELECT HIGHER_ORDER_FUNCTION("
+ + "1, (\"Y\", \"X\") -> \"X\" + CHAR_LENGTH(\"Y\") + 1)\n"
+ + "FROM (VALUES (0)) AS \"t\" (\"ZERO\")";
+ sql(sql6).ok(expected6);
+ }
+
/** 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 e828a9ddf3..6a43a487f7 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -101,6 +101,42 @@ class SqlToRelConverterTest extends SqlToRelTestBase {
sql(sql).ok();
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-3679">[CALCITE-3679]
+ * Allow lambda expressions in SQL queries</a>. */
+ @Test void testLambdaExpression() {
+ final String sql = "select higher_order_function(1, (x, y) -> y + 1)";
+ fixture()
+ .withFactory(c ->
+ c.withOperatorTable(t -> MockSqlOperatorTable.standard().extend()))
+ .withSql(sql)
+ .ok();
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-3679">[CALCITE-3679]
+ * Allow lambda expressions in SQL queries</a>. */
+ @Test void testLambdaExpression2() {
+ final String sql = "select higher_order_function2(1, () -> -1)";
+ fixture()
+ .withFactory(c ->
+ c.withOperatorTable(t -> MockSqlOperatorTable.standard().extend()))
+ .withSql(sql)
+ .ok();
+ }
+
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-3679">[CALCITE-3679]
+ * Allow lambda expressions in SQL queries</a>. */
+ @Test void testLambdaExpression3() {
+ final String sql = "select higher_order_function(deptno, (x, deptno) ->
deptno + 1) from emp";
+ fixture()
+ .withFactory(c ->
+ c.withOperatorTable(t -> MockSqlOperatorTable.standard().extend()))
+ .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 0e3d14607d..4dc78de70d 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -6687,6 +6687,48 @@ public class SqlValidatorTest extends
SqlValidatorTestCase {
.fails("'PERCENTILE_DISC' requires precisely one ORDER BY key");
}
+ /** Test case for
+ * <a
href="https://issues.apache.org/jira/browse/CALCITE-3679">[CALCITE-3679]
+ * Allow lambda expressions in SQL queries</a>. */
+ @Test void testHigherOrderFunction() {
+ final SqlValidatorFixture s = fixture()
+ .withOperatorTable(MockSqlOperatorTable.standard().extend());
+ 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_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)")
+ .type("RecordType(INTEGER NOT NULL EXPR$0) NOT NULL");
+ s.withSql("select HIGHER_ORDER_FUNCTION2(1, () -> 0.1)")
+ .type("RecordType(INTEGER NOT NULL EXPR$0) NOT NULL");
+
+ // test for type check
+ s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> ^x + 1^)")
+ .withTypeCoercion(false)
+ .fails("(?s)Cannot apply '\\+' to arguments of type '<VARCHAR> \\+
<INTEGER>'\\..*");
+ s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> ^null^)")
+ .withTypeCoercion(false)
+ .fails("(?s)Illegal use of 'NULL'.*");
+ s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> ^array[1] = x^)")
+ .fails("Cannot apply '=' to arguments of type '<INTEGER ARRAY> =
<VARCHAR>'.*");
+ s.withSql("select ^HIGHER_ORDER_FUNCTION(1, null)^")
+ .fails("Cannot apply '(?s).*HIGHER_ORDER_FUNCTION' to arguments of
type "
+ + "'HIGHER_ORDER_FUNCTION\\(<INTEGER>, <NULL>\\)'.*");
+ s.withSql("select ^HIGHER_ORDER_FUNCTION(1, (x, y, z) -> x + 1)^")
+ .fails("Cannot apply '(?s).*HIGHER_ORDER_FUNCTION' to arguments of
type "
+ + "'HIGHER_ORDER_FUNCTION\\(<INTEGER>, <FUNCTION\\(ANY, ANY,
ANY\\) -> ANY>\\)'.*");
+
+ // test for illegal parameters
+ s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> x + 1 + ^emp.deptno^)
from emp")
+ .fails("Param 'EMP\\.DEPTNO' not found in lambda expression "
+ + "'\\(`X`, `Y`\\) -> `X` \\+ 1 \\+ `EMP`\\.`DEPTNO`'");
+ s.withSql("select HIGHER_ORDER_FUNCTION(1, (x, y) -> x + 1 + ^deptno^)
from emp")
+ .fails("Param 'DEPTNO' not found in lambda expression "
+ + "'\\(`X`, `Y`\\) -> `X` \\+ 1 \\+ `DEPTNO`'");
+ }
+
@Test void testPercentileFunctionsBigQuery() {
final SqlOperatorTable opTable = operatorTableFor(SqlLibrary.BIG_QUERY);
final String sql = "select\n"
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 524f8c8f4f..bf246b3a0b 100644
--- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml
@@ -4150,6 +4150,39 @@ from emp]]>
<![CDATA[
LogicalProject(EXPR$0=[FORMAT JSON($1)], EXPR$1=[FORMAT JSON($1)],
EXPR$2=[FORMAT JSON($1)], EXPR$3=[FORMAT JSON($1)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testLambdaExpression">
+ <Resource name="sql">
+ <![CDATA[select higher_order_function(1, (x, y) -> y + 1)]]>
+ </Resource>
+ <Resource name="plan">
+ <![CDATA[
+LogicalProject(EXPR$0=[HIGHER_ORDER_FUNCTION(1, (X, Y) -> +(Y, 1))])
+ LogicalValues(tuples=[[{ 0 }]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testLambdaExpression2">
+ <Resource name="sql">
+ <![CDATA[select higher_order_function2(1, () -> -1)]]>
+ </Resource>
+ <Resource name="plan">
+ <![CDATA[
+LogicalProject(EXPR$0=[HIGHER_ORDER_FUNCTION2(1, () -> -1)])
+ LogicalValues(tuples=[[{ 0 }]])
+]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testLambdaExpression3">
+ <Resource name="sql">
+ <![CDATA[select higher_order_function(deptno, (x, deptno) -> deptno + 1)
from emp]]>
+ </Resource>
+ <Resource name="plan">
+ <![CDATA[
+LogicalProject(EXPR$0=[HIGHER_ORDER_FUNCTION($7, (X, DEPTNO) -> +(DEPTNO, 1))])
+ LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
</TestCase>
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index 873b658057..80ca0e3a92 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -1205,6 +1205,7 @@ Note:
| MULTISET | Unordered collection that may contain duplicates | Example: int
multiset
| ARRAY | Ordered, contiguous collection that may contain duplicates |
Example: varchar(10) array
| CURSOR | Cursor over the result of executing a query |
+| FUNCTION | A function definition that is not bound to an identifier, it is
not fully supported in CAST or DDL | Example FUNCTION(INTEGER, VARCHAR(30)) ->
INTEGER
Note:
@@ -3117,6 +3118,24 @@ Result
|:-----------:|:-----------:|:-----------:|:-----------:|
| Aa_Bb_CcD_d | Aa_Bb_CcD_d | Aa_Bb_CcD_d | Aa_Bb_CcD_d |
+### Higher-order Functions
+
+A higher-order function takes one or more lambda expressions as arguments.
+
+Lambda Expression Syntax:
+{% highlight sql %}
+lambdaExpression:
+ parameters '->' expression
+
+parameters:
+ '(' [ identifier [, identifier ] ] ')'
+ | identifier
+{% endhighlight %}
+
+Higher-order functions are not included in the SQL standard, so all the
functions will be listed in the
+[Dialect-specific OperatorsPermalink]({{ site.baseurl
}}/docs/reference.html#dialect-specific-operators)
+as well.
+
## 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/sql/parser/SqlParserTest.java
b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
index 7672a2fba9..1b6612a37f 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java
@@ -21,6 +21,7 @@ import org.apache.calcite.sql.SqlDialect;
import org.apache.calcite.sql.SqlExplain;
import org.apache.calcite.sql.SqlIdentifier;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlLambda;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlNodeList;
@@ -9215,6 +9216,46 @@ public class SqlParserTest {
assertThat(hoisted.substitute(SqlParserTest::varToStr), is(expected2));
}
+ /** Tests {@link SqlLambda}. */
+ @Test public void testLambdaExpression() {
+ sql("select higher_order_func(1, (x, y) -> (x + y)) from t")
+ .ok("SELECT `HIGHER_ORDER_FUNC`(1, (`X`, `Y`) -> (`X` + `Y`))\n"
+ + "FROM `T`");
+
+ sql("select higher_order_func(1, (x, y) -> x + char_length(y)) from t")
+ .ok("SELECT `HIGHER_ORDER_FUNC`(1, (`X`, `Y`) -> (`X` +
CHAR_LENGTH(`Y`)))\n"
+ + "FROM `T`");
+
+ sql("select higher_order_func(a -> a + 1, 1) from t")
+ .ok("SELECT `HIGHER_ORDER_FUNC`(`A` -> (`A` + 1), 1)\n"
+ + "FROM `T`");
+
+ sql("select higher_order_func((a) -> 1, 1) from t")
+ .ok("SELECT `HIGHER_ORDER_FUNC`(`A` -> 1, 1)\n"
+ + "FROM `T`");
+
+ sql("select higher_order_func(() -> 1 + 1, 1) from t")
+ .ok("SELECT `HIGHER_ORDER_FUNC`(() -> (1 + 1), 1)\n"
+ + "FROM `T`");
+
+ final String errorMessage1 = "(?s).*Encountered \"\\)\" at .*";
+ sql("select (^)^ -> 1")
+ .fails(errorMessage1);
+
+ sql("select * from t where (^)^ -> a = 1")
+ .fails(errorMessage1);
+
+ final String errorMessage2 = "(?s).*Encountered \"->\" at .*";
+ sql("select (a, b) ^->^ a + b")
+ .fails(errorMessage2);
+
+ sql("select * from t where (a, b) ^->^ a = 1")
+ .fails(errorMessage2);
+
+ sql("select 1 || (a, b) ^->^ a + b")
+ .fails(errorMessage2);
+ }
+
protected static String varToStr(Hoist.Variable v) {
if (v.node instanceof SqlLiteral) {
SqlLiteral literal = (SqlLiteral) v.node;
diff --git
a/testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java
b/testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java
index 02d32c9eca..32c39f4b47 100644
--- a/testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java
+++ b/testkit/src/main/java/org/apache/calcite/sql/test/AbstractSqlTester.java
@@ -25,6 +25,7 @@ import org.apache.calcite.rex.RexNode;
import org.apache.calcite.runtime.PairList;
import org.apache.calcite.runtime.Utilities;
import org.apache.calcite.sql.SqlCall;
+import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlLiteral;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.SqlOperator;
@@ -363,6 +364,9 @@ public abstract class AbstractSqlTester implements
SqlTester, AutoCloseable {
@Override public SqlNode visit(SqlCall call) {
SqlOperator operator = call.getOperator();
+ if (operator.getKind() == SqlKind.LAMBDA) {
+ return call;
+ }
if (operator instanceof SqlUnresolvedFunction) {
final SqlUnresolvedFunction unresolvedFunction =
(SqlUnresolvedFunction) operator;
diff --git
a/testkit/src/main/java/org/apache/calcite/test/MockSqlOperatorTable.java
b/testkit/src/main/java/org/apache/calcite/test/MockSqlOperatorTable.java
index b4faea11c2..513d0f1086 100644
--- a/testkit/src/main/java/org/apache/calcite/test/MockSqlOperatorTable.java
+++ b/testkit/src/main/java/org/apache/calcite/test/MockSqlOperatorTable.java
@@ -19,6 +19,7 @@ package org.apache.calcite.test;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.sql.SqlAggFunction;
+import org.apache.calcite.sql.SqlBasicFunction;
import org.apache.calcite.sql.SqlCallBinding;
import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlFunctionCategory;
@@ -97,7 +98,9 @@ public class MockSqlOperatorTable extends
ChainedSqlOperatorTable {
new ScoreTableFunction(),
new TopNTableFunction(),
new SimilarlityTableFunction(),
- new InvalidTableFunction())));
+ new InvalidTableFunction(),
+ HIGHER_ORDER_FUNCTION,
+ HIGHER_ORDER_FUNCTION2)));
}
/** Adds a library set. */
@@ -623,4 +626,21 @@ public class MockSqlOperatorTable extends
ChainedSqlOperatorTable {
return typeFactory.createSqlType(SqlTypeName.BIGINT);
}
}
+
+ private static final SqlFunction HIGHER_ORDER_FUNCTION =
+ SqlBasicFunction.create("HIGHER_ORDER_FUNCTION",
+ ReturnTypes.ARG0,
+ OperandTypes.sequence("HIGHER_ORDER_FUNCTION(INTEGER,
FUNCTION(STRING, ANY) -> NUMERIC)",
+ OperandTypes.family(SqlTypeFamily.INTEGER),
+ OperandTypes.function(
+ SqlTypeFamily.NUMERIC, SqlTypeFamily.STRING,
SqlTypeFamily.ANY)),
+ SqlFunctionCategory.SYSTEM);
+
+ private static final SqlFunction HIGHER_ORDER_FUNCTION2 =
+ SqlBasicFunction.create("HIGHER_ORDER_FUNCTION2",
+ ReturnTypes.ARG0,
+ OperandTypes.sequence("HIGHER_ORDER_FUNCTION(INTEGER, FUNCTION() ->
NUMERIC)",
+ OperandTypes.family(SqlTypeFamily.INTEGER),
+ OperandTypes.function(SqlTypeFamily.NUMERIC)),
+ SqlFunctionCategory.SYSTEM);
}