This is an automated email from the ASF dual-hosted git repository.

mbudiu 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 4a1da22e6e [CALCITE-5855] Implement frame exclusion in window functions
4a1da22e6e is described below

commit 4a1da22e6e223440de65f0130a7f947c85b8ff5b
Author: Itiel Sadeh <iti...@sqreamtech.com>
AuthorDate: Mon Jun 3 15:39:09 2024 +0300

    [CALCITE-5855] Implement frame exclusion in window functions
---
 core/src/main/codegen/templates/Parser.jj          |  26 ++++-
 .../adapter/enumerable/EnumerableWindow.java       |  46 ++++++++-
 .../calcite/adapter/enumerable/RexImpTable.java    |   3 +-
 .../calcite/adapter/enumerable/WinAggContext.java  |   4 +
 .../java/org/apache/calcite/rel/core/Window.java   |  15 ++-
 .../apache/calcite/rel/externalize/RelJson.java    |  29 +++++-
 .../apache/calcite/rel/logical/LogicalWindow.java  |  16 ++-
 .../apache/calcite/rel/rel2sql/SqlImplementor.java |  17 +++-
 .../rel/rules/ProjectWindowTransposeRule.java      |   2 +-
 .../calcite/rel/rules/ReduceExpressionsRule.java   |   2 +-
 .../java/org/apache/calcite/rex/RexBuilder.java    |  50 +++++++++-
 .../java/org/apache/calcite/rex/RexShuttle.java    |   3 +-
 .../java/org/apache/calcite/rex/RexWindow.java     |  22 ++++-
 .../org/apache/calcite/rex/RexWindowExclusion.java |  64 ++++++++++++
 .../java/org/apache/calcite/sql/SqlWindow.java     | 109 +++++++++++++++++++--
 .../apache/calcite/sql2rel/SqlToRelConverter.java  |   9 +-
 .../java/org/apache/calcite/tools/RelBuilder.java  |  32 ++++--
 .../rel/logical/ToLogicalConverterTest.java        |  27 +++++
 core/src/test/resources/sql/winagg.iq              |  64 ++++++++++++
 .../calcite/piglet/PigRelToSqlConverter.java       |   1 +
 site/_docs/algebra.md                              |   1 +
 site/_docs/reference.md                            |  17 +++-
 .../apache/calcite/sql/parser/SqlParserTest.java   |  16 +++
 23 files changed, 532 insertions(+), 43 deletions(-)

diff --git a/core/src/main/codegen/templates/Parser.jj 
b/core/src/main/codegen/templates/Parser.jj
index e8c59ba374..c11606d589 100644
--- a/core/src/main/codegen/templates/Parser.jj
+++ b/core/src/main/codegen/templates/Parser.jj
@@ -2736,6 +2736,7 @@ SqlWindow WindowSpecification() :
     final SqlNodeList orderList;
     final SqlLiteral isRows;
     final SqlNode lowerBound, upperBound;
+    final SqlLiteral exclude;
     final Span s, s1, s2;
     final SqlLiteral allowPartial;
 }
@@ -2768,9 +2769,11 @@ SqlWindow WindowSpecification() :
             lowerBound = WindowRange()
             { upperBound = null; }
         )
+        exclude = WindowExclusion()
     |
         {
             isRows = SqlLiteral.createBoolean(false, SqlParserPos.ZERO);
+            exclude = SqlWindow.createExcludeNoOthers(getPos());
             lowerBound = upperBound = null;
         }
     )
@@ -2787,7 +2790,7 @@ SqlWindow WindowSpecification() :
     <RPAREN>
     {
         return SqlWindow.create(null, id, partitionList, orderList,
-            isRows, lowerBound, upperBound, allowPartial, s.end(this));
+            isRows, lowerBound, upperBound, allowPartial, exclude, 
s.end(this));
     }
 }
 
@@ -2826,6 +2829,27 @@ SqlNode WindowRange() :
     )
 }
 
+/** Parses an exclusion clause for WINDOW FRAME. */
+SqlLiteral WindowExclusion() :
+{
+}
+{
+    (
+        <EXCLUDE>
+        (
+            <CURRENT> <ROW> { return 
SqlWindow.createExcludeCurrentRow(getPos()); }
+        |
+            <NO> <OTHERS> { return SqlWindow.createExcludeNoOthers(getPos()); }
+        |
+            <GROUP> { return SqlWindow.createExcludeGroup(getPos()); }
+        |
+            <TIES> { return SqlWindow.createExcludeTies(getPos()); }
+        )
+    |
+        { return SqlWindow.createExcludeNoOthers(SqlParserPos.ZERO); }
+    )
+}
+
 /** Parses a QUALIFY clause for SELECT. */
 SqlNode Qualify() :
 {
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
index 3ee72a35d1..7a4b2687be 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindow.java
@@ -46,6 +46,7 @@ import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexWindowBound;
+import org.apache.calcite.rex.RexWindowExclusion;
 import org.apache.calcite.runtime.SortedMultiMap;
 import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.sql.validate.SqlConformance;
@@ -301,7 +302,7 @@ public class EnumerableWindow extends Window implements 
EnumerableRel {
       }
 
       declareAndResetState(typeFactory, builder, result, windowIdx, aggs,
-          outputPhysType, outputRow);
+          outputPhysType, outputRow, group.exclude);
 
       // There are assumptions that minX==0. If ever change this, look for
       // frameRowCount, bounds checking, etc
@@ -403,8 +404,14 @@ public class EnumerableWindow extends Window implements 
EnumerableRel {
           group.lowerBound.isUnbounded() && group.lowerBound.isPreceding()
           ? Expressions.constant(false)
           : Expressions.notEqual(startX, prevStart);
+
+      Expression isExcluding =
+          Expressions.constant(group.exclude != 
RexWindowExclusion.EXCLUDE_NO_OTHER);
+
+      // If there's exclude clause we need to recompute the window every time, 
as rows can affect
+      // differently for the same frame.
       Expression needRecomputeWindow =
-          Expressions.orElse(lowerBoundCanChange,
+          Expressions.orElse(Expressions.orElse(isExcluding, 
lowerBoundCanChange),
               Expressions.lessThan(endX, prevEnd));
 
       BlockStatement resetWindowState = builder6.toBlock();
@@ -458,15 +465,17 @@ public class EnumerableWindow extends Window implements 
EnumerableRel {
       };
 
       implementAdd(aggs, builder7, resultContextBuilder, rexArguments, jDecl);
-
       BlockStatement forBlock = builder7.toBlock();
+
+      // Don't run the aggregate function if current row is excluded
+      Statement exclude = buildExcludeGuard(group, comparator_, i_, jDecl, 
rows_, forBlock);
       if (!forBlock.statements.isEmpty()) {
         // For instance, row_number does not use for loop to compute the value
         Statement forAggLoop =
             Expressions.for_(Arrays.asList(jDecl),
                 Expressions.lessThanOrEqual(jDecl.parameter, endX),
                 Expressions.preIncrementAssign(jDecl.parameter),
-                forBlock);
+                exclude);
         if (!hasRows.equals(Expressions.constant(true))) {
           forAggLoop = Expressions.ifThen(hasRows, forAggLoop);
         }
@@ -520,6 +529,29 @@ public class EnumerableWindow extends Window implements 
EnumerableRel {
     return implementor.result(inputPhysType, builder.toBlock());
   }
 
+  private Statement buildExcludeGuard(Group group, Expression comparator,
+      ParameterExpression currentRow,
+      DeclarationStatement jDecl, Expression rows, BlockStatement forBlock) {
+    if (group.exclude == RexWindowExclusion.EXCLUDE_CURRENT_ROW) {
+      return Expressions.ifThen(Expressions.notEqual(currentRow, 
jDecl.parameter), forBlock);
+    } else if (group.exclude == RexWindowExclusion.EXCLUDE_GROUP) {
+      return Expressions.ifThen(
+          Expressions.notEqual(Expressions.constant(0),
+              Expressions.call(comparator, 
BuiltInMethod.COMPARATOR_COMPARE.method,
+                  Expressions.arrayIndex(rows, currentRow),
+                  Expressions.arrayIndex(rows, jDecl.parameter))), forBlock);
+    } else if (group.exclude == RexWindowExclusion.EXCLUDE_TIES) {
+      return Expressions.ifThen(
+          Expressions.or(Expressions.equal(currentRow, jDecl.parameter),
+              Expressions.notEqual(Expressions.constant(0),
+                  Expressions.call(comparator, 
BuiltInMethod.COMPARATOR_COMPARE.method,
+                      Expressions.arrayIndex(rows, currentRow),
+                      Expressions.arrayIndex(rows, jDecl.parameter)))), 
forBlock);
+    } else {
+      return forBlock;
+    }
+  }
+
   private static Function<BlockBuilder, WinAggFrameResultContext>
       getBlockBuilderWinAggFrameResultContextFunction(
       final JavaTypeFactory typeFactory, final SqlConformance conformance,
@@ -741,7 +773,7 @@ public class EnumerableWindow extends Window implements 
EnumerableRel {
   private void declareAndResetState(final JavaTypeFactory typeFactory,
       BlockBuilder builder, final Result result, int windowIdx,
       List<AggImpState> aggs, PhysType outputPhysType,
-      List<Expression> outputRow) {
+      List<Expression> outputRow, RexWindowExclusion exclusion) {
     for (final AggImpState agg : aggs) {
       agg.context =
           new WinAggContext() {
@@ -782,6 +814,10 @@ public class EnumerableWindow extends Window implements 
EnumerableRel {
             @Override public List<? extends Type> keyTypes() {
               throw new UnsupportedOperationException();
             }
+
+            @Override public RexWindowExclusion getExclude() {
+              return exclusion;
+            }
           };
       String aggName = "a" + agg.aggIdx;
       if (CalciteSystemProperty.DEBUG.value()) {
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
index a76eb0aa01..18a745e7c8 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexImpTable.java
@@ -44,6 +44,7 @@ import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexPatternFieldRef;
+import org.apache.calcite.rex.RexWindowExclusion;
 import org.apache.calcite.runtime.FlatLists;
 import org.apache.calcite.runtime.SqlFunctions;
 import org.apache.calcite.schema.FunctionContext;
@@ -1441,7 +1442,7 @@ public class RexImpTable {
           break;
         }
       }
-      if (!hasNullable) {
+      if (!hasNullable && info.getExclude() == 
RexWindowExclusion.EXCLUDE_NO_OTHER) {
         justFrameRowCount = true;
         return Collections.emptyList();
       }
diff --git 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/WinAggContext.java 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/WinAggContext.java
index aa96d09b77..28fe9a96f5 100644
--- 
a/core/src/main/java/org/apache/calcite/adapter/enumerable/WinAggContext.java
+++ 
b/core/src/main/java/org/apache/calcite/adapter/enumerable/WinAggContext.java
@@ -16,10 +16,14 @@
  */
 package org.apache.calcite.adapter.enumerable;
 
+import org.apache.calcite.rex.RexWindowExclusion;
+
 /**
  * Marker interface to allow
  * {@link org.apache.calcite.adapter.enumerable.AggImplementor}
  * to tell if it is used in regular or windowed context.
  */
 public interface WinAggContext extends AggContext {
+  /** The exclude clause of the group of the window function. */
+  RexWindowExclusion getExclude();
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Window.java 
b/core/src/main/java/org/apache/calcite/rel/core/Window.java
index 4f76256930..86a8822dc7 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Window.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Window.java
@@ -39,6 +39,7 @@ import org.apache.calcite.rex.RexLocalRef;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexSlot;
 import org.apache.calcite.rex.RexWindowBound;
+import org.apache.calcite.rex.RexWindowExclusion;
 import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.ImmutableIntList;
@@ -223,8 +224,8 @@ public abstract class Window extends SingleRel implements 
Hintable {
   /**
    * Group of windowed aggregate calls that have the same window specification.
    *
-   * <p>The specification is defined by an upper and lower bound, and
-   * also has zero or more partitioning columns.
+   * <p>The specification is defined by an upper and lower bound, exclusion 
clause,
+   * and also has zero or more partitioning columns.
    *
    * <p>A window is either logical or physical. A physical window is measured
    * in terms of row count. A logical window is measured in terms of rows
@@ -247,6 +248,7 @@ public abstract class Window extends SingleRel implements 
Hintable {
     public final boolean isRows;
     public final RexWindowBound lowerBound;
     public final RexWindowBound upperBound;
+    public final RexWindowExclusion exclude;
     public final RelCollation orderKeys;
     private final String digest;
 
@@ -262,12 +264,14 @@ public abstract class Window extends SingleRel implements 
Hintable {
         boolean isRows,
         RexWindowBound lowerBound,
         RexWindowBound upperBound,
+        RexWindowExclusion exclude,
         RelCollation orderKeys,
         List<RexWinAggCall> aggCalls) {
       this.keys = Objects.requireNonNull(keys, "keys");
       this.isRows = isRows;
       this.lowerBound = Objects.requireNonNull(lowerBound, "lowerBound");
       this.upperBound = Objects.requireNonNull(upperBound, "upperBound");
+      this.exclude = exclude;
       this.orderKeys = Objects.requireNonNull(orderKeys, "orderKeys");
       this.aggCalls = ImmutableList.copyOf(aggCalls);
       this.digest = computeString();
@@ -319,6 +323,9 @@ public abstract class Window extends SingleRel implements 
Hintable {
         buf.append(lowerBound);
         buf.append(" and ");
         buf.append(upperBound);
+        if (exclude != RexWindowExclusion.EXCLUDE_NO_OTHER) {
+          buf.append(" ").append(exclude);
+        }
       }
       if (!aggCalls.isEmpty()) {
         if (buf.length() > i) {
@@ -358,7 +365,9 @@ public abstract class Window extends SingleRel implements 
Hintable {
     public boolean isAlwaysNonEmpty() {
       int lowerKey = lowerBound.getOrderKey();
       int upperKey = upperBound.getOrderKey();
-      return lowerKey > -1 && lowerKey <= upperKey;
+      return lowerKey > -1 && lowerKey <= upperKey
+            && (exclude == RexWindowExclusion.EXCLUDE_NO_OTHER
+               || exclude == RexWindowExclusion.EXCLUDE_TIES);
     }
 
     /**
diff --git a/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java 
b/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java
index 3f7cbbca97..c1bcab8616 100644
--- a/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelJson.java
@@ -50,6 +50,7 @@ import org.apache.calcite.rex.RexSlot;
 import org.apache.calcite.rex.RexWindow;
 import org.apache.calcite.rex.RexWindowBound;
 import org.apache.calcite.rex.RexWindowBounds;
+import org.apache.calcite.rex.RexWindowExclusion;
 import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.sql.SqlFunction;
 import org.apache.calcite.sql.SqlIdentifier;
@@ -772,11 +773,18 @@ public class RelJson {
             upperBound = null;
             physical = false;
           }
+          final RexWindowExclusion exclude;
+          if (window.get("exclude") != null) {
+            exclude = toRexWindowExclusion((Map) window.get("exclude"));
+          } else {
+            exclude = RexWindowExclusion.EXCLUDE_NO_OTHER;
+          }
           final boolean distinct = get((Map<String, Object>) map, "distinct");
           return rexBuilder.makeOver(type, operator, rexOperands, 
partitionKeys,
               ImmutableList.copyOf(orderKeys),
               requireNonNull(lowerBound, "lowerBound"),
               requireNonNull(upperBound, "upperBound"),
+              requireNonNull(exclude, "exclude"),
               physical,
               true, false, distinct, false);
         } else {
@@ -969,6 +977,25 @@ public class RelJson {
     }
   }
 
+  private @Nullable RexWindowExclusion toRexWindowExclusion(@Nullable 
Map<String, Object> map) {
+    if (map == null) {
+      return null;
+    }
+    final String type = get(map, "type");
+    switch (type) {
+    case "CURRENT_ROW":
+      return RexWindowExclusion.EXCLUDE_CURRENT_ROW;
+    case "GROUP":
+      return RexWindowExclusion.EXCLUDE_GROUP;
+    case "TIES":
+      return RexWindowExclusion.EXCLUDE_TIES;
+    case "NO OTHERS":
+      return RexWindowExclusion.EXCLUDE_NO_OTHER;
+    default:
+      throw new UnsupportedOperationException(
+          "cannot convert " + type + " to rex window exclusion");
+    }
+  }
   private @Nullable RexWindowBound toRexWindowBound(RelInput input,
       @Nullable Map<String, Object> map) {
     if (map == null) {
@@ -988,7 +1015,7 @@ public class RelJson {
     case "FOLLOWING":
       return RexWindowBounds.following(toRex(input, get(map, "offset")));
     default:
-      throw new UnsupportedOperationException("cannot convert type to rex 
window bound " + type);
+      throw new UnsupportedOperationException("cannot convert " + type + " to 
rex window bound");
     }
   }
 
diff --git 
a/core/src/main/java/org/apache/calcite/rel/logical/LogicalWindow.java 
b/core/src/main/java/org/apache/calcite/rel/logical/LogicalWindow.java
index 3bc23b31f8..3ea854331c 100644
--- a/core/src/main/java/org/apache/calcite/rel/logical/LogicalWindow.java
+++ b/core/src/main/java/org/apache/calcite/rel/logical/LogicalWindow.java
@@ -34,6 +34,7 @@ import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.rex.RexWindow;
 import org.apache.calcite.rex.RexWindowBound;
+import org.apache.calcite.rex.RexWindowExclusion;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.Litmus;
@@ -68,8 +69,8 @@ public final class LogicalWindow extends Window {
    *
    * @param cluster Cluster
    * @param traitSet Trait set
-   * @param hints   Hints for this node
-   * @param input   Input relational expression
+   * @param hints Hints for this node
+   * @param input Input relational expression
    * @param constants List of constants that are additional inputs
    * @param rowType Output row type
    * @param groups Window groups
@@ -199,6 +200,7 @@ public final class LogicalWindow extends Window {
               windowKey.isRows,
               windowKey.lowerBound.accept(toInputRefs),
               windowKey.upperBound.accept(toInputRefs),
+              windowKey.exclude,
               windowKey.orderKeys,
               aggCalls));
     }
@@ -334,22 +336,25 @@ public final class LogicalWindow extends Window {
     private final boolean isRows;
     private final RexWindowBound lowerBound;
     private final RexWindowBound upperBound;
+    private final RexWindowExclusion exclude;
 
     WindowKey(
         ImmutableBitSet groupSet,
         RelCollation orderKeys,
         boolean isRows,
         RexWindowBound lowerBound,
-        RexWindowBound upperBound) {
+        RexWindowBound upperBound,
+        RexWindowExclusion exclude) {
       this.groupSet = groupSet;
       this.orderKeys = orderKeys;
       this.isRows = isRows;
       this.lowerBound = lowerBound;
       this.upperBound = upperBound;
+      this.exclude = exclude;
     }
 
     @Override public int hashCode() {
-      return Objects.hash(groupSet, orderKeys, isRows, lowerBound, upperBound);
+      return Objects.hash(groupSet, orderKeys, isRows, lowerBound, upperBound, 
exclude);
     }
 
     @Override public boolean equals(@Nullable Object obj) {
@@ -359,6 +364,7 @@ public final class LogicalWindow extends Window {
           && orderKeys.equals(((WindowKey) obj).orderKeys)
           && Objects.equals(lowerBound, ((WindowKey) obj).lowerBound)
           && Objects.equals(upperBound, ((WindowKey) obj).upperBound)
+          && exclude == ((WindowKey) obj).exclude
           && isRows == ((WindowKey) obj).isRows;
     }
   }
@@ -390,7 +396,7 @@ public final class LogicalWindow extends Window {
     WindowKey windowKey =
         new WindowKey(
             groupSet, orderKeys, aggWindow.isRows(),
-            aggWindow.getLowerBound(), aggWindow.getUpperBound());
+            aggWindow.getLowerBound(), aggWindow.getUpperBound(), 
aggWindow.getExclude());
     windowMap.put(windowKey, over);
   }
 
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 9950674ea1..23ca30e952 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
@@ -58,6 +58,7 @@ import org.apache.calcite.rex.RexUnknownAs;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.rex.RexWindow;
 import org.apache.calcite.rex.RexWindowBound;
+import org.apache.calcite.rex.RexWindowExclusion;
 import org.apache.calcite.sql.JoinType;
 import org.apache.calcite.sql.SqlAggFunction;
 import org.apache.calcite.sql.SqlBasicCall;
@@ -958,6 +959,16 @@ public abstract class SqlImplementor {
       }
     }
 
+    public SqlLiteral toSql(RexWindowExclusion exclude) {
+      switch (exclude) {
+      case EXCLUDE_CURRENT_ROW: return SqlWindow.createExcludeCurrentRow(POS);
+      case EXCLUDE_GROUP: return SqlWindow.createExcludeGroup(POS);
+      case EXCLUDE_TIES: return SqlWindow.createExcludeTies(POS);
+      case EXCLUDE_NO_OTHER: return SqlWindow.createExcludeNoOthers(POS);
+      default: throw new AssertionError("Unsupported Window Exclusion: " + 
exclude);
+      }
+    }
+
     public List<SqlNode> toSql(Window.Group group, ImmutableList<RexLiteral> 
constants,
         int inputFieldCount) {
       final List<SqlNode> rexOvers = new ArrayList<>();
@@ -981,7 +992,8 @@ public abstract class SqlImplementor {
                 SqlWindow.create(null, null,
                     new SqlNodeList(partitionKeys, POS),
                     new SqlNodeList(orderByKeys, POS),
-                    isRows, lowerBound, upperBound, allowPartial, POS);
+                    isRows, lowerBound, upperBound, allowPartial,
+                    toSql(group.exclude), POS);
         if (aggFunction.allowsFraming()) {
           lowerBound = createSqlWindowBound(group.lowerBound);
           upperBound = createSqlWindowBound(group.upperBound);
@@ -1040,6 +1052,7 @@ public abstract class SqlImplementor {
 
       SqlNode lowerBound = null;
       SqlNode upperBound = null;
+      SqlLiteral exclude = toSql(rexWindow.getExclude());
 
       if (sqlAggregateFunction.allowsFraming()) {
         lowerBound = createSqlWindowBound(rexWindow.getLowerBound());
@@ -1048,7 +1061,7 @@ public abstract class SqlImplementor {
 
       final SqlWindow sqlWindow =
           SqlWindow.create(null, null, partitionList,
-              orderList, isRows, lowerBound, upperBound, allowPartial, POS);
+              orderList, isRows, lowerBound, upperBound, allowPartial, 
exclude, POS);
 
       final List<SqlNode> nodeList = toSql(program, rexOver.getOperands());
       return createOverCall(sqlAggregateFunction, nodeList, sqlWindow, 
rexOver.isDistinct());
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/ProjectWindowTransposeRule.java
 
b/core/src/main/java/org/apache/calcite/rel/rules/ProjectWindowTransposeRule.java
index 385e2dbba4..1576bda75a 100644
--- 
a/core/src/main/java/org/apache/calcite/rel/rules/ProjectWindowTransposeRule.java
+++ 
b/core/src/main/java/org/apache/calcite/rel/rules/ProjectWindowTransposeRule.java
@@ -168,7 +168,7 @@ public class ProjectWindowTransposeRule
 
       groups.add(
           new Window.Group(keys.build(), group.isRows, group.lowerBound,
-              group.upperBound, RelCollations.of(orderKeys), aggCalls));
+              group.upperBound, group.exclude, RelCollations.of(orderKeys), 
aggCalls));
     }
 
     final LogicalWindow newLogicalWindow =
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java 
b/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
index 1cec66baf1..54dc204885 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ReduceExpressionsRule.java
@@ -612,7 +612,7 @@ public abstract class ReduceExpressionsRule<C extends 
ReduceExpressionsRule.Conf
             : group.orderKeys;
         groups.add(
             new Window.Group(keys, group.isRows, group.lowerBound,
-                group.upperBound, relCollation, aggCalls));
+                group.upperBound, group.exclude, relCollation, aggCalls));
       }
       if (reduced) {
         call.transformTo(LogicalWindow
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 7a7f247749..3ec66e94e5 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexBuilder.java
@@ -394,13 +394,36 @@ public class RexBuilder {
       boolean nullWhenCountZero,
       boolean distinct,
       boolean ignoreNulls) {
+    return makeOver(type, operator, exprs, partitionKeys, orderKeys, 
lowerBound, upperBound,
+        RexWindowExclusion.EXCLUDE_NO_OTHER, rows, allowPartial, 
nullWhenCountZero, distinct,
+        ignoreNulls);
+  }
+
+  /**
+   * Creates a call to a windowed agg.
+   */
+  public RexNode makeOver(
+      RelDataType type,
+      SqlAggFunction operator,
+      List<RexNode> exprs,
+      List<RexNode> partitionKeys,
+      ImmutableList<RexFieldCollation> orderKeys,
+      RexWindowBound lowerBound,
+      RexWindowBound upperBound,
+      RexWindowExclusion exclude,
+      boolean rows,
+      boolean allowPartial,
+      boolean nullWhenCountZero,
+      boolean distinct,
+      boolean ignoreNulls) {
     final RexWindow window =
         makeWindow(
             partitionKeys,
             orderKeys,
             lowerBound,
             upperBound,
-            rows);
+            rows,
+            exclude);
     final RexOver over =
         new RexOver(type, operator, exprs, window, distinct, ignoreNulls);
     RexNode result = over;
@@ -467,6 +490,28 @@ public class RexBuilder {
       RexWindowBound lowerBound,
       RexWindowBound upperBound,
       boolean rows) {
+    return makeWindow(partitionKeys, orderKeys, lowerBound,
+        upperBound, rows, RexWindowExclusion.EXCLUDE_NO_OTHER);
+  }
+
+  /**
+   * Creates a window specification.
+   *
+   * @param partitionKeys Partition keys
+   * @param orderKeys     Order keys
+   * @param lowerBound    Lower bound
+   * @param upperBound    Upper bound
+   * @param rows          Whether physical. True if row-based, false if
+   *                      range-based
+   * @return window specification
+   */
+  public RexWindow makeWindow(
+      List<RexNode> partitionKeys,
+      ImmutableList<RexFieldCollation> orderKeys,
+      RexWindowBound lowerBound,
+      RexWindowBound upperBound,
+      boolean rows,
+      RexWindowExclusion exclude) {
     if (lowerBound.isUnbounded() && lowerBound.isPreceding()
         && upperBound.isUnbounded() && upperBound.isFollowing()) {
       // RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
@@ -480,7 +525,8 @@ public class RexBuilder {
         orderKeys,
         lowerBound,
         upperBound,
-        rows);
+        rows,
+        exclude);
   }
 
   /**
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 0e14159fef..a836a4ac65 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexShuttle.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexShuttle.java
@@ -89,7 +89,8 @@ public class RexShuttle implements RexVisitor<RexNode> {
         clonedOrderKeys,
         lowerBound,
         upperBound,
-        rows);
+        rows,
+        window.getExclude());
   }
 
   @Override public RexNode visitSubQuery(RexSubQuery subQuery) {
diff --git a/core/src/main/java/org/apache/calcite/rex/RexWindow.java 
b/core/src/main/java/org/apache/calcite/rex/RexWindow.java
index ecf5391163..9c1b39f5b7 100644
--- a/core/src/main/java/org/apache/calcite/rex/RexWindow.java
+++ b/core/src/main/java/org/apache/calcite/rex/RexWindow.java
@@ -40,6 +40,7 @@ public class RexWindow {
   public final ImmutableList<RexFieldCollation> orderKeys;
   private final RexWindowBound lowerBound;
   private final RexWindowBound upperBound;
+  private final RexWindowExclusion exclude;
   private final boolean isRows;
   private final String digest;
   public final int nodeCount;
@@ -76,11 +77,13 @@ public class RexWindow {
       List<RexFieldCollation> orderKeys,
       RexWindowBound lowerBound,
       RexWindowBound upperBound,
-      boolean isRows) {
+      boolean isRows,
+      RexWindowExclusion exclude) {
     this.partitionKeys = ImmutableList.copyOf(partitionKeys);
     this.orderKeys = ImmutableList.copyOf(orderKeys);
     this.lowerBound = Objects.requireNonNull(lowerBound, "lowerBound");
     this.upperBound = Objects.requireNonNull(upperBound, "upperBound");
+    this.exclude = exclude;
     this.isRows = isRows;
     this.nodeCount = computeCodeCount();
     this.digest = computeDigest();
@@ -90,6 +93,16 @@ public class RexWindow {
         "use RANGE for unbounded, not ROWS");
   }
 
+  RexWindow(
+      List<RexNode> partitionKeys,
+      List<RexFieldCollation> orderKeys,
+      RexWindowBound lowerBound,
+      RexWindowBound upperBound,
+      boolean isRows) {
+    this(partitionKeys, orderKeys, lowerBound, upperBound, isRows,
+        RexWindowExclusion.EXCLUDE_NO_OTHER);
+  }
+
   //~ Methods ----------------------------------------------------------------
 
   @Override public String toString() {
@@ -178,6 +191,9 @@ public class RexWindow {
           .append(" AND ")
           .append(upperBound);
     }
+    if (exclude != RexWindowExclusion.EXCLUDE_NO_OTHER) {
+      sb.append(" ").append(exclude).append(" ");
+    }
     return sb;
   }
 
@@ -189,6 +205,10 @@ public class RexWindow {
     return upperBound;
   }
 
+  public RexWindowExclusion getExclude() {
+    return exclude;
+  }
+
   public boolean isRows() {
     return isRows;
   }
diff --git a/core/src/main/java/org/apache/calcite/rex/RexWindowExclusion.java 
b/core/src/main/java/org/apache/calcite/rex/RexWindowExclusion.java
new file mode 100644
index 0000000000..81df89b821
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rex/RexWindowExclusion.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.rex;
+
+import org.apache.calcite.sql.SqlLiteral;
+import org.apache.calcite.sql.SqlNode;
+import org.apache.calcite.sql.SqlWindow;
+
+/**
+ * Representation of different kinds of exclude clause in window functions.
+ */
+public enum RexWindowExclusion {
+  EXCLUDE_NO_OTHER("EXCLUDE NO OTHER"),
+  EXCLUDE_CURRENT_ROW("EXCLUDE CURRENT ROW"),
+  EXCLUDE_TIES("EXCLUDE TIES"),
+  EXCLUDE_GROUP("EXCLUDE GROUP");
+
+  private final String sql;
+
+  RexWindowExclusion(String sql) {
+    this.sql = sql;
+  }
+
+  @Override public String toString() {
+    return sql;
+  }
+
+  /**
+   * Creates a window exclusion from a {@link SqlNode}.
+   *
+   * @param node SqlLiteral of the exclusion clause
+   * @return RexWindowExclusion
+   */
+  public static RexWindowExclusion create(SqlNode node) {
+    SqlLiteral exclude = (SqlLiteral) node;
+    if (SqlWindow.isExcludeCurrentRow(exclude)) {
+      return EXCLUDE_CURRENT_ROW;
+    }
+    if (SqlWindow.isExcludeNoOthers(exclude)) {
+      return EXCLUDE_NO_OTHER;
+    }
+    if (SqlWindow.isExcludeGroup(exclude)) {
+      return EXCLUDE_GROUP;
+    }
+    if (SqlWindow.isExcludeTies(exclude)) {
+      return EXCLUDE_TIES;
+    }
+    throw new AssertionError("Unexpected Exclusion clause");
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlWindow.java 
b/core/src/main/java/org/apache/calcite/sql/SqlWindow.java
index 7318241223..3f25c4d6e1 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlWindow.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlWindow.java
@@ -99,7 +99,10 @@ public class SqlWindow extends SqlCall {
   /** The upper bound of the window. */
   @Nullable SqlNode upperBound;
 
-  /** Whether to allow partial results. It may be null. */
+  /** Exclude rows from the frame.   */
+  SqlLiteral exclude;
+
+  /** Whether to allow partial results. It may be null.   */
   @Nullable SqlLiteral allowPartial;
 
   private @Nullable SqlCall windowCall = null;
@@ -113,6 +116,17 @@ public class SqlWindow extends SqlCall {
       @Nullable SqlIdentifier refName, SqlNodeList partitionList, SqlNodeList 
orderList,
       SqlLiteral isRows, @Nullable SqlNode lowerBound, @Nullable SqlNode 
upperBound,
       @Nullable SqlLiteral allowPartial) {
+    this(pos, declName, refName, partitionList, orderList,
+        isRows, lowerBound, upperBound, allowPartial, 
createExcludeNoOthers(SqlParserPos.ZERO));
+  }
+
+  /**
+   * Creates a window.
+   */
+  public SqlWindow(SqlParserPos pos, @Nullable SqlIdentifier declName,
+      @Nullable SqlIdentifier refName, SqlNodeList partitionList, SqlNodeList 
orderList,
+      SqlLiteral isRows, @Nullable SqlNode lowerBound, @Nullable SqlNode 
upperBound,
+      @Nullable SqlLiteral allowPartial, SqlLiteral exclude) {
     super(pos);
     this.declName = declName;
     this.refName = refName;
@@ -122,7 +136,10 @@ public class SqlWindow extends SqlCall {
     this.lowerBound = lowerBound;
     this.upperBound = upperBound;
     this.allowPartial = allowPartial;
+    this.exclude = exclude;
 
+    assert exclude.symbolValue(Exclusion.class) == Exclusion.EXCLUDE_NO_OTHER
+        || (lowerBound != null || upperBound != null);
     assert declName == null || declName.isSimple();
     assert partitionList != null;
     assert orderList != null;
@@ -132,6 +149,14 @@ public class SqlWindow extends SqlCall {
       SqlNodeList partitionList, SqlNodeList orderList, SqlLiteral isRows,
       @Nullable SqlNode lowerBound, @Nullable SqlNode upperBound, @Nullable 
SqlLiteral allowPartial,
       SqlParserPos pos) {
+    return create(declName, refName, partitionList, orderList, isRows, 
lowerBound, upperBound,
+        allowPartial, createExcludeNoOthers(SqlParserPos.ZERO), pos);
+  }
+
+  public static SqlWindow create(@Nullable SqlIdentifier declName, @Nullable 
SqlIdentifier refName,
+      SqlNodeList partitionList, SqlNodeList orderList, SqlLiteral isRows,
+      @Nullable SqlNode lowerBound, @Nullable SqlNode upperBound, @Nullable 
SqlLiteral allowPartial,
+      SqlLiteral exclude, SqlParserPos pos) {
     // If there's only one bound and it's 'FOLLOWING', make it the upper
     // bound.
     if (upperBound == null
@@ -141,7 +166,7 @@ public class SqlWindow extends SqlCall {
       lowerBound = null;
     }
     return new SqlWindow(pos, declName, refName, partitionList, orderList,
-        isRows, lowerBound, upperBound, allowPartial);
+        isRows, lowerBound, upperBound, allowPartial, exclude);
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -157,7 +182,7 @@ public class SqlWindow extends SqlCall {
   @SuppressWarnings("nullness")
   @Override public List<SqlNode> getOperandList() {
     return ImmutableNullableList.of(declName, refName, partitionList, 
orderList,
-        isRows, lowerBound, upperBound, allowPartial);
+        isRows, lowerBound, upperBound, allowPartial, exclude);
   }
 
   @SuppressWarnings("assignment.type.incompatible")
@@ -187,6 +212,9 @@ public class SqlWindow extends SqlCall {
     case 7:
       this.allowPartial = (SqlLiteral) operand;
       break;
+    case 8:
+      this.exclude = (SqlLiteral) operand;
+      break;
     default:
       throw new AssertionError(i);
     }
@@ -227,6 +255,10 @@ public class SqlWindow extends SqlCall {
     this.upperBound = upperBound;
   }
 
+  public SqlLiteral getExclude() {
+    return exclude;
+  }
+
   /**
    * Returns if the window is guaranteed to have rows.
    * This is useful to refine data type of window aggregates.
@@ -364,6 +396,38 @@ public class SqlWindow extends SqlCall {
     }
   }
 
+  public static SqlLiteral createExcludeNoOthers(SqlParserPos pos) {
+    return Exclusion.EXCLUDE_NO_OTHER.symbol(pos);
+  }
+
+  public static SqlLiteral createExcludeCurrentRow(SqlParserPos pos) {
+    return Exclusion.EXCLUDE_CURRENT_ROW.symbol(pos);
+  }
+
+  public static SqlLiteral createExcludeTies(SqlParserPos pos) {
+    return Exclusion.EXCLUDE_TIES.symbol(pos);
+  }
+
+  public static SqlLiteral createExcludeGroup(SqlParserPos pos) {
+    return Exclusion.EXCLUDE_GROUP.symbol(pos);
+  }
+
+  public static boolean isExcludeNoOthers(SqlLiteral node) {
+    return node.symbolValue(Exclusion.class) == Exclusion.EXCLUDE_NO_OTHER;
+  }
+
+  public static boolean isExcludeCurrentRow(SqlLiteral node) {
+    return node.symbolValue(Exclusion.class) == Exclusion.EXCLUDE_CURRENT_ROW;
+  }
+
+  public static boolean isExcludeGroup(SqlLiteral node) {
+    return node.symbolValue(Exclusion.class) == Exclusion.EXCLUDE_GROUP;
+  }
+
+  public static boolean isExcludeTies(SqlLiteral node) {
+    return node.symbolValue(Exclusion.class) == Exclusion.EXCLUDE_TIES;
+  }
+
   public static SqlNode createCurrentRow(SqlParserPos pos) {
     return Bound.CURRENT_ROW.symbol(pos);
   }
@@ -448,7 +512,9 @@ public class SqlWindow extends SqlCall {
     // 711 rule 10e
     final SqlNode lowerBound = that.getLowerBound();
     final SqlNode upperBound = that.getUpperBound();
-    if ((null != lowerBound) || (null != upperBound)) {
+    final SqlLiteral exclude = that.getExclude();
+    if ((null != lowerBound) || (null != upperBound)
+        || exclude.symbolValue(Exclusion.class) != Exclusion.EXCLUDE_NO_OTHER) 
{
       throw validator.newValidationError(that.isRows,
           RESOURCE.refWindowWithFrame());
     }
@@ -488,7 +554,8 @@ public class SqlWindow extends SqlCall {
         isRowsNew,
         lowerBoundNew,
         upperBoundNew,
-        allowPartialNew);
+        allowPartialNew,
+        exclude);
   }
 
   private static boolean setOperand(@Nullable SqlNode clonedOperand, @Nullable 
SqlNode thatOperand,
@@ -793,6 +860,28 @@ public class SqlWindow extends SqlCall {
     }
   }
 
+  /**
+   * An enumeration of types of exclusion rows in a window: <code>EXCLUDE NO 
OTHERS</code>,
+   * <code>EXCLUDE CURRENT ROW</code>, <code>EXCLUDE TIES</code> and 
<code>EXCLUDE GROUP</code>.
+   */
+  enum Exclusion implements Symbolizable {
+    EXCLUDE_NO_OTHER("EXCLUDE NO OTHER"),
+    EXCLUDE_CURRENT_ROW("EXCLUDE CURRENT ROW"),
+    EXCLUDE_TIES("EXCLUDE TIES"),
+    EXCLUDE_GROUP("EXCLUDE GROUP");
+
+    private final String sql;
+
+    Exclusion(String sql) {
+      this.sql = sql;
+    }
+
+    @Override public String toString() {
+      return sql;
+    }
+
+  }
+
   /**
    * An enumeration of types of bounds in a window: <code>CURRENT ROW</code>,
    * <code>UNBOUNDED PRECEDING</code>, and <code>UNBOUNDED FOLLOWING</code>.
@@ -831,7 +920,7 @@ public class SqlWindow extends SqlCall {
         SqlParserPos pos,
         @Nullable SqlNode... operands) {
       assert functionQualifier == null;
-      assert operands.length == 8;
+      assert operands.length == 9;
       return create(
           (SqlIdentifier) operands[0],
           (SqlIdentifier) operands[1],
@@ -841,6 +930,7 @@ public class SqlWindow extends SqlCall {
           operands[5],
           operands[6],
           (SqlLiteral) operands[7],
+          (SqlLiteral) operands[8],
           pos);
     }
 
@@ -893,6 +983,7 @@ public class SqlWindow extends SqlCall {
       }
       SqlNode lowerBound = window.lowerBound;
       SqlNode upperBound = window.upperBound;
+      SqlLiteral exclude = window.exclude;
       if (lowerBound == null) {
         // No ROWS or RANGE clause
       } else if (upperBound == null) {
@@ -902,6 +993,9 @@ public class SqlWindow extends SqlCall {
           writer.sep("RANGE");
         }
         lowerBound.unparse(writer, 0, 0);
+        if (!isExcludeNoOthers(exclude)) {
+          exclude.unparse(writer, 0, 0);
+        }
       } else {
         if (window.isRows()) {
           writer.sep("ROWS BETWEEN");
@@ -911,6 +1005,9 @@ public class SqlWindow extends SqlCall {
         lowerBound.unparse(writer, 0, 0);
         writer.keyword("AND");
         upperBound.unparse(writer, 0, 0);
+        if (!isExcludeNoOthers(exclude)) {
+          exclude.unparse(writer, 0, 0);
+        }
       }
 
       // ALLOW PARTIAL/DISALLOW PARTIAL
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 e9b9eff95e..b29bf3884c 100644
--- a/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
+++ b/core/src/main/java/org/apache/calcite/sql2rel/SqlToRelConverter.java
@@ -94,6 +94,7 @@ import org.apache.calcite.rex.RexSubQuery;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.rex.RexWindowBound;
 import org.apache.calcite.rex.RexWindowBounds;
+import org.apache.calcite.rex.RexWindowExclusion;
 import org.apache.calcite.runtime.PairList;
 import org.apache.calcite.schema.ColumnStrategy;
 import org.apache.calcite.schema.ModifiableTable;
@@ -2286,6 +2287,7 @@ public class SqlToRelConverter {
             "Relation should have sort key for implicit ORDER BY");
       }
     }
+    final RexWindowExclusion exclude = 
RexWindowExclusion.create(window.getExclude());
 
     final ImmutableList.Builder<RexNode> orderKeys =
         ImmutableList.builder();
@@ -2318,6 +2320,7 @@ public class SqlToRelConverter {
           new HistogramShuttle(partitionKeys.build(), orderKeys.build(), rows,
               RexWindowBounds.create(sqlLowerBound, lowerBound),
               RexWindowBounds.create(sqlUpperBound, upperBound),
+              exclude,
               window.isAllowPartial(), isDistinct, ignoreNulls);
       return rexAgg.accept(visitor);
     } finally {
@@ -5982,6 +5985,7 @@ public class SqlToRelConverter {
     private final ImmutableList<RexNode> orderKeys;
     private final RexWindowBound lowerBound;
     private final RexWindowBound upperBound;
+    private final RexWindowExclusion exclude;
     private final boolean rows;
     private final boolean allowPartial;
     private final boolean distinct;
@@ -5989,12 +5993,13 @@ public class SqlToRelConverter {
 
     HistogramShuttle(ImmutableList<RexNode> partitionKeys,
         ImmutableList<RexNode> orderKeys, boolean rows,
-        RexWindowBound lowerBound, RexWindowBound upperBound,
+        RexWindowBound lowerBound, RexWindowBound upperBound, 
RexWindowExclusion exclude,
         boolean allowPartial, boolean distinct, boolean ignoreNulls) {
       this.partitionKeys = partitionKeys;
       this.orderKeys = orderKeys;
       this.lowerBound = lowerBound;
       this.upperBound = upperBound;
+      this.exclude = exclude;
       this.rows = rows;
       this.allowPartial = allowPartial;
       this.distinct = distinct;
@@ -6045,6 +6050,7 @@ public class SqlToRelConverter {
                 .let(c ->
                     rows ? c.rowsBetween(lowerBound, upperBound)
                         : c.rangeBetween(lowerBound, upperBound))
+                .exclude(exclude)
                 .allowPartial(allowPartial)
                 .toRex();
 
@@ -6085,6 +6091,7 @@ public class SqlToRelConverter {
             .let(c ->
                 rows ? c.rowsBetween(lowerBound, upperBound)
                     : c.rangeBetween(lowerBound, upperBound))
+            .exclude(exclude)
             .allowPartial(allowPartial)
             .nullWhenCountZero(needSum0)
             .toRex();
diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java 
b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
index 7456c2e1de..f0ae2f37cd 100644
--- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
+++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java
@@ -84,6 +84,7 @@ import org.apache.calcite.rex.RexSubQuery;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.rex.RexWindowBound;
 import org.apache.calcite.rex.RexWindowBounds;
+import org.apache.calcite.rex.RexWindowExclusion;
 import org.apache.calcite.runtime.Hook;
 import org.apache.calcite.runtime.ImmutablePairList;
 import org.apache.calcite.runtime.PairList;
@@ -4524,6 +4525,9 @@ public class RelBuilder {
       return rangeBetween(RexWindowBounds.UNBOUNDED_PRECEDING, upper);
     }
 
+    /** Sets the frame to EXCLUDE rows; default to EXCLUDE_NO_OTHER. */
+    OverCall exclude(RexWindowExclusion exclude);
+
     /** Sets a RANGE window with lower and upper bounds,
      * equivalent to SQL {@code RANGE BETWEEN lower ROW AND upper}. */
     OverCall rangeBetween(RexWindowBound lower, RexWindowBound upper);
@@ -4553,6 +4557,7 @@ public class RelBuilder {
     private final boolean rows;
     private final RexWindowBound lowerBound;
     private final RexWindowBound upperBound;
+    private final RexWindowExclusion exclude;
     private final ImmutableList<RexNode> partitionKeys;
     private final ImmutableList<RexFieldCollation> sortKeys;
     private final SqlAggFunction op;
@@ -4563,7 +4568,7 @@ public class RelBuilder {
         @Nullable String alias, ImmutableList<RexNode> partitionKeys,
         ImmutableList<RexFieldCollation> sortKeys, boolean rows,
         RexWindowBound lowerBound, RexWindowBound upperBound,
-        boolean nullWhenCountZero, boolean allowPartial) {
+        boolean nullWhenCountZero, boolean allowPartial, RexWindowExclusion 
exclude) {
       this.op = op;
       this.distinct = distinct;
       this.operands = operands;
@@ -4576,6 +4581,7 @@ public class RelBuilder {
       this.rows = rows;
       this.lowerBound = lowerBound;
       this.upperBound = upperBound;
+      this.exclude = exclude;
     }
 
     /** Creates an OverCallImpl with default settings. */
@@ -4584,7 +4590,7 @@ public class RelBuilder {
         @Nullable String alias) {
       this(op, distinct, operands, ignoreNulls, alias, ImmutableList.of(),
           ImmutableList.of(), true, RexWindowBounds.UNBOUNDED_PRECEDING,
-          RexWindowBounds.UNBOUNDED_FOLLOWING, false, true);
+          RexWindowBounds.UNBOUNDED_FOLLOWING, false, true, 
RexWindowExclusion.EXCLUDE_NO_OTHER);
     }
 
     @Override public OverCall partitionBy(
@@ -4599,13 +4605,13 @@ public class RelBuilder {
     private OverCall partitionBy_(ImmutableList<RexNode> partitionKeys) {
       return new OverCallImpl(op, distinct, operands, ignoreNulls, alias,
           partitionKeys, sortKeys, rows, lowerBound, upperBound,
-          nullWhenCountZero, allowPartial);
+          nullWhenCountZero, allowPartial, exclude);
     }
 
     private OverCall orderBy_(ImmutableList<RexFieldCollation> sortKeys) {
       return new OverCallImpl(op, distinct, operands, ignoreNulls, alias,
           partitionKeys, sortKeys, rows, lowerBound, upperBound,
-          nullWhenCountZero, allowPartial);
+          nullWhenCountZero, allowPartial, exclude);
     }
 
     @Override public OverCall orderBy(Iterable<? extends RexNode> sortKeys) {
@@ -4626,32 +4632,38 @@ public class RelBuilder {
         RexWindowBound upperBound) {
       return new OverCallImpl(op, distinct, operands, ignoreNulls, alias,
           partitionKeys, sortKeys, true, lowerBound, upperBound,
-          nullWhenCountZero, allowPartial);
+          nullWhenCountZero, allowPartial, exclude);
     }
 
     @Override public OverCall rangeBetween(RexWindowBound lowerBound,
         RexWindowBound upperBound) {
       return new OverCallImpl(op, distinct, operands, ignoreNulls, alias,
           partitionKeys, sortKeys, false, lowerBound, upperBound,
-          nullWhenCountZero, allowPartial);
+          nullWhenCountZero, allowPartial, exclude);
+    }
+
+    @Override public OverCall exclude(RexWindowExclusion exclude) {
+      return new OverCallImpl(op, distinct, operands, ignoreNulls, alias,
+          partitionKeys, sortKeys, rows, lowerBound, upperBound,
+          nullWhenCountZero, allowPartial, exclude);
     }
 
     @Override public OverCall allowPartial(boolean allowPartial) {
       return new OverCallImpl(op, distinct, operands, ignoreNulls, alias,
           partitionKeys, sortKeys, rows, lowerBound, upperBound,
-          nullWhenCountZero, allowPartial);
+          nullWhenCountZero, allowPartial, exclude);
     }
 
     @Override public OverCall nullWhenCountZero(boolean nullWhenCountZero) {
       return new OverCallImpl(op, distinct, operands, ignoreNulls, alias,
           partitionKeys, sortKeys, rows, lowerBound, upperBound,
-          nullWhenCountZero, allowPartial);
+          nullWhenCountZero, allowPartial, exclude);
     }
 
     @Override public RexNode as(String alias) {
       return new OverCallImpl(op, distinct, operands, ignoreNulls, alias,
           partitionKeys, sortKeys, rows, lowerBound, upperBound,
-          nullWhenCountZero, allowPartial).toRex();
+          nullWhenCountZero, allowPartial, exclude).toRex();
     }
 
     @Override public RexNode toRex() {
@@ -4665,7 +4677,7 @@ public class RelBuilder {
       final RelDataType type = op.inferReturnType(bind);
       final RexNode over = getRexBuilder()
           .makeOver(type, op, operands, partitionKeys, sortKeys,
-              lowerBound, upperBound, rows, allowPartial, nullWhenCountZero,
+              lowerBound, upperBound, exclude, rows, allowPartial, 
nullWhenCountZero,
               distinct, ignoreNulls);
       return aliasMaybe(over, alias);
     }
diff --git 
a/core/src/test/java/org/apache/calcite/rel/logical/ToLogicalConverterTest.java 
b/core/src/test/java/org/apache/calcite/rel/logical/ToLogicalConverterTest.java
index a2a486570f..accd611a86 100644
--- 
a/core/src/test/java/org/apache/calcite/rel/logical/ToLogicalConverterTest.java
+++ 
b/core/src/test/java/org/apache/calcite/rel/logical/ToLogicalConverterTest.java
@@ -46,6 +46,8 @@ import com.google.common.collect.ImmutableSet;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.junit.jupiter.api.Test;
 
+import java.util.Locale;
+
 import static org.apache.calcite.test.Matchers.hasTree;
 
 import static org.hamcrest.CoreMatchers.is;
@@ -467,6 +469,31 @@ class ToLogicalConverterTest {
     verify(rel(sql), expectedPhysical, expectedLogical);
   }
 
+  void testWindowExcludeImp(String excludeClause, String 
expectedExcludeString) {
+    // forbiddenApiTest requires to use Locale even though it's no needed here.
+    String sql = String.format(Locale.ROOT, "SELECT sum(\"salary\") over 
(order by \"hire_date\" "
+        + "rows between unbounded preceding and current row %s) FROM 
\"employee\"", excludeClause);
+    String expectedPhysical =
+        String.format(Locale.ROOT, "EnumerableProject($0=[$17])\n"
+            + "  EnumerableWindow(window#0=[window(order by [9] rows between"
+            + " UNBOUNDED PRECEDING and CURRENT ROW %saggs [SUM($11)])])\n"
+            + "    JdbcToEnumerableConverter\n"
+            + "      JdbcTableScan(table=[[foodmart, employee]])\n", 
expectedExcludeString);
+    String expectedLogical =
+        String.format(Locale.ROOT, "LogicalProject($0=[$17])\n"
+            + "  LogicalWindow(window#0=[window(order by [9] rows between"
+            + " UNBOUNDED PRECEDING and CURRENT ROW %saggs [SUM($11)])])\n"
+            + "    LogicalTableScan(table=[[foodmart, employee]])\n", 
expectedExcludeString);
+    verify(rel(sql), expectedPhysical, expectedLogical);
+  }
+
+  @Test void testWindowExclude() {
+    testWindowExcludeImp("exclude current row", "EXCLUDE CURRENT ROW ");
+    testWindowExcludeImp("exclude group", "EXCLUDE GROUP ");
+    testWindowExcludeImp("exclude ties", "EXCLUDE TIES ");
+    testWindowExcludeImp("exclude no others", "");
+  }
+
   @Test void testTableModify() {
     final String sql = "insert into \"employee\" select * from \"employee\"";
     final String expectedPhysical = ""
diff --git a/core/src/test/resources/sql/winagg.iq 
b/core/src/test/resources/sql/winagg.iq
index ed02f556f4..963c38eb67 100644
--- a/core/src/test/resources/sql/winagg.iq
+++ b/core/src/test/resources/sql/winagg.iq
@@ -839,5 +839,69 @@ EnumerableCalc(expr#0..3=[{inputs}], GENDER=[$t2], 
$1=[$t3])
 +--------+--------+
 (6 rows)
 
+!ok
+
+# [CALCITE-5855] Implement frame exclusion in window functions
+# Verified against PostgreSQL
+!use scott
+
+select empno,
+  avg(comm) over (order by empno rows 2 preceding exclude current row) as e1,
+  avg(comm) over (order by empno rows 2 preceding exclude group) as e2,
+  avg(comm) over (order by empno rows 2 preceding exclude ties) as e3,
+  avg(comm) over (order by empno rows 2 preceding exclude no others) as e4,
+  avg(comm) over (order by empno rows 2 preceding) as e5
+from emp
+order by 1;
++-------+---------+---------+---------+---------+---------+
+| EMPNO | E1      | E2      | E3      | E4      | E5      |
++-------+---------+---------+---------+---------+---------+
+|  7369 |         |         |         |         |         |
+|  7499 |         |         |  300.00 |  300.00 |  300.00 |
+|  7521 |  300.00 |  300.00 |  400.00 |  400.00 |  400.00 |
+|  7566 |  400.00 |  400.00 |  400.00 |  400.00 |  400.00 |
+|  7654 |  500.00 |  500.00 |  950.00 |  950.00 |  950.00 |
+|  7698 | 1400.00 | 1400.00 | 1400.00 | 1400.00 | 1400.00 |
+|  7782 | 1400.00 | 1400.00 | 1400.00 | 1400.00 | 1400.00 |
+|  7788 |         |         |         |         |         |
+|  7839 |         |         |         |         |         |
+|  7844 |         |         |    0.00 |    0.00 |    0.00 |
+|  7876 |    0.00 |    0.00 |    0.00 |    0.00 |    0.00 |
+|  7900 |    0.00 |    0.00 |    0.00 |    0.00 |    0.00 |
+|  7902 |         |         |         |         |         |
+|  7934 |         |         |         |         |         |
++-------+---------+---------+---------+---------+---------+
+(14 rows)
+
+!ok
+
+select empno,
+  stddev(comm) over (order by empno rows between 2 preceding and 5 following 
exclude current row) as e1,
+  count(*) over (order by empno rows between 2 preceding and 5 following 
exclude group) as e2,
+  sum(sal) over (order by empno rows between 2 preceding and unbounded 
following exclude ties) as e3,
+  sum(sal) over (order by empno rows between unbounded preceding and unbounded 
following exclude no others) as e4,
+  sum(sal) over (order by empno rows between unbounded preceding and unbounded 
following) as e5
+from emp
+order by 1;
++-------+-------------------------------------------------+----+----------+----------+----------+
+| EMPNO | E1                                              | E2 | E3       | E4 
      | E5       |
++-------+-------------------------------------------------+----+----------+----------+----------+
+|  7369 | 585.9465277082316561063635163009166717529296875 |  5 | 29025.00 | 
29025.00 | 29025.00 |
+|  7499 | 636.3961030678927954795653931796550750732421875 |  6 | 29025.00 | 
29025.00 | 29025.00 |
+|  7521 | 777.8174593052023055861354805529117584228515625 |  7 | 29025.00 | 
29025.00 | 29025.00 |
+|  7566 | 585.9465277082316561063635163009166717529296875 |  7 | 28225.00 | 
29025.00 | 29025.00 |
+|  7654 | 353.5533905932737752664252184331417083740234375 |  7 | 26625.00 | 
29025.00 | 29025.00 |
+|  7698 |  989.949493661166570745990611612796783447265625 |  7 | 25375.00 | 
29025.00 | 29025.00 |
+|  7782 |  989.949493661166570745990611612796783447265625 |  7 | 22400.00 | 
29025.00 | 29025.00 |
+|  7788 |                                                 |  7 | 21150.00 | 
29025.00 | 29025.00 |
+|  7839 |                                                 |  7 | 18300.00 | 
29025.00 | 29025.00 |
+|  7844 |                                                 |  6 | 15850.00 | 
29025.00 | 29025.00 |
+|  7876 |                                                 |  5 | 12850.00 | 
29025.00 | 29025.00 |
+|  7900 |                                                 |  4 |  7850.00 | 
29025.00 | 29025.00 |
+|  7902 |                                                 |  3 |  6350.00 | 
29025.00 | 29025.00 |
+|  7934 |                                                 |  2 |  5250.00 | 
29025.00 | 29025.00 |
++-------+-------------------------------------------------+----+----------+----------+----------+
+(14 rows)
+
 !ok
 # End winagg.iq
diff --git 
a/piglet/src/main/java/org/apache/calcite/piglet/PigRelToSqlConverter.java 
b/piglet/src/main/java/org/apache/calcite/piglet/PigRelToSqlConverter.java
index 752cd7ccf9..2b6ea23de2 100644
--- a/piglet/src/main/java/org/apache/calcite/piglet/PigRelToSqlConverter.java
+++ b/piglet/src/main/java/org/apache/calcite/piglet/PigRelToSqlConverter.java
@@ -114,6 +114,7 @@ public class PigRelToSqlConverter extends RelToSqlConverter 
{
               builder.context.toSql(winGroup.lowerBound),
               builder.context.toSql(winGroup.upperBound),
               null, // allowPartial
+              builder.context.toSql(winGroup.exclude),
               POS);
 
       for (Window.RexWinAggCall winFunc : winGroup.aggCalls) {
diff --git a/site/_docs/algebra.md b/site/_docs/algebra.md
index bc78331a04..30a6b8175b 100644
--- a/site/_docs/algebra.md
+++ b/site/_docs/algebra.md
@@ -540,6 +540,7 @@ To further modify the `OverCall`, call its methods:
 | `rowsFrom(lower)`    | Creates a row-based window bounded below, `ROWS 
BETWEEN lower AND CURRENT ROW`
 | `rowsTo(upper)`      | Creates a row-based window bounded above, `ROWS 
BETWEEN CURRENT ROW AND upper`
 | `rowsBetween(lower, upper)` | Creates a rows-based window, `ROWS BETWEEN 
lower AND upper`
+| `exclude(excludeType)` | Exclude certain rows from the frame (see SQL 
`EXCLUDE`)
 | `partitionBy(expr...)`<br/>`partitionBy(exprList)` | Partitions the window 
on the given expressions (see SQL `PARTITION BY`)
 | `orderBy(expr...)`<br/>`sort(exprList)` | Sorts the rows in the window (see 
SQL `ORDER BY`)
 | `allowPartial(b)`    | Sets whether to allow partial width windows; default 
true
diff --git a/site/_docs/reference.md b/site/_docs/reference.md
index c74832c9ac..f9edf9c4b9 100644
--- a/site/_docs/reference.md
+++ b/site/_docs/reference.md
@@ -331,10 +331,17 @@ windowSpec:
       [ ORDER BY orderItem [, orderItem ]* ]
       [ PARTITION BY expression [, expression ]* ]
       [
-          RANGE numericOrIntervalExpression { PRECEDING | FOLLOWING }
-      |   ROWS numericExpression { PRECEDING | FOLLOWING }
+          RANGE numericOrIntervalExpression { PRECEDING | FOLLOWING [exclude]}
+      |   ROWS numericExpression { PRECEDING | FOLLOWING [exclude] }
       ]
       ')'
+
+exclude:
+      EXCLUDE NO OTHERS
+  |   EXCLUDE CURRENT ROW
+  |   EXCLUDE GROUP
+  |   EXCLUDE TIES
+
 {% endhighlight %}
 
 In *insert*, if the INSERT or UPSERT statement does not specify a
@@ -1968,6 +1975,12 @@ windowedAggregateCall:
 where *agg* is one of the operators in the following table, or a user-defined
 aggregate function.
 
+The *exclude* clause can be one of:
+- EXCLUDE NO OTHER: Does not exclude any row from the frame. This is the 
default.
+- EXCLUDE CURRENT ROW: Exclude the current row from the frame.
+- EXCLUDE GROUP: Exclude the current row and its ordering peers from the frame.
+- EXCLUDE TIES: Exclude all the ordering peers of the current row, but not the 
current row itself.
+
 `DISTINCT`, `FILTER` and `WITHIN GROUP` are as described for aggregate
 functions.
 
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 092690cd9b..1fdf6c95a5 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
@@ -5780,6 +5780,22 @@ public class SqlParserTest {
             + "FROM `BIDS`");
   }
 
+  @Test void testWindowSpecExclusion() {
+    sql("select sum(x) over (order by x rows between 2 preceding and 2 
following exclude group) "
+        + "from emp")
+        .ok("SELECT (SUM(`X`) OVER (ORDER BY `X` ROWS BETWEEN 2 PRECEDING AND 
2 "
+            + "FOLLOWING EXCLUDE GROUP))\n"
+            + "FROM `EMP`");
+    // doesn't parse without frame definition
+    sql("select sum(x) over (order by x ^exclude^ current row) from bids")
+        .fails("(?s).*Encountered \"exclude\".*");
+    // EXCLUDE NO OTHERS is the default behavior, and omitted from UNPARSE
+    sql("select sum(x) over (order by x rows between 2 preceding and 2 
following exclude no others)"
+        + " from emp")
+        .ok("SELECT (SUM(`X`) OVER (ORDER BY `X` ROWS BETWEEN 2 PRECEDING AND 
2 FOLLOWING))\n"
+            + "FROM `EMP`");
+  }
+
   @Test void testQualify() {
     final String sql = "SELECT empno, ename,\n"
         + " ROW_NUMBER() over (partition by ename order by deptno) as rn\n"

Reply via email to