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);
 }

Reply via email to