Skip to content

Commit 03007c4

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

File tree

8 files changed

+402
-4
lines changed

8 files changed

+402
-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.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: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
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.sql.SQLException;
29+
import java.util.HashMap;
30+
import java.util.Locale;
31+
import java.util.Map;
32+
import java.util.concurrent.atomic.AtomicInteger;
33+
import java.util.function.Consumer;
34+
35+
/**
36+
* Utility class to enable RuleMatchVisualizer for Calcite connections.
37+
*
38+
* <p>This class provides hooks to automatically attach a RuleMatchVisualizer
39+
* to planners when a connection specifies the ruleVisualizerDir property.
40+
*
41+
* <p>Usage in JDBC URL:
42+
* <blockquote><pre>
43+
* jdbc:calcite:ruleVisualizerDir=/tmp/calcite-viz
44+
* </pre></blockquote>
45+
*
46+
* <p>Or programmatically:
47+
* <blockquote><pre>
48+
* RuleMatchVisualizerHook.enable("/tmp/calcite-viz");
49+
* </pre></blockquote>
50+
*/
51+
public class RuleMatchVisualizerHook {
52+
53+
private static final Map<RelOptPlanner, RuleMatchVisualizer> VISUALIZERS = new HashMap<>();
54+
private static final AtomicInteger QUERY_COUNTER = new AtomicInteger(0);
55+
private static String visualizerDir = null;
56+
private static 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 static synchronized void enable(String outputDir) {
67+
if (hookCloseable != null) {
68+
hookCloseable.close();
69+
}
70+
71+
visualizerDir = outputDir;
72+
73+
// Ensure the output directory exists
74+
File dir = new File(outputDir);
75+
if (!dir.exists()) {
76+
dir.mkdirs();
77+
}
78+
79+
// Install the hook
80+
hookCloseable = Hook.PLANNER.addThread((Consumer<RelOptPlanner>) planner -> {
81+
attachVisualizer(planner, outputDir);
82+
});
83+
}
84+
85+
/**
86+
* Enables the visualizer using the connection's configuration.
87+
* This method checks if the connection has the ruleVisualizerDir property set.
88+
*
89+
* @param connection The Calcite connection
90+
* @throws SQLException if there's an error accessing the connection
91+
*/
92+
public static synchronized void enableFromConnection(Connection connection)
93+
throws SQLException {
94+
if (connection instanceof CalciteConnection) {
95+
CalciteConnection calciteConn = (CalciteConnection) connection;
96+
CalciteConnectionConfig config = calciteConn.config();
97+
String vizDir = config.ruleVisualizerDir();
98+
99+
if (vizDir != null && !vizDir.isEmpty()) {
100+
enable(vizDir);
101+
}
102+
}
103+
}
104+
105+
/**
106+
* Disables the visualizer.
107+
*/
108+
public static synchronized void disable() {
109+
if (hookCloseable != null) {
110+
hookCloseable.close();
111+
hookCloseable = null;
112+
}
113+
visualizerDir = null;
114+
115+
// Write any pending visualizations
116+
for (RuleMatchVisualizer viz : VISUALIZERS.values()) {
117+
viz.writeToFile();
118+
}
119+
VISUALIZERS.clear();
120+
}
121+
122+
/**
123+
* Attaches a visualizer to the given planner.
124+
*/
125+
private static void attachVisualizer(RelOptPlanner planner, String outputDir) {
126+
if (planner == null || outputDir == null) {
127+
return;
128+
}
129+
130+
// Check if we've already attached a visualizer to this planner
131+
if (VISUALIZERS.containsKey(planner)) {
132+
return;
133+
}
134+
135+
int queryNum = QUERY_COUNTER.incrementAndGet();
136+
String suffix = String.format(Locale.ROOT, "query_%d", queryNum);
137+
138+
// Create and attach the visualizer
139+
RuleMatchVisualizer visualizer = new RuleMatchVisualizer(outputDir, suffix);
140+
visualizer.attachTo(planner);
141+
VISUALIZERS.put(planner, visualizer);
142+
143+
// For HepPlanner, we need to manually write the output
144+
if (planner instanceof HepPlanner) {
145+
// Add a hook to write the visualization after the planner finishes
146+
Hook.PLAN_BEFORE_IMPLEMENTATION.addThread(relRoot -> {
147+
RuleMatchVisualizer viz = VISUALIZERS.get(planner);
148+
if (viz != null) {
149+
viz.writeToFile();
150+
VISUALIZERS.remove(planner);
151+
}
152+
});
153+
}
154+
// VolcanoPlanner automatically calls writeToFile() when done
155+
156+
System.out.println("RuleMatchVisualizer enabled: Output will be written to "
157+
+ outputDir + File.separator + suffix + "*");
158+
}
159+
160+
/**
161+
* Checks the system property and enables visualization if set.
162+
* This can be called at application startup.
163+
*/
164+
public static void checkSystemProperty() {
165+
String vizDir = System.getProperty("calcite.visualizer.dir");
166+
if (vizDir != null && !vizDir.isEmpty()) {
167+
enable(vizDir);
168+
}
169+
}
170+
}

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)