You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by ch...@apache.org on 2020/09/14 02:04:15 UTC
[calcite] branch master updated: [CALCITE-4197] Provide utility to
visualize RelNode plans
This is an automated email from the ASF dual-hosted git repository.
chunwei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git
The following commit(s) were added to refs/heads/master by this push:
new 99c0fef [CALCITE-4197] Provide utility to visualize RelNode plans
99c0fef is described below
commit 99c0fefeca7caf638de875133f8c836472e8b28c
Author: liyafan82 <fa...@foxmail.com>
AuthorDate: Fri Sep 11 16:00:24 2020 +0800
[CALCITE-4197] Provide utility to visualize RelNode plans
---
.../java/org/apache/calcite/plan/RelOptUtil.java | 4 +
.../calcite/rel/externalize/RelDotWriter.java | 286 +++++++++++++++++++++
.../calcite/rel/externalize/RelWriterImpl.java | 4 +-
.../org/apache/calcite/sql/SqlExplainFormat.java | 5 +-
.../org/apache/calcite/plan/RelWriterTest.java | 114 ++++++--
.../calcite/plan/volcano/VolcanoPlannerTest.java | 36 +++
.../org/apache/calcite/test/HepPlannerTest.java | 29 +++
.../apache/calcite/test/SqlToRelConverterTest.java | 18 ++
8 files changed, 468 insertions(+), 28 deletions(-)
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 8e30ee5..6398e08 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -39,6 +39,7 @@ import org.apache.calcite.rel.core.Project;
import org.apache.calcite.rel.core.RelFactories;
import org.apache.calcite.rel.core.Sort;
import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.externalize.RelDotWriter;
import org.apache.calcite.rel.externalize.RelJsonWriter;
import org.apache.calcite.rel.externalize.RelWriterImpl;
import org.apache.calcite.rel.externalize.RelXmlWriter;
@@ -2091,6 +2092,9 @@ public abstract class RelOptUtil {
planWriter = new RelJsonWriter();
rel.explain(planWriter);
return ((RelJsonWriter) planWriter).asString();
+ case DOT:
+ planWriter = new RelDotWriter(pw, detailLevel, false);
+ break;
default:
planWriter = new RelWriterImpl(pw, detailLevel, false);
}
diff --git a/core/src/main/java/org/apache/calcite/rel/externalize/RelDotWriter.java b/core/src/main/java/org/apache/calcite/rel/externalize/RelDotWriter.java
new file mode 100644
index 0000000..d2b2b47
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelDotWriter.java
@@ -0,0 +1,286 @@
+/*
+ * 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.externalize;
+
+import org.apache.calcite.plan.hep.HepRelVertex;
+import org.apache.calcite.plan.volcano.RelSubset;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.sql.SqlExplainLevel;
+import org.apache.calcite.util.ImmutableBeans;
+import org.apache.calcite.util.Pair;
+import org.apache.calcite.util.Util;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * Utility to dump a rel node plan in dot format.
+ */
+public class RelDotWriter extends RelWriterImpl {
+
+ //~ Instance fields --------------------------------------------------------
+
+ /**
+ * Adjacent list of the plan graph.
+ */
+ private final Map<RelNode, List<RelNode>> outArcTable = new LinkedHashMap<>();
+
+ private Map<RelNode, String> nodeLabels = new HashMap<>();
+
+ private Multimap<RelNode, String> nodeStyles = HashMultimap.create();
+
+ private final WriteOption option;
+
+ //~ Constructors -----------------------------------------------------------
+
+ public RelDotWriter(
+ PrintWriter pw, SqlExplainLevel detailLevel,
+ boolean withIdPrefix) {
+ this(pw, detailLevel, withIdPrefix, WriteOption.DEFAULT);
+ }
+
+ public RelDotWriter(
+ PrintWriter pw, SqlExplainLevel detailLevel,
+ boolean withIdPrefix, WriteOption option) {
+ super(pw, detailLevel, withIdPrefix);
+ this.option = option;
+ }
+
+ //~ Methods ----------------------------------------------------------------
+
+ protected void explain_(RelNode rel,
+ List<Pair<String, Object>> values) {
+ // get inputs
+ List<RelNode> inputs = getInputs(rel);
+ outArcTable.put(rel, inputs);
+
+ // generate node label
+ String label = getRelNodeLabel(rel, values);
+ nodeLabels.put(rel, label);
+
+ if (highlightNode(rel)) {
+ nodeStyles.put(rel, "bold");
+ }
+
+ explainInputs(inputs);
+ }
+
+ protected String getRelNodeLabel(
+ RelNode rel,
+ List<Pair<String, Object>> values) {
+ List<String> labels = new ArrayList<>();
+ StringBuilder sb = new StringBuilder();
+
+ final RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
+ if (withIdPrefix) {
+ sb.append(rel.getId()).append(":");
+ }
+ sb.append(rel.getRelTypeName());
+ labels.add(sb.toString());
+ sb.setLength(0);
+
+ if (detailLevel != SqlExplainLevel.NO_ATTRIBUTES) {
+ for (Pair<String, Object> value : values) {
+ if (value.right instanceof RelNode) {
+ continue;
+ }
+ sb.append(value.left)
+ .append(" = ")
+ .append(value.right);
+ labels.add(sb.toString());
+ sb.setLength(0);
+ }
+ }
+
+ switch (detailLevel) {
+ case ALL_ATTRIBUTES:
+ sb.append("rowcount = ")
+ .append(mq.getRowCount(rel))
+ .append(" cumulative cost = ")
+ .append(mq.getCumulativeCost(rel))
+ .append(" ");
+ }
+ switch (detailLevel) {
+ case NON_COST_ATTRIBUTES:
+ case ALL_ATTRIBUTES:
+ if (!withIdPrefix) {
+ // If we didn't print the rel id at the start of the line, print
+ // it at the end.
+ sb.append("id = ").append(rel.getId());
+ }
+ break;
+ }
+ labels.add(sb.toString().trim());
+ sb.setLength(0);
+
+ // format labels separately and then concat them
+ int leftSpace = option.maxNodeLabelLength();
+ List<String> newlabels = new ArrayList<>();
+ for (int i = 0; i < labels.size(); i++) {
+ if (option.maxNodeLabelLength() != -1 && leftSpace <= 0) {
+ if (i < labels.size() - 1) {
+ // this is not the last label, but we have to stop here
+ newlabels.add("...");
+ }
+ break;
+ }
+ String formatted = formatNodeLabel(labels.get(i), option.maxNodeLabelLength());
+ newlabels.add(formatted);
+ leftSpace -= formatted.length();
+ }
+
+ return "\"" + String.join("\\n", newlabels) + "\"";
+ }
+
+ private List<RelNode> getInputs(RelNode parent) {
+ return parent.getInputs().stream().map(child -> {
+ if (child instanceof HepRelVertex) {
+ return ((HepRelVertex) child).getCurrentRel();
+ } else if (child instanceof RelSubset) {
+ RelSubset subset = (RelSubset) child;
+ return Util.first(subset.getBest(), subset.getOriginal());
+ } else {
+ return child;
+ }
+ }).collect(Collectors.toList());
+ }
+
+ private void explainInputs(List<RelNode> inputs) {
+ for (RelNode input : inputs) {
+ if (input == null || nodeLabels.containsKey(input)) {
+ continue;
+ }
+ input.explain(this);
+ }
+ }
+
+ public RelWriter done(RelNode node) {
+ int numOfVisitedNodes = nodeLabels.size();
+ super.done(node);
+ if (numOfVisitedNodes == 0) {
+ // When we enter this method call, no node
+ // has been visited. So the current node must be the root of the plan.
+ // Now we are exiting the method, all nodes in the plan
+ // have been visited, so it is time to dump the plan.
+
+ pw.println("digraph {");
+
+ // print nodes with styles
+ for (RelNode rel : nodeStyles.keySet()) {
+ String style = String.join(",", nodeStyles.get(rel));
+ pw.println(nodeLabels.get(rel) + " [style=\"" + style + "\"]");
+ }
+
+ // ordinary arcs
+ for (Map.Entry<RelNode, List<RelNode>> entry : outArcTable.entrySet()) {
+ RelNode src = entry.getKey();
+ String srcDesc = nodeLabels.get(src);
+ for (int i = 0; i < entry.getValue().size(); i++) {
+ RelNode dst = entry.getValue().get(i);
+
+ // label is the ordinal of the arc
+ // arc direction from child to parent, to reflect the direction of data flow
+ pw.println(nodeLabels.get(dst) + " -> " + srcDesc + " [label=\"" + i + "\"]");
+ }
+ }
+ pw.println("}");
+ pw.flush();
+ }
+ return this;
+ }
+
+ /**
+ * Format the label into multiple lines according to the options.
+ * @param label the original label.
+ * @param limit the maximal length of the formatted label.
+ * -1 means no limit.
+ * @return the formatted label.
+ */
+ private String formatNodeLabel(String label, int limit) {
+ label = label.trim();
+
+ // escape quotes in the label.
+ label.replace("\"", "\\\"");
+
+ boolean trimmed = false;
+ if (limit != -1 && label.length() > limit) {
+ label = label.substring(0, limit);
+ trimmed = true;
+ }
+
+ if (option.maxNodeLabelPerLine() == -1) {
+ // no need to split into multiple lines.
+ return label + (trimmed ? "..." : "");
+ }
+
+ List<String> descParts = new ArrayList<>();
+ for (int idx = 0; idx < label.length(); idx += option.maxNodeLabelPerLine()) {
+ int endIdx = idx + option.maxNodeLabelPerLine() > label.length() ? label.length()
+ : idx + option.maxNodeLabelPerLine();
+ descParts.add(label.substring(idx, endIdx));
+ }
+
+ return String.join("\\n", descParts) + (trimmed ? "..." : "");
+ }
+
+ boolean highlightNode(RelNode node) {
+ return option.nodePredicate() != null && option.nodePredicate().test(node);
+ }
+
+ /**
+ * Options for displaying the rel node plan in dot format.
+ */
+ public interface WriteOption {
+
+ /** Default configuration. */
+ WriteOption DEFAULT = ImmutableBeans.create(WriteOption.class);
+
+ /**
+ * The max length of node labels.
+ * If the label is too long, the visual display would be messy.
+ * -1 means no limit to the label length.
+ */
+ @ImmutableBeans.Property
+ @ImmutableBeans.IntDefault(100)
+ int maxNodeLabelLength();
+
+ /**
+ * The max length of node label in a line.
+ * -1 means no limitation.
+ */
+ @ImmutableBeans.Property
+ @ImmutableBeans.IntDefault(20)
+ int maxNodeLabelPerLine();
+
+ /**
+ * Predicate for nodes that need to be highlighted.
+ */
+ @ImmutableBeans.Property
+ Predicate<RelNode> nodePredicate();
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/rel/externalize/RelWriterImpl.java b/core/src/main/java/org/apache/calcite/rel/externalize/RelWriterImpl.java
index 3fd1ebc..9fa52d8 100644
--- a/core/src/main/java/org/apache/calcite/rel/externalize/RelWriterImpl.java
+++ b/core/src/main/java/org/apache/calcite/rel/externalize/RelWriterImpl.java
@@ -37,8 +37,8 @@ public class RelWriterImpl implements RelWriter {
//~ Instance fields --------------------------------------------------------
protected final PrintWriter pw;
- private final SqlExplainLevel detailLevel;
- private final boolean withIdPrefix;
+ protected final SqlExplainLevel detailLevel;
+ protected final boolean withIdPrefix;
protected final Spacer spacer = new Spacer();
private final List<Pair<String, Object>> values = new ArrayList<>();
diff --git a/core/src/main/java/org/apache/calcite/sql/SqlExplainFormat.java b/core/src/main/java/org/apache/calcite/sql/SqlExplainFormat.java
index 5c25bba..ba9f602 100644
--- a/core/src/main/java/org/apache/calcite/sql/SqlExplainFormat.java
+++ b/core/src/main/java/org/apache/calcite/sql/SqlExplainFormat.java
@@ -27,5 +27,8 @@ public enum SqlExplainFormat implements Symbolizable {
XML,
/** Indicates that the plan should be output in JSON format. */
- JSON
+ JSON,
+
+ /** Indicates that the plan should be output in dot format. */
+ DOT
}
diff --git a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
index 340eab3..7cd1951 100644
--- a/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/RelWriterTest.java
@@ -69,6 +69,8 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.math.BigDecimal;
@@ -76,6 +78,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.stream.Stream;
import static org.apache.calcite.test.Matchers.isLinux;
@@ -414,6 +417,10 @@ class RelWriterTest {
+ " ]\n"
+ "}";
+ static Stream<SqlExplainFormat> explainFormats() {
+ return Stream.of(SqlExplainFormat.TEXT, SqlExplainFormat.DOT);
+ }
+
/**
* Unit test for {@link org.apache.calcite.rel.externalize.RelJsonWriter} on
* a simple tree of relational expressions, consisting of a table and a
@@ -640,7 +647,9 @@ class RelWriterTest {
assertThat(s, isLinux(expected));
}
- @Test void testAggregateWithAlias() {
+ @ParameterizedTest
+ @MethodSource("explainFormats")
+ void testAggregateWithAlias(SqlExplainFormat format) {
final FrameworkConfig config = RelBuilderTest.config().build();
final RelBuilder builder = RelBuilder.create(config);
// The rel node stands for sql: SELECT max(SAL) as max_sal from EMP group by JOB;
@@ -658,13 +667,27 @@ class RelWriterTest {
final RelJsonWriter jsonWriter = new RelJsonWriter();
rel.explain(jsonWriter);
final String relJson = jsonWriter.asString();
- String s = deserializeAndDumpToTextFormat(getSchema(rel), relJson);
- final String expected = ""
- + "LogicalProject(max_sal=[$1])\n"
- + " LogicalAggregate(group=[{0}], max_sal=[MAX($1)])\n"
- + " LogicalProject(JOB=[$2], SAL=[$5])\n"
- + " LogicalTableScan(table=[[scott, EMP]])\n";
-
+ String s = deserializeAndDump(getSchema(rel), relJson, format);
+ String expected = null;
+ switch (format) {
+ case TEXT:
+ expected = ""
+ + "LogicalProject(max_sal=[$1])\n"
+ + " LogicalAggregate(group=[{0}], max_sal=[MAX($1)])\n"
+ + " LogicalProject(JOB=[$2], SAL=[$5])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ break;
+ case DOT:
+ expected = "digraph {\n"
+ + "\"LogicalAggregate\\ngroup = {0}\\nmax_sal = MAX($1)\\n\" -> "
+ + "\"LogicalProject\\nmax_sal = $1\\n\" [label=\"0\"]\n"
+ + "\"LogicalProject\\nJOB = $2\\nSAL = $5\\n\" -> \"LogicalAggregate\\ngroup = "
+ + "{0}\\nmax_sal = MAX($1)\\n\" [label=\"0\"]\n"
+ + "\"LogicalTableScan\\ntable = [scott, EMP]\\n\" -> \"LogicalProject\\nJOB = $2\\nSAL = "
+ + "$5\\n\" [label=\"0\"]\n"
+ + "}\n";
+ break;
+ }
assertThat(s, isLinux(expected));
}
@@ -732,7 +755,9 @@ class RelWriterTest {
assertThat(s, isLinux(expected));
}
- @Test void testCorrelateQuery() {
+ @ParameterizedTest
+ @MethodSource("explainFormats")
+ void testCorrelateQuery(SqlExplainFormat format) {
final FrameworkConfig config = RelBuilderTest.config().build();
final RelBuilder builder = RelBuilder.create(config);
final Holder<RexCorrelVariable> v = Holder.of(null);
@@ -747,13 +772,28 @@ class RelWriterTest {
RelJsonWriter jsonWriter = new RelJsonWriter();
relNode.explain(jsonWriter);
final String relJson = jsonWriter.asString();
- String s = deserializeAndDumpToTextFormat(getSchema(relNode), relJson);
- final String expected = ""
- + "LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{7}])\n"
- + " LogicalTableScan(table=[[scott, EMP]])\n"
- + " LogicalFilter(condition=[=($0, $cor0.DEPTNO)])\n"
- + " LogicalTableScan(table=[[scott, DEPT]])\n";
-
+ String s = deserializeAndDump(getSchema(relNode), relJson, format);
+ String expected = null;
+ switch (format) {
+ case TEXT:
+ expected = ""
+ + "LogicalCorrelate(correlation=[$cor0], joinType=[inner], requiredColumns=[{7}])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n"
+ + " LogicalFilter(condition=[=($0, $cor0.DEPTNO)])\n"
+ + " LogicalTableScan(table=[[scott, DEPT]])\n";
+ break;
+ case DOT:
+ expected = "digraph {\n"
+ + "\"LogicalTableScan\\ntable = [scott, EMP]\\n\" -> \"LogicalCorrelate\\ncorrelation = "
+ + "$cor0\\njoinType = inner\\nrequiredColumns = {7\\n}\\n\" [label=\"0\"]\n"
+ + "\"LogicalFilter\\ncondition = =($0, $c\\nor0.DEPTNO)\\n\" -> "
+ + "\"LogicalCorrelate\\ncorrelation = $cor0\\njoinType = inner\\nrequiredColumns = "
+ + "{7\\n}\\n\" [label=\"1\"]\n"
+ + "\"LogicalTableScan\\ntable = [scott, DEPT\\n]\\n\" -> \"LogicalFilter\\ncondition = ="
+ + "($0, $c\\nor0.DEPTNO)\\n\" [label=\"0\"]\n"
+ + "}\n";
+ break;
+ }
assertThat(s, isLinux(expected));
}
@@ -826,7 +866,9 @@ class RelWriterTest {
assertThat(s, isLinux(expected));
}
- @Test void testUDAF() {
+ @ParameterizedTest
+ @MethodSource("explainFormats")
+ void testUDAF(SqlExplainFormat format) {
final FrameworkConfig config = RelBuilderTest.config().build();
final RelBuilder builder = RelBuilder.create(config);
final RelNode rel = builder
@@ -839,11 +881,24 @@ class RelWriterTest {
.build();
final String relJson = RelOptUtil.dumpPlan("", rel,
SqlExplainFormat.JSON, SqlExplainLevel.EXPPLAN_ATTRIBUTES);
- final String result = deserializeAndDumpToTextFormat(getSchema(rel), relJson);
- final String expected = ""
- + "LogicalAggregate(group=[{0}], agg#0=[myAggFunc($1)])\n"
- + " LogicalProject(ENAME=[$1], DEPTNO=[$7])\n"
- + " LogicalTableScan(table=[[scott, EMP]])\n";
+ final String result = deserializeAndDump(getSchema(rel), relJson, format);
+ String expected = null;
+ switch (format) {
+ case TEXT:
+ expected = ""
+ + "LogicalAggregate(group=[{0}], agg#0=[myAggFunc($1)])\n"
+ + " LogicalProject(ENAME=[$1], DEPTNO=[$7])\n"
+ + " LogicalTableScan(table=[[scott, EMP]])\n";
+ break;
+ case DOT:
+ expected = "digraph {\n"
+ + "\"LogicalProject\\nENAME = $1\\nDEPTNO = $7\\n\" -> \"LogicalAggregate\\ngroup = "
+ + "{0}\\nagg#0 = myAggFunc($1\\n)\\n\" [label=\"0\"]\n"
+ + "\"LogicalTableScan\\ntable = [scott, EMP]\\n\" -> \"LogicalProject\\nENAME = "
+ + "$1\\nDEPTNO = $7\\n\" [label=\"0\"]\n"
+ + "}\n";
+ break;
+ }
assertThat(result, isLinux(expected));
}
@@ -881,9 +936,10 @@ class RelWriterTest {
/**
* Deserialize a relnode from the json string by {@link RelJsonReader},
- * and dump it to text format.
+ * and dump it to the given format.
*/
- private String deserializeAndDumpToTextFormat(RelOptSchema schema, String relJson) {
+ private String deserializeAndDump(
+ RelOptSchema schema, String relJson, SqlExplainFormat format) {
String s =
Frameworks.withPlanner((cluster, relOptSchema, rootSchema) -> {
final RelJsonReader reader = new RelJsonReader(
@@ -894,13 +950,21 @@ class RelWriterTest {
} catch (IOException e) {
throw TestUtil.rethrow(e);
}
- return RelOptUtil.dumpPlan("", node, SqlExplainFormat.TEXT,
+ return RelOptUtil.dumpPlan("", node, format,
SqlExplainLevel.EXPPLAN_ATTRIBUTES);
});
return s;
}
/**
+ * Deserialize a relnode from the json string by {@link RelJsonReader},
+ * and dump it to text format.
+ */
+ private String deserializeAndDumpToTextFormat(RelOptSchema schema, String relJson) {
+ return deserializeAndDump(schema, relJson, SqlExplainFormat.TEXT);
+ }
+
+ /**
* Creates a mock {@link RelNode} that contains OVER. The SQL is as follows:
*
* <blockquote>
diff --git a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
index b45dced..fddb5de 100644
--- a/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
+++ b/core/src/test/java/org/apache/calcite/plan/volcano/VolcanoPlannerTest.java
@@ -35,8 +35,10 @@ import org.apache.calcite.rel.convert.ConverterImpl;
import org.apache.calcite.rel.convert.ConverterRule;
import org.apache.calcite.rel.core.JoinRelType;
import org.apache.calcite.rel.core.RelFactories;
+import org.apache.calcite.rel.externalize.RelDotWriter;
import org.apache.calcite.rel.logical.LogicalProject;
import org.apache.calcite.rel.rules.CoreRules;
+import org.apache.calcite.sql.SqlExplainLevel;
import org.apache.calcite.tools.RelBuilder;
import org.apache.calcite.util.ImmutableBeans;
import org.apache.calcite.util.Pair;
@@ -44,6 +46,8 @@ import org.apache.calcite.util.Pair;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -129,6 +133,38 @@ class VolcanoPlannerTest {
assertTrue(result instanceof PhysSingleRel);
}
+ @Test void testPlanToDot() {
+ VolcanoPlanner planner = new VolcanoPlanner();
+ planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+
+ RelOptCluster cluster = newCluster(planner);
+ NoneLeafRel leafRel =
+ new NoneLeafRel(
+ cluster,
+ "a");
+ NoneSingleRel singleRel =
+ new NoneSingleRel(
+ cluster,
+ leafRel);
+ RelNode convertedRel =
+ planner.changeTraits(
+ singleRel,
+ cluster.traitSetOf(PHYS_CALLING_CONVENTION));
+ planner.setRoot(convertedRel);
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+
+ RelDotWriter planWriter = new RelDotWriter(pw, SqlExplainLevel.NO_ATTRIBUTES, false);
+ planner.getRoot().explain(planWriter);
+ String planStr = sw.toString();
+
+ assertThat(
+ planStr, isLinux("digraph {\n"
+ + "\"NoneLeafRel\\n\" -> \"NoneSingleRel\\n\" [label=\"0\"]\n"
+ + "}\n"));
+ }
+
/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-3118">[CALCITE-3118]
* VolcanoRuleCall should look at RelSubset rather than RelSet
diff --git a/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java b/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
index c486523..3a2c516 100644
--- a/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
+++ b/core/src/test/java/org/apache/calcite/test/HepPlannerTest.java
@@ -24,15 +24,22 @@ import org.apache.calcite.plan.hep.HepProgram;
import org.apache.calcite.plan.hep.HepProgramBuilder;
import org.apache.calcite.rel.RelNode;
import org.apache.calcite.rel.RelRoot;
+import org.apache.calcite.rel.externalize.RelDotWriter;
import org.apache.calcite.rel.logical.LogicalIntersect;
import org.apache.calcite.rel.logical.LogicalUnion;
import org.apache.calcite.rel.rules.CoerceInputsRule;
import org.apache.calcite.rel.rules.CoreRules;
+import org.apache.calcite.sql.SqlExplainLevel;
import com.google.common.collect.ImmutableList;
import org.junit.jupiter.api.Test;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import static org.apache.calcite.test.Matchers.isLinux;
+
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -155,6 +162,28 @@ class HepPlannerTest extends RelOptTestBase {
best.getDigest(), "LogicalUnion");
}
+ @Test void testPlanToDot() {
+ HepProgramBuilder programBuilder = HepProgram.builder();
+ HepPlanner planner =
+ new HepPlanner(
+ programBuilder.build());
+ RelRoot root = tester.convertSqlToRel("select name from sales.dept");
+ planner.setRoot(root.rel);
+
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+
+ RelDotWriter planWriter = new RelDotWriter(pw, SqlExplainLevel.EXPPLAN_ATTRIBUTES, false);
+ planner.getRoot().explain(planWriter);
+ String planStr = sw.toString();
+
+ assertThat(
+ planStr, isLinux("digraph {\n"
+ + "\"LogicalTableScan\\ntable = [CATALOG, SA\\nLES, DEPT]\\n\" -> "
+ + "\"LogicalProject\\nNAME = $1\\n\" [label=\"0\"]\n"
+ + "}\n"));
+ }
+
private void assertIncludesExactlyOnce(String message, String digest, String substring) {
int pos = 0;
int cnt = 0;
diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
index 0f76773..d0a928b 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java
@@ -33,6 +33,7 @@ import org.apache.calcite.rel.RelRoot;
import org.apache.calcite.rel.RelShuttleImpl;
import org.apache.calcite.rel.RelVisitor;
import org.apache.calcite.rel.core.CorrelationId;
+import org.apache.calcite.rel.externalize.RelDotWriter;
import org.apache.calcite.rel.externalize.RelXmlWriter;
import org.apache.calcite.rel.logical.LogicalCalc;
import org.apache.calcite.rel.logical.LogicalFilter;
@@ -2233,6 +2234,23 @@ class SqlToRelConverterTest extends SqlToRelTestBase {
Util.toLinux(sw.toString()));
}
+ @Test void testExplainAsDot() {
+ String sql = "select 1 + 2, 3 from (values (true))";
+ final RelNode rel = tester.convertSqlToRel(sql).rel;
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ RelDotWriter planWriter =
+ new RelDotWriter(pw, SqlExplainLevel.EXPPLAN_ATTRIBUTES, false);
+ rel.explain(planWriter);
+ pw.flush();
+ TestUtil.assertEqualsVerbose(
+ "digraph {\n"
+ + "\"LogicalValues\\ntuples = [{ true }]\\n\" -> \"LogicalProject\\nEXPR$0 = +(1, 2)"
+ + "\\nEXPR$1 = 3\\n\" [label=\"0\"]\n"
+ + "}\n",
+ Util.toLinux(sw.toString()));
+ }
+
/** Test case for
* <a href="https://issues.apache.org/jira/browse/CALCITE-412">[CALCITE-412]
* RelFieldTrimmer: when trimming Sort, the collation and trait set don't