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

zabetak 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 bdafeec27c [CALCITE-6011] Add FilterWindowTransposeRule to push a 
Filter past a Window
bdafeec27c is described below

commit bdafeec27c529d0089434a4d8b0fc7a1e49efcd1
Author: shenlang <shenl...@zbyte-inc.com>
AuthorDate: Fri Sep 22 23:23:08 2023 +0800

    [CALCITE-6011] Add FilterWindowTransposeRule to push a Filter past a Window
    
    Close apache/calcite#3439
---
 .../java/org/apache/calcite/plan/RelOptRules.java  |   3 +-
 .../org/apache/calcite/rel/rules/CoreRules.java    |   4 +
 .../rel/rules/FilterWindowTransposeRule.java       | 131 +++++++++++++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.java   |  61 ++++++++++
 .../org/apache/calcite/test/RelOptRulesTest.xml    |  97 +++++++++++++++
 core/src/test/resources/sql/winagg.iq              |  41 +++++++
 6 files changed, 336 insertions(+), 1 deletion(-)

diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptRules.java 
b/core/src/main/java/org/apache/calcite/plan/RelOptRules.java
index 46f39fc326..816008f34e 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptRules.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptRules.java
@@ -89,7 +89,8 @@ public class RelOptRules {
           CoreRules.EXCHANGE_REMOVE_CONSTANT_KEYS,
           CoreRules.SORT_EXCHANGE_REMOVE_CONSTANT_KEYS,
           CoreRules.SAMPLE_TO_FILTER,
-          CoreRules.FILTER_SAMPLE_TRANSPOSE);
+          CoreRules.FILTER_SAMPLE_TRANSPOSE,
+          CoreRules.FILTER_WINDOW_TRANSPOSE);
 
   static final List<RelOptRule> ABSTRACT_RULES =
       ImmutableList.of(CoreRules.AGGREGATE_ANY_PULL_UP_CONSTANTS,
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java 
b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
index 77c19d58ab..24a104f151 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
@@ -321,6 +321,10 @@ public class CoreRules {
   public static final FilterSetOpTransposeRule FILTER_SET_OP_TRANSPOSE =
       FilterSetOpTransposeRule.Config.DEFAULT.toRule();
 
+  /** Rule that pushes a {@link Filter} past a {@link 
org.apache.calcite.rel.core.Window}. */
+  public static final FilterWindowTransposeRule FILTER_WINDOW_TRANSPOSE =
+      FilterWindowTransposeRule.Config.DEFAULT.toRule();
+
   /** Rule that reduces constants inside a {@link LogicalFilter}.
    *
    * @see #JOIN_REDUCE_EXPRESSIONS
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/FilterWindowTransposeRule.java
 
b/core/src/main/java/org/apache/calcite/rel/rules/FilterWindowTransposeRule.java
new file mode 100644
index 0000000000..bc38dafd03
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/rel/rules/FilterWindowTransposeRule.java
@@ -0,0 +1,131 @@
+/*
+ * 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.rel.rules;
+
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.core.Window;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.util.ImmutableBitSet;
+
+import com.google.common.collect.ImmutableList;
+
+import org.immutables.value.Value;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Planner rule that pushes a {@link org.apache.calcite.rel.core.Filter}
+ * past a {@link org.apache.calcite.rel.core.Window}.
+ *
+ * <p> If {@code Filter} condition used columns belongs {@code Window} 
partition keys,
+ * then we could push the condition past the {@code Window}.
+ *
+ * <p> For example:
+ *  <blockquote><pre>{@code
+ *    LogicalProject(NAME=[$0], DEPTNO=[$1], EXPR$2=[$2])
+ *     LogicalFilter(condition=[>($1, 0)])
+ *       LogicalProject(NAME=[$1], DEPTNO=[$0], EXPR$2=[$2])
+ *         LogicalWindow(window#0=[window(partition {0} aggs [COUNT()])])
+ *           LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ *  }</pre></blockquote>
+ *
+ * <p> will convert to:
+ * <blockquote><pre>{@code
+ *    LogicalProject(NAME=[$1], DEPTNO=[$0], EXPR$2=[$2])
+ *      LogicalWindow(window#0=[window(partition {0} aggs [COUNT()])])
+ *        LogicalFilter(condition=[>($0, 0)])
+ *          LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ * }</pre></blockquote>
+ *
+ * @see CoreRules#FILTER_PROJECT_TRANSPOSE
+ * @see CoreRules#PROJECT_TO_LOGICAL_PROJECT_AND_WINDOW
+ * @see CoreRules#FILTER_WINDOW_TRANSPOSE
+ */
+@Value.Enclosing
+public class FilterWindowTransposeRule
+    extends RelRule<FilterWindowTransposeRule.Config>
+    implements TransformationRule {
+
+  protected FilterWindowTransposeRule(final Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(final RelOptRuleCall call) {
+    final Filter filterRel = call.rel(0);
+    final Window windowRel = call.rel(1);
+
+    // Get the window all groups
+    List<Window.Group> windowGroups = windowRel.groups;
+
+    // The window may have multi groups,now we could only
+    // deal one group case,so that we could know the partition keys.
+    if (windowGroups.size() != 1) {
+      return;
+    }
+
+    final List<RexNode> conditions =
+        RelOptUtil.conjunctions(filterRel.getCondition());
+    // The conditions which could be pushed to past window
+    final List<RexNode> pushedConditions = new ArrayList<>();
+    final List<RexNode> remainingConditions = new ArrayList<>();
+    final Window.Group group = windowGroups.get(0);
+    // Get the window partition keys
+    final ImmutableBitSet partitionKeys = group.keys;
+
+    for (RexNode condition : conditions) {
+      // Find the condition used columns
+      ImmutableBitSet rCols = RelOptUtil.InputFinder.bits(condition);
+      // If the window partition columns contains the condition used columns,
+      // then we could push the condition to past window.
+      if (partitionKeys.contains(rCols)) {
+        pushedConditions.add(condition);
+      } else {
+        remainingConditions.add(condition);
+      }
+    }
+
+    final RelBuilder builder = call.builder();
+    // Use the pushed conditions to create a new filter above the window's 
input.
+    RelNode rel = 
builder.push(windowRel.getInput()).filter(pushedConditions).build();
+    if (rel == windowRel.getInput(0)) {
+      return;
+    }
+    rel = windowRel.copy(windowRel.getTraitSet(), ImmutableList.of(rel));
+    rel = builder.push(rel).filter(remainingConditions).build();
+    call.transformTo(rel);
+  }
+
+  /** Rule configuration. */
+  @Value.Immutable
+  public interface Config extends RelRule.Config {
+
+    Config DEFAULT = ImmutableFilterWindowTransposeRule.Config.of()
+        .withOperandSupplier(b0 ->
+            b0.operand(Filter.class).oneInput(b1 ->
+                b1.operand(Window.class).anyInputs()));
+
+    @Override default FilterWindowTransposeRule toRule() {
+      return new FilterWindowTransposeRule(this);
+    }
+  }
+}
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java 
b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
index 789adc9377..945c0952ec 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptRulesTest.java
@@ -588,6 +588,21 @@ class RelOptRulesTest extends RelOptTestBase {
         .checkUnchanged();
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6011";>[CALCITE-6011]
+   * Add the planner rule that pushes the Filter past a Window</a>. */
+  @Test void testNotPushFilterPastWindowWhenPredicateNotOnPartitionKey() {
+    final String sql = "select * from\n"
+        + "(select NAME, DEPTNO, count(*) over (partition by NAME) from dept) 
t\n"
+        + "where DEPTNO = 0";
+    sql(sql)
+        .withPreRule(CoreRules.PROJECT_TO_LOGICAL_PROJECT_AND_WINDOW)
+        .withRule(
+            CoreRules.FILTER_PROJECT_TRANSPOSE,
+            CoreRules.FILTER_WINDOW_TRANSPOSE,
+            CoreRules.PROJECT_REMOVE).check();
+  }
+
   @Test void testAddRedundantSemiJoinRule() {
     final String sql = "select 1 from emp inner join dept on emp.deptno = 
dept.deptno";
     sql(sql).withRule(CoreRules.JOIN_ADD_REDUNDANT_SEMI_JOIN).check();
@@ -951,6 +966,52 @@ class RelOptRulesTest extends RelOptTestBase {
     sql(sql).withRule(CoreRules.FILTER_AGGREGATE_TRANSPOSE).check();
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6011";>[CALCITE-6011]
+   * Add the planner rule that pushes the Filter past a Window</a>. */
+  @Test void testPushFilterPastWindowWithOnePartitionColumn() {
+    final String sql = "select * from\n"
+        + "(select NAME, DEPTNO, count(*) over (partition by DEPTNO) from 
dept)\n"
+        + "where DEPTNO > 0";
+    sql(sql)
+        .withPreRule(CoreRules.PROJECT_TO_LOGICAL_PROJECT_AND_WINDOW)
+        .withRule(
+            CoreRules.FILTER_PROJECT_TRANSPOSE,
+            CoreRules.FILTER_WINDOW_TRANSPOSE,
+            CoreRules.PROJECT_REMOVE).check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6011";>[CALCITE-6011]
+   * Add the planner rule that pushes the Filter past a Window</a>. */
+  @Test void testPushFilterPastWindowWithTwoPartitionColumns() {
+    final String sql = "select * from\n"
+        + "(select NAME, DEPTNO, count(*) over (partition by NAME, DEPTNO) 
from dept)\n"
+        + "where DEPTNO > 0";
+    sql(sql)
+        .withPreRule(CoreRules.PROJECT_TO_LOGICAL_PROJECT_AND_WINDOW)
+        .withRule(
+            CoreRules.FILTER_PROJECT_TRANSPOSE,
+            CoreRules.FILTER_WINDOW_TRANSPOSE,
+            CoreRules.PROJECT_REMOVE).check();
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-6011";>[CALCITE-6011]
+   * Add the planner rule that pushes the Filter past a Window</a>. */
+  @Test void testPushFilterPastWindowWithDoubleWindows() {
+    final String sql = "select * from\n"
+        + "(select NAME, DEPTNO, count(*) over (partition by NAME, DEPTNO) as 
cnt,\n"
+        + "sum(1) over (partition by DEPTNO) as all_sum from dept) t\n"
+        + "where DEPTNO = 1";
+    sql(sql)
+        .withPreRule(CoreRules.PROJECT_TO_LOGICAL_PROJECT_AND_WINDOW)
+        .withRule(
+            CoreRules.FILTER_PROJECT_TRANSPOSE,
+            CoreRules.FILTER_WINDOW_TRANSPOSE,
+            CoreRules.PROJECT_REMOVE).check();
+  }
+
   private RelOptFixture basePushFilterPastAggWithGroupingSets() {
     return sql("${sql}")
         .withPreRule(CoreRules.PROJECT_MERGE,
diff --git 
a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml 
b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
index 90a846762e..6d8b120543 100644
--- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
+++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml
@@ -7118,6 +7118,30 @@ LogicalProject(EXPR$0=[1])
   LogicalJoin(condition=[AND(=($7, $9), IS NOT NULL($1))], joinType=[inner])
     LogicalTableScan(table=[[CATALOG, SALES, EMP]])
     LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testNotPushFilterPastWindowWhenPredicateNotOnPartitionKey">
+    <Resource name="sql">
+      <![CDATA[select * from
+(select NAME, DEPTNO, count(*) over (partition by NAME) from dept) t
+where DEPTNO = 0]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(NAME=[$0], DEPTNO=[$1], EXPR$2=[$2])
+  LogicalFilter(condition=[=($1, 0)])
+    LogicalProject(NAME=[$1], DEPTNO=[$0], EXPR$2=[$2])
+      LogicalWindow(window#0=[window(partition {1} aggs [COUNT()])])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(NAME=[$1], DEPTNO=[$0], EXPR$2=[$2])
+  LogicalFilter(condition=[=($0, 0)])
+    LogicalWindow(window#0=[window(partition {1} aggs [COUNT()])])
+      LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
 ]]>
     </Resource>
   </TestCase>
@@ -9907,6 +9931,79 @@ LogicalProject(NAME=[$1])
               LogicalFilter(condition=[>($0, 10)])
                 LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
     LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testPushFilterPastWindowWithDoubleWindows">
+    <Resource name="sql">
+      <![CDATA[select * from
+(select NAME, DEPTNO, count(*) over (partition by NAME, DEPTNO) as cnt,
+sum(1) over (partition by DEPTNO) as all_sum from dept) t
+where DEPTNO = 1]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(NAME=[$0], DEPTNO=[$1], CNT=[$2], ALL_SUM=[$3])
+  LogicalFilter(condition=[=($1, 1)])
+    LogicalProject(NAME=[$1], DEPTNO=[$0], CNT=[$2], ALL_SUM=[$3])
+      LogicalWindow(window#0=[window(partition {0, 1} aggs [COUNT()])], 
window#1=[window(partition {0} aggs [SUM($2)])])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(NAME=[$1], DEPTNO=[$0], CNT=[$2], ALL_SUM=[$3])
+  LogicalFilter(condition=[=($0, 1)])
+    LogicalWindow(window#0=[window(partition {0, 1} aggs [COUNT()])], 
window#1=[window(partition {0} aggs [SUM($2)])])
+      LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testPushFilterPastWindowWithOnePartitionColumn">
+    <Resource name="sql">
+      <![CDATA[select * from
+(select NAME, DEPTNO, count(*) over (partition by DEPTNO) from dept)
+where DEPTNO > 0]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(NAME=[$0], DEPTNO=[$1], EXPR$2=[$2])
+  LogicalFilter(condition=[>($1, 0)])
+    LogicalProject(NAME=[$1], DEPTNO=[$0], EXPR$2=[$2])
+      LogicalWindow(window#0=[window(partition {0} aggs [COUNT()])])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(NAME=[$1], DEPTNO=[$0], EXPR$2=[$2])
+  LogicalWindow(window#0=[window(partition {0} aggs [COUNT()])])
+    LogicalFilter(condition=[>($0, 0)])
+      LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testPushFilterPastWindowWithTwoPartitionColumns">
+    <Resource name="sql">
+      <![CDATA[select * from
+(select NAME, DEPTNO, count(*) over (partition by NAME, DEPTNO) from dept)
+where DEPTNO > 0]]>
+    </Resource>
+    <Resource name="planBefore">
+      <![CDATA[
+LogicalProject(NAME=[$0], DEPTNO=[$1], EXPR$2=[$2])
+  LogicalFilter(condition=[>($1, 0)])
+    LogicalProject(NAME=[$1], DEPTNO=[$0], EXPR$2=[$2])
+      LogicalWindow(window#0=[window(partition {0, 1} aggs [COUNT()])])
+        LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+    </Resource>
+    <Resource name="planAfter">
+      <![CDATA[
+LogicalProject(NAME=[$1], DEPTNO=[$0], EXPR$2=[$2])
+  LogicalWindow(window#0=[window(partition {0, 1} aggs [COUNT()])])
+    LogicalFilter(condition=[>($0, 0)])
+      LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
 ]]>
     </Resource>
   </TestCase>
diff --git a/core/src/test/resources/sql/winagg.iq 
b/core/src/test/resources/sql/winagg.iq
index 7304a00382..ed02f556f4 100644
--- a/core/src/test/resources/sql/winagg.iq
+++ b/core/src/test/resources/sql/winagg.iq
@@ -798,5 +798,46 @@ from emp;
 +--------+-------+-------+
 (9 rows)
 
+!ok
+
+# [CALCITE-6011] Add the planner rule that pushes the Filter past a Window
+# Get the initial result which not push filter past window.
+select gender, count(*) over(partition by gender order by ename) as count1 
from emp;
++--------+--------+
+| GENDER | COUNT1 |
++--------+--------+
+| F      |      1 |
+| F      |      2 |
+| F      |      3 |
+| F      |      4 |
+| F      |      5 |
+| F      |      6 |
+| M      |      1 |
+| M      |      2 |
+| M      |      3 |
++--------+--------+
+(9 rows)
+
+!ok
+
+# Get the plan and result which push filter past window
+select gender, count(*) over(partition by gender order by ename) as count1 
from emp where gender = 'F';
+EnumerableCalc(expr#0..3=[{inputs}], GENDER=[$t2], $1=[$t3])
+  EnumerableWindow(window#0=[window(partition {2} order by [0] aggs 
[COUNT()])])
+    EnumerableCalc(expr#0..2=[{inputs}], expr#3=['F'], expr#4=[=($t2, $t3)], 
proj#0..2=[{exprs}], $condition=[$t4])
+      EnumerableValues(tuples=[[{ 'Jane', 10, 'F' }, { 'Bob', 10, 'M' }, { 
'Eric', 20, 'M' }, { 'Susan', 30, 'F' }, { 'Alice', 30, 'F' }, { 'Adam', 50, 
'M' }, { 'Eve', 50, 'F' }, { 'Grace', 60, 'F' }, { 'Wilma', null, 'F' }]])
+!plan
++--------+--------+
+| GENDER | COUNT1 |
++--------+--------+
+| F      |      1 |
+| F      |      2 |
+| F      |      3 |
+| F      |      4 |
+| F      |      5 |
+| F      |      6 |
++--------+--------+
+(6 rows)
+
 !ok
 # End winagg.iq

Reply via email to