1616 */
1717package org .apache .calcite .test ;
1818
19+ import org .apache .calcite .adapter .enumerable .EnumerableRules ;
1920import org .apache .calcite .avatica .AvaticaUtils ;
2021import org .apache .calcite .config .CalciteConnectionProperty ;
2122import org .apache .calcite .jdbc .CalciteConnection ;
23+ import org .apache .calcite .plan .RelOptPlanner ;
24+ import org .apache .calcite .plan .RelOptRule ;
2225import org .apache .calcite .prepare .Prepare ;
26+ import org .apache .calcite .rel .rules .CoreRules ;
2327import org .apache .calcite .rel .type .RelDataType ;
2428import org .apache .calcite .rel .type .RelDataTypeFactory ;
2529import org .apache .calcite .runtime .Hook ;
5660import java .util .ArrayList ;
5761import java .util .Collection ;
5862import java .util .List ;
63+ import java .util .function .Consumer ;
5964import java .util .function .Function ;
65+ import java .util .regex .Matcher ;
6066import java .util .regex .Pattern ;
6167
6268import static org .junit .jupiter .api .Assertions .fail ;
@@ -71,6 +77,9 @@ public abstract class QuidemTest {
7177
7278 private static final Pattern PATTERN = Pattern .compile ("\\ .iq$" );
7379
80+ // Saved original planner rules
81+ private static @ Nullable List <RelOptRule > originalRules ;
82+
7483 private static @ Nullable Object getEnv (String varName ) {
7584 switch (varName ) {
7685 case "jdk18" :
@@ -171,6 +180,26 @@ protected void checkRun(String path) throws Exception {
171180 int thresholdValue = ((BigDecimal ) value ).intValue ();
172181 closer .add (Prepare .THREAD_INSUBQUERY_THRESHOLD .push (thresholdValue ));
173182 }
183+ // Configures query planner rules via "!set planner-rules" command.
184+ // The value can be set as follows:
185+ // - Add rule: "+EnumerableRules.ENUMERABLE_INTERSECT_RULE"
186+ // - Remove rule: "-CoreRules.INTERSECT_TO_DISTINCT"
187+ // - Short form: "+INTERSECT_TO_DISTINCT" (CoreRules prefix may be omitted)
188+ // - Reset defaults: "original"
189+ if (propertyName .equals ("planner-rules" )) {
190+ if (value .equals ("original" )) {
191+ closer .add (Hook .PLANNER .addThread (this ::resetPlanner ));
192+ } else {
193+ closer .add (
194+ Hook .PLANNER .addThread ((Consumer <RelOptPlanner >)
195+ planner -> {
196+ if (originalRules == null ) {
197+ originalRules = planner .getRules ();
198+ }
199+ updatePlanner (planner , (String ) value );
200+ }));
201+ }
202+ }
174203 })
175204 .withEnv (QuidemTest ::getEnv )
176205 .build ();
@@ -187,6 +216,57 @@ protected void checkRun(String path) throws Exception {
187216 }
188217 }
189218
219+ private void updatePlanner (RelOptPlanner planner , String value ) {
220+ List <RelOptRule > rulesAdd = new ArrayList <>();
221+ List <RelOptRule > rulesRemove = new ArrayList <>();
222+ parseRules (value , rulesAdd , rulesRemove );
223+ rulesRemove .forEach (planner ::removeRule );
224+ rulesAdd .forEach (planner ::addRule );
225+ }
226+
227+ private void resetPlanner (RelOptPlanner planner ) {
228+ if (originalRules != null ) {
229+ planner .getRules ().forEach (planner ::removeRule );
230+ originalRules .forEach (planner ::addRule );
231+ }
232+ }
233+
234+ private void parseRules (String value , List <RelOptRule > rulesAdd , List <RelOptRule > rulesRemove ) {
235+ Pattern pattern = Pattern .compile ("([+-])((CoreRules|EnumerableRules)\\ .)?(\\ w+)" );
236+ Matcher matcher = pattern .matcher (value );
237+
238+ while (matcher .find ()) {
239+ char operation = matcher .group (1 ).charAt (0 );
240+ String ruleSource = matcher .group (3 );
241+ String ruleName = matcher .group (4 );
242+
243+ try {
244+ if (ruleSource == null || ruleSource .equals ("CoreRules" )) {
245+ Object rule = CoreRules .class .getField (ruleName ).get (null );
246+ setRules (operation , (RelOptRule ) rule , rulesAdd , rulesRemove );
247+ } else if (ruleSource .equals ("EnumerableRules" )) {
248+ Object rule = EnumerableRules .class .getField (ruleName ).get (null );
249+ setRules (operation , (RelOptRule ) rule , rulesAdd , rulesRemove );
250+ } else {
251+ throw new RuntimeException ("Unknown rule: " + ruleName );
252+ }
253+ } catch (NoSuchFieldException | IllegalAccessException e ) {
254+ throw new RuntimeException ("set rules failed: " + e .getMessage (), e );
255+ }
256+ }
257+ }
258+
259+ private void setRules (char operation , RelOptRule rule ,
260+ List <RelOptRule > rulesAdd , List <RelOptRule > rulesRemove ) {
261+ if (operation == '+' ) {
262+ rulesAdd .add (rule );
263+ } else if (operation == '-' ) {
264+ rulesRemove .add (rule );
265+ } else {
266+ throw new RuntimeException ("unknown operation '" + operation + "'" );
267+ }
268+ }
269+
190270 /** Returns a file, replacing one directory with another.
191271 *
192272 * <p>For example, {@code replaceDir("/abc/str/astro.txt", "str", "xyz")}
0 commit comments