You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by ru...@apache.org on 2021/12/17 19:54:14 UTC
[calcite] branch master updated: [CALCITE-4737] Add RelOptPlanner visualizer for debugging (Zuozhi Wang, Thomas Rebele)
This is an automated email from the ASF dual-hosted git repository.
rubenql 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 ce25311 [CALCITE-4737] Add RelOptPlanner visualizer for debugging (Zuozhi Wang, Thomas Rebele)
ce25311 is described below
commit ce2531148f0c9990792068b12000deac827fc831
Author: Thomas Rebele <th...@gmail.com>
AuthorDate: Fri Dec 17 10:35:59 2021 +0100
[CALCITE-4737] Add RelOptPlanner visualizer for debugging (Zuozhi Wang, Thomas Rebele)
---
.../plan/visualizer/InputExcludedRelWriter.java | 89 ++++
.../calcite/plan/visualizer/NodeUpdateHelper.java | 107 +++++
.../plan/visualizer/RuleMatchVisualizer.java | 487 +++++++++++++++++++++
.../apache/calcite/plan/visualizer/StepInfo.java | 50 +++
.../calcite/plan/visualizer/package-info.java | 23 +
.../calcite/plan/visualizer/viz-template.html | 421 ++++++++++++++++++
.../org/apache/calcite/test/RelOptTestBase.java | 4 +-
.../calcite/test/RuleMatchVisualizerTest.java | 138 ++++++
.../calcite/test/RuleMatchVisualizerTest.xml | 264 +++++++++++
9 files changed, 1581 insertions(+), 2 deletions(-)
diff --git a/core/src/main/java/org/apache/calcite/plan/visualizer/InputExcludedRelWriter.java b/core/src/main/java/org/apache/calcite/plan/visualizer/InputExcludedRelWriter.java
new file mode 100644
index 0000000..5608da8
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/plan/visualizer/InputExcludedRelWriter.java
@@ -0,0 +1,89 @@
+/*
+ * 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.visualizer;
+
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.sql.SqlExplainLevel;
+import org.apache.calcite.util.Pair;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An implement of RelWriter for explaining a single RelNode.
+ * The result only contains the properties of the RelNode,
+ * but does not explain the children.
+ *
+ * <pre>{@code
+ * InputExcludedRelWriter relWriter = new InputExcludedRelWriter();
+ * rel.explain(relWriter);
+ * String digest = relWriter.toString();
+ * }</pre>
+ *
+ */
+class InputExcludedRelWriter implements RelWriter {
+
+ private final Map<String, @Nullable Object> values = new LinkedHashMap<>();
+
+ InputExcludedRelWriter() {
+ }
+
+
+ @Override public void explain(RelNode rel, List<Pair<String, @Nullable Object>> valueList) {
+ valueList.forEach(pair -> {
+ assert pair.left != null;
+ this.values.put(pair.left, pair.right);
+ });
+ }
+
+ @Override public SqlExplainLevel getDetailLevel() {
+ return SqlExplainLevel.EXPPLAN_ATTRIBUTES;
+ }
+
+ @Override public RelWriter input(String term, RelNode input) {
+ // do nothing, ignore input
+ return this;
+ }
+
+ @Override public RelWriter item(String term, @Nullable Object value) {
+ this.values.put(term, value);
+ return this;
+ }
+
+ @Override public RelWriter itemIf(String term, @Nullable Object value, boolean condition) {
+ if (condition) {
+ this.values.put(term, value);
+ }
+ return this;
+ }
+
+ @Override public RelWriter done(RelNode node) {
+ return this;
+ }
+
+ @Override public boolean nest() {
+ return false;
+ }
+
+ @Override public String toString() {
+ return values.toString();
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/plan/visualizer/NodeUpdateHelper.java b/core/src/main/java/org/apache/calcite/plan/visualizer/NodeUpdateHelper.java
new file mode 100644
index 0000000..cdbbb9d
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/plan/visualizer/NodeUpdateHelper.java
@@ -0,0 +1,107 @@
+/*
+ * 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.visualizer;
+
+import org.apache.calcite.rel.RelNode;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Helper class to create the node update.
+ */
+class NodeUpdateHelper {
+
+ private final String key;
+ private final @Nullable RelNode rel;
+ private final NodeUpdateInfo state;
+ private @Nullable NodeUpdateInfo update = null;
+
+ NodeUpdateHelper(String key, @Nullable RelNode rel) {
+ this.key = key;
+ this.rel = rel;
+ this.state = new NodeUpdateInfo();
+ }
+
+ String getKey() {
+ return key;
+ }
+
+ @Nullable RelNode getRel() {
+ return this.rel;
+ }
+
+ void updateAttribute(final String attr, final Object newValue) {
+ if (Objects.equals(newValue, state.get(attr))) {
+ return;
+ }
+
+ state.put(attr, newValue);
+
+ if (update == null) {
+ update = new NodeUpdateInfo();
+ }
+
+ if (newValue instanceof List
+ && ((List<?>) newValue).size() == 0
+ && !update.containsKey(attr)) {
+ return;
+ }
+
+ update.put(attr, newValue);
+ }
+
+ boolean isEmptyUpdate() {
+ return this.update == null || update.isEmpty();
+ }
+
+ /**
+ * Gets an object representing all the changes since the last call to this method.
+ *
+ * @return an object or null if there are no changes.
+ */
+ @Nullable Object getAndResetUpdate() {
+ if (isEmptyUpdate()) {
+ return null;
+ }
+ NodeUpdateInfo update = this.update;
+ this.update = null;
+ return update;
+ }
+
+ Map<String, Object> getState() {
+ return Collections.unmodifiableMap(this.state);
+ }
+
+ /**
+ * Get the current value for the attribute.
+ */
+ @Nullable Object getValue(final String attr) {
+ return this.state.get(attr);
+ }
+
+ /**
+ * Type alias.
+ */
+ private static class NodeUpdateInfo extends LinkedHashMap<String, Object> {
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/plan/visualizer/RuleMatchVisualizer.java b/core/src/main/java/org/apache/calcite/plan/visualizer/RuleMatchVisualizer.java
new file mode 100644
index 0000000..37d88a2
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/plan/visualizer/RuleMatchVisualizer.java
@@ -0,0 +1,487 @@
+/*
+ * 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.visualizer;
+
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptListener;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptRuleCall;
+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.metadata.RelMetadataQuery;
+
+import org.apache.commons.io.IOUtils;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Charsets;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.text.DecimalFormat;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * This is a tool to visualize the rule match process of a RelOptPlanner.
+ *
+ * <pre>{@code
+ * // create the visualizer
+ * RuleMatchVisualizer viz = new RuleMatchVisualizer("/path/to/output/dir", "file-name-suffix");
+ * viz.attachTo(planner)
+ *
+ * planner.findBestExpr();
+ *
+ * // extra step for HepPlanner: write the output to files
+ * // a VolcanoPlanner will call it automatically
+ * viz.writeToFile();
+ * }</pre>
+ */
+public class RuleMatchVisualizer implements RelOptListener {
+
+ private static final String INITIAL = "INITIAL";
+ private static final String FINAL = "FINAL";
+ public static final String DEFAULT_SET = "default";
+
+ // default HTML template can be edited at
+ // core/src/main/resources/org/apache/calcite/plan/visualizer/viz-template.html
+ private final String templateDirectory = "org/apache/calcite/plan/visualizer";
+ private final @Nullable String outputDirectory;
+ private final @Nullable String outputSuffix;
+
+ private String latestRuleID = "";
+ private int latestRuleTransformCount = 1;
+ private boolean initialized = false;
+
+ private @Nullable RelOptPlanner planner = null;
+
+ private boolean includeTransitiveEdges = false;
+ private boolean includeIntermediateCosts = false;
+
+ private final List<StepInfo> steps = new ArrayList<>();
+ private final Map<String, NodeUpdateHelper> allNodes = new LinkedHashMap<>();
+
+ /**
+ * Use this constructor to save the result on disk at the end of the planning phase.
+ * <p>
+ * Note: when using HepPlanner, {@link #writeToFile()} needs to be called manually.
+ * </p>
+ */
+ public RuleMatchVisualizer(
+ String outputDirectory,
+ String outputSuffix) {
+ this.outputDirectory = Objects.requireNonNull(outputDirectory, "outputDirectory");
+ this.outputSuffix = Objects.requireNonNull(outputSuffix, "outputSuffix");
+ }
+
+ /**
+ * Use this constructor when the result shall not be written to disk.
+ */
+ public RuleMatchVisualizer() {
+ this.outputDirectory = null;
+ this.outputSuffix = null;
+ }
+
+ /**
+ * Attaches the visualizer to the planner.
+ * Must be called before applying the rules.
+ * Must be called exactly once.
+ */
+ public void attachTo(RelOptPlanner planner) {
+ assert this.planner == null;
+ planner.addListener(this);
+ this.planner = planner;
+ }
+
+ /**
+ * Output edges from a subset to the nodes of all subsets that satisfy it.
+ */
+ public void setIncludeTransitiveEdges(final boolean includeTransitiveEdges) {
+ this.includeTransitiveEdges = includeTransitiveEdges;
+ }
+
+ /**
+ * Output intermediate costs, including all cost updates.
+ */
+ public void setIncludeIntermediateCosts(final boolean includeIntermediateCosts) {
+ this.includeIntermediateCosts = includeIntermediateCosts;
+ }
+
+ @Override public void ruleAttempted(RuleAttemptedEvent event) {
+ // HepPlanner compatibility
+ if (!initialized) {
+ assert planner != null;
+ RelNode root = planner.getRoot();
+ assert root != null;
+ initialized = true;
+ updateInitialPlan(root);
+ }
+ }
+
+ /**
+ * Register initial plan.
+ * (Workaround for HepPlanner)
+ */
+ private void updateInitialPlan(RelNode node) {
+ if (node instanceof HepRelVertex) {
+ HepRelVertex v = (HepRelVertex) node;
+ updateInitialPlan(v.getCurrentRel());
+ return;
+ }
+ this.registerRelNode(node);
+ for (RelNode input : getInputs(node)) {
+ updateInitialPlan(input);
+ }
+ }
+
+ /**
+ * Get the inputs for a node, unwrapping {@link HepRelVertex} nodes.
+ * (Workaround for HepPlanner)
+ */
+ private Collection<RelNode> getInputs(final RelNode node) {
+ return node.getInputs().stream().map(n -> {
+ if (n instanceof HepRelVertex) {
+ return ((HepRelVertex) n).getCurrentRel();
+ }
+ return n;
+ }).collect(Collectors.toList());
+ }
+
+ @Override public void relChosen(RelChosenEvent event) {
+ if (event.getRel() == null) {
+ assert this.planner != null;
+ RelNode root = this.planner.getRoot();
+ assert root != null;
+ updateFinalPlan(root);
+ this.addStep(FINAL, null);
+ this.writeToFile();
+ }
+ }
+
+ /**
+ * Mark nodes that are part of the final plan.
+ */
+ private void updateFinalPlan(RelNode node) {
+ int size = this.steps.size();
+ if (size > 0 && FINAL.equals(this.steps.get(size - 1).getId())) {
+ return;
+ }
+
+ this.registerRelNode(node).updateAttribute("inFinalPlan", Boolean.TRUE);
+ if (node instanceof RelSubset) {
+ RelNode best = ((RelSubset) node).getBest();
+ if (best == null) {
+ return;
+ }
+ updateFinalPlan(best);
+ } else {
+ for (RelNode input : getInputs(node)) {
+ updateFinalPlan(input);
+ }
+ }
+ }
+
+ @Override public void ruleProductionSucceeded(RuleProductionEvent event) {
+ // method is called once before ruleMatch, and once after ruleMatch
+ if (event.isBefore()) {
+ // add the initialState
+ if (latestRuleID.isEmpty()) {
+ this.addStep(INITIAL, null);
+ this.latestRuleID = INITIAL;
+ }
+ return;
+ }
+
+ // we add the state after the rule is applied
+ RelOptRuleCall ruleCall = event.getRuleCall();
+ String ruleID = Integer.toString(ruleCall.id);
+ String displayRuleName = ruleCall.id + "-" + ruleCall.getRule();
+
+ // a rule might call transform to multiple times, handle it by modifying the rule name
+ if (ruleID.equals(this.latestRuleID)) {
+ latestRuleTransformCount++;
+ displayRuleName += "-" + latestRuleTransformCount;
+ } else {
+ latestRuleTransformCount = 1;
+ }
+ this.latestRuleID = ruleID;
+
+ this.addStep(displayRuleName, ruleCall);
+ }
+
+ @Override public void relDiscarded(RelDiscardedEvent event) {
+ }
+
+ @Override public void relEquivalenceFound(RelEquivalenceEvent event) {
+ RelNode rel = event.getRel();
+ assert rel != null;
+ Object eqClass = event.getEquivalenceClass();
+ if (eqClass instanceof String) {
+ String eqClassStr = (String) eqClass;
+ eqClassStr = eqClassStr.replace("equivalence class ", "");
+ String setId = "set-" + eqClassStr;
+ registerSet(setId);
+ registerRelNode(rel).updateAttribute("set", setId);
+ }
+ // register node
+ this.registerRelNode(rel);
+ }
+
+ /**
+ * Add a set.
+ */
+ private void registerSet(final String setID) {
+ this.allNodes.computeIfAbsent(setID, k -> {
+ NodeUpdateHelper h = new NodeUpdateHelper(setID, null);
+ h.updateAttribute("label", DEFAULT_SET.equals(setID) ? "" : setID);
+ h.updateAttribute("kind", "set");
+ return h;
+ });
+ }
+
+ /**
+ * Add a RelNode to track its changes.
+ */
+ private NodeUpdateHelper registerRelNode(final RelNode rel) {
+ return this.allNodes.computeIfAbsent(key(rel), k -> {
+ NodeUpdateHelper h = new NodeUpdateHelper(key(rel), rel);
+ // attributes that need to be set only once
+ h.updateAttribute("label", getNodeLabel(rel));
+ h.updateAttribute("explanation", getNodeExplanation(rel));
+ h.updateAttribute("set", DEFAULT_SET);
+
+ if (rel instanceof RelSubset) {
+ h.updateAttribute("kind", "subset");
+ }
+ return h;
+ });
+ }
+
+ /**
+ * Check and store the changes of the rel node.
+ */
+ private void updateNodeInfo(final RelNode rel, final boolean isLastStep) {
+ NodeUpdateHelper helper = registerRelNode(rel);
+ if (this.includeIntermediateCosts || isLastStep) {
+ RelOptPlanner planner = this.planner;
+ assert planner != null;
+ RelMetadataQuery mq = rel.getCluster().getMetadataQuery();
+ RelOptCost cost = planner.getCost(rel, mq);
+ Double rowCount = mq.getRowCount(rel);
+ helper.updateAttribute("cost", formatCost(rowCount, cost));
+ }
+
+ List<String> inputs = new ArrayList<>();
+ if (rel instanceof RelSubset) {
+ RelSubset relSubset = (RelSubset) rel;
+ relSubset.getRels().forEach(input -> inputs.add(key(input)));
+ Set<String> transitive = new HashSet<>();
+ relSubset.getSubsetsSatisfyingThis()
+ .filter(other -> !other.equals(relSubset))
+ .forEach(input -> {
+ inputs.add(key(input));
+ if (!includeTransitiveEdges) {
+ input.getRels().forEach(r -> transitive.add(key(r)));
+ }
+ });
+ inputs.removeAll(transitive);
+ } else {
+ getInputs(rel).forEach(input -> inputs.add(key(input)));
+ }
+
+ helper.updateAttribute("inputs", inputs);
+ }
+
+ /**
+ * Add the updates since the last step to {@link #steps}.
+ */
+ private void addStep(String stepID, @Nullable RelOptRuleCall ruleCall) {
+ Map<String, Object> nextNodeUpdates = new LinkedHashMap<>();
+
+ // HepPlanner compatibility
+ boolean usesDefaultSet = this.allNodes.values()
+ .stream()
+ .anyMatch(h -> DEFAULT_SET.equals(h.getValue("set")));
+ if (usesDefaultSet) {
+ this.registerSet(DEFAULT_SET);
+ }
+
+ for (NodeUpdateHelper h : allNodes.values()) {
+ RelNode rel = h.getRel();
+ if (rel != null) {
+ updateNodeInfo(rel, FINAL.equals(stepID));
+ }
+ if (h.isEmptyUpdate()) {
+ continue;
+ }
+ Object update = h.getAndResetUpdate();
+ if (update != null) {
+ nextNodeUpdates.put(h.getKey(), update);
+ }
+ }
+
+ List<String> matchedRels = ruleCall == null
+ ? Collections.emptyList()
+ : Arrays.stream(ruleCall.rels).map(this::key).collect(Collectors.toList());
+ this.steps.add(new StepInfo(stepID, nextNodeUpdates, matchedRels));
+ }
+
+ public String getJsonStringResult() {
+ try {
+ LinkedHashMap<String, Object> data = new LinkedHashMap<>();
+ data.put("steps", steps);
+ ObjectMapper objectMapper = new ObjectMapper();
+ DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
+ printer = printer.withoutSpacesInObjectEntries();
+ return objectMapper.writer(printer).writeValueAsString(data);
+ } catch (JsonProcessingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Writes the HTML and JS files of the rule match visualization.
+ * <p>
+ * The old files with the same name will be replaced.
+ */
+ public void writeToFile() {
+ if (outputDirectory == null || outputSuffix == null) {
+ return;
+ }
+
+ try {
+ String templatePath = Paths.get(templateDirectory).resolve("viz-template.html").toString();
+ ClassLoader cl = getClass().getClassLoader();
+ assert cl != null;
+ InputStream resourceAsStream = cl.getResourceAsStream(templatePath);
+ assert resourceAsStream != null;
+ String htmlTemplate = IOUtils.toString(resourceAsStream, StandardCharsets.UTF_8);
+
+ String htmlFileName = "planner-viz" + outputSuffix + ".html";
+ String dataFileName = "planner-viz-data" + outputSuffix + ".js";
+
+ String replaceString = "src=\"planner-viz-data.js\"";
+ int replaceIndex = htmlTemplate.indexOf(replaceString);
+ String htmlContent = htmlTemplate.substring(0, replaceIndex)
+ + "src=\"" + dataFileName + "\""
+ + htmlTemplate.substring(replaceIndex + replaceString.length());
+
+ String dataJsContent = "var data = " + getJsonStringResult() + ";\n";
+
+ Path outputDirPath = Paths.get(outputDirectory);
+ Path htmlOutput = outputDirPath.resolve(htmlFileName);
+ Path dataOutput = outputDirPath.resolve(dataFileName);
+
+ if (!Files.exists(outputDirPath)) {
+ Files.createDirectories(outputDirPath);
+ }
+
+ Files.write(htmlOutput, htmlContent.getBytes(Charsets.UTF_8), StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING);
+ Files.write(dataOutput, dataJsContent.getBytes(Charsets.UTF_8), StandardOpenOption.CREATE,
+ StandardOpenOption.TRUNCATE_EXISTING);
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ //--------------------------------------------------------------------------------
+ // methods related to string representation
+ //--------------------------------------------------------------------------------
+
+ private String key(final RelNode rel) {
+ return "" + rel.getId();
+ }
+
+ private String getNodeLabel(final RelNode relNode) {
+ if (relNode instanceof RelSubset) {
+ final RelSubset relSubset = (RelSubset) relNode;
+ String setId = getSetId(relSubset);
+ return "subset#" + relSubset.getId() + "-set" + setId + "-\n"
+ + relSubset.getTraitSet();
+ }
+
+ return "#" + relNode.getId() + "-" + relNode.getRelTypeName();
+ }
+
+ private String getSetId(final RelSubset relSubset) {
+ String explanation = getNodeExplanation(relSubset);
+ int start = explanation.indexOf("RelSubset") + "RelSubset".length();
+ if (start < 0) {
+ return "";
+ }
+ int end = explanation.indexOf(".", start);
+ if (end < 0) {
+ return "";
+ }
+ return explanation.substring(start, end);
+ }
+
+ private String getNodeExplanation(final RelNode relNode) {
+ InputExcludedRelWriter relWriter = new InputExcludedRelWriter();
+ relNode.explain(relWriter);
+ return relWriter.toString();
+ }
+
+ private static String formatCost(Double rowCount, @Nullable RelOptCost cost) {
+ if (cost == null) {
+ return "null";
+ }
+ String originalStr = cost.toString();
+ if (originalStr.contains("inf") || originalStr.contains("huge")
+ || originalStr.contains("tiny")) {
+ return originalStr;
+ }
+ return new MessageFormat("\nrowCount: {0}\nrows: {1}\ncpu: {2}\nio: {3}",
+ Locale.ROOT).format(new String[]{
+ formatCostScientific(rowCount),
+ formatCostScientific(cost.getRows()),
+ formatCostScientific(cost.getCpu()),
+ formatCostScientific(cost.getIo())
+ }
+ );
+ }
+
+ private static String formatCostScientific(double costNumber) {
+ long costRounded = Math.round(costNumber);
+ DecimalFormat formatter = (DecimalFormat) DecimalFormat.getInstance(Locale.ROOT);
+ formatter.applyPattern("#.#############################################E0");
+ return formatter.format(costRounded);
+ }
+
+}
diff --git a/core/src/main/java/org/apache/calcite/plan/visualizer/StepInfo.java b/core/src/main/java/org/apache/calcite/plan/visualizer/StepInfo.java
new file mode 100644
index 0000000..ce91058
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/plan/visualizer/StepInfo.java
@@ -0,0 +1,50 @@
+/*
+ * 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.visualizer;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A step in the visualizer represents one rule call of the planner.
+ */
+class StepInfo {
+ private final String id;
+ private final Map<String, Object> updates;
+ private final List<String> matchedRels;
+
+ StepInfo(final String id,
+ final Map<String, Object> updates,
+ final List<String> matchedRels) {
+ this.id = id;
+ this.updates = Collections.unmodifiableMap(updates);
+ this.matchedRels = Collections.unmodifiableList(matchedRels);
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public Map<String, Object> getUpdates() {
+ return updates;
+ }
+
+ public List<String> getMatchedRels() {
+ return matchedRels;
+ }
+}
diff --git a/core/src/main/java/org/apache/calcite/plan/visualizer/package-info.java b/core/src/main/java/org/apache/calcite/plan/visualizer/package-info.java
new file mode 100644
index 0000000..4f23efb
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/plan/visualizer/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+/**
+ * A visualizer showing how the rules are applied step-by-step.
+ *
+ * @see org.apache.calcite.plan.visualizer.RuleMatchVisualizer
+ */
+package org.apache.calcite.plan.visualizer;
diff --git a/core/src/main/resources/org/apache/calcite/plan/visualizer/viz-template.html b/core/src/main/resources/org/apache/calcite/plan/visualizer/viz-template.html
new file mode 100644
index 0000000..dfaf6ae
--- /dev/null
+++ b/core/src/main/resources/org/apache/calcite/plan/visualizer/viz-template.html
@@ -0,0 +1,421 @@
+<!doctype html>
+<html lang="en">
+<!--
+{% comment %}
+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.
+{% endcomment %}
+-->
+<meta charset="utf-8">
+<title>Calcite Rule Match Visualization</title>
+
+<script src="https://unpkg.com/d3@7.2.1/dist/d3.min.js" charset="utf-8"></script>
+<script src="https://unpkg.com/dagre-d3@0.6.4/dist/dagre-d3.min.js"></script>
+<script src="https://unpkg.com/tippy.js@3/dist/tippy.all.min.js"></script>
+<script src="planner-viz-data.js"></script>
+
+<style id="css">
+ body {
+ height: 100vh;
+ width: 100vw;
+ margin: 0 0;
+ color: #333;
+ font-weight: 300;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
+ }
+
+ li a {
+ display: block;
+ padding: 5px 20px;
+ }
+
+ section {
+ margin-bottom: 3em;
+ }
+
+ section p {
+ text-align: justify;
+ }
+
+ svg {
+ overflow: hidden;
+ margin: 0 auto;
+ }
+
+ pre {
+ border: 1px solid #ccc;
+ }
+
+ #step-list-column {
+ border-left: 1px solid #ccc;
+ }
+
+ .clusters rect {
+ fill: #FFFFE0;
+ stroke: #999;
+ stroke-width: 1.5px;
+ }
+
+ text {
+ font-weight: 300;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
+ font-size: 2em;
+ }
+
+ .node rect {
+ stroke: #999;
+ fill: #fff;
+ stroke-width: 1.5px;
+ }
+
+ .edgePath path {
+ stroke: #333;
+ stroke-width: 2px;
+ }
+
+ .container {
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-flow: row nowrap;
+ }
+
+ .column1 {
+ display: flex;
+ flex-flow: column nowrap;
+ }
+
+ .column2 {
+ flex: 0 1 100%;
+ }
+
+ .tippy-content {
+ word-break: break-all;
+ word-wrap: break-word;
+ }
+
+ button {
+ padding: 0.1em;
+ display: inline-block;
+ font-size: 2em;
+ min-width: 1.5em;
+ }
+</style>
+
+<div class="container">
+ <div class="column2">
+ <div id="toolbar">
+ <button id="left-button" style="display:inline-block">⤿</button>
+ <button id="right-button" style="display:inline-block">⤾</button>
+ <button id="fit-content-button" style="display:inline-block">⇿</button>
+ <span> </span>
+ <button id="toggle-list-button">≡</button>
+ <button id="prev-button" disabled>◀</button>
+ <button id="next-button" disabled>▶</button>
+ <div id="current-step" style="display: inline-block"></div>
+ </div>
+ <svg id="svg-canvas" width="100%" height="100%" ></svg>
+ </div>
+ <div id="step-list-column" class="column1">
+ <div style="width: 100%; text-align: center">
+ </div>
+ <ol id="step-list" style="overflow: auto" start="0">
+ </ul>
+ </div>
+</div>
+
+<script id="js">
+
+ var rankDirs = ["BT", "RL", "TB", "LR"];
+
+ /*
+ * Graph data and D3 JS render related variables
+ */
+
+ // Create the input graph
+ var g = new dagreD3.graphlib.Graph({
+ compound: true
+ })
+ .setGraph({
+ rankdir: 'LR',
+ })
+ .setDefaultEdgeLabel(function () {
+ return {};
+ });
+
+ // Create the renderer
+ var render = new dagreD3.render();
+
+ // Set up an SVG group so that we can translate the final graph.
+ var svg = d3.select("svg");
+ var svgGroup = svg.append("g");
+
+ // Set up zoom support
+ const zoom = d3.zoom().on('zoom', (e) => svgGroup.attr('transform', e.transform));
+ var svg = d3.select('svg')
+ .call(zoom);
+
+ var fitContent = () => {
+ const { x, y, width, height } = svgGroup.node().getBBox();
+ const { clientWidth, clientHeight } = svg.node();
+ if (width && height) {
+ const scale = Math.min(clientWidth / width, clientHeight / height) * 0.98
+ zoom.scaleTo(svg, scale)
+ zoom.translateTo(svg, width/2+x , height/2+y )
+ }
+ };
+
+ /*
+ * Global State
+ */
+
+ var currentStepIndex = 0;
+ var currentRankDirIdx = 0;
+
+ /*
+ * Event Handler functions
+ */
+
+ var updateLocation = () => {
+ var urlParams = new URLSearchParams(location.search);
+ urlParams.set("step", currentStepIndex);
+ urlParams.set("dir", currentRankDirIdx);
+ window.history.pushState({}, "", "?" + urlParams.toString());
+ };
+
+ var parseLocation = () => {
+ var urlParams = new URLSearchParams(location.search);
+ if (urlParams.has("step")) {
+ var stepIdx = Number(urlParams.get("step"))
+ if (Number.isInteger(stepIdx))
+ currentStepIndex = stepIdx;
+ }
+ if (urlParams.has("dir")) {
+ var dirIdx = Number(urlParams.get("dir"))
+ if (Number.isInteger(dirIdx) && dirIdx >= 0 && dirIdx < rankDirs.length)
+ currentRankDirIdx = dirIdx;
+ }
+ };
+
+ var setCurrentStep = (stepIndex) => {
+ // un-highlight previous entry
+ var prevStepIndex = currentStepIndex;
+ if (prevStepIndex !== undefined) {
+ var prevStepElement = document.getElementById(data.steps[prevStepIndex]["id"]);
+ prevStepElement.style.backgroundColor = "#FFFFFF";
+ }
+
+ currentStepIndex = stepIndex;
+ var currentStepID = data.steps[stepIndex]["id"];
+ document.getElementById('current-step').innerText = currentStepIndex + ": " + currentStepID;
+
+ var currentStepElement = document.getElementById(currentStepID);
+ currentStepElement.style.backgroundColor = "#D3D3D3";
+
+ document.getElementById("prev-button").disabled = false;
+ document.getElementById("next-button").disabled = false;
+
+ if (currentStepIndex === 0) {
+ document.getElementById("prev-button").disabled = true;
+ }
+ if (currentStepIndex === data.steps.length - 1) {
+ document.getElementById("next-button").disabled = true;
+ }
+
+ updateGraph();
+ }
+
+ var getCurrentState = () => {
+ var nodes = {};
+ for(var i=0; i<=currentStepIndex; i++) {
+ // recreate state by merging all updates
+ var updates = data.steps[i]["updates"];
+ Object.entries(updates).forEach(e => {
+ const [key, value] = e;
+ var nodeInfo = nodes[key] ??= {
+ id: key,
+ addedInStep: i,
+ };
+ Object.assign(nodeInfo, value);
+ });
+ }
+ //var newNodes = Object.values(nodes).filter(n => n.addedInStep === currentStepIndex);
+ var matchedRels = data.steps[currentStepIndex]["matchedRels"] ?? [];
+ return { nodes, matchedRels };
+ };
+
+ var updateGraph = () => {
+ updateLocation();
+
+ var state = getCurrentState();
+ var stepID = data.steps[currentStepIndex]["id"];
+
+ // remove previous rendered view and clear graph model
+ d3.select("svg g").selectAll("*").remove();
+ g.nodes().slice().forEach(nodeID => g.removeNode(nodeID));
+
+ for(var n of Object.values(state.nodes)) {
+ var nodeID = n.id;
+ if(n.kind === "set") {
+ // add set
+ var setLabel = n.Label;
+ if (setLabel === null || setLabel === undefined) {
+ setLabel = nodeID;
+ }
+ g.setNode(nodeID, {
+ label: setLabel,
+ clusterLabelPos: 'top'
+ });
+ }
+ else {
+ var nodeLabel;
+ if (stepID === "FINAL") {
+ nodeLabel = n.label + "\n" + n.cost;
+ } else {
+ nodeLabel = n.label;
+ }
+ var nodeStyle;
+ if (stepID === "FINAL" && n.inFinalPlan === true) {
+ if(n.kind === "subset")
+ nodeStyle = "fill: #E0FFFF";
+ else
+ nodeStyle = "fill: #C8C8F3";
+ }
+ else if (stepID !== "INITIAL" && n.addedInStep == currentStepIndex) {
+ nodeStyle = "fill: #E0FFFF";
+ } else if (state.matchedRels.includes(nodeID)) {
+ nodeStyle = "fill: #C8C8F3";
+ } else {
+ nodeStyle = "fill: #FFFFFF";
+ }
+ g.setNode(nodeID, {
+ label: nodeLabel,
+ style: nodeStyle
+ });
+ // node-set parent relationship
+ g.setParent(nodeID, n.set);
+
+ // create links
+ if(n.inputs)
+ for(var inputID of n.inputs) {
+ var input = state.nodes[inputID];
+ var edgeOptions = { arrowheadStyle: "normal" };
+ if (n.kind === "subset" && input.kind === "subset") {
+ edgeOptions = { style: "stroke-dasharray: 5, 5; fill: none;" };
+ }
+ g.setEdge(inputID, nodeID, edgeOptions);
+ }
+ }
+ }
+
+ g.setGraph({
+ rankdir: rankDirs[currentRankDirIdx]
+ })
+
+ // re-render
+ render(d3.select("svg g"), g);
+
+ // register tooltip popup
+ const allD3Nodes = d3.select('svg').selectAll('.node');
+ const allD3NodeElements = allD3Nodes.nodes();
+
+ tippy.setDefaults({
+ trigger: "click",
+ interactive: true,
+ });
+
+ var i = 0;
+ allD3Nodes.each(nodeID => {
+ var nodeElement = allD3NodeElements[i];
+ var node = state.nodes[nodeID];
+ var stepName = data.steps[node.addedInStep].id;
+ var popupContent;
+ if(node.kind === "subset")
+ popupContent = "inputs: " + node.inputs;
+ else
+ popupContent = node.explanation;
+ popupContent += "<br>Added in Step '" + stepName + "'";
+
+ tippy(nodeElement, { content: popupContent })
+ i++;
+ });
+ }
+
+ /*
+ * render HTML Element and add event hanlders
+ */
+
+ // populate UI list
+ var stepListElement = document.getElementById("step-list");
+ data.steps.forEach((step, index) => {
+ var stepID = step["id"];
+ var listItem = document.createElement("li");
+ var textItem = document.createElement("a");
+ textItem.innerText = stepID;
+ textItem.id = stepID;
+ textItem.setAttribute("href", "#");
+
+ listItem.appendChild(textItem);
+ stepListElement.appendChild(listItem);
+ listItem.addEventListener("click", event => {
+ setCurrentStep(index);
+ })
+ })
+
+ document.getElementById("prev-button").addEventListener("click", event => {
+ if (currentStepIndex !== 0) {
+ setCurrentStep(currentStepIndex - 1);
+ }
+ });
+
+ document.getElementById("next-button").addEventListener("click", event => {
+ if (currentStepIndex !== data.steps.length - 1) {
+ setCurrentStep(currentStepIndex + 1);
+ }
+ });
+
+ document.getElementById("left-button").addEventListener("click", event => {
+ currentRankDirIdx += 1;
+ if (currentRankDirIdx >= rankDirs.length)
+ currentRankDirIdx = 0;
+ updateGraph();
+ });
+
+ document.getElementById("right-button").addEventListener("click", event => {
+ currentRankDirIdx -= 1;
+ if (currentRankDirIdx < 0)
+ currentRankDirIdx = 3;
+ updateGraph();
+ });
+
+ document.getElementById("fit-content-button").addEventListener("click", fitContent);
+
+ document.getElementById("toggle-list-button").addEventListener("click", () => {
+ var col1 = document.getElementById("step-list-column");
+ if (col1.style.display === "none")
+ col1.style.display = "";
+ else
+ col1.style.display = "none";
+ });
+
+ // render initial state
+
+ parseLocation();
+ setCurrentStep(currentStepIndex);
+ fitContent();
+</script>
+
+</html>
diff --git a/core/src/test/java/org/apache/calcite/test/RelOptTestBase.java b/core/src/test/java/org/apache/calcite/test/RelOptTestBase.java
index f94242a..e599bc7 100644
--- a/core/src/test/java/org/apache/calcite/test/RelOptTestBase.java
+++ b/core/src/test/java/org/apache/calcite/test/RelOptTestBase.java
@@ -221,8 +221,8 @@ abstract class RelOptTestBase extends SqlToRelTestBase {
return withPre(builder.build());
}
- public Sql with(HepPlanner hepPlanner) {
- return new Sql(tester, sql, preProgram, hepPlanner, hooks, transforms, relFn);
+ public Sql with(RelOptPlanner planner) {
+ return new Sql(tester, sql, preProgram, planner, hooks, transforms, relFn);
}
public Sql with(HepProgram program) {
diff --git a/core/src/test/java/org/apache/calcite/test/RuleMatchVisualizerTest.java b/core/src/test/java/org/apache/calcite/test/RuleMatchVisualizerTest.java
new file mode 100644
index 0000000..52445db
--- /dev/null
+++ b/core/src/test/java/org/apache/calcite/test/RuleMatchVisualizerTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.hep.HepPlanner;
+import org.apache.calcite.plan.hep.HepProgram;
+import org.apache.calcite.plan.visualizer.RuleMatchVisualizer;
+import org.apache.calcite.plan.volcano.VolcanoPlanner;
+import org.apache.calcite.rel.RelCollationTraitDef;
+import org.apache.calcite.rel.rules.CoreRules;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Check the output of {@link RuleMatchVisualizer}.
+ */
+public class RuleMatchVisualizerTest extends RelOptTestBase {
+
+ protected DiffRepository getDiffRepos() {
+ return DiffRepository.lookup(RuleMatchVisualizerTest.class);
+ }
+
+ @Test void testHepPlanner() {
+ final String sql = "select a.name from dept a\n"
+ + "union all\n"
+ + "select b.name from dept b\n"
+ + "order by name limit 10";
+
+ final HepProgram program = HepProgram.builder()
+ .addRuleInstance(CoreRules.PROJECT_SET_OP_TRANSPOSE)
+ .addRuleInstance(CoreRules.SORT_UNION_TRANSPOSE)
+ .build();
+ HepPlanner planner = new HepPlanner(program);
+
+ RuleMatchVisualizer viz = new RuleMatchVisualizer();
+ viz.attachTo(planner);
+
+ sql(sql).with(planner).check();
+
+ String result = normalize(viz.getJsonStringResult());
+ getDiffRepos().assertEquals("visualizer", "${visualizer}", result);
+ }
+
+ @Test void testVolcanoPlanner() {
+ final String sql = "select a.name from dept a";
+
+ VolcanoPlanner planner = new VolcanoPlanner();
+ planner.setTopDownOpt(false);
+ planner.addRelTraitDef(ConventionTraitDef.INSTANCE);
+ planner.addRelTraitDef(RelCollationTraitDef.INSTANCE);
+
+ RelOptUtil.registerDefaultRules(planner, false, false);
+
+ RuleMatchVisualizer viz = new RuleMatchVisualizer();
+ viz.attachTo(planner);
+
+
+ sql(sql)
+ .with(planner)
+ .withTester(
+ t -> t.withClusterFactory(
+ cluster -> RelOptCluster.create(planner, cluster.getRexBuilder())))
+ .check();
+
+ String result = normalize(viz.getJsonStringResult());
+ getDiffRepos().assertEquals("visualizer", "${visualizer}", result);
+ }
+
+ /**
+ * Normalize the visualizer output, so that it is independent of other tests.
+ */
+ private String normalize(String str) {
+ // rename rel ids
+ str = renameMatches(
+ str, Pattern.compile("\"([0-9]+)\"|"
+ + "\"label\" *: *\"#([0-9]+)-|"
+ + "\"label\" *: *\"subset#([0-9]+)-|"
+ + "\"explanation\" *: *\"\\{subset=rel#([0-9]+):"), 1000);
+ // rename rule call ids
+ str = renameMatches(str, Pattern.compile("\"id\" *: *\"([0-9]+)-"), 100);
+ return str;
+ }
+
+ /**
+ * Rename the first group of each match to a consecutive index, starting at the offset.
+ */
+ private String renameMatches(final String str,
+ final Pattern pattern, int offset) {
+ Map<String, String> rename = new HashMap<>();
+ StringBuilder sb = new StringBuilder();
+ Matcher m = pattern.matcher(str);
+
+ int last = 0;
+ while (m.find()) {
+ int start = -1;
+ int end = -1;
+ String oldName = null;
+ for (int i = 1; i <= m.groupCount(); i++) {
+ if (m.group(i) != null) {
+ oldName = m.group(i);
+ start = m.start(i);
+ end = m.end(i);
+ break;
+ }
+ }
+ assert oldName != null;
+ String newName = rename.computeIfAbsent(oldName, k -> "" + (rename.size() + offset));
+ sb.append(str, last, start);
+ sb.append(newName);
+ last = end;
+ }
+ sb.append(str.substring(last));
+ return sb.toString();
+ }
+
+}
diff --git a/core/src/test/resources/org/apache/calcite/test/RuleMatchVisualizerTest.xml b/core/src/test/resources/org/apache/calcite/test/RuleMatchVisualizerTest.xml
new file mode 100644
index 0000000..7f07d0b
--- /dev/null
+++ b/core/src/test/resources/org/apache/calcite/test/RuleMatchVisualizerTest.xml
@@ -0,0 +1,264 @@
+<?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="testHepPlanner">
+ <Resource name="sql">
+ <![CDATA[select a.name from dept a
+union all
+select b.name from dept b
+order by name limit 10]]>
+ </Resource>
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalSort(sort0=[$0], dir0=[ASC], fetch=[10])
+ LogicalProject(NAME=[$0])
+ LogicalUnion(all=[true])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+LogicalSort(sort0=[$0], dir0=[ASC], fetch=[10])
+ LogicalUnion(all=[true])
+ LogicalSort(sort0=[$0], dir0=[ASC], fetch=[10])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+ LogicalSort(sort0=[$0], dir0=[ASC], fetch=[10])
+ LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+ </Resource>
+ <Resource name="visualizer">
+ <![CDATA[{
+ "steps":[ {
+ "id":"INITIAL",
+ "updates":{
+ "1000":{
+ "label":"#1000-LogicalTableScan",
+ "explanation":"{table=[CATALOG, SALES, DEPT]}",
+ "set":"default"
+ },
+ "1001":{
+ "label":"#1001-LogicalProject",
+ "explanation":"{NAME=$1}",
+ "set":"default",
+ "inputs":[ "1000" ]
+ },
+ "1002":{
+ "label":"#1002-LogicalUnion",
+ "explanation":"{all=true}",
+ "set":"default",
+ "inputs":[ "1001", "1001" ]
+ },
+ "1003":{
+ "label":"#1003-LogicalProject",
+ "explanation":"{NAME=$0}",
+ "set":"default",
+ "inputs":[ "1002" ]
+ },
+ "1004":{
+ "label":"#1004-LogicalSort",
+ "explanation":"{sort0=$0, dir0=ASC, fetch=10}",
+ "set":"default",
+ "inputs":[ "1003" ]
+ },
+ "default":{
+ "label":"",
+ "kind":"set"
+ }
+ },
+ "matchedRels":[ ]
+ }, {
+ "id":"100-ProjectSetOpTransposeRule",
+ "updates":{
+ "1004":{
+ "inputs":[ "1002" ]
+ }
+ },
+ "matchedRels":[ "1003", "1002" ]
+ }, {
+ "id":"101-SortUnionTransposeRule",
+ "updates":{
+ "1005":{
+ "label":"#1005-LogicalSort",
+ "explanation":"{sort0=$0, dir0=ASC, fetch=10}",
+ "set":"default",
+ "inputs":[ "1001" ]
+ },
+ "1006":{
+ "label":"#1006-LogicalUnion",
+ "explanation":"{all=true}",
+ "set":"default",
+ "inputs":[ "1005", "1005" ]
+ },
+ "1007":{
+ "label":"#1007-LogicalSort",
+ "explanation":"{sort0=$0, dir0=ASC, fetch=10}",
+ "set":"default",
+ "inputs":[ "1006" ]
+ }
+ },
+ "matchedRels":[ "1004", "1002" ]
+ } ]
+}]]>
+ </Resource>
+ </TestCase>
+ <TestCase name="testVolcanoPlanner">
+ <Resource name="sql">
+ <![CDATA[select a.name from dept a]]>
+ </Resource>
+ <Resource name="planBefore">
+ <![CDATA[
+LogicalProject(NAME=[$1])
+ LogicalTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+ </Resource>
+ <Resource name="planAfter">
+ <![CDATA[
+EnumerableProject(NAME=[$1])
+ EnumerableTableScan(table=[[CATALOG, SALES, DEPT]])
+]]>
+ </Resource>
+ <Resource name="visualizer">
+ <![CDATA[{
+ "steps":[ {
+ "id":"INITIAL",
+ "updates":{
+ "set-0":{
+ "label":"set-0",
+ "kind":"set"
+ },
+ "1000":{
+ "label":"subset#1000-set#0-\nNONE.[]",
+ "explanation":"{subset=rel#1000:RelSubset#0.NONE.[]}",
+ "set":"set-0",
+ "kind":"subset",
+ "inputs":[ "1001" ]
+ },
+ "1001":{
+ "label":"#1001-LogicalTableScan",
+ "explanation":"{table=[CATALOG, SALES, DEPT]}",
+ "set":"set-0"
+ },
+ "set-1":{
+ "label":"set-1",
+ "kind":"set"
+ },
+ "1002":{
+ "label":"subset#1002-set#1-\nNONE.[]",
+ "explanation":"{subset=rel#1002:RelSubset#1.NONE.[]}",
+ "set":"set-1",
+ "kind":"subset",
+ "inputs":[ "1003" ]
+ },
+ "1003":{
+ "label":"#1003-LogicalProject",
+ "explanation":"{NAME=$1}",
+ "set":"set-1",
+ "inputs":[ "1000" ]
+ },
+ "1004":{
+ "label":"subset#1004-set#1-\nENUMERABLE.[]",
+ "explanation":"{subset=rel#1004:RelSubset#1.ENUMERABLE.[], NAME=$1}",
+ "set":"set-1",
+ "kind":"subset",
+ "inputs":[ "1005" ]
+ },
+ "1005":{
+ "label":"#1005-AbstractConverter",
+ "explanation":"{convention=ENUMERABLE, sort=[]}",
+ "set":"set-1",
+ "inputs":[ "1002" ]
+ }
+ },
+ "matchedRels":[ ]
+ }, {
+ "id":"100-EnumerableTableScanRule(in:NONE,out:ENUMERABLE)",
+ "updates":{
+ "1006":{
+ "label":"subset#1006-set#0-\nENUMERABLE.[]",
+ "explanation":"{subset=rel#1006:RelSubset#0.ENUMERABLE.[], table=[CATALOG, SALES, DEPT]}",
+ "set":"set-0",
+ "kind":"subset",
+ "inputs":[ "1007" ]
+ },
+ "1007":{
+ "label":"#1007-EnumerableTableScan",
+ "explanation":"{table=[CATALOG, SALES, DEPT]}",
+ "set":"set-0"
+ }
+ },
+ "matchedRels":[ "1001" ]
+ }, {
+ "id":"101-EnumerableProjectRule(in:NONE,out:ENUMERABLE)",
+ "updates":{
+ "1004":{
+ "inputs":[ "1005", "1008" ]
+ },
+ "1008":{
+ "label":"#1008-EnumerableProject",
+ "explanation":"{NAME=$1}",
+ "set":"set-1",
+ "inputs":[ "1006" ]
+ }
+ },
+ "matchedRels":[ "1003" ]
+ }, {
+ "id":"FINAL",
+ "updates":{
+ "1000":{
+ "cost":"{inf}"
+ },
+ "1001":{
+ "cost":"{inf}"
+ },
+ "1002":{
+ "cost":"{inf}"
+ },
+ "1003":{
+ "cost":"{inf}"
+ },
+ "1004":{
+ "inFinalPlan":true,
+ "cost":"\nrowCount: 4E0\nrows: 8E0\ncpu: 9E0\nio: 0E0"
+ },
+ "1005":{
+ "cost":"{inf}"
+ },
+ "1006":{
+ "inFinalPlan":true,
+ "cost":"\nrowCount: 4E0\nrows: 4E0\ncpu: 5E0\nio: 0E0"
+ },
+ "1007":{
+ "inFinalPlan":true,
+ "cost":"\nrowCount: 4E0\nrows: 4E0\ncpu: 5E0\nio: 0E0"
+ },
+ "1008":{
+ "inFinalPlan":true,
+ "cost":"\nrowCount: 4E0\nrows: 8E0\ncpu: 9E0\nio: 0E0"
+ }
+ },
+ "matchedRels":[ ]
+ } ]
+}]]>
+ </Resource>
+ </TestCase>
+</Root>