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