Skip to content

Commit 008d47e

Browse files
committed
[CALCITE-7427] Connection config for enabling RuleMatchVisualize
1 parent 90712c8 commit 008d47e

File tree

8 files changed

+396
-4
lines changed

8 files changed

+396
-4
lines changed

core/src/main/java/org/apache/calcite/config/CalciteConnectionConfig.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ public interface CalciteConnectionConfig extends ConnectionConfig {
111111
/** Returns the value of {@link CalciteConnectionProperty#TOPDOWN_OPT}. */
112112
boolean topDownOpt();
113113

114+
/** Returns the value of {@link CalciteConnectionProperty#RULE_VISUALIZER_DIR}. */
115+
@Nullable String ruleVisualizerDir();
116+
114117
/** Returns the value of {@link CalciteConnectionProperty#META_TABLE_FACTORY},
115118
* or a default meta table factory if not set. If
116119
* {@code defaultMetaTableFactory} is not null, the result is never null. */

core/src/main/java/org/apache/calcite/config/CalciteConnectionConfigImpl.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,11 @@ public boolean isSet(CalciteConnectionProperty property) {
215215
.getBoolean();
216216
}
217217

218+
@Override public @Nullable String ruleVisualizerDir() {
219+
return CalciteConnectionProperty.RULE_VISUALIZER_DIR.wrap(properties)
220+
.getString();
221+
}
222+
218223
@Override public <T> @PolyNull T metaTableFactory(
219224
Class<T> metaTableFactoryClass,
220225
@PolyNull T defaultMetaTableFactory) {

core/src/main/java/org/apache/calcite/config/CalciteConnectionProperty.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,12 @@ public enum CalciteConnectionProperty implements ConnectionProperty {
186186
LENIENT_OPERATOR_LOOKUP("lenientOperatorLookup", Type.BOOLEAN, false, false),
187187

188188
/** Whether to enable top-down optimization in Volcano planner. */
189-
TOPDOWN_OPT("topDownOpt", Type.BOOLEAN, CalciteSystemProperty.TOPDOWN_OPT.value(), false);
189+
TOPDOWN_OPT("topDownOpt", Type.BOOLEAN, CalciteSystemProperty.TOPDOWN_OPT.value(), false),
190+
191+
/** Directory path for RuleMatchVisualizer output.
192+
* If set, enables visualization of the rule matching process during query optimization.
193+
* The visualizer will create HTML and JSON files in the specified directory. */
194+
RULE_VISUALIZER_DIR("ruleVisualizerDir", Type.STRING, null, false);
190195

191196
private final String camelName;
192197
private final Type type;

core/src/main/java/org/apache/calcite/jdbc/CalciteConnectionImpl.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
import org.apache.calcite.tools.RelRunner;
6868
import org.apache.calcite.util.BuiltInMethod;
6969
import org.apache.calcite.util.Holder;
70+
import org.apache.calcite.util.RuleMatchVisualizerHook;
7071
import org.apache.calcite.util.Util;
7172

7273
import com.google.common.collect.ImmutableList;
@@ -163,6 +164,17 @@ protected CalciteConnectionImpl(Driver driver, AvaticaFactory factory,
163164
this.properties.put(InternalProperty.UNQUOTED_CASING, cfg.unquotedCasing());
164165
this.properties.put(InternalProperty.QUOTED_CASING, cfg.quotedCasing());
165166
this.properties.put(InternalProperty.QUOTING, cfg.quoting());
167+
168+
// Enable RuleMatchVisualizer if configured
169+
String vizDir = cfg.ruleVisualizerDir();
170+
if (vizDir != null && !vizDir.isEmpty()) {
171+
try {
172+
RuleMatchVisualizerHook.INSTANCE.enableFromConnection(this);
173+
} catch (Exception e) {
174+
// Log but don't fail connection if visualizer setup fails
175+
System.err.println("Warning: Failed to enable RuleMatchVisualizer: " + e.getMessage());
176+
}
177+
}
166178
}
167179

168180
CalciteMetaImpl meta() {

core/src/main/java/org/apache/calcite/plan/visualizer/RuleMatchVisualizer.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public class RuleMatchVisualizer implements RelOptListener {
8080
private static final String INITIAL = "INITIAL";
8181
private static final String FINAL = "FINAL";
8282
public static final String DEFAULT_SET = "default";
83+
public static final String HTML_FILE_PREFIX = "planner-viz";
84+
public static final String DATA_FILE_PREFIX = "planner-viz-data";
8385

8486
// default HTML template can be edited at
8587
// core/src/main/resources/org/apache/calcite/plan/visualizer/viz-template.html
@@ -393,10 +395,10 @@ public void writeToFile() {
393395
requireNonNull(cl.getResourceAsStream(templatePath));
394396
String htmlTemplate = IOUtils.toString(resourceAsStream, UTF_8);
395397

396-
String htmlFileName = "planner-viz" + outputSuffix + ".html";
397-
String dataFileName = "planner-viz-data" + outputSuffix + ".js";
398+
String htmlFileName = HTML_FILE_PREFIX + outputSuffix + ".html";
399+
String dataFileName = DATA_FILE_PREFIX + outputSuffix + ".js";
398400

399-
String replaceString = "src=\"planner-viz-data.js\"";
401+
String replaceString = "src=\"" + DATA_FILE_PREFIX + ".js\"";
400402
int replaceIndex = htmlTemplate.indexOf(replaceString);
401403
String htmlContent = htmlTemplate.substring(0, replaceIndex)
402404
+ "src=\"" + dataFileName + "\""
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.calcite.util;
18+
19+
import org.apache.calcite.config.CalciteConnectionConfig;
20+
import org.apache.calcite.jdbc.CalciteConnection;
21+
import org.apache.calcite.plan.RelOptPlanner;
22+
import org.apache.calcite.plan.hep.HepPlanner;
23+
import org.apache.calcite.plan.visualizer.RuleMatchVisualizer;
24+
import org.apache.calcite.runtime.Hook;
25+
26+
import java.io.File;
27+
import java.sql.Connection;
28+
import java.util.HashMap;
29+
import java.util.Locale;
30+
import java.util.Map;
31+
import java.util.concurrent.atomic.AtomicInteger;
32+
import java.util.function.Consumer;
33+
34+
/**
35+
* Utility class to enable RuleMatchVisualizer for Calcite connections.
36+
*
37+
* <p>This class provides hooks to automatically attach a RuleMatchVisualizer
38+
* to planners when a connection specifies the ruleVisualizerDir property.
39+
*
40+
* <p>Usage in JDBC URL:
41+
* <blockquote><pre>
42+
* jdbc:calcite:ruleVisualizerDir=/tmp/calcite-viz
43+
* </pre></blockquote>
44+
*
45+
* <p>Or programmatically:
46+
* <blockquote><pre>
47+
* RuleMatchVisualizerHook.enable("/tmp/calcite-viz");
48+
* </pre></blockquote>
49+
*/
50+
public class RuleMatchVisualizerHook {
51+
public static final RuleMatchVisualizerHook INSTANCE = new RuleMatchVisualizerHook();
52+
53+
private final Map<RelOptPlanner, RuleMatchVisualizer> visualizerMap = new HashMap<>();
54+
private final AtomicInteger queryCounter = new AtomicInteger(0);
55+
56+
private Hook.Closeable hookCloseable = null;
57+
58+
/** Private constructor to prevent instantiation. */
59+
private RuleMatchVisualizerHook() {}
60+
61+
/**
62+
* Enables the visualizer for all subsequent queries with the specified output directory.
63+
*
64+
* @param outputDir Directory where visualization files will be created
65+
*/
66+
public synchronized void enable(String outputDir) {
67+
if (hookCloseable != null) {
68+
hookCloseable.close();
69+
}
70+
71+
// Ensure the output directory exists
72+
File dir = new File(outputDir);
73+
if (!dir.exists()) {
74+
boolean madeDir = dir.mkdirs();
75+
assert madeDir : "Failed to create directory: " + outputDir;
76+
}
77+
78+
// Install the hook
79+
hookCloseable = Hook.PLANNER.addThread((Consumer<RelOptPlanner>) planner -> {
80+
attachVisualizer(planner, outputDir);
81+
});
82+
}
83+
84+
/**
85+
* Enables the visualizer using the connection's configuration.
86+
* This method checks if the connection has the ruleVisualizerDir property set.
87+
*
88+
* @param connection The Calcite connection
89+
*/
90+
public synchronized void enableFromConnection(Connection connection) {
91+
if (connection instanceof CalciteConnection) {
92+
CalciteConnection calciteConn = (CalciteConnection) connection;
93+
CalciteConnectionConfig config = calciteConn.config();
94+
String vizDir = config.ruleVisualizerDir();
95+
96+
if (vizDir != null && !vizDir.isEmpty()) {
97+
enable(vizDir);
98+
}
99+
}
100+
}
101+
102+
/**
103+
* Disables the visualizer.
104+
*/
105+
public synchronized void disable() {
106+
if (hookCloseable != null) {
107+
hookCloseable.close();
108+
hookCloseable = null;
109+
}
110+
111+
// Write any pending visualizations
112+
for (RuleMatchVisualizer viz : visualizerMap.values()) {
113+
viz.writeToFile();
114+
}
115+
visualizerMap.clear();
116+
}
117+
118+
/**
119+
* Attaches a visualizer to the given planner.
120+
*/
121+
private void attachVisualizer(RelOptPlanner planner, String outputDir) {
122+
123+
// Check if we've already attached a visualizer to this planner
124+
if (visualizerMap.containsKey(planner)) {
125+
return;
126+
}
127+
128+
int queryNum = queryCounter.incrementAndGet();
129+
int queryStart = (int) System.currentTimeMillis() / 1000;
130+
String suffix = String.format(Locale.ROOT, "query_%d_%d", queryNum, queryStart);
131+
132+
// Create and attach the visualizer
133+
RuleMatchVisualizer visualizer = new RuleMatchVisualizer(outputDir, suffix);
134+
visualizer.attachTo(planner);
135+
visualizerMap.put(planner, visualizer);
136+
137+
// For HepPlanner, we need to manually write the output
138+
if (planner instanceof HepPlanner) {
139+
// Add a hook to write the visualization after the planner finishes
140+
Hook.PLAN_BEFORE_IMPLEMENTATION.addThread(relRoot -> {
141+
RuleMatchVisualizer viz = visualizerMap.get(planner);
142+
if (viz != null) {
143+
viz.writeToFile();
144+
visualizerMap.remove(planner);
145+
}
146+
});
147+
}
148+
// VolcanoPlanner automatically calls writeToFile() when done
149+
150+
System.out.println("RuleMatchVisualizer enabled: Output will be written to "
151+
+ outputDir + File.separator + suffix + "*");
152+
}
153+
154+
/**
155+
* Checks the system property and enables visualization if set.
156+
* This can be called at application startup.
157+
*/
158+
public void checkSystemProperty() {
159+
String vizDir = System.getProperty("calcite.visualizer.dir");
160+
if (vizDir != null && !vizDir.isEmpty()) {
161+
enable(vizDir);
162+
}
163+
}
164+
}

core/src/test/java/org/apache/calcite/test/JdbcTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,7 @@ static void checkMockDdl(AtomicInteger counter, boolean hasCommit,
975975
assertTrue(names.contains("SCHEMA"));
976976
assertTrue(names.contains("TIME_ZONE"));
977977
assertTrue(names.contains("MATERIALIZATIONS_ENABLED"));
978+
assertTrue(names.contains("RULE_VISUALIZER_DIR"));
978979
}
979980

980981
/**

0 commit comments

Comments
 (0)