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 d22a464b7a [CALCITE-7111] Add an API for finding common relational 
sub-expressions
d22a464b7a is described below

commit d22a464b7ae4e75cddfb044f6885fd766cf231c9
Author: Stamatis Zampetakis <[email protected]>
AuthorDate: Mon Aug 4 16:59:22 2025 +0300

    [CALCITE-7111] Add an API for finding common relational sub-expressions
    
    1. Add basic/sample implementation for the new interface.
    2. Add RelOptUtil#stripAll for stripping all planner nodes (HepVertex, 
RelSubset) from a RelNode tree.
    3. Remove staleness TODO from CommonRelSubExprRule since the rule now has a 
concrete use-case and is covered by test cases.
    4. Add a fixture for making suggester tests modular and extensible.
---
 .../calcite/plan/CommonRelExpressionRegistry.java  |  60 +++++++++++
 .../apache/calcite/plan/CommonRelSubExprRule.java  |   2 -
 .../java/org/apache/calcite/plan/RelOptUtil.java   |  14 +++
 .../rel/RelCommonExpressionBasicSuggester.java     |  54 ++++++++++
 .../calcite/rel/RelCommonExpressionSuggester.java  |  53 +++++++++
 .../rel/rules/CommonRelSubExprRegisterRule.java    | 103 ++++++++++++++++++
 .../rel/RelCommonExpressionBasicSuggesterTest.java | 115 ++++++++++++++++++++
 .../rel/RelCommonExpressionBasicSuggesterTest.xml  |  93 ++++++++++++++++
 .../apache/calcite/test/RelSuggesterFixture.java   | 118 +++++++++++++++++++++
 9 files changed, 610 insertions(+), 2 deletions(-)

diff --git 
a/core/src/main/java/org/apache/calcite/plan/CommonRelExpressionRegistry.java 
b/core/src/main/java/org/apache/calcite/plan/CommonRelExpressionRegistry.java
new file mode 100644
index 0000000000..9ced7130d8
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/plan/CommonRelExpressionRegistry.java
@@ -0,0 +1,60 @@
+/*
+ * 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.plan;
+
+import org.apache.calcite.rel.RelNode;
+
+import org.apiguardian.api.API;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Stream;
+
+/**
+ * A registry of common relational expressions for a given query.
+ *
+ * <p>The registry is only meant to hold common expressions for a single query.
+ *
+ * <p>The class is not thread-safe.
+ */
+@API(since = "1.41.0", status = API.Status.INTERNAL)
+public final class CommonRelExpressionRegistry {
+  /**
+   * A unique collection of common expressions.
+   *
+   * <p>The expressions may contain internal planning concepts such as {@link 
org.apache.calcite.plan.hep.HepRelVertex}.
+   */
+  private final Map<RelDigest, RelNode> rels = new HashMap<>();
+
+  /**
+   * Adds the specified expression to this registry.
+   *
+   * @param rel a relational expression to be added to the registry.
+   */
+  public void add(RelNode rel) {
+    this.rels.put(rel.getRelDigest(), rel);
+  }
+
+  /**
+   * Returns a stream with all expression entries.
+   *
+   * @return a stream with all expression entries
+   */
+  public Stream<RelNode> entries() {
+    return rels.values().stream().map(RelOptUtil::stripAll);
+  }
+}
diff --git 
a/core/src/main/java/org/apache/calcite/plan/CommonRelSubExprRule.java 
b/core/src/main/java/org/apache/calcite/plan/CommonRelSubExprRule.java
index 434ee4325c..1dc86ea2fe 100644
--- a/core/src/main/java/org/apache/calcite/plan/CommonRelSubExprRule.java
+++ b/core/src/main/java/org/apache/calcite/plan/CommonRelSubExprRule.java
@@ -21,8 +21,6 @@
  * that are fired only on relational expressions that appear more than once
  * in a query tree.
  */
-
-// TODO: obsolete this?
 public abstract class CommonRelSubExprRule
     extends RelRule<CommonRelSubExprRule.Config> {
   //~ Constructors -----------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java 
b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
index 9245b21e7f..6698ec9b0d 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -3164,6 +3164,20 @@ public static void splitFilters(
     }
   }
 
+  /**
+   * Strips all wrapper nodes from the specified relational expression.
+   *
+   * @param node a relational expression which may have wrapper nodes
+   * @return the stripped relational expression
+   */
+  public static RelNode stripAll(RelNode node) {
+    return node.accept(new RelHomogeneousShuttle() {
+      @Override public RelNode visit(final RelNode other) {
+        return super.visit(other.stripped());
+      }
+    });
+  }
+
   @Deprecated // to be removed before 2.0
   public static boolean checkProjAndChildInputs(
       Project project,
diff --git 
a/core/src/main/java/org/apache/calcite/rel/RelCommonExpressionBasicSuggester.java
 
b/core/src/main/java/org/apache/calcite/rel/RelCommonExpressionBasicSuggester.java
new file mode 100644
index 0000000000..4c74237aee
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/rel/RelCommonExpressionBasicSuggester.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import org.apache.calcite.plan.CommonRelExpressionRegistry;
+import org.apache.calcite.plan.Context;
+import org.apache.calcite.plan.Contexts;
+import org.apache.calcite.plan.hep.HepPlanner;
+import org.apache.calcite.plan.hep.HepProgram;
+import org.apache.calcite.plan.hep.HepProgramBuilder;
+import org.apache.calcite.rel.rules.CommonRelSubExprRegisterRule;
+
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+/**
+ * Suggester for common relational expressions that appear as is (identical 
trees)
+ * more than once in the query plan.
+ */
+@API(since = "1.41.0", status = API.Status.EXPERIMENTAL)
+public class RelCommonExpressionBasicSuggester implements 
RelCommonExpressionSuggester {
+
+  @Override public Collection<RelNode> suggest(RelNode input, @Nullable 
Context context) {
+    CommonRelExpressionRegistry localRegistry = new 
CommonRelExpressionRegistry();
+    HepProgram ruleProgram = new HepProgramBuilder()
+        .addRuleInstance(CommonRelSubExprRegisterRule.Config.JOIN.toRule())
+        
.addRuleInstance(CommonRelSubExprRegisterRule.Config.AGGREGATE.toRule())
+        .addRuleInstance(CommonRelSubExprRegisterRule.Config.FILTER.toRule())
+        .addRuleInstance(CommonRelSubExprRegisterRule.Config.PROJECT.toRule())
+        .build();
+    HepPlanner planner = new HepPlanner(ruleProgram, 
Contexts.of(localRegistry));
+    planner.setRoot(input);
+    planner.findBestExp();
+    return localRegistry.entries().collect(Collectors.toList());
+  }
+
+}
diff --git 
a/core/src/main/java/org/apache/calcite/rel/RelCommonExpressionSuggester.java 
b/core/src/main/java/org/apache/calcite/rel/RelCommonExpressionSuggester.java
new file mode 100644
index 0000000000..872a955409
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/rel/RelCommonExpressionSuggester.java
@@ -0,0 +1,53 @@
+/*
+ * 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;
+
+import org.apache.calcite.plan.Context;
+
+import org.apiguardian.api.API;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Collection;
+
+/**
+ * Suggester for finding and returning interesting expressions that appear 
more than once in a
+ * query. The notion of "interesting" is specific to the actual implementation 
of this interface.
+ *
+ * <p>In some cases the interesting expressions may be readily available in 
the input query while in
+ * others they could be revealed after applying various transformations and 
algebraic
+ * equivalences.
+ *
+ * <p>The final decision about using (or not) the suggestions provided by this 
class lies to the
+ * query optimizer. For various reasons (e.g, incorrect or expensive 
expressions) the optimizer may
+ * choose to reject/ignore the results provided by this class.
+ *
+ * <p>Implementations of this interface must have a public no argument 
constructor to allow
+ * instantiation via reflection.
+ *
+ * <p>The interface is experimental and subject to change without notice.
+ */
+@API(since = "1.41.0", status = API.Status.EXPERIMENTAL)
+public interface RelCommonExpressionSuggester {
+  /**
+   * Suggests interesting expressions for the specified input expression and 
context.
+   *
+   * @param input a relational expression representing the query under 
compilation.
+   * @param context a context for tuning aspects of the suggestion process.
+   * @return a collection with interesting expressions for the specified 
relational expression
+   */
+  Collection<RelNode> suggest(RelNode input, @Nullable Context context);
+}
diff --git 
a/core/src/main/java/org/apache/calcite/rel/rules/CommonRelSubExprRegisterRule.java
 
b/core/src/main/java/org/apache/calcite/rel/rules/CommonRelSubExprRegisterRule.java
new file mode 100644
index 0000000000..89f2eae2ae
--- /dev/null
+++ 
b/core/src/main/java/org/apache/calcite/rel/rules/CommonRelSubExprRegisterRule.java
@@ -0,0 +1,103 @@
+/*
+ * 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.CommonRelExpressionRegistry;
+import org.apache.calcite.plan.CommonRelSubExprRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+
+import com.google.common.collect.Multimap;
+
+import org.immutables.value.Value;
+
+import java.util.function.Predicate;
+
+/**
+ * Rule for saving relational expressions that appear more than once in a 
query tree to the planner
+ * context.
+ */
[email protected]
+public final class CommonRelSubExprRegisterRule extends CommonRelSubExprRule {
+
+  private CommonRelSubExprRegisterRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(final RelOptRuleCall call) {
+    CommonRelExpressionRegistry r =
+        
call.getPlanner().getContext().unwrap(CommonRelExpressionRegistry.class);
+    if (r != null) {
+      r.add(call.rel(0));
+    }
+  }
+
+  /**
+   * A predicate determining if a relational expression is interesting.
+   *
+   * <p>The notion of interesting is loosely defined on purpose since it may 
change as the API
+   * evolves. At the moment an expression is considered interesting if it 
contains at least one of
+   * the following RelNode types:
+   * <ul>
+   *   <li>{@link Join}</li>
+   *   <li>{@link Aggregate}</li>
+   *   <li>{@link Filter}</li>
+   * </ul>
+   */
+  private static final class InterestingRelNodePredicate implements 
Predicate<RelNode> {
+    @Override public boolean test(final RelNode rel) {
+      RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
+      Multimap<Class<? extends RelNode>, RelNode> types = mq.getNodeTypes(rel);
+      if (types == null) {
+        return false;
+      }
+      return types.keySet().stream().anyMatch(
+          t -> Join.class.isAssignableFrom(t) || 
Aggregate.class.isAssignableFrom(t)
+              || Filter.class.isAssignableFrom(t));
+    }
+  }
+
+  /** Rule configuration. */
+  @Value.Immutable
+  public interface Config extends CommonRelSubExprRule.Config {
+    Config JOIN = ImmutableCommonRelSubExprRegisterRule.Config.builder()
+        .withOperandSupplier(o -> o.operand(Join.class)
+            .predicate(j -> JoinRelType.INNER == j.getJoinType()).anyInputs())
+        .build();
+    Config AGGREGATE = ImmutableCommonRelSubExprRegisterRule.Config.builder()
+        .withOperandSupplier(o -> 
o.operand(Aggregate.class).anyInputs()).build();
+
+    Config FILTER = ImmutableCommonRelSubExprRegisterRule.Config.builder()
+        .withOperandSupplier(o -> o.operand(Filter.class).anyInputs()).build();
+
+    Config PROJECT = ImmutableCommonRelSubExprRegisterRule.Config.builder()
+        .withOperandSupplier(o -> o.operand(Project.class)
+            .predicate(new InterestingRelNodePredicate()).anyInputs())
+        .build();
+
+    @Override default CommonRelSubExprRegisterRule toRule() {
+      return new CommonRelSubExprRegisterRule(this);
+    }
+  }
+
+}
diff --git 
a/core/src/test/java/org/apache/calcite/rel/RelCommonExpressionBasicSuggesterTest.java
 
b/core/src/test/java/org/apache/calcite/rel/RelCommonExpressionBasicSuggesterTest.java
new file mode 100644
index 0000000000..f78a22a655
--- /dev/null
+++ 
b/core/src/test/java/org/apache/calcite/rel/RelCommonExpressionBasicSuggesterTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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;
+
+import org.apache.calcite.test.RelSuggesterFixture;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link RelCommonExpressionBasicSuggester}.
+ * The tests ensure that the suggester can correctly identify common 
expressions in the query plan
+ * that have a certain structure.
+ */
+public class RelCommonExpressionBasicSuggesterTest {
+
+  private static RelSuggesterFixture fixture() {
+    return RelSuggesterFixture.of(RelCommonExpressionBasicSuggesterTest.class,
+        new RelCommonExpressionBasicSuggester());
+  }
+
+  private static RelSuggesterFixture sql(String sql) {
+    return fixture().withSql(sql);
+  }
+
+  @Test void testSuggestReturnsCommonFilterTableScan() {
+    String sql = "SELECT '2025' FROM emp WHERE empno = 1\n"
+        + "UNION\n"
+        + "SELECT '2024' FROM emp WHERE empno = 1";
+    sql(sql).checkSuggestions();
+  }
+
+  @Test void testSuggestReturnsTwoCommonFilterTableScans() {
+    String sql = "SELECT '2025' FROM emp WHERE empno = 1\n"
+        + "UNION\n"
+        + "SELECT '2024' FROM emp WHERE empno = 1\n"
+        + "UNION\n"
+        + "SELECT '2023' FROM emp WHERE empno = 2\n"
+        + "UNION\n"
+        + "SELECT '2022' FROM emp WHERE empno = 2";
+    sql(sql).checkSuggestions();
+  }
+
+  @Test void testSuggestReturnsCommonProjectFilterTableScan() {
+    String sql = "SELECT ename FROM emp WHERE empno = 1\n"
+        + "UNION\n"
+        + "SELECT ename FROM emp WHERE empno = 1";
+    sql(sql).checkSuggestions();
+  }
+
+  @Test void testSuggestReturnsCommonAggregateProjectTableScan() {
+    String sql = "SELECT COUNT(*), 'A' FROM emp GROUP BY ename\n"
+        + "UNION\n"
+        + "SELECT COUNT(*), 'B' FROM emp GROUP BY ename";
+    sql(sql).checkSuggestions();
+  }
+
+  @Test void testSuggestReturnsCommonAggregateProjectFilterTableScan() {
+    String sql = "SELECT COUNT(*), 'A' FROM emp WHERE empno > 50 GROUP BY 
ename\n"
+        + "UNION\n"
+        + "SELECT COUNT(*), 'B' FROM emp WHERE empno > 50 GROUP BY ename";
+    sql(sql).checkSuggestions();
+  }
+
+  @Test void testSuggestReturnsCommonProjectJoinTableScan() {
+    String sql = "WITH cx AS (\n"
+        + "SELECT e.empno, d.dname FROM emp e\n"
+        + "INNER JOIN dept d ON e.deptno = d.deptno)\n"
+        + "SELECT * FROM cx WHERE cx.empno > 50\n"
+        + "INTERSECT\n"
+        + "SELECT * FROM cx WHERE cx.empno < 100";
+    sql(sql).checkSuggestions();
+  }
+
+  @Test void testSuggestReturnsCommonProjectFilterJoinTableScan() {
+    String sql = "WITH cx AS (\n"
+        + "SELECT e.empno, d.dname FROM emp e\n"
+        + "INNER JOIN dept d ON e.deptno = d.deptno\n"
+        + "WHERE d.dname = 'Sales')\n"
+        + "SELECT * FROM cx WHERE cx.empno > 50\n"
+        + "INTERSECT\n"
+        + "SELECT * FROM cx WHERE cx.empno < 100";
+    sql(sql).checkSuggestions();
+  }
+
+  @Test void testSuggestReturnsCommonAggregateProjectFilterJoinTableScan() {
+    String sql = "WITH cx AS (\n"
+        + "SELECT ename, COUNT(*) as cnt FROM emp e\n"
+        + "INNER JOIN dept d ON e.deptno = d.deptno\n"
+        + "WHERE d.dname = 'Sales' GROUP BY ename)\n"
+        + "SELECT * FROM cx WHERE cnt > 50\n"
+        + "INTERSECT\n"
+        + "SELECT * FROM cx WHERE cnt < 100";
+    sql(sql).checkSuggestions();
+  }
+
+  @AfterAll static void checkActualAndReferenceFiles() {
+    fixture().checkActualAndReferenceFiles();
+  }
+
+}
diff --git 
a/core/src/test/resources/org/apache/calcite/rel/RelCommonExpressionBasicSuggesterTest.xml
 
b/core/src/test/resources/org/apache/calcite/rel/RelCommonExpressionBasicSuggesterTest.xml
new file mode 100644
index 0000000000..591f75d278
--- /dev/null
+++ 
b/core/src/test/resources/org/apache/calcite/rel/RelCommonExpressionBasicSuggesterTest.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" ?>
+<!--
+  ~ 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.
+  -->
+<Root>
+  <TestCase name="testSuggestReturnsCommonAggregateProjectFilterJoinTableScan">
+    <Resource name="suggestion_0">
+      <![CDATA[LogicalAggregate(group=[{0}], CNT=[COUNT()])
+  LogicalProject(ENAME=[$1])
+    LogicalFilter(condition=[=($4, 'Sales')])
+      LogicalJoin(condition=[=($2, $3)], joinType=[inner])
+        LogicalTableScan(table=[[EMP]])
+        LogicalTableScan(table=[[DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testSuggestReturnsCommonAggregateProjectFilterTableScan">
+    <Resource name="suggestion_0">
+      <![CDATA[LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+  LogicalProject(ENAME=[$1])
+    LogicalFilter(condition=[>($0, 50)])
+      LogicalTableScan(table=[[EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testSuggestReturnsCommonAggregateProjectTableScan">
+    <Resource name="suggestion_0">
+      <![CDATA[LogicalAggregate(group=[{0}], EXPR$0=[COUNT()])
+  LogicalProject(ENAME=[$1])
+    LogicalTableScan(table=[[EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testSuggestReturnsCommonFilterTableScan">
+    <Resource name="suggestion_0">
+      <![CDATA[LogicalFilter(condition=[=($0, 1)])
+  LogicalTableScan(table=[[EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testSuggestReturnsCommonProjectFilterJoinTableScan">
+    <Resource name="suggestion_0">
+      <![CDATA[LogicalProject(EMPNO=[$0], DNAME=[$4])
+  LogicalFilter(condition=[=($4, 'Sales')])
+    LogicalJoin(condition=[=($2, $3)], joinType=[inner])
+      LogicalTableScan(table=[[EMP]])
+      LogicalTableScan(table=[[DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testSuggestReturnsCommonProjectFilterTableScan">
+    <Resource name="suggestion_0">
+      <![CDATA[LogicalProject(ENAME=[$1])
+  LogicalFilter(condition=[=($0, 1)])
+    LogicalTableScan(table=[[EMP]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testSuggestReturnsCommonProjectJoinTableScan">
+    <Resource name="suggestion_0">
+      <![CDATA[LogicalProject(EMPNO=[$0], DNAME=[$4])
+  LogicalJoin(condition=[=($2, $3)], joinType=[inner])
+    LogicalTableScan(table=[[EMP]])
+    LogicalTableScan(table=[[DEPT]])
+]]>
+    </Resource>
+  </TestCase>
+  <TestCase name="testSuggestReturnsTwoCommonFilterTableScans">
+    <Resource name="suggestion_0">
+      <![CDATA[LogicalFilter(condition=[=($0, 1)])
+  LogicalTableScan(table=[[EMP]])
+]]>
+    </Resource>
+    <Resource name="suggestion_1">
+      <![CDATA[LogicalFilter(condition=[=($0, 2)])
+  LogicalTableScan(table=[[EMP]])
+]]>
+    </Resource>
+  </TestCase>
+</Root>
diff --git 
a/testkit/src/main/java/org/apache/calcite/test/RelSuggesterFixture.java 
b/testkit/src/main/java/org/apache/calcite/test/RelSuggesterFixture.java
new file mode 100644
index 0000000000..5b672a6841
--- /dev/null
+++ b/testkit/src/main/java/org/apache/calcite/test/RelSuggesterFixture.java
@@ -0,0 +1,118 @@
+/*
+ * 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.test;
+
+import org.apache.calcite.jdbc.CalciteSchema;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.rel.RelCommonExpressionSuggester;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.schema.impl.AbstractTable;
+import org.apache.calcite.sql.parser.SqlParseException;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.tools.Frameworks;
+import org.apache.calcite.tools.Planner;
+import org.apache.calcite.tools.RelConversionException;
+import org.apache.calcite.tools.ValidationException;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A fixture for testing implementations of the {@link 
RelCommonExpressionSuggester} API.
+ */
+public class RelSuggesterFixture {
+
+  /** Creates the default fixture for a given test class and suggester 
implementation. */
+  public static RelSuggesterFixture of(Class<?> clazz, 
RelCommonExpressionSuggester suggester) {
+    return new RelSuggesterFixture("?", suggester, 
DiffRepository.lookup(clazz), defaultSchema());
+  }
+
+  private final String sql;
+  private final RelCommonExpressionSuggester suggester;
+  private final DiffRepository diffRepo;
+  private final SchemaPlus schema;
+
+  private RelSuggesterFixture(String sql, RelCommonExpressionSuggester 
suggester,
+      DiffRepository diffRepo, SchemaPlus schema) {
+    this.sql = requireNonNull(sql, "sql");
+    this.suggester = requireNonNull(suggester, "suggester");
+    this.diffRepo = requireNonNull(diffRepo, "diffRepo");
+    this.schema = requireNonNull(schema, "schema");
+  }
+
+  /** Creates a copy of this fixture that uses a given SQL query. */
+  public RelSuggesterFixture withSql(String sql) {
+    return new RelSuggesterFixture(sql, suggester, diffRepo, schema);
+  }
+
+  /**
+   * Checks that the suggester returns the expected plans for the specified 
SQL statement.
+   *
+   * <p>The expected suggestions are defined in a reference file handled by 
the provided
+   * DiffRepository.
+   */
+  public void checkSuggestions() {
+    RelNode rel = toRel(sql);
+    AtomicInteger i = new AtomicInteger();
+    suggester.suggest(rel, 
null).stream().map(RelOptUtil::toString).sorted().forEach(plan -> {
+      String tag = "suggestion_" + i.getAndIncrement();
+      diffRepo.assertEquals(tag, "${" + tag + "}", plan);
+    });
+  }
+
+  /** Checks that the actual and reference file are consistent. */
+  public void checkActualAndReferenceFiles() {
+    diffRepo.checkActualAndReferenceFiles();
+  }
+
+  private RelNode toRel(String sql) {
+    Planner planner =
+        
Frameworks.getPlanner(Frameworks.newConfigBuilder().defaultSchema(schema).build());
+    try {
+      return planner.rel(planner.validate(planner.parse(sql))).rel;
+    } catch (SqlParseException | RelConversionException | ValidationException 
e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static SchemaPlus defaultSchema() {
+    SchemaPlus schema = CalciteSchema.createRootSchema(false, false).plus();
+    schema.add("EMP", new AbstractTable() {
+      @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+        return typeFactory.builder()
+            .add("EMPNO", SqlTypeName.INTEGER)
+            .add("ENAME", SqlTypeName.VARCHAR)
+            .add("DEPTNO", SqlTypeName.INTEGER)
+            .build();
+      }
+    });
+    schema.add("DEPT", new AbstractTable() {
+      @Override public RelDataType getRowType(RelDataTypeFactory typeFactory) {
+        return typeFactory.builder()
+            .add("DEPTNO", SqlTypeName.INTEGER)
+            .add("DNAME", SqlTypeName.VARCHAR)
+            .add("EMPNO", SqlTypeName.INTEGER)
+            .build();
+      }
+    });
+    return schema;
+  }
+}

Reply via email to