You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@calcite.apache.org by jh...@apache.org on 2020/07/29 00:59:14 UTC

[calcite] 01/09: [CALCITE-3923] Refactor how planner rules are parameterized

This is an automated email from the ASF dual-hosted git repository.

jhyde pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/calcite.git

commit 998cd83ebe2fcd7ff3ee565299fed6e199269715
Author: Julian Hyde <jh...@apache.org>
AuthorDate: Wed Jul 8 14:31:40 2020 -0700

    [CALCITE-3923] Refactor how planner rules are parameterized
    
    Create operands using a builder interface, OperandBuilder, and
    deprecate methods RelOptRule.operand etc.
    
    The change is backwards compatible, in the sense that
    existing rule constructors are deprecated but still work.
    From now on, to create rules, call RelOptRule.Config.toRule()
    (which calls the rule's (Config) constructor).
    
    Sub-classes of ConverterRule are a little different.  They
    don't need their own sub-class of Config.  You just need to
    call Config.withRuleFactory to specify the constructor of the
    sub-class of ConverterRule.
    
    Move rule instances into holder classes such as CoreRules,
    MaterializedViewRules. Deprecate existing rule INSTANCE fields
    (to be removed in 1.25). Deprecate previous rule constructors
    (to be removed in 2.0).
    
    Describe how to write rules in howto and tutorial.
    
    Remove rule instances marked 'deprecated, to be removed before
    1.25'.
    
    Close apache/calcite#2024
---
 .../calcite/adapter/cassandra/CassandraRules.java  | 178 +++--
 .../CassandraToEnumerableConverterRule.java        |  27 +-
 .../enumerable/EnumerableAggregateRule.java        |  16 +-
 .../EnumerableBatchNestedLoopJoinRule.java         |  68 +-
 .../adapter/enumerable/EnumerableBindable.java     |  26 +-
 .../adapter/enumerable/EnumerableCalcRule.java     |  26 +-
 .../adapter/enumerable/EnumerableCollectRule.java  |  16 +-
 .../enumerable/EnumerableCorrelateRule.java        |  26 +-
 .../adapter/enumerable/EnumerableFilterRule.java   |  23 +-
 .../enumerable/EnumerableFilterToCalcRule.java     |  37 +-
 .../enumerable/EnumerableInterpreterRule.java      |  25 +-
 .../enumerable/EnumerableIntersectRule.java        |  16 +-
 .../adapter/enumerable/EnumerableJoinRule.java     |  19 +-
 .../adapter/enumerable/EnumerableLimitRule.java    |  28 +-
 .../adapter/enumerable/EnumerableMatchRule.java    |  14 +-
 .../enumerable/EnumerableMergeJoinRule.java        |  15 +-
 .../adapter/enumerable/EnumerableMinusRule.java    |  16 +-
 .../adapter/enumerable/EnumerableProjectRule.java  |  23 +-
 .../enumerable/EnumerableProjectToCalcRule.java    |  36 +-
 .../enumerable/EnumerableRepeatUnionRule.java      |  20 +-
 .../adapter/enumerable/EnumerableRules.java        | 126 +++-
 .../adapter/enumerable/EnumerableSortRule.java     |  14 +-
 .../enumerable/EnumerableSortedAggregateRule.java  |  14 +-
 .../EnumerableTableFunctionScanRule.java           |  40 +-
 .../enumerable/EnumerableTableModifyRule.java      |  28 +-
 .../enumerable/EnumerableTableScanRule.java        |  36 +-
 .../enumerable/EnumerableTableSpoolRule.java       |  15 +-
 .../enumerable/EnumerableUncollectRule.java        |  16 +-
 .../adapter/enumerable/EnumerableUnionRule.java    |  16 +-
 .../adapter/enumerable/EnumerableValuesRule.java   |  25 +-
 .../adapter/enumerable/EnumerableWindowRule.java   |  16 +-
 .../org/apache/calcite/adapter/jdbc/JdbcRules.java | 274 +++----
 .../jdbc/JdbcToEnumerableConverterRule.java        |  19 +-
 .../org/apache/calcite/interpreter/Bindables.java  | 269 +++----
 .../interpreter/NoneToBindableConverterRule.java   |  25 +-
 .../apache/calcite/plan/CommonRelSubExprRule.java  |  23 +-
 .../apache/calcite/plan/RelOptMaterialization.java |   4 +-
 .../java/org/apache/calcite/plan/RelOptRule.java   |  34 +-
 .../org/apache/calcite/plan/RelOptRuleCall.java    |   2 +-
 .../calcite/plan/RelOptRuleOperandChildren.java    |   5 +-
 .../java/org/apache/calcite/plan/RelOptUtil.java   |  12 +-
 .../main/java/org/apache/calcite/plan/RelRule.java | 324 +++++++++
 .../org/apache/calcite/plan/hep/HepPlanner.java    |   3 +-
 .../apache/calcite/plan/hep/HepProgramBuilder.java |   3 +-
 .../org/apache/calcite/plan/hep/HepRuleCall.java   |   3 +-
 .../calcite/plan/volcano/AbstractConverter.java    |  44 +-
 .../calcite/plan/volcano/VolcanoRuleCall.java      |   3 +-
 .../apache/calcite/rel/convert/ConverterRule.java  | 100 ++-
 .../calcite/rel/convert/TraitMatchingRule.java     |  77 +-
 .../java/org/apache/calcite/rel/core/Calc.java     |   6 +
 .../java/org/apache/calcite/rel/core/Filter.java   |   6 +
 .../java/org/apache/calcite/rel/core/Project.java  |   6 +
 .../org/apache/calcite/rel/core/RelFactories.java  |   2 +-
 .../apache/calcite/rel/rel2sql/SqlImplementor.java |   2 +-
 .../rel/rules/AbstractJoinExtractFilterRule.java   |  22 +-
 .../rel/rules/AbstractMaterializedViewRule.java    |  48 --
 .../rel/rules/AggregateCaseToFilterRule.java       |  34 +-
 .../AggregateExpandDistinctAggregatesRule.java     |  62 +-
 .../rel/rules/AggregateExtractProjectRule.java     |  61 +-
 .../rel/rules/AggregateFilterTransposeRule.java    |  58 +-
 .../rel/rules/AggregateJoinJoinRemoveRule.java     |  66 +-
 .../calcite/rel/rules/AggregateJoinRemoveRule.java |  51 +-
 .../rel/rules/AggregateJoinTransposeRule.java      |  79 ++-
 .../calcite/rel/rules/AggregateMergeRule.java      |  48 +-
 .../rel/rules/AggregateProjectMergeRule.java       |  46 +-
 .../rules/AggregateProjectPullUpConstantsRule.java |  71 +-
 .../rel/rules/AggregateProjectStarTableRule.java   |  78 ++
 .../rel/rules/AggregateReduceFunctionsRule.java    | 130 ++--
 .../calcite/rel/rules/AggregateRemoveRule.java     |  52 +-
 .../calcite/rel/rules/AggregateStarTableRule.java  |  62 +-
 .../rel/rules/AggregateUnionAggregateRule.java     |  89 ++-
 .../rel/rules/AggregateUnionTransposeRule.java     |  48 +-
 .../calcite/rel/rules/AggregateValuesRule.java     |  53 +-
 .../apache/calcite/rel/rules/CalcMergeRule.java    |  46 +-
 .../apache/calcite/rel/rules/CalcRemoveRule.java   |  50 +-
 .../apache/calcite/rel/rules/CalcSplitRule.java    |  36 +-
 .../apache/calcite/rel/rules/CoerceInputsRule.java |  87 ++-
 .../org/apache/calcite/rel/rules/CoreRules.java    | 470 ++++--------
 .../apache/calcite/rel/rules/DateRangeRules.java   |  68 +-
 .../rel/rules/ExchangeRemoveConstantKeysRule.java  | 199 +++---
 .../rel/rules/FilterAggregateTransposeRule.java    |  72 +-
 .../calcite/rel/rules/FilterCalcMergeRule.java     |  57 +-
 .../calcite/rel/rules/FilterCorrelateRule.java     |  59 +-
 .../apache/calcite/rel/rules/FilterJoinRule.java   | 195 ++---
 .../apache/calcite/rel/rules/FilterMergeRule.java  |  44 +-
 .../rel/rules/FilterMultiJoinMergeRule.java        |  61 +-
 .../rel/rules/FilterProjectTransposeRule.java      | 153 +++-
 .../rules/FilterRemoveIsNotDistinctFromRule.java   |  46 +-
 .../rel/rules/FilterSetOpTransposeRule.java        |  47 +-
 .../rules/FilterTableFunctionTransposeRule.java    |  42 +-
 .../calcite/rel/rules/FilterTableScanRule.java     |  74 +-
 .../apache/calcite/rel/rules/FilterToCalcRule.java |  42 +-
 .../calcite/rel/rules/IntersectToDistinctRule.java |  43 +-
 .../rel/rules/JoinAddRedundantSemiJoinRule.java    |  46 +-
 .../calcite/rel/rules/JoinAssociateRule.java       |  51 +-
 .../apache/calcite/rel/rules/JoinCommuteRule.java  |  86 ++-
 .../calcite/rel/rules/JoinExtractFilterRule.java   |  38 +-
 .../rel/rules/JoinProjectTransposeRule.java        | 251 ++++---
 .../calcite/rel/rules/JoinPushExpressionsRule.java |  47 +-
 .../calcite/rel/rules/JoinPushThroughJoinRule.java |  87 ++-
 .../rules/JoinPushTransitivePredicatesRule.java    |  66 +-
 .../calcite/rel/rules/JoinToCorrelateRule.java     |  71 +-
 .../calcite/rel/rules/JoinToMultiJoinRule.java     |  56 +-
 .../calcite/rel/rules/JoinUnionTransposeRule.java  |  65 +-
 .../calcite/rel/rules/LoptOptimizeJoinRule.java    |  42 +-
 .../org/apache/calcite/rel/rules/MatchRule.java    |  32 +-
 .../rel/rules/MaterializedViewFilterScanRule.java  |  61 +-
 .../rel/rules/MultiJoinOptimizeBushyRule.java      |  36 +-
 .../rel/rules/MultiJoinProjectTransposeRule.java   |  87 ++-
 .../calcite/rel/rules/ProjectCalcMergeRule.java    |  60 +-
 .../rel/rules/ProjectCorrelateTransposeRule.java   | 129 ++--
 .../rel/rules/ProjectFilterTransposeRule.java      | 163 +++--
 .../rel/rules/ProjectJoinJoinRemoveRule.java       |  67 +-
 .../calcite/rel/rules/ProjectJoinRemoveRule.java   |  56 +-
 .../rel/rules/ProjectJoinTransposeRule.java        | 124 ++--
 .../apache/calcite/rel/rules/ProjectMergeRule.java |  99 ++-
 .../rel/rules/ProjectMultiJoinMergeRule.java       |  61 +-
 .../calcite/rel/rules/ProjectRemoveRule.java       |  46 +-
 .../rel/rules/ProjectSetOpTransposeRule.java       | 101 +--
 .../rel/rules/ProjectSortTransposeRule.java        |  85 ---
 .../calcite/rel/rules/ProjectTableScanRule.java    |  77 +-
 .../calcite/rel/rules/ProjectToCalcRule.java       |  44 +-
 .../calcite/rel/rules/ProjectToWindowRule.java     | 119 ++--
 .../rel/rules/ProjectWindowTransposeRule.java      |  61 +-
 .../apache/calcite/rel/rules/PruneEmptyRules.java  | 425 ++++++-----
 .../calcite/rel/rules/ReduceDecimalsRule.java      | 104 +--
 .../calcite/rel/rules/ReduceExpressionsRule.java   | 286 +++++---
 .../rel/rules/SemiJoinFilterTransposeRule.java     |  55 +-
 .../rel/rules/SemiJoinJoinTransposeRule.java       |  91 ++-
 .../rel/rules/SemiJoinProjectTransposeRule.java    |  62 +-
 .../calcite/rel/rules/SemiJoinRemoveRule.java      |  45 +-
 .../org/apache/calcite/rel/rules/SemiJoinRule.java | 131 ++--
 .../apache/calcite/rel/rules/SortJoinCopyRule.java |  48 +-
 .../calcite/rel/rules/SortJoinTransposeRule.java   |  51 +-
 .../rel/rules/SortProjectTransposeRule.java        |  85 ++-
 .../rel/rules/SortRemoveConstantKeysRule.java      |  30 +-
 .../apache/calcite/rel/rules/SortRemoveRule.java   |  38 +-
 .../calcite/rel/rules/SortUnionTransposeRule.java  |  82 ++-
 .../calcite/rel/rules/SubQueryRemoveRule.java      | 241 ++++---
 .../apache/calcite/rel/rules/TableScanRule.java    |  35 +-
 .../calcite/rel/rules/UnionEliminatorRule.java     |  45 +-
 .../apache/calcite/rel/rules/UnionMergeRule.java   |  79 ++-
 .../rel/rules/UnionPullUpConstantsRule.java        |  50 +-
 .../calcite/rel/rules/UnionToDistinctRule.java     |  54 +-
 .../apache/calcite/rel/rules/ValuesReduceRule.java | 113 ++-
 .../materialize/MaterializedViewAggregateRule.java | 138 ++--
 .../materialize/MaterializedViewJoinRule.java      |  28 +-
 .../MaterializedViewOnlyAggregateRule.java         |  63 +-
 .../MaterializedViewOnlyFilterRule.java            |  40 +-
 .../materialize/MaterializedViewOnlyJoinRule.java  |  40 +-
 .../MaterializedViewProjectAggregateRule.java      |  67 +-
 .../MaterializedViewProjectFilterRule.java         |  44 +-
 .../MaterializedViewProjectJoinRule.java           |  44 +-
 .../rules/materialize/MaterializedViewRule.java    |  64 +-
 .../rules/materialize/MaterializedViewRules.java   |  34 +-
 .../org/apache/calcite/rel/stream/StreamRules.java | 293 +++++---
 .../calcite/runtime/ResultSetEnumerable.java       |   4 +-
 .../org/apache/calcite/sql/ddl/SqlDdlNodes.java    |   2 +-
 .../apache/calcite/sql2rel/RelDecorrelator.java    | 346 ++++++---
 .../java/org/apache/calcite/tools/Programs.java    |   3 +-
 .../org/apache/calcite/plan/RelOptUtilTest.java    | 102 ++-
 .../plan/volcano/CollationConversionTest.java      |  58 +-
 .../apache/calcite/plan/volcano/ComboRuleTest.java |  60 +-
 .../apache/calcite/plan/volcano/PlannerTests.java  |  73 +-
 .../calcite/plan/volcano/TraitConversionTest.java  |  50 +-
 .../calcite/plan/volcano/TraitPropagationTest.java | 116 ++-
 .../calcite/plan/volcano/VolcanoPlannerTest.java   | 309 +++++---
 .../plan/volcano/VolcanoPlannerTraitTest.java      | 182 +++--
 .../calcite/rel/rel2sql/RelToSqlConverterTest.java |  10 +-
 .../calcite/rel/rules/SortRemoveRuleTest.java      |   3 +-
 .../org/apache/calcite/test/HepPlannerTest.java    |  36 +-
 .../java/org/apache/calcite/test/LatticeTest.java  |  10 +-
 .../org/apache/calcite/test/MockRelOptPlanner.java |   3 +-
 .../org/apache/calcite/test/MutableRelTest.java    |   4 +-
 .../org/apache/calcite/test/RelOptRulesTest.java   | 789 ++++++++++++---------
 .../apache/calcite/test/SqlHintsConverterTest.java |  60 +-
 .../org/apache/calcite/tools/FrameworksTest.java   |  31 +-
 .../java/org/apache/calcite/tools/PlannerTest.java | 167 +++--
 .../apache/calcite/adapter/druid/DruidRules.java   | 452 +++++-------
 .../adapter/elasticsearch/ElasticsearchRules.java  |  64 +-
 .../ElasticsearchToEnumerableConverterRule.java    |  26 +-
 .../adapter/csv/CsvProjectTableScanRule.java       |  39 +-
 .../org/apache/calcite/adapter/csv/CsvRules.java   |   4 +-
 .../calcite/adapter/geode/rel/GeodeRules.java      | 104 ++-
 .../geode/rel/GeodeToEnumerableConverterRule.java  |  12 +-
 .../apache/calcite/adapter/mongodb/MongoRules.java |  61 +-
 .../mongodb/MongoToEnumerableConverterRule.java    |  24 +-
 .../org/apache/calcite/adapter/pig/PigRules.java   |  74 +-
 .../adapter/pig/PigToEnumerableConverterRule.java  |  12 +-
 .../calcite/test/PigRelBuilderStyleTest.java       |  23 +-
 .../org/apache/calcite/piglet/PigConverter.java    |   3 +-
 .../calcite/piglet/PigToSqlAggregateRule.java      |  97 +--
 site/_docs/howto.md                                |  73 ++
 site/_docs/tutorial.md                             |  50 +-
 .../spark/EnumerableToSparkConverterRule.java      |  56 --
 .../adapter/spark/JdbcToSparkConverterRule.java    |  20 +-
 .../apache/calcite/adapter/spark/SparkRules.java   |  98 ++-
 .../calcite/adapter/splunk/SplunkPushDownRule.java |  96 ++-
 198 files changed, 9018 insertions(+), 5503 deletions(-)

diff --git a/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraRules.java b/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraRules.java
index 04a2d54..6aad2a5 100644
--- a/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraRules.java
+++ b/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraRules.java
@@ -22,15 +22,14 @@ import org.apache.calcite.adapter.java.JavaTypeFactory;
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
-import org.apache.calcite.plan.RelOptRuleOperand;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelFieldCollation;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.logical.LogicalFilter;
 import org.apache.calcite.rel.logical.LogicalProject;
@@ -54,18 +53,29 @@ import java.util.function.Predicate;
  * calling convention.
  */
 public class CassandraRules {
+
   private CassandraRules() {}
 
+  public static final CassandraFilterRule FILTER =
+      CassandraFilterRule.Config.DEFAULT.toRule();
+  public static final CassandraProjectRule PROJECT =
+      CassandraProjectRule.DEFAULT_CONFIG.toRule(CassandraProjectRule.class);
+  public static final CassandraSortRule SORT =
+      CassandraSortRule.Config.DEFAULT.toRule();
+  public static final CassandraLimitRule LIMIT =
+      CassandraLimitRule.Config.DEFAULT.toRule();
+
   /** Rule to convert a relational expression from
    * {@link CassandraRel#CONVENTION} to {@link EnumerableConvention}. */
-  public static final ConverterRule TO_ENUMERABLE =
-      new CassandraToEnumerableConverterRule(RelFactories.LOGICAL_BUILDER);
+  public static final CassandraToEnumerableConverterRule TO_ENUMERABLE =
+      CassandraToEnumerableConverterRule.DEFAULT_CONFIG
+          .toRule(CassandraToEnumerableConverterRule.class);
 
   public static final RelOptRule[] RULES = {
-      CassandraFilterRule.INSTANCE,
-      CassandraProjectRule.INSTANCE,
-      CassandraSortRule.INSTANCE,
-      CassandraLimitRule.INSTANCE
+      FILTER,
+      PROJECT,
+      SORT,
+      LIMIT
   };
 
   static List<String> cassandraFieldNames(final RelDataType rowType) {
@@ -94,39 +104,29 @@ public class CassandraRules {
   /** Base class for planner rules that convert a relational expression to
    * Cassandra calling convention. */
   abstract static class CassandraConverterRule extends ConverterRule {
-    protected final Convention out;
-
-    CassandraConverterRule(Class<? extends RelNode> clazz,
-        String description) {
-      this(clazz, r -> true, description);
-    }
-
-    <R extends RelNode> CassandraConverterRule(Class<R> clazz,
-        Predicate<? super R> predicate,
-        String description) {
-      super(clazz, predicate, Convention.NONE,
-          CassandraRel.CONVENTION, RelFactories.LOGICAL_BUILDER, description);
-      this.out = CassandraRel.CONVENTION;
+    CassandraConverterRule(Config config) {
+      super(config);
     }
   }
 
   /**
    * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalFilter} to a
    * {@link CassandraFilter}.
+   *
+   * @see #FILTER
    */
-  private static class CassandraFilterRule extends RelOptRule {
+  public static class CassandraFilterRule
+      extends RelRule<CassandraFilterRule.Config> {
+    /** Creates a CassandraFilterRule. */
+    protected CassandraFilterRule(Config config) {
+      super(config);
+    }
+
     private static final Predicate<LogicalFilter> PREDICATE =
         // TODO: Check for an equality predicate on the partition key
         // Right now this just checks if we have a single top-level AND
         filter -> RelOptUtil.disjunctions(filter.getCondition()).size() == 1;
 
-    private static final CassandraFilterRule INSTANCE = new CassandraFilterRule();
-
-    private CassandraFilterRule() {
-      super(operand(LogicalFilter.class, operand(CassandraTableScan.class, none())),
-          "CassandraFilterRule");
-    }
-
     @Override public boolean matches(RelOptRuleCall call) {
       // Get the condition from the filter operation
       LogicalFilter filter = call.rel(0);
@@ -206,7 +206,7 @@ public class CassandraRules {
     }
 
     /** @see org.apache.calcite.rel.convert.ConverterRule */
-    public void onMatch(RelOptRuleCall call) {
+    @Override public void onMatch(RelOptRuleCall call) {
       LogicalFilter filter = call.rel(0);
       CassandraTableScan scan = call.rel(1);
       if (filter.getTraitSet().contains(Convention.NONE)) {
@@ -217,7 +217,7 @@ public class CassandraRules {
       }
     }
 
-    public RelNode convert(LogicalFilter filter, CassandraTableScan scan) {
+    RelNode convert(LogicalFilter filter, CassandraTableScan scan) {
       final RelTraitSet traitSet = filter.getTraitSet().replace(CassandraRel.CONVENTION);
       final Pair<List<String>, List<String>> keyFields = scan.cassandraTable.getKeyFields();
       return new CassandraFilter(
@@ -229,17 +229,37 @@ public class CassandraRules {
           keyFields.right,
           scan.cassandraTable.getClusteringOrder());
     }
+
+    /** Rule configuration. */
+    public interface Config extends RelRule.Config {
+      Config DEFAULT = EMPTY
+          .withOperandSupplier(b0 ->
+              b0.operand(LogicalFilter.class)
+                  .oneInput(b1 -> b1.operand(CassandraTableScan.class)
+                      .noInputs()))
+          .as(Config.class);
+
+      @Override default CassandraFilterRule toRule() {
+        return new CassandraFilterRule(this);
+      }
+    }
   }
 
   /**
    * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalProject}
    * to a {@link CassandraProject}.
+   *
+   * @see #PROJECT
    */
-  private static class CassandraProjectRule extends CassandraConverterRule {
-    private static final CassandraProjectRule INSTANCE = new CassandraProjectRule();
-
-    private CassandraProjectRule() {
-      super(LogicalProject.class, "CassandraProjectRule");
+  public static class CassandraProjectRule extends CassandraConverterRule {
+    /** Default configuration. */
+    private static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(LogicalProject.class, Convention.NONE,
+            CassandraRel.CONVENTION, "CassandraProjectRule")
+        .withRuleFactory(CassandraProjectRule::new);
+
+    protected CassandraProjectRule(Config config) {
+      super(config);
     }
 
     @Override public boolean matches(RelOptRuleCall call) {
@@ -253,7 +273,7 @@ public class CassandraRules {
       return true;
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final LogicalProject project = (LogicalProject) rel;
       final RelTraitSet traitSet = project.getTraitSet().replace(out);
       return new CassandraProject(project.getCluster(), traitSet,
@@ -265,23 +285,14 @@ public class CassandraRules {
   /**
    * Rule to convert a {@link org.apache.calcite.rel.core.Sort} to a
    * {@link CassandraSort}.
+   *
+   * @see #SORT
    */
-  private static class CassandraSortRule extends RelOptRule {
-
-    private static final RelOptRuleOperand CASSANDRA_OP =
-        operand(CassandraToEnumerableConverter.class,
-            operandJ(CassandraFilter.class, null,
-                // We can only use implicit sorting within a single partition
-                CassandraFilter::isSinglePartition, any()));
-
-    private static final CassandraSortRule INSTANCE = new CassandraSortRule();
-
-    private CassandraSortRule() {
-      super(
-          operandJ(Sort.class, null,
-              // Limits are handled by CassandraLimit
-              sort -> sort.offset == null && sort.fetch == null, CASSANDRA_OP),
-          "CassandraSortRule");
+  public static class CassandraSortRule
+      extends RelRule<CassandraSortRule.Config> {
+    /** Creates a CassandraSortRule. */
+    protected CassandraSortRule(Config config) {
+      super(config);
     }
 
     public RelNode convert(Sort sort, CassandraFilter filter) {
@@ -293,7 +304,7 @@ public class CassandraRules {
           sort.getCollation());
     }
 
-    public boolean matches(RelOptRuleCall call) {
+    @Override public boolean matches(RelOptRuleCall call) {
       final Sort sort = call.rel(0);
       final CassandraFilter filter = call.rel(2);
       return collationsCompatible(sort.getCollation(), filter.getImplicitCollation());
@@ -360,7 +371,7 @@ public class CassandraRules {
     }
 
     /** @see org.apache.calcite.rel.convert.ConverterRule */
-    public void onMatch(RelOptRuleCall call) {
+    @Override public void onMatch(RelOptRuleCall call) {
       final Sort sort = call.rel(0);
       CassandraFilter filter = call.rel(2);
       final RelNode converted = convert(sort, filter);
@@ -368,18 +379,44 @@ public class CassandraRules {
         call.transformTo(converted);
       }
     }
+
+    /** Rule configuration. */
+    public interface Config extends RelRule.Config {
+      Config DEFAULT = EMPTY
+          .withOperandSupplier(b0 ->
+              b0.operand(Sort.class)
+                  // Limits are handled by CassandraLimit
+                  .predicate(sort ->
+                      sort.offset == null && sort.fetch == null)
+                  .oneInput(b1 ->
+                      b1.operand(CassandraToEnumerableConverter.class)
+                          .oneInput(b2 ->
+                              b2.operand(CassandraFilter.class)
+                                  // We can only use implicit sorting within a
+                                  // single partition
+                                  .predicate(
+                                      CassandraFilter::isSinglePartition)
+                                  .anyInputs())))
+          .as(Config.class);
+
+      @Override default CassandraSortRule toRule() {
+        return new CassandraSortRule(this);
+      }
+    }
   }
 
   /**
-   * Rule to convert a {@link org.apache.calcite.adapter.enumerable.EnumerableLimit} to a
+   * Rule to convert a
+   * {@link org.apache.calcite.adapter.enumerable.EnumerableLimit} to a
    * {@link CassandraLimit}.
+   *
+   * @see #LIMIT
    */
-  private static class CassandraLimitRule extends RelOptRule {
-    private static final CassandraLimitRule INSTANCE = new CassandraLimitRule();
-
-    private CassandraLimitRule() {
-      super(operand(EnumerableLimit.class, operand(CassandraToEnumerableConverter.class, any())),
-          "CassandraLimitRule");
+  public static class CassandraLimitRule
+      extends RelRule<CassandraLimitRule.Config> {
+    /** Creates a CassandraLimitRule. */
+    protected CassandraLimitRule(Config config) {
+      super(config);
     }
 
     public RelNode convert(EnumerableLimit limit) {
@@ -390,12 +427,27 @@ public class CassandraRules {
     }
 
     /** @see org.apache.calcite.rel.convert.ConverterRule */
-    public void onMatch(RelOptRuleCall call) {
+    @Override public void onMatch(RelOptRuleCall call) {
       final EnumerableLimit limit = call.rel(0);
       final RelNode converted = convert(limit);
       if (converted != null) {
         call.transformTo(converted);
       }
     }
+
+    /** Rule configuration. */
+    public interface Config extends RelRule.Config {
+      Config DEFAULT = EMPTY
+          .withOperandSupplier(b0 ->
+              b0.operand(EnumerableLimit.class)
+                  .oneInput(b1 ->
+                      b1.operand(CassandraToEnumerableConverter.class)
+                          .anyInputs()))
+          .as(Config.class);
+
+      @Override default CassandraLimitRule toRule() {
+        return new CassandraLimitRule(this);
+      }
+    }
   }
 }
diff --git a/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraToEnumerableConverterRule.java b/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraToEnumerableConverterRule.java
index 523ecef..3c3fe25 100644
--- a/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraToEnumerableConverterRule.java
+++ b/cassandra/src/main/java/org/apache/calcite/adapter/cassandra/CassandraToEnumerableConverterRule.java
@@ -20,30 +20,23 @@ import org.apache.calcite.adapter.enumerable.EnumerableConvention;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.tools.RelBuilderFactory;
-
-import java.util.function.Predicate;
 
 /**
  * Rule to convert a relational expression from
  * {@link CassandraRel#CONVENTION} to {@link EnumerableConvention}.
+ *
+ * @see CassandraRules#TO_ENUMERABLE
  */
 public class CassandraToEnumerableConverterRule extends ConverterRule {
-  /** @deprecated Use {@link CassandraRules#TO_ENUMERABLE}. */
-  @Deprecated // to be removed before 1.25
-  public static final ConverterRule INSTANCE =
-      CassandraRules.TO_ENUMERABLE;
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(RelNode.class, CassandraRel.CONVENTION,
+          EnumerableConvention.INSTANCE, "CassandraToEnumerableConverterRule")
+      .withRuleFactory(CassandraToEnumerableConverterRule::new);
 
-  /**
-   * Creates a CassandraToEnumerableConverterRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
-  public CassandraToEnumerableConverterRule(
-      RelBuilderFactory relBuilderFactory) {
-    super(RelNode.class, (Predicate<RelNode>) r -> true,
-        CassandraRel.CONVENTION, EnumerableConvention.INSTANCE,
-        relBuilderFactory, "CassandraToEnumerableConverterRule");
+  /** Creates a CassandraToEnumerableConverterRule. */
+  protected CassandraToEnumerableConverterRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregateRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregateRule.java
index a34a63a..78612aa 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregateRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableAggregateRule.java
@@ -26,14 +26,22 @@ import org.apache.calcite.rel.logical.LogicalAggregate;
 /**
  * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalAggregate}
  * to an {@link EnumerableAggregate}.
+ *
+ * @see EnumerableRules#ENUMERABLE_AGGREGATE_RULE
  */
 class EnumerableAggregateRule extends ConverterRule {
-  EnumerableAggregateRule() {
-    super(LogicalAggregate.class, Convention.NONE,
-        EnumerableConvention.INSTANCE, "EnumerableAggregateRule");
+  /** Default configuration. */
+  static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalAggregate.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableAggregateRule")
+      .withRuleFactory(EnumerableAggregateRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableAggregateRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final LogicalAggregate agg = (LogicalAggregate) rel;
     final RelTraitSet traitSet = rel.getCluster()
         .traitSet().replace(EnumerableConvention.INSTANCE);
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableBatchNestedLoopJoinRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableBatchNestedLoopJoinRule.java
index 09072b6..eefa7df 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableBatchNestedLoopJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableBatchNestedLoopJoinRule.java
@@ -17,8 +17,8 @@
 package org.apache.calcite.adapter.enumerable;
 
 import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.CorrelationId;
 import org.apache.calcite.rel.core.Join;
@@ -31,6 +31,7 @@ import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
 import org.apache.calcite.util.ImmutableBitSet;
 
 import java.util.ArrayList;
@@ -40,29 +41,38 @@ import java.util.Set;
 
 /** Planner rule that converts a
  * {@link org.apache.calcite.rel.logical.LogicalJoin} into an
- * {@link org.apache.calcite.adapter.enumerable.EnumerableBatchNestedLoopJoin}. */
-public class EnumerableBatchNestedLoopJoinRule extends RelOptRule {
-
-  private final int batchSize;
-  private static final int DEFAULT_BATCH_SIZE = 100;
-
+ * {@link org.apache.calcite.adapter.enumerable.EnumerableBatchNestedLoopJoin}.
+ *
+ * @see EnumerableRules#ENUMERABLE_BATCH_NESTED_LOOP_JOIN_RULE
+ */
+public class EnumerableBatchNestedLoopJoinRule
+    extends RelRule<EnumerableBatchNestedLoopJoinRule.Config> {
   /** Creates an EnumerableBatchNestedLoopJoinRule. */
+  protected EnumerableBatchNestedLoopJoinRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   protected EnumerableBatchNestedLoopJoinRule(Class<? extends Join> clazz,
       RelBuilderFactory relBuilderFactory, int batchSize) {
-    super(operand(clazz, any()),
-        relBuilderFactory, "EnumerableBatchNestedLoopJoinRule");
-    this.batchSize = batchSize;
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b -> b.operand(clazz).anyInputs())
+        .as(Config.class)
+        .withBatchSize(batchSize));
   }
-  /** Creates an EnumerableBatchNestedLoopJoinRule with default batch size of 100. */
+
+  @Deprecated // to be removed before 2.0
   public EnumerableBatchNestedLoopJoinRule(RelBuilderFactory relBuilderFactory) {
-    this(LogicalJoin.class, relBuilderFactory, DEFAULT_BATCH_SIZE);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
-  /** Creates an EnumerableBatchNestedLoopJoinRule with given batch size.
-   * Warning: if the batch size is around or bigger than 1000 there
-   * can be an error because the generated code exceeds the size limit */
-  public EnumerableBatchNestedLoopJoinRule(RelBuilderFactory relBuilderFactory, int batchSize) {
-    this(LogicalJoin.class, relBuilderFactory, batchSize);
+  @Deprecated // to be removed before 2.0
+  public EnumerableBatchNestedLoopJoinRule(RelBuilderFactory relBuilderFactory,
+      int batchSize) {
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withBatchSize(batchSize));
   }
 
   @Override public boolean matches(RelOptRuleCall call) {
@@ -84,6 +94,7 @@ public class EnumerableBatchNestedLoopJoinRule extends RelOptRule {
     final Set<CorrelationId> correlationIds = new HashSet<>();
     final ArrayList<RexNode> corrVar = new ArrayList<>();
 
+    final int batchSize = config.batchSize();
     for (int i = 0; i < batchSize; i++) {
       CorrelationId correlationId = cluster.createCorrel();
       correlationIds.add(correlationId);
@@ -137,4 +148,27 @@ public class EnumerableBatchNestedLoopJoinRule extends RelOptRule {
             correlationIds,
             joinType));
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b -> b.operand(LogicalJoin.class).anyInputs())
+        .withDescription("EnumerableBatchNestedLoopJoinRule")
+        .as(Config.class);
+
+    @Override default EnumerableBatchNestedLoopJoinRule toRule() {
+      return new EnumerableBatchNestedLoopJoinRule(this);
+    }
+
+    /** Batch size.
+     *
+     * <p>Warning: if the batch size is around or bigger than 1000 there
+     * can be an error because the generated code exceeds the size limit. */
+    @ImmutableBeans.Property
+    @ImmutableBeans.IntDefault(100)
+    int batchSize();
+
+    /** Sets {@link #batchSize()}. */
+    Config withBatchSize(int batchSize);
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableBindable.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableBindable.java
index 61714e0..fc2223f 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableBindable.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableBindable.java
@@ -32,12 +32,10 @@ import org.apache.calcite.rel.convert.ConverterImpl;
 import org.apache.calcite.rel.convert.ConverterRule;
 import org.apache.calcite.runtime.ArrayBindable;
 import org.apache.calcite.runtime.Bindable;
-import org.apache.calcite.tools.RelBuilderFactory;
 
 import com.google.common.collect.ImmutableMap;
 
 import java.util.List;
-import java.util.function.Predicate;
 
 /**
  * Relational expression that converts an enumerable input to interpretable
@@ -83,23 +81,19 @@ public class EnumerableBindable extends ConverterImpl implements BindableRel {
 
   /**
    * Rule that converts any enumerable relational expression to bindable.
+   *
+   * @see EnumerableRules#TO_BINDABLE
    */
   public static class EnumerableToBindableConverterRule extends ConverterRule {
-    /** @deprecated Use {@link EnumerableRules#TO_BINDABLE}. */
-    @Deprecated // to be removed before 1.25
-    public static final EnumerableToBindableConverterRule INSTANCE =
-        EnumerableRules.TO_BINDABLE;
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(EnumerableRel.class,
+            EnumerableConvention.INSTANCE, BindableConvention.INSTANCE,
+            "EnumerableToBindableConverterRule")
+        .withRuleFactory(EnumerableToBindableConverterRule::new);
 
-    /**
-     * Creates an EnumerableToBindableConverterRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public EnumerableToBindableConverterRule(
-        RelBuilderFactory relBuilderFactory) {
-      super(EnumerableRel.class, (Predicate<RelNode>) r -> true,
-          EnumerableConvention.INSTANCE, BindableConvention.INSTANCE,
-          relBuilderFactory, "EnumerableToBindableConverterRule");
+    protected EnumerableToBindableConverterRule(Config config) {
+      super(config);
     }
 
     @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCalcRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCalcRule.java
index 07bbfdc..d8c5fe5 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCalcRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCalcRule.java
@@ -20,27 +20,31 @@ import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.core.Calc;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.logical.LogicalCalc;
 
-import java.util.function.Predicate;
 
 /**
  * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalCalc} to an
  * {@link EnumerableCalc}.
+ *
+ * @see EnumerableRules#ENUMERABLE_CALC_RULE
  */
 class EnumerableCalcRule extends ConverterRule {
-  EnumerableCalcRule() {
-    // The predicate ensures that if there's a multiset, FarragoMultisetSplitter
-    // will work on it first.
-    super(LogicalCalc.class,
-        (Predicate<Calc>) RelOptUtil::notContainsWindowedAgg,
-        Convention.NONE, EnumerableConvention.INSTANCE,
-        RelFactories.LOGICAL_BUILDER, "EnumerableCalcRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.EMPTY
+      .as(Config.class)
+      // The predicate ensures that if there's a multiset,
+      // FarragoMultisetSplitter will work on it first.
+      .withConversion(LogicalCalc.class, RelOptUtil::notContainsWindowedAgg,
+          Convention.NONE, EnumerableConvention.INSTANCE,
+          "EnumerableCalcRule")
+      .withRuleFactory(EnumerableCalcRule::new);
+
+  protected EnumerableCalcRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final LogicalCalc calc = (LogicalCalc) rel;
     final RelNode input = calc.getInput();
     return EnumerableCalc.create(
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollectRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollectRule.java
index b1a0984..d7b486a 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollectRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCollectRule.java
@@ -25,14 +25,22 @@ import org.apache.calcite.rel.core.Collect;
 /**
  * Rule to convert an {@link org.apache.calcite.rel.core.Collect} to an
  * {@link EnumerableCollect}.
+ *
+ * @see EnumerableRules#ENUMERABLE_COLLECT_RULE
  */
 class EnumerableCollectRule extends ConverterRule {
-  EnumerableCollectRule() {
-    super(Collect.class, Convention.NONE, EnumerableConvention.INSTANCE,
-        "EnumerableCollectRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(Collect.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableCollectRule")
+      .withRuleFactory(EnumerableCollectRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableCollectRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final Collect collect = (Collect) rel;
     final RelTraitSet traitSet =
         collect.getTraitSet().replace(EnumerableConvention.INSTANCE);
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCorrelateRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCorrelateRule.java
index ea6ad243..fd1456c 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCorrelateRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableCorrelateRule.java
@@ -20,26 +20,26 @@ import org.apache.calcite.plan.Convention;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
 import org.apache.calcite.rel.logical.LogicalCorrelate;
-import org.apache.calcite.tools.RelBuilderFactory;
-
-import java.util.function.Predicate;
 
 /**
  * Implementation of nested loops over enumerable inputs.
+ *
+ * @see EnumerableRules#ENUMERABLE_CORRELATE_RULE
  */
 public class EnumerableCorrelateRule extends ConverterRule {
-  /**
-   * Creates an EnumerableCorrelateRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
-  public EnumerableCorrelateRule(RelBuilderFactory relBuilderFactory) {
-    super(LogicalCorrelate.class, (Predicate<RelNode>) r -> true,
-        Convention.NONE, EnumerableConvention.INSTANCE, relBuilderFactory,
-        "EnumerableCorrelateRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.EMPTY
+      .as(Config.class)
+      .withConversion(LogicalCorrelate.class, r -> true, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableCorrelateRule")
+      .withRuleFactory(EnumerableCorrelateRule::new);
+
+  /** Creates an EnumerableCorrelateRule. */
+  protected EnumerableCorrelateRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final LogicalCorrelate c = (LogicalCorrelate) rel;
     return EnumerableCorrelate.create(
         convert(c.getLeft(), c.getLeft().getTraitSet()
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilterRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilterRule.java
index df73609..4f1df08 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilterRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilterRule.java
@@ -17,27 +17,30 @@
 package org.apache.calcite.adapter.enumerable;
 
 import org.apache.calcite.plan.Convention;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.logical.LogicalFilter;
 
-import java.util.function.Predicate;
-
 /**
  * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalFilter} to an
  * {@link EnumerableFilter}.
+ *
+ * @see EnumerableRules#ENUMERABLE_FILTER_RULE
  */
 class EnumerableFilterRule extends ConverterRule {
-  EnumerableFilterRule() {
-    super(LogicalFilter.class,
-        (Predicate<LogicalFilter>) RelOptUtil::notContainsWindowedAgg,
-        Convention.NONE, EnumerableConvention.INSTANCE,
-        RelFactories.LOGICAL_BUILDER, "EnumerableFilterRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.EMPTY
+      .as(Config.class)
+      .withConversion(LogicalFilter.class, f -> !f.containsOver(),
+          Convention.NONE, EnumerableConvention.INSTANCE,
+          "EnumerableFilterRule")
+      .withRuleFactory(EnumerableFilterRule::new);
+
+  protected EnumerableFilterRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final LogicalFilter filter = (LogicalFilter) rel;
     return new EnumerableFilter(rel.getCluster(),
         rel.getTraitSet().replace(EnumerableConvention.INSTANCE),
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilterToCalcRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilterToCalcRule.java
index 8e05a47..db260a0 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilterToCalcRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableFilterToCalcRule.java
@@ -16,8 +16,8 @@
  */
 package org.apache.calcite.adapter.enumerable;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexBuilder;
@@ -26,18 +26,23 @@ import org.apache.calcite.rex.RexProgramBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
 
 /** Variant of {@link org.apache.calcite.rel.rules.FilterToCalcRule} for
- * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}. */
-public class EnumerableFilterToCalcRule extends RelOptRule {
-  /**
-   * Creates an EnumerableFilterToCalcRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+ * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}.
+ *
+ * @see EnumerableRules#ENUMERABLE_FILTER_TO_CALC_RULE */
+public class EnumerableFilterToCalcRule
+    extends RelRule<EnumerableFilterToCalcRule.Config> {
+  /** Creates an EnumerableFilterToCalcRule. */
+  protected EnumerableFilterToCalcRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public EnumerableFilterToCalcRule(RelBuilderFactory relBuilderFactory) {
-    super(operand(EnumerableFilter.class, any()), relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final EnumerableFilter filter = call.rel(0);
     final RelNode input = filter.getInput();
 
@@ -53,4 +58,16 @@ public class EnumerableFilterToCalcRule extends RelOptRule {
     final EnumerableCalc calc = EnumerableCalc.create(input, program);
     call.transformTo(calc);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b ->
+            b.operand(EnumerableFilter.class).anyInputs())
+        .as(Config.class);
+
+    @Override default EnumerableFilterToCalcRule toRule() {
+      return new EnumerableFilterToCalcRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableInterpreterRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableInterpreterRule.java
index bb0b635..ccb21d5 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableInterpreterRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableInterpreterRule.java
@@ -19,30 +19,23 @@ package org.apache.calcite.adapter.enumerable;
 import org.apache.calcite.interpreter.BindableConvention;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.tools.RelBuilderFactory;
-
-import java.util.function.Predicate;
 
 /**
  * Planner rule that converts {@link org.apache.calcite.interpreter.BindableRel}
  * to {@link org.apache.calcite.adapter.enumerable.EnumerableRel} by creating
  * an {@link org.apache.calcite.adapter.enumerable.EnumerableInterpreter}.
+ *
+ * @see EnumerableRules#TO_INTERPRETER
  */
 public class EnumerableInterpreterRule extends ConverterRule {
-  /** @deprecated Use {@link EnumerableRules#TO_INTERPRETER}. */
-  @Deprecated // to be removed before 1.25
-  public static final EnumerableInterpreterRule INSTANCE =
-      EnumerableRules.TO_INTERPRETER;
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(RelNode.class, BindableConvention.INSTANCE,
+          EnumerableConvention.INSTANCE, "EnumerableInterpreterRule")
+      .withRuleFactory(EnumerableInterpreterRule::new);
 
-  /**
-   * Creates an EnumerableInterpreterRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
-  public EnumerableInterpreterRule(RelBuilderFactory relBuilderFactory) {
-    super(RelNode.class, (Predicate<RelNode>) r -> true,
-        BindableConvention.INSTANCE, EnumerableConvention.INSTANCE,
-        relBuilderFactory, "EnumerableInterpreterRule");
+  protected EnumerableInterpreterRule(Config config) {
+    super(config);
   }
 
   //~ Methods ----------------------------------------------------------------
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableIntersectRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableIntersectRule.java
index 4bd6ad2..e5fadf5 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableIntersectRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableIntersectRule.java
@@ -26,14 +26,22 @@ import org.apache.calcite.rel.logical.LogicalIntersect;
  * Rule to convert a
  * {@link org.apache.calcite.rel.logical.LogicalIntersect} to an
  * {@link EnumerableIntersect}.
+ *
+ * @see EnumerableRules#ENUMERABLE_INTERSECT_RULE
  */
 class EnumerableIntersectRule extends ConverterRule {
-  EnumerableIntersectRule() {
-    super(LogicalIntersect.class, Convention.NONE,
-        EnumerableConvention.INSTANCE, "EnumerableIntersectRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalIntersect.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableIntersectRule")
+      .withRuleFactory(EnumerableIntersectRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableIntersectRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final LogicalIntersect intersect = (LogicalIntersect) rel;
     final EnumerableConvention out = EnumerableConvention.INSTANCE;
     final RelTraitSet traitSet = intersect.getTraitSet().replace(out);
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableJoinRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableJoinRule.java
index fdbde84..a71bf00 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableJoinRule.java
@@ -31,14 +31,19 @@ import java.util.List;
 
 /** Planner rule that converts a
  * {@link org.apache.calcite.rel.logical.LogicalJoin} relational expression
- * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}. */
+ * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}.
+ *
+ * @see EnumerableRules#ENUMERABLE_JOIN_RULE */
 class EnumerableJoinRule extends ConverterRule {
-  EnumerableJoinRule() {
-    super(
-        LogicalJoin.class,
-        Convention.NONE,
-        EnumerableConvention.INSTANCE,
-        "EnumerableJoinRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalJoin.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableJoinRule")
+      .withRuleFactory(EnumerableJoinRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableJoinRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitRule.java
index 8440686..1a5292c 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableLimitRule.java
@@ -16,8 +16,8 @@
  */
 package org.apache.calcite.adapter.enumerable;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Sort;
 
@@ -26,12 +26,19 @@ import org.apache.calcite.rel.core.Sort;
  * {@code offset} or {@code fetch} set to an
  * {@link EnumerableLimit}
  * on top of a "pure" {@code Sort} that has no offset or fetch.
+ *
+ * @see EnumerableRules#ENUMERABLE_LIMIT_RULE
  */
-class EnumerableLimitRule extends RelOptRule {
+public class EnumerableLimitRule
+    extends RelRule<EnumerableLimitRule.Config> {
+  /** Creates an EnumerableLimitRule. */
+  protected EnumerableLimitRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   EnumerableLimitRule() {
-    super(
-        operand(Sort.class, any()),
-        "EnumerableLimitRule");
+    this(Config.DEFAULT);
   }
 
   @Override public void onMatch(RelOptRuleCall call) {
@@ -55,4 +62,15 @@ class EnumerableLimitRule extends RelOptRule {
             sort.offset,
             sort.fetch));
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b -> b.operand(Sort.class).anyInputs())
+        .as(Config.class);
+
+    @Override default EnumerableLimitRule toRule() {
+      return new EnumerableLimitRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMatchRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMatchRule.java
index 57a6fdb..73192f9 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMatchRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMatchRule.java
@@ -24,11 +24,19 @@ import org.apache.calcite.rel.logical.LogicalMatch;
 /**
  * Rule to convert a {@link LogicalMatch} to an
  * {@link EnumerableMatch}.
+ *
+ * @see EnumerableRules#ENUMERABLE_MATCH_RULE
  */
 public class EnumerableMatchRule extends ConverterRule {
-  EnumerableMatchRule() {
-    super(LogicalMatch.class, Convention.NONE, EnumerableConvention.INSTANCE,
-        "EnumerableMatchRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalMatch.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableMatchRule")
+      .withRuleFactory(EnumerableMatchRule::new);
+
+  /** Creates an EnumerableMatchRule. */
+  protected EnumerableMatchRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoinRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoinRule.java
index 9c06eaa..0a0ad07 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMergeJoinRule.java
@@ -40,13 +40,18 @@ import java.util.List;
  * {@link EnumerableConvention enumerable calling convention}.
  *
  * @see EnumerableJoinRule
+ * @see EnumerableRules#ENUMERABLE_MERGE_JOIN_RULE
  */
 class EnumerableMergeJoinRule extends ConverterRule {
-  EnumerableMergeJoinRule() {
-    super(LogicalJoin.class,
-        Convention.NONE,
-        EnumerableConvention.INSTANCE,
-        "EnumerableMergeJoinRule");
+  /** Default configuration. */
+  static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalJoin.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableMergeJoinRule")
+      .withRuleFactory(EnumerableMergeJoinRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableMergeJoinRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMinusRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMinusRule.java
index f81df96..ecbd0e0 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMinusRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableMinusRule.java
@@ -25,14 +25,22 @@ import org.apache.calcite.rel.logical.LogicalMinus;
 /**
  * Rule to convert an {@link org.apache.calcite.rel.logical.LogicalMinus} to an
  * {@link EnumerableMinus}.
+ *
+ * @see EnumerableRules#ENUMERABLE_MINUS_RULE
  */
 class EnumerableMinusRule extends ConverterRule {
-  EnumerableMinusRule() {
-    super(LogicalMinus.class, Convention.NONE, EnumerableConvention.INSTANCE,
-        "EnumerableMinusRule");
+  /** Default configuration. */
+  static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalMinus.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableMinusRule")
+      .withRuleFactory(EnumerableMinusRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableMinusRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final LogicalMinus minus = (LogicalMinus) rel;
     final EnumerableConvention out = EnumerableConvention.INSTANCE;
     final RelTraitSet traitSet =
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProjectRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProjectRule.java
index 6ab1a6e..b5da3cb 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProjectRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProjectRule.java
@@ -17,27 +17,32 @@
 package org.apache.calcite.adapter.enumerable;
 
 import org.apache.calcite.plan.Convention;
-import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.logical.LogicalProject;
 
-import java.util.function.Predicate;
 
 /**
  * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalProject} to an
  * {@link EnumerableProject}.
+ *
+ * @see EnumerableRules#ENUMERABLE_PROJECT_RULE
  */
 class EnumerableProjectRule extends ConverterRule {
-  EnumerableProjectRule() {
-    super(LogicalProject.class,
-        (Predicate<LogicalProject>) RelOptUtil::notContainsWindowedAgg,
-        Convention.NONE, EnumerableConvention.INSTANCE,
-        RelFactories.LOGICAL_BUILDER, "EnumerableProjectRule");
+  /** Default configuration. */
+  static final Config DEFAULT_CONFIG = Config.EMPTY
+      .as(Config.class)
+      .withConversion(LogicalProject.class, p -> !p.containsOver(),
+          Convention.NONE, EnumerableConvention.INSTANCE,
+          "EnumerableProjectRule")
+      .withRuleFactory(EnumerableProjectRule::new);
+
+  /** Creates an EnumerableProjectRule. */
+  protected EnumerableProjectRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final LogicalProject project = (LogicalProject) rel;
     return EnumerableProject.create(
         convert(project.getInput(),
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProjectToCalcRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProjectToCalcRule.java
index 4f6f226..dd344ba 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProjectToCalcRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableProjectToCalcRule.java
@@ -16,25 +16,29 @@
  */
 package org.apache.calcite.adapter.enumerable;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.rules.ProjectToCalcRule;
 import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.tools.RelBuilderFactory;
 
 /** Variant of {@link org.apache.calcite.rel.rules.ProjectToCalcRule} for
- * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}. */
-public class EnumerableProjectToCalcRule extends RelOptRule {
-  /**
-   * Creates an EnumerableProjectToCalcRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+ * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}.
+ *
+ * @see EnumerableRules#ENUMERABLE_PROJECT_TO_CALC_RULE */
+public class EnumerableProjectToCalcRule extends ProjectToCalcRule {
+  /** Creates an EnumerableProjectToCalcRule. */
+  protected EnumerableProjectToCalcRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public EnumerableProjectToCalcRule(RelBuilderFactory relBuilderFactory) {
-    super(operand(EnumerableProject.class, any()), relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final EnumerableProject project = call.rel(0);
     final RelNode input = project.getInput();
     final RexProgram program =
@@ -46,4 +50,16 @@ public class EnumerableProjectToCalcRule extends RelOptRule {
     final EnumerableCalc calc = EnumerableCalc.create(input, program);
     call.transformTo(calc);
   }
+
+  /** Rule configuration. */
+  public interface Config extends ProjectToCalcRule.Config {
+    Config DEFAULT = ProjectToCalcRule.Config.DEFAULT
+        .withOperandSupplier(b ->
+            b.operand(EnumerableProject.class).anyInputs())
+        .as(Config.class);
+
+    @Override default EnumerableProjectToCalcRule toRule() {
+      return new EnumerableProjectToCalcRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRepeatUnionRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRepeatUnionRule.java
index 89aa707..1ae3511 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRepeatUnionRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRepeatUnionRule.java
@@ -23,17 +23,21 @@ import org.apache.calcite.rel.convert.ConverterRule;
 import org.apache.calcite.rel.logical.LogicalRepeatUnion;
 
 /**
- * Rule to convert a {@link LogicalRepeatUnion} into an {@link EnumerableRepeatUnion}.
+ * Rule to convert a {@link LogicalRepeatUnion} into an
+ * {@link EnumerableRepeatUnion}.
+ *
+ * @see EnumerableRules#ENUMERABLE_REPEAT_UNION_RULE
  */
 public class EnumerableRepeatUnionRule extends ConverterRule {
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalRepeatUnion.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableRepeatUnionRule")
+      .withRuleFactory(EnumerableRepeatUnionRule::new);
 
-  EnumerableRepeatUnionRule() {
-    super(
-        LogicalRepeatUnion.class,
-        Convention.NONE,
-        EnumerableConvention.INSTANCE,
-        "EnumerableRepeatUnionRule");
-
+  /** Called from the Config. */
+  protected EnumerableRepeatUnionRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRules.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRules.java
index 2672a67..1222353 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRules.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableRules.java
@@ -16,8 +16,12 @@
  */
 package org.apache.calcite.adapter.enumerable;
 
+import org.apache.calcite.linq4j.function.Experimental;
 import org.apache.calcite.plan.RelOptRule;
-import org.apache.calcite.rel.core.RelFactories;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalMatch;
+import org.apache.calcite.rel.logical.LogicalRepeatUnion;
+import org.apache.calcite.rel.logical.LogicalTableSpool;
 import org.apache.calcite.util.trace.CalciteTrace;
 
 import com.google.common.collect.ImmutableList;
@@ -38,94 +42,158 @@ public class EnumerableRules {
   private EnumerableRules() {
   }
 
+  /** Rule that converts a
+   * {@link org.apache.calcite.rel.logical.LogicalJoin} to
+   * {@link EnumerableConvention enumerable calling convention}. */
   public static final RelOptRule ENUMERABLE_JOIN_RULE =
-      new EnumerableJoinRule();
+      EnumerableJoinRule.DEFAULT_CONFIG.toRule(EnumerableJoinRule.class);
 
+  /** Rule that converts a
+   * {@link org.apache.calcite.rel.logical.LogicalJoin} to
+   * {@link EnumerableConvention enumerable calling convention}. */
   public static final RelOptRule ENUMERABLE_MERGE_JOIN_RULE =
-      new EnumerableMergeJoinRule();
+      EnumerableMergeJoinRule.DEFAULT_CONFIG
+          .toRule(EnumerableMergeJoinRule.class);
 
   public static final RelOptRule ENUMERABLE_CORRELATE_RULE =
-      new EnumerableCorrelateRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableCorrelateRule.DEFAULT_CONFIG
+          .toRule(EnumerableCorrelateRule.class);
 
+  /** Rule that converts a
+   * {@link org.apache.calcite.rel.logical.LogicalJoin} into an
+   * {@link org.apache.calcite.adapter.enumerable.EnumerableBatchNestedLoopJoin}. */
   public static final RelOptRule ENUMERABLE_BATCH_NESTED_LOOP_JOIN_RULE =
-      new EnumerableBatchNestedLoopJoinRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableBatchNestedLoopJoinRule.Config.DEFAULT.toRule();
 
+  /** Rule that converts a
+   *  {@link org.apache.calcite.rel.logical.LogicalProject} to an
+   * {@link EnumerableProject}. */
   public static final EnumerableProjectRule ENUMERABLE_PROJECT_RULE =
-      new EnumerableProjectRule();
+      EnumerableProjectRule.DEFAULT_CONFIG.toRule(EnumerableProjectRule.class);
 
   public static final EnumerableFilterRule ENUMERABLE_FILTER_RULE =
-      new EnumerableFilterRule();
+      EnumerableFilterRule.DEFAULT_CONFIG.toRule(EnumerableFilterRule.class);
 
   public static final EnumerableCalcRule ENUMERABLE_CALC_RULE =
-      new EnumerableCalcRule();
+      EnumerableCalcRule.DEFAULT_CONFIG.toRule(EnumerableCalcRule.class);
 
   public static final EnumerableAggregateRule ENUMERABLE_AGGREGATE_RULE =
-      new EnumerableAggregateRule();
+      EnumerableAggregateRule.DEFAULT_CONFIG
+          .toRule(EnumerableAggregateRule.class);
 
+  /** Rule that converts a {@link org.apache.calcite.rel.core.Sort} to an
+   * {@link EnumerableSort}. */
   public static final EnumerableSortRule ENUMERABLE_SORT_RULE =
-      new EnumerableSortRule();
+      EnumerableSortRule.DEFAULT_CONFIG.toRule(EnumerableSortRule.class);
 
   public static final EnumerableLimitRule ENUMERABLE_LIMIT_RULE =
-      new EnumerableLimitRule();
+      EnumerableLimitRule.Config.DEFAULT.toRule();
 
+  /** Rule that converts a {@link org.apache.calcite.rel.logical.LogicalUnion}
+   * to an {@link EnumerableUnion}. */
   public static final EnumerableUnionRule ENUMERABLE_UNION_RULE =
-      new EnumerableUnionRule();
+      EnumerableUnionRule.DEFAULT_CONFIG.toRule(EnumerableUnionRule.class);
 
+  /** Rule that converts a {@link LogicalRepeatUnion} into an
+   * {@link EnumerableRepeatUnion}. */
   public static final EnumerableRepeatUnionRule ENUMERABLE_REPEAT_UNION_RULE =
-      new EnumerableRepeatUnionRule();
+      EnumerableRepeatUnionRule.DEFAULT_CONFIG
+          .toRule(EnumerableRepeatUnionRule.class);
 
+  /** Rule that converts a {@link LogicalTableSpool} into an
+   * {@link EnumerableTableSpool}. */
+  @Experimental
   public static final EnumerableTableSpoolRule ENUMERABLE_TABLE_SPOOL_RULE =
-      new EnumerableTableSpoolRule();
+      EnumerableTableSpoolRule.DEFAULT_CONFIG
+          .toRule(EnumerableTableSpoolRule.class);
 
+  /** Rule that converts a
+   * {@link org.apache.calcite.rel.logical.LogicalIntersect} to an
+   * {@link EnumerableIntersect}. */
   public static final EnumerableIntersectRule ENUMERABLE_INTERSECT_RULE =
-      new EnumerableIntersectRule();
+      EnumerableIntersectRule.DEFAULT_CONFIG
+          .toRule(EnumerableIntersectRule.class);
 
+  /** Rule that converts a
+   * {@link org.apache.calcite.rel.logical.LogicalMinus} to an
+   * {@link EnumerableMinus}. */
   public static final EnumerableMinusRule ENUMERABLE_MINUS_RULE =
-      new EnumerableMinusRule();
+      EnumerableMinusRule.DEFAULT_CONFIG.toRule(EnumerableMinusRule.class);
 
+  /** Rule that converts a
+   * {@link org.apache.calcite.rel.logical.LogicalTableModify} to
+   * {@link EnumerableConvention enumerable calling convention}. */
   public static final EnumerableTableModifyRule ENUMERABLE_TABLE_MODIFICATION_RULE =
-      new EnumerableTableModifyRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableTableModifyRule.DEFAULT_CONFIG
+          .toRule(EnumerableTableModifyRule.class);
 
+  /** Rule that converts a
+   * {@link org.apache.calcite.rel.logical.LogicalValues} to
+   * {@link EnumerableConvention enumerable calling convention}. */
   public static final EnumerableValuesRule ENUMERABLE_VALUES_RULE =
-      new EnumerableValuesRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableValuesRule.DEFAULT_CONFIG.toRule(EnumerableValuesRule.class);
 
+  /** Rule that converts a {@link org.apache.calcite.rel.logical.LogicalWindow}
+   * to an {@link org.apache.calcite.adapter.enumerable.EnumerableWindow}. */
   public static final EnumerableWindowRule ENUMERABLE_WINDOW_RULE =
-      new EnumerableWindowRule();
+      EnumerableWindowRule.DEFAULT_CONFIG.toRule(EnumerableWindowRule.class);
 
+  /** Rule that converts an {@link org.apache.calcite.rel.core.Collect}
+   * to an {@link EnumerableCollect}. */
   public static final EnumerableCollectRule ENUMERABLE_COLLECT_RULE =
-      new EnumerableCollectRule();
+      EnumerableCollectRule.DEFAULT_CONFIG.toRule(EnumerableCollectRule.class);
 
+  /** Rule that converts an {@link org.apache.calcite.rel.core.Uncollect}
+   * to an {@link EnumerableUncollect}. */
   public static final EnumerableUncollectRule ENUMERABLE_UNCOLLECT_RULE =
-      new EnumerableUncollectRule();
+      EnumerableUncollectRule.DEFAULT_CONFIG
+          .toRule(EnumerableUncollectRule.class);
 
   public static final EnumerableFilterToCalcRule ENUMERABLE_FILTER_TO_CALC_RULE =
-      new EnumerableFilterToCalcRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableFilterToCalcRule.Config.DEFAULT.toRule();
 
+  /** Variant of {@link org.apache.calcite.rel.rules.ProjectToCalcRule} for
+   * {@link EnumerableConvention enumerable calling convention}. */
   public static final EnumerableProjectToCalcRule ENUMERABLE_PROJECT_TO_CALC_RULE =
-      new EnumerableProjectToCalcRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableProjectToCalcRule.Config.DEFAULT.toRule();
 
+  /** Rule that converts a
+   *  {@link org.apache.calcite.rel.logical.LogicalTableScan} to
+   * {@link EnumerableConvention enumerable calling convention}. */
   public static final EnumerableTableScanRule ENUMERABLE_TABLE_SCAN_RULE =
-      new EnumerableTableScanRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableTableScanRule.DEFAULT_CONFIG
+          .toRule(EnumerableTableScanRule.class);
 
+  /** Rule that converts a
+   * {@link org.apache.calcite.rel.logical.LogicalTableFunctionScan} to
+   * {@link EnumerableConvention enumerable calling convention}. */
   public static final EnumerableTableFunctionScanRule ENUMERABLE_TABLE_FUNCTION_SCAN_RULE =
-      new EnumerableTableFunctionScanRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableTableFunctionScanRule.DEFAULT_CONFIG
+          .toRule(EnumerableTableFunctionScanRule.class);
 
+  /** Rule that converts a {@link LogicalMatch} to an
+   *  {@link EnumerableMatch}. */
   public static final EnumerableMatchRule ENUMERABLE_MATCH_RULE =
-      new EnumerableMatchRule();
+      EnumerableMatchRule.DEFAULT_CONFIG.toRule(EnumerableMatchRule.class);
 
+  /** Rule to convert a {@link LogicalAggregate}
+   * to an {@link EnumerableSortedAggregate}. */
   public static final EnumerableSortedAggregateRule ENUMERABLE_SORTED_AGGREGATE_RULE =
-      new EnumerableSortedAggregateRule();
+      EnumerableSortedAggregateRule.DEFAULT_CONFIG
+          .toRule(EnumerableSortedAggregateRule.class);
 
   /** Rule that converts any enumerable relational expression to bindable. */
   public static final EnumerableBindable.EnumerableToBindableConverterRule TO_BINDABLE =
-      new EnumerableBindable.EnumerableToBindableConverterRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableBindable.EnumerableToBindableConverterRule.DEFAULT_CONFIG
+          .toRule(EnumerableBindable.EnumerableToBindableConverterRule.class);
 
   /**
    * Rule that converts {@link org.apache.calcite.interpreter.BindableRel}
    * to {@link org.apache.calcite.adapter.enumerable.EnumerableRel} by creating
    * an {@link org.apache.calcite.adapter.enumerable.EnumerableInterpreter}. */
   public static final EnumerableInterpreterRule TO_INTERPRETER =
-      new EnumerableInterpreterRule(RelFactories.LOGICAL_BUILDER);
+      EnumerableInterpreterRule.DEFAULT_CONFIG
+          .toRule(EnumerableInterpreterRule.class);
 
   public static final List<RelOptRule> ENUMERABLE_RULES = ImmutableList.of(
       EnumerableRules.ENUMERABLE_JOIN_RULE,
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortRule.java
index 4e9941b..84ef93c 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortRule.java
@@ -24,11 +24,19 @@ import org.apache.calcite.rel.core.Sort;
 /**
  * Rule to convert an {@link org.apache.calcite.rel.core.Sort} to an
  * {@link EnumerableSort}.
+ *
+ * @see EnumerableRules#ENUMERABLE_SORT_RULE
  */
 class EnumerableSortRule extends ConverterRule {
-  EnumerableSortRule() {
-    super(Sort.class, Convention.NONE, EnumerableConvention.INSTANCE,
-        "EnumerableSortRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(Sort.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableSortRule")
+      .withRuleFactory(EnumerableSortRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableSortRule(Config config) {
+    super(config);
   }
 
   public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregateRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregateRule.java
index 2f6e810..06577c2 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregateRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableSortedAggregateRule.java
@@ -29,11 +29,19 @@ import org.apache.calcite.util.ImmutableIntList;
 /**
  * Rule to convert a {@link LogicalAggregate}
  * to an {@link EnumerableSortedAggregate}.
+ *
+ * @see EnumerableRules#ENUMERABLE_SORTED_AGGREGATE_RULE
  */
 class EnumerableSortedAggregateRule extends ConverterRule {
-  EnumerableSortedAggregateRule() {
-    super(LogicalAggregate.class, Convention.NONE,
-        EnumerableConvention.INSTANCE, "EnumerableSortedAggregateRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalAggregate.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableSortedAggregateRule")
+      .withRuleFactory(EnumerableSortedAggregateRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableSortedAggregateRule(Config config) {
+    super(config);
   }
 
   public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableFunctionScanRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableFunctionScanRule.java
index 744f95e..f684a00 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableFunctionScanRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableFunctionScanRule.java
@@ -20,39 +20,33 @@ import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.logical.LogicalTableFunctionScan;
-import org.apache.calcite.tools.RelBuilderFactory;
-
-import java.util.function.Predicate;
 
 /** Planner rule that converts a
- * {@link org.apache.calcite.rel.logical.LogicalTableFunctionScan}
- * relational expression
- * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}. */
+ * {@link org.apache.calcite.rel.logical.LogicalTableFunctionScan} to
+ * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}.
+ *
+ * @see EnumerableRules#ENUMERABLE_TABLE_FUNCTION_SCAN_RULE */
 public class EnumerableTableFunctionScanRule extends ConverterRule {
-  @Deprecated // to be removed before 2.0
-  public EnumerableTableFunctionScanRule() {
-    this(RelFactories.LOGICAL_BUILDER);
-  }
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.EMPTY
+      .as(Config.class)
+      .withConversion(LogicalTableFunctionScan.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableTableFunctionScanRule")
+      .withRuleFactory(EnumerableTableFunctionScanRule::new);
 
-  /**
-   * Creates an EnumerableTableFunctionScanRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
-  public EnumerableTableFunctionScanRule(RelBuilderFactory relBuilderFactory) {
-    super(LogicalTableFunctionScan.class, (Predicate<RelNode>) r -> true,
-        Convention.NONE, EnumerableConvention.INSTANCE, relBuilderFactory,
-        "EnumerableTableFunctionScanRule");
+  /** Creates an EnumerableTableFunctionScanRule. */
+  protected EnumerableTableFunctionScanRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
     final RelTraitSet traitSet =
         rel.getTraitSet().replace(EnumerableConvention.INSTANCE);
-    LogicalTableFunctionScan tbl = (LogicalTableFunctionScan) rel;
+    LogicalTableFunctionScan scan = (LogicalTableFunctionScan) rel;
     return new EnumerableTableFunctionScan(rel.getCluster(), traitSet,
-        convertList(tbl.getInputs(), traitSet.getTrait(0)), tbl.getElementType(), tbl.getRowType(),
-        tbl.getCall(), tbl.getColumnMappings());
+        convertList(scan.getInputs(), traitSet.getTrait(0)),
+        scan.getElementType(), scan.getRowType(),
+        scan.getCall(), scan.getColumnMappings());
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableModifyRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableModifyRule.java
index 3186c61..0019d3a 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableModifyRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableModifyRule.java
@@ -22,24 +22,22 @@ import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
 import org.apache.calcite.rel.logical.LogicalTableModify;
 import org.apache.calcite.schema.ModifiableTable;
-import org.apache.calcite.tools.RelBuilderFactory;
-
-import java.util.function.Predicate;
 
 /** Planner rule that converts a
- * {@link org.apache.calcite.rel.logical.LogicalTableModify}
- * relational expression
- * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}. */
+ * {@link org.apache.calcite.rel.logical.LogicalTableModify} to
+ * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}.
+ *
+ * @see EnumerableRules#ENUMERABLE_TABLE_MODIFICATION_RULE */
 public class EnumerableTableModifyRule extends ConverterRule {
-  /**
-   * Creates an EnumerableTableModifyRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
-  public EnumerableTableModifyRule(RelBuilderFactory relBuilderFactory) {
-    super(LogicalTableModify.class, (Predicate<RelNode>) r -> true,
-        Convention.NONE, EnumerableConvention.INSTANCE, relBuilderFactory,
-        "EnumerableTableModificationRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalTableModify.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableTableModificationRule")
+      .withRuleFactory(EnumerableTableModifyRule::new);
+
+  /** Creates an EnumerableTableModifyRule. */
+  protected EnumerableTableModifyRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableScanRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableScanRule.java
index 341e79d..1ecb226 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableScanRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableScanRule.java
@@ -20,35 +20,27 @@ import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.RelOptTable;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.logical.LogicalTableScan;
 import org.apache.calcite.schema.QueryableTable;
 import org.apache.calcite.schema.Table;
-import org.apache.calcite.tools.RelBuilderFactory;
-
-import java.util.function.Predicate;
 
 /** Planner rule that converts a
- * {@link org.apache.calcite.rel.logical.LogicalTableFunctionScan}
- * relational expression
- * {@link EnumerableConvention enumerable calling convention}. */
+ * {@link org.apache.calcite.rel.logical.LogicalTableScan} to
+ * {@link EnumerableConvention enumerable calling convention}.
+ *
+ * @see EnumerableRules#ENUMERABLE_TABLE_SCAN_RULE */
 public class EnumerableTableScanRule extends ConverterRule {
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.EMPTY
+      .as(Config.class)
+      .withConversion(LogicalTableScan.class,
+          r -> EnumerableTableScan.canHandle(r.getTable()),
+          Convention.NONE, EnumerableConvention.INSTANCE,
+          "EnumerableTableScanRule")
+      .withRuleFactory(EnumerableTableScanRule::new);
 
-  @Deprecated // to be removed before 2.0
-  public EnumerableTableScanRule() {
-    this(RelFactories.LOGICAL_BUILDER);
-  }
-
-  /**
-   * Creates an EnumerableTableScanRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
-  public EnumerableTableScanRule(RelBuilderFactory relBuilderFactory) {
-    super(LogicalTableScan.class,
-        (Predicate<LogicalTableScan>) r -> EnumerableTableScan.canHandle(r.getTable()),
-        Convention.NONE, EnumerableConvention.INSTANCE, relBuilderFactory,
-        "EnumerableTableScanRule");
+  protected EnumerableTableScanRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableSpoolRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableSpoolRule.java
index 12e3a01..dd546ff 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableSpoolRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableTableSpoolRule.java
@@ -28,15 +28,20 @@ import org.apache.calcite.rel.logical.LogicalTableSpool;
  *
  * <p>NOTE: The current API is experimental and subject to change without
  * notice.
+ *
+ * @see EnumerableRules#ENUMERABLE_TABLE_SPOOL_RULE
  */
 @Experimental
 public class EnumerableTableSpoolRule extends ConverterRule {
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalTableSpool.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableTableSpoolRule")
+      .withRuleFactory(EnumerableTableSpoolRule::new);
 
-  EnumerableTableSpoolRule() {
-    super(LogicalTableSpool.class,
-        Convention.NONE,
-        EnumerableConvention.INSTANCE,
-        "EnumerableTableSpoolRule");
+  /** Called from the Config. */
+  protected EnumerableTableSpoolRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollectRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollectRule.java
index f3b3c75..9f33b71 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollectRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUncollectRule.java
@@ -25,14 +25,22 @@ import org.apache.calcite.rel.core.Uncollect;
 /**
  * Rule to convert an {@link org.apache.calcite.rel.core.Uncollect} to an
  * {@link EnumerableUncollect}.
+ *
+ * @see EnumerableRules#ENUMERABLE_UNCOLLECT_RULE
  */
 class EnumerableUncollectRule extends ConverterRule {
-  EnumerableUncollectRule() {
-    super(Uncollect.class, Convention.NONE, EnumerableConvention.INSTANCE,
-        "EnumerableUncollectRule");
+  /** Default configuration. */
+  static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(Uncollect.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableUncollectRule")
+      .withRuleFactory(EnumerableUncollectRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableUncollectRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final Uncollect uncollect = (Uncollect) rel;
     final RelTraitSet traitSet =
         uncollect.getTraitSet().replace(EnumerableConvention.INSTANCE);
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUnionRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUnionRule.java
index d611c51..08fc857 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUnionRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableUnionRule.java
@@ -29,14 +29,22 @@ import java.util.List;
 /**
  * Rule to convert an {@link org.apache.calcite.rel.logical.LogicalUnion} to an
  * {@link EnumerableUnion}.
+ *
+ * @see EnumerableRules#ENUMERABLE_UNION_RULE
  */
 class EnumerableUnionRule extends ConverterRule {
-  EnumerableUnionRule() {
-    super(LogicalUnion.class, Convention.NONE, EnumerableConvention.INSTANCE,
-        "EnumerableUnionRule");
+  /** Default configuration. */
+  static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalUnion.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableUnionRule")
+      .withRuleFactory(EnumerableUnionRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableUnionRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final LogicalUnion union = (LogicalUnion) rel;
     final EnumerableConvention out = EnumerableConvention.INSTANCE;
     final RelTraitSet traitSet = rel.getCluster().traitSet().replace(out);
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableValuesRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableValuesRule.java
index c981a8b..605e922 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableValuesRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableValuesRule.java
@@ -20,24 +20,23 @@ import org.apache.calcite.plan.Convention;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
 import org.apache.calcite.rel.logical.LogicalValues;
-import org.apache.calcite.tools.RelBuilderFactory;
-
-import java.util.function.Predicate;
 
 /** Planner rule that converts a
  * {@link org.apache.calcite.rel.logical.LogicalValues}
  * relational expression
- * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}. */
+ * {@link org.apache.calcite.adapter.enumerable.EnumerableConvention enumerable calling convention}.
+ *
+ * @see EnumerableRules#ENUMERABLE_VALUES_RULE */
 public class EnumerableValuesRule extends ConverterRule {
-  /**
-   * Creates an EnumerableValuesRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
-  public EnumerableValuesRule(RelBuilderFactory relBuilderFactory) {
-    super(LogicalValues.class, (Predicate<RelNode>) r -> true,
-        Convention.NONE, EnumerableConvention.INSTANCE, relBuilderFactory,
-        "EnumerableValuesRule");
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalValues.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableValuesRule")
+      .withRuleFactory(EnumerableValuesRule::new);
+
+  /** Creates an EnumerableValuesRule. */
+  protected EnumerableValuesRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindowRule.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindowRule.java
index bc3e1dd..837a664 100644
--- a/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindowRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/EnumerableWindowRule.java
@@ -25,14 +25,22 @@ import org.apache.calcite.rel.logical.LogicalWindow;
 /**
  * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalWindow} to
  * an {@link org.apache.calcite.adapter.enumerable.EnumerableWindow}.
+ *
+ * @see EnumerableRules#ENUMERABLE_WINDOW_RULE
  */
 class EnumerableWindowRule extends ConverterRule {
-  EnumerableWindowRule() {
-    super(LogicalWindow.class, Convention.NONE, EnumerableConvention.INSTANCE,
-        "EnumerableWindowRule");
+  /** Default configuration. */
+  static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(LogicalWindow.class, Convention.NONE,
+          EnumerableConvention.INSTANCE, "EnumerableWindowRule")
+      .withRuleFactory(EnumerableWindowRule::new);
+
+  /** Called from the Config. */
+  protected EnumerableWindowRule(Config config) {
+    super(config);
   }
 
-  public RelNode convert(RelNode rel) {
+  @Override public RelNode convert(RelNode rel) {
     final LogicalWindow winAgg = (LogicalWindow) rel;
     final RelTraitSet traitSet =
         winAgg.getTraitSet().replace(EnumerableConvention.INSTANCE);
diff --git a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java
index 438781b..f7f8ce8 100644
--- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java
+++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcRules.java
@@ -25,7 +25,7 @@ import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptTable;
-import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.prepare.Prepare;
 import org.apache.calcite.rel.InvalidRelException;
@@ -58,7 +58,6 @@ import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexLiteral;
 import org.apache.calcite.rex.RexMultisetUtil;
 import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.rex.RexOver;
 import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.rex.RexVisitorImpl;
@@ -82,7 +81,7 @@ import org.slf4j.Logger;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
-import java.util.function.Predicate;
+import java.util.function.Consumer;
 
 /**
  * Rules and relational operators for
@@ -213,69 +212,59 @@ public class JdbcRules {
               TABLE_SCAN_FACTORY,
               SNAPSHOT_FACTORY));
 
+  /** Creates a list of rules with the given JDBC convention instance. */
   public static List<RelOptRule> rules(JdbcConvention out) {
-    return rules(out, RelFactories.LOGICAL_BUILDER);
+    final ImmutableList.Builder<RelOptRule> b = ImmutableList.builder();
+    foreachRule(out, b::add);
+    return b.build();
   }
 
+  /** Creates a list of rules with the given JDBC convention instance
+   * and builder factory. */
   public static List<RelOptRule> rules(JdbcConvention out,
       RelBuilderFactory relBuilderFactory) {
-    return ImmutableList.of(
-        new JdbcToEnumerableConverterRule(out, relBuilderFactory),
-        new JdbcJoinRule(out, relBuilderFactory),
-        new JdbcCalcRule(out, relBuilderFactory),
-        new JdbcProjectRule(out, relBuilderFactory),
-        new JdbcFilterRule(out, relBuilderFactory),
-        new JdbcAggregateRule(out, relBuilderFactory),
-        new JdbcSortRule(out, relBuilderFactory),
-        new JdbcUnionRule(out, relBuilderFactory),
-        new JdbcIntersectRule(out, relBuilderFactory),
-        new JdbcMinusRule(out, relBuilderFactory),
-        new JdbcTableModificationRule(out, relBuilderFactory),
-        new JdbcValuesRule(out, relBuilderFactory));
+    final ImmutableList.Builder<RelOptRule> b = ImmutableList.builder();
+    foreachRule(out, r ->
+        b.add(r.config.withRelBuilderFactory(relBuilderFactory).toRule()));
+    return b.build();
+  }
+
+  private static void foreachRule(JdbcConvention out,
+      Consumer<RelRule<?>> consumer) {
+    consumer.accept(JdbcToEnumerableConverterRule.create(out));
+    consumer.accept(JdbcJoinRule.create(out));
+    consumer.accept(JdbcCalcRule.create(out));
+    consumer.accept(JdbcProjectRule.create(out));
+    consumer.accept(JdbcFilterRule.create(out));
+    consumer.accept(JdbcAggregateRule.create(out));
+    consumer.accept(JdbcSortRule.create(out));
+    consumer.accept(JdbcUnionRule.create(out));
+    consumer.accept(JdbcIntersectRule.create(out));
+    consumer.accept(JdbcMinusRule.create(out));
+    consumer.accept(JdbcTableModificationRule.create(out));
+    consumer.accept(JdbcValuesRule.create(out));
   }
 
   /** Abstract base class for rule that converts to JDBC. */
   abstract static class JdbcConverterRule extends ConverterRule {
-    protected final JdbcConvention out;
-
-    @SuppressWarnings("unchecked")
-    @Deprecated // to be removed before 2.0
-    JdbcConverterRule(Class<? extends RelNode> clazz, RelTrait in,
-        JdbcConvention out, String description) {
-      this(clazz, (Predicate<RelNode>) r -> true, in, out,
-          RelFactories.LOGICAL_BUILDER, description);
-    }
-
-    <R extends RelNode> JdbcConverterRule(Class<R> clazz,
-        Predicate<? super R> predicate, RelTrait in, JdbcConvention out,
-        RelBuilderFactory relBuilderFactory, String description) {
-      super(clazz, predicate, in, out, relBuilderFactory, description);
-      this.out = out;
-    }
-
-    @SuppressWarnings({"Guava", "unchecked"})
-    @Deprecated // to be removed before 2.0
-    <R extends RelNode> JdbcConverterRule(Class<R> clazz,
-        com.google.common.base.Predicate<? super R> predicate,
-        RelTrait in, JdbcConvention out,
-        RelBuilderFactory relBuilderFactory, String description) {
-      this(clazz, (Predicate<R>) predicate, in, out, relBuilderFactory,
-          description);
+    protected JdbcConverterRule(Config config) {
+      super(config);
     }
   }
 
   /** Rule that converts a join to JDBC. */
   public static class JdbcJoinRule extends JdbcConverterRule {
-    @Deprecated // to be removed before 2.0
-    public JdbcJoinRule(JdbcConvention out) {
-      this(out, RelFactories.LOGICAL_BUILDER);
+    /** Creates a JdbcJoinRule. */
+    public static JdbcJoinRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Join.class, Convention.NONE, out, "JdbcJoinRule")
+          .withRuleFactory(JdbcJoinRule::new)
+          .toRule(JdbcJoinRule.class);
     }
 
-    /** Creates a JdbcJoinRule. */
-    public JdbcJoinRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Join.class, (Predicate<RelNode>) r -> true, Convention.NONE,
-          out, relBuilderFactory, "JdbcJoinRule");
+    /** Called from the Config. */
+    protected JdbcJoinRule(Config config) {
+      super(config);
     }
 
     @Override public RelNode convert(RelNode rel) {
@@ -431,10 +420,16 @@ public class JdbcRules {
    */
   private static class JdbcCalcRule extends JdbcConverterRule {
     /** Creates a JdbcCalcRule. */
-    private JdbcCalcRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Calc.class, (Predicate<RelNode>) r -> true, Convention.NONE,
-          out, relBuilderFactory, "JdbcCalcRule");
+    public static JdbcCalcRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Calc.class, Convention.NONE, out, "JdbcCalcRule")
+          .withRuleFactory(JdbcCalcRule::new)
+          .toRule(JdbcCalcRule.class);
+    }
+
+    /** Called from the Config. */
+    protected JdbcCalcRule(Config config) {
+      super(config);
     }
 
     public RelNode convert(RelNode rel) {
@@ -506,19 +501,21 @@ public class JdbcRules {
    * an {@link org.apache.calcite.adapter.jdbc.JdbcRules.JdbcProject}.
    */
   public static class JdbcProjectRule extends JdbcConverterRule {
-    @Deprecated // to be removed before 2.0
-    public JdbcProjectRule(final JdbcConvention out) {
-      this(out, RelFactories.LOGICAL_BUILDER);
+    /** Creates a JdbcProjectRule. */
+    public static JdbcProjectRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Project.class, project ->
+                  (out.dialect.supportsWindowFunctions()
+                      || !project.containsOver())
+                      && !userDefinedFunctionInProject(project),
+              Convention.NONE, out, "JdbcProjectRule")
+          .withRuleFactory(JdbcProjectRule::new)
+          .toRule(JdbcProjectRule.class);
     }
 
-    /** Creates a JdbcProjectRule. */
-    public JdbcProjectRule(final JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Project.class, (Predicate<Project>) project ->
-              (out.dialect.supportsWindowFunctions()
-                  || !RexOver.containsOver(project.getProjects(), null))
-                  && !userDefinedFunctionInProject(project),
-          Convention.NONE, out, relBuilderFactory, "JdbcProjectRule");
+    /** Called from the Config. */
+    protected JdbcProjectRule(Config config) {
+      super(config);
     }
 
     private static boolean userDefinedFunctionInProject(Project project) {
@@ -532,7 +529,7 @@ public class JdbcRules {
       return false;
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final Project project = (Project) rel;
 
       return new JdbcProject(
@@ -589,17 +586,18 @@ public class JdbcRules {
    * an {@link org.apache.calcite.adapter.jdbc.JdbcRules.JdbcFilter}.
    */
   public static class JdbcFilterRule extends JdbcConverterRule {
-    @Deprecated // to be removed before 2.0
-    public JdbcFilterRule(JdbcConvention out) {
-      this(out, RelFactories.LOGICAL_BUILDER);
+    /** Creates a JdbcFilterRule. */
+    public static JdbcFilterRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Filter.class, r -> !userDefinedFunctionInFilter(r),
+              Convention.NONE, out, "JdbcFilterRule")
+          .withRuleFactory(JdbcFilterRule::new)
+          .toRule(JdbcFilterRule.class);
     }
 
-    /** Creates a JdbcFilterRule. */
-    public JdbcFilterRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Filter.class,
-          (Predicate<Filter>) r -> !userDefinedFunctionInFilter(r),
-          Convention.NONE, out, relBuilderFactory, "JdbcFilterRule");
+    /** Called from the Config. */
+    protected JdbcFilterRule(Config config) {
+      super(config);
     }
 
     private static boolean userDefinedFunctionInFilter(Filter filter) {
@@ -608,7 +606,7 @@ public class JdbcRules {
       return visitor.containsUserDefinedFunction();
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final Filter filter = (Filter) rel;
 
       return new JdbcFilter(
@@ -647,19 +645,21 @@ public class JdbcRules {
    * to a {@link org.apache.calcite.adapter.jdbc.JdbcRules.JdbcAggregate}.
    */
   public static class JdbcAggregateRule extends JdbcConverterRule {
-    @Deprecated // to be removed before 2.0
-    public JdbcAggregateRule(JdbcConvention out) {
-      this(out, RelFactories.LOGICAL_BUILDER);
+    /** Creates a JdbcAggregateRule. */
+    public static JdbcAggregateRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Aggregate.class, Convention.NONE, out,
+              "JdbcAggregateRule")
+          .withRuleFactory(JdbcAggregateRule::new)
+          .toRule(JdbcAggregateRule.class);
     }
 
-    /** Creates a JdbcAggregateRule. */
-    public JdbcAggregateRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Aggregate.class, (Predicate<RelNode>) r -> true, Convention.NONE,
-          out, relBuilderFactory, "JdbcAggregateRule");
+    /** Called from the Config. */
+    protected JdbcAggregateRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final Aggregate agg = (Aggregate) rel;
       if (agg.getGroupSets().size() != 1) {
         // GROUPING SETS not supported; see
@@ -670,7 +670,7 @@ public class JdbcRules {
           agg.getTraitSet().replace(out);
       try {
         return new JdbcAggregate(rel.getCluster(), traitSet,
-            convert(agg.getInput(), out), false, agg.getGroupSet(),
+            convert(agg.getInput(), out), agg.getGroupSet(),
             agg.getGroupSets(), agg.getAggCallList());
       } catch (InvalidRelException e) {
         LOGGER.debug(e.toString());
@@ -739,19 +739,20 @@ public class JdbcRules {
    * {@link org.apache.calcite.adapter.jdbc.JdbcRules.JdbcSort}.
    */
   public static class JdbcSortRule extends JdbcConverterRule {
-    @Deprecated // to be removed before 2.0
-    public JdbcSortRule(JdbcConvention out) {
-      this(out, RelFactories.LOGICAL_BUILDER);
+    /** Creates a JdbcSortRule. */
+    public static JdbcSortRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Sort.class, Convention.NONE, out, "JdbcSortRule")
+          .withRuleFactory(JdbcSortRule::new)
+          .toRule(JdbcSortRule.class);
     }
 
-    /** Creates a JdbcSortRule. */
-    public JdbcSortRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Sort.class, (Predicate<RelNode>) r -> true, Convention.NONE, out,
-          relBuilderFactory, "JdbcSortRule");
+    /** Called from the Config. */
+    protected JdbcSortRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       return convert((Sort) rel, true);
     }
 
@@ -816,19 +817,20 @@ public class JdbcRules {
    * {@link org.apache.calcite.adapter.jdbc.JdbcRules.JdbcUnion}.
    */
   public static class JdbcUnionRule extends JdbcConverterRule {
-    @Deprecated // to be removed before 2.0
-    public JdbcUnionRule(JdbcConvention out) {
-      this(out, RelFactories.LOGICAL_BUILDER);
+    /** Creates a JdbcUnionRule. */
+    public static JdbcUnionRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Union.class, Convention.NONE, out, "JdbcUnionRule")
+          .withRuleFactory(JdbcUnionRule::new)
+          .toRule(JdbcUnionRule.class);
     }
 
-    /** Creates a JdbcUnionRule. */
-    public JdbcUnionRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Union.class, (Predicate<RelNode>) r -> true, Convention.NONE, out,
-          relBuilderFactory, "JdbcUnionRule");
+    /** Called from the Config. */
+    protected JdbcUnionRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final Union union = (Union) rel;
       final RelTraitSet traitSet =
           union.getTraitSet().replace(out);
@@ -868,13 +870,20 @@ public class JdbcRules {
    */
   public static class JdbcIntersectRule extends JdbcConverterRule {
     /** Creates a JdbcIntersectRule. */
-    private JdbcIntersectRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Intersect.class, (Predicate<RelNode>) r -> true, Convention.NONE,
-          out, relBuilderFactory, "JdbcIntersectRule");
+    public static JdbcIntersectRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Intersect.class, Convention.NONE, out,
+              "JdbcIntersectRule")
+          .withRuleFactory(JdbcIntersectRule::new)
+          .toRule(JdbcIntersectRule.class);
     }
 
-    public RelNode convert(RelNode rel) {
+    /** Called from the Config. */
+    protected JdbcIntersectRule(Config config) {
+      super(config);
+    }
+
+    @Override public RelNode convert(RelNode rel) {
       final Intersect intersect = (Intersect) rel;
       if (intersect.all) {
         return null; // INTERSECT ALL not implemented
@@ -915,13 +924,19 @@ public class JdbcRules {
    */
   public static class JdbcMinusRule extends JdbcConverterRule {
     /** Creates a JdbcMinusRule. */
-    private JdbcMinusRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Minus.class, (Predicate<RelNode>) r -> true, Convention.NONE, out,
-          relBuilderFactory, "JdbcMinusRule");
+    public static JdbcMinusRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Minus.class, Convention.NONE, out, "JdbcMinusRule")
+          .withRuleFactory(JdbcMinusRule::new)
+          .toRule(JdbcMinusRule.class);
     }
 
-    public RelNode convert(RelNode rel) {
+    /** Called from the Config. */
+    protected JdbcMinusRule(Config config) {
+      super(config);
+    }
+
+    @Override public RelNode convert(RelNode rel) {
       final Minus minus = (Minus) rel;
       if (minus.all) {
         return null; // EXCEPT ALL not implemented
@@ -953,11 +968,18 @@ public class JdbcRules {
 
   /** Rule that converts a table-modification to JDBC. */
   public static class JdbcTableModificationRule extends JdbcConverterRule {
-    /** Creates a JdbcTableModificationRule. */
-    private JdbcTableModificationRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(TableModify.class, (Predicate<RelNode>) r -> true,
-          Convention.NONE, out, relBuilderFactory, "JdbcTableModificationRule");
+    /** Creates a JdbcToEnumerableConverterRule. */
+    public static JdbcTableModificationRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(TableModify.class, Convention.NONE, out,
+              "JdbcTableModificationRule")
+          .withRuleFactory(JdbcTableModificationRule::new)
+          .toRule(JdbcTableModificationRule.class);
+    }
+
+    /** Called from the Config. */
+    protected JdbcTableModificationRule(Config config) {
+      super(config);
     }
 
     @Override public RelNode convert(RelNode rel) {
@@ -1030,10 +1052,16 @@ public class JdbcRules {
   /** Rule that converts a values operator to JDBC. */
   public static class JdbcValuesRule extends JdbcConverterRule {
     /** Creates a JdbcValuesRule. */
-    private JdbcValuesRule(JdbcConvention out,
-        RelBuilderFactory relBuilderFactory) {
-      super(Values.class, (Predicate<RelNode>) r -> true, Convention.NONE,
-          out, relBuilderFactory, "JdbcValuesRule");
+    public static JdbcValuesRule create(JdbcConvention out) {
+      return Config.INSTANCE
+          .withConversion(Values.class, Convention.NONE, out, "JdbcValuesRule")
+          .withRuleFactory(JdbcValuesRule::new)
+          .toRule(JdbcValuesRule.class);
+    }
+
+    /** Called from the Config. */
+    protected JdbcValuesRule(Config config) {
+      super(config);
     }
 
     @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverterRule.java b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverterRule.java
index 86fbc3d..3446ba4 100644
--- a/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverterRule.java
+++ b/core/src/main/java/org/apache/calcite/adapter/jdbc/JdbcToEnumerableConverterRule.java
@@ -20,9 +20,6 @@ import org.apache.calcite.adapter.enumerable.EnumerableConvention;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.tools.RelBuilderFactory;
-
-import java.util.function.Predicate;
 
 /**
  * Rule to convert a relational expression from
@@ -31,11 +28,17 @@ import java.util.function.Predicate;
  */
 public class JdbcToEnumerableConverterRule extends ConverterRule {
   /** Creates a JdbcToEnumerableConverterRule. */
-  public JdbcToEnumerableConverterRule(JdbcConvention out,
-      RelBuilderFactory relBuilderFactory) {
-    super(RelNode.class, (Predicate<RelNode>) r -> true, out,
-        EnumerableConvention.INSTANCE, relBuilderFactory,
-        "JdbcToEnumerableConverterRule");
+  public static JdbcToEnumerableConverterRule create(JdbcConvention out) {
+    return Config.INSTANCE
+        .withConversion(RelNode.class, out, EnumerableConvention.INSTANCE,
+            "JdbcToEnumerableConverterRule")
+        .withRuleFactory(JdbcToEnumerableConverterRule::new)
+        .toRule(JdbcToEnumerableConverterRule.class);
+  }
+
+  /** Called from the Config. */
+  protected JdbcToEnumerableConverterRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/interpreter/Bindables.java b/core/src/main/java/org/apache/calcite/interpreter/Bindables.java
index a61d033..7d9adac 100644
--- a/core/src/main/java/org/apache/calcite/interpreter/Bindables.java
+++ b/core/src/main/java/org/apache/calcite/interpreter/Bindables.java
@@ -27,7 +27,7 @@ import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptTable;
-import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.InvalidRelException;
 import org.apache.calcite.rel.RelCollation;
@@ -45,7 +45,6 @@ import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.core.Match;
 import org.apache.calcite.rel.core.Minus;
 import org.apache.calcite.rel.core.Project;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.SetOp;
 import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.core.TableScan;
@@ -85,7 +84,6 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.SortedSet;
-import java.util.function.Predicate;
 
 /**
  * Utilities pertaining to {@link BindableRel} and {@link BindableConvention}.
@@ -94,40 +92,45 @@ public class Bindables {
   private Bindables() {}
 
   public static final RelOptRule BINDABLE_TABLE_SCAN_RULE =
-      new BindableTableScanRule(RelFactories.LOGICAL_BUILDER);
+      BindableTableScanRule.Config.DEFAULT.toRule();
 
   public static final RelOptRule BINDABLE_FILTER_RULE =
-      new BindableFilterRule(RelFactories.LOGICAL_BUILDER);
+      BindableFilterRule.DEFAULT_CONFIG.toRule(BindableFilterRule.class);
 
   public static final RelOptRule BINDABLE_PROJECT_RULE =
-      new BindableProjectRule(RelFactories.LOGICAL_BUILDER);
+      BindableProjectRule.DEFAULT_CONFIG.toRule(BindableProjectRule.class);
 
   public static final RelOptRule BINDABLE_SORT_RULE =
-      new BindableSortRule(RelFactories.LOGICAL_BUILDER);
+      BindableSortRule.DEFAULT_CONFIG.toRule(BindableSortRule.class);
 
   public static final RelOptRule BINDABLE_JOIN_RULE =
-      new BindableJoinRule(RelFactories.LOGICAL_BUILDER);
+      BindableJoinRule.DEFAULT_CONFIG.toRule(BindableJoinRule.class);
 
+  public static final RelOptRule BINDABLE_SET_OP_RULE =
+      BindableSetOpRule.DEFAULT_CONFIG.toRule(BindableSetOpRule.class);
+
+  /** @deprecated Use {@link #BINDABLE_SET_OP_RULE}. */
   public static final RelOptRule BINDABLE_SETOP_RULE =
-      new BindableSetOpRule(RelFactories.LOGICAL_BUILDER);
+      BINDABLE_SET_OP_RULE;
 
   public static final RelOptRule BINDABLE_VALUES_RULE =
-      new BindableValuesRule(RelFactories.LOGICAL_BUILDER);
+      BindableValuesRule.DEFAULT_CONFIG.toRule(BindableValuesRule.class);
 
   public static final RelOptRule BINDABLE_AGGREGATE_RULE =
-      new BindableAggregateRule(RelFactories.LOGICAL_BUILDER);
+      BindableAggregateRule.DEFAULT_CONFIG.toRule(BindableAggregateRule.class);
 
   public static final RelOptRule BINDABLE_WINDOW_RULE =
-      new BindableWindowRule(RelFactories.LOGICAL_BUILDER);
+      BindableWindowRule.DEFAULT_CONFIG.toRule(BindableWindowRule.class);
 
   public static final RelOptRule BINDABLE_MATCH_RULE =
-      new BindableMatchRule(RelFactories.LOGICAL_BUILDER);
+      BindableMatchRule.DEFAULT_CONFIG.toRule(BindableMatchRule.class);
 
   /** Rule that converts a relational expression from
    * {@link org.apache.calcite.plan.Convention#NONE}
    * to {@link org.apache.calcite.interpreter.BindableConvention}. */
-  public static final ConverterRule FROM_NONE_RULE =
-      new NoneToBindableConverterRule(RelFactories.LOGICAL_BUILDER);
+  public static final NoneToBindableConverterRule FROM_NONE_RULE =
+      NoneToBindableConverterRule.DEFAULT_CONFIG
+          .toRule(NoneToBindableConverterRule.class);
 
   /** All rules that convert logical relational expression to bindable. */
   public static final ImmutableList<RelOptRule> RULES =
@@ -137,7 +140,7 @@ public class Bindables {
           BINDABLE_PROJECT_RULE,
           BINDABLE_SORT_RULE,
           BINDABLE_JOIN_RULE,
-          BINDABLE_SETOP_RULE,
+          BINDABLE_SET_OP_RULE,
           BINDABLE_VALUES_RULE,
           BINDABLE_AGGREGATE_RULE,
           BINDABLE_WINDOW_RULE,
@@ -155,16 +158,20 @@ public class Bindables {
   }
 
   /** Rule that converts a {@link org.apache.calcite.rel.core.TableScan}
-   * to bindable convention. */
-  public static class BindableTableScanRule extends RelOptRule {
+   * to bindable convention.
+   *
+   * @see #BINDABLE_TABLE_SCAN_RULE */
+  public static class BindableTableScanRule
+      extends RelRule<BindableTableScanRule.Config> {
+    /** Called from Config. */
+    protected BindableTableScanRule(Config config) {
+      super(config);
+    }
 
-    /**
-     * Creates a BindableTableScanRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
+    @Deprecated // to be removed before 2.0
     public BindableTableScanRule(RelBuilderFactory relBuilderFactory) {
-      super(operand(LogicalTableScan.class, none()), relBuilderFactory, null);
+      this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+          .as(Config.class));
     }
 
     @Override public void onMatch(RelOptRuleCall call) {
@@ -175,6 +182,18 @@ public class Bindables {
             BindableTableScan.create(scan.getCluster(), table));
       }
     }
+
+    /** Rule configuration. */
+    public interface Config extends RelRule.Config {
+      Config DEFAULT = EMPTY
+          .withOperandSupplier(b ->
+              b.operand(LogicalTableScan.class).noInputs())
+          .as(Config.class);
+
+      @Override default BindableTableScanRule toRule() {
+        return new BindableTableScanRule(this);
+      }
+    }
   }
 
   /** Scan of a table that implements {@link ScannableTable} and therefore can
@@ -277,22 +296,23 @@ public class Bindables {
     }
   }
 
-  /** Rule that converts a {@link Filter} to bindable convention. */
+  /** Rule that converts a {@link Filter} to bindable convention.
+   *
+   * @see #BINDABLE_FILTER_RULE */
   public static class BindableFilterRule extends ConverterRule {
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(LogicalFilter.class, f -> !f.containsOver(),
+            Convention.NONE, BindableConvention.INSTANCE,
+            "BindableFilterRule")
+        .withRuleFactory(BindableFilterRule::new);
 
-    /**
-     * Creates a BindableFilterRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public BindableFilterRule(RelBuilderFactory relBuilderFactory) {
-      super(LogicalFilter.class,
-          (Predicate<LogicalFilter>) RelOptUtil::notContainsWindowedAgg,
-          Convention.NONE, BindableConvention.INSTANCE, relBuilderFactory,
-          "BindableFilterRule");
+    /** Called from the Config. */
+    protected BindableFilterRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final LogicalFilter filter = (LogicalFilter) rel;
       return BindableFilter.create(
           convert(filter.getInput(),
@@ -344,22 +364,23 @@ public class Bindables {
   /**
    * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalProject}
    * to a {@link BindableProject}.
+   *
+   * @see #BINDABLE_PROJECT_RULE
    */
   public static class BindableProjectRule extends ConverterRule {
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(LogicalProject.class, p -> !p.containsOver(),
+            Convention.NONE, BindableConvention.INSTANCE,
+            "BindableProjectRule")
+        .withRuleFactory(BindableProjectRule::new);
 
-    /**
-     * Creates a BindableProjectRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public BindableProjectRule(RelBuilderFactory relBuilderFactory) {
-      super(LogicalProject.class,
-          (Predicate<LogicalProject>) RelOptUtil::notContainsWindowedAgg,
-          Convention.NONE, BindableConvention.INSTANCE, relBuilderFactory,
-          "BindableProjectRule");
+    /** Called from the Config. */
+    protected BindableProjectRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final LogicalProject project = (LogicalProject) rel;
       return new BindableProject(rel.getCluster(),
           rel.getTraitSet().replace(BindableConvention.INSTANCE),
@@ -402,20 +423,22 @@ public class Bindables {
   /**
    * Rule to convert an {@link org.apache.calcite.rel.core.Sort} to a
    * {@link org.apache.calcite.interpreter.Bindables.BindableSort}.
+   *
+   * @see #BINDABLE_SORT_RULE
    */
   public static class BindableSortRule extends ConverterRule {
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(Sort.class, Convention.NONE,
+            BindableConvention.INSTANCE, "BindableSortRule")
+        .withRuleFactory(BindableSortRule::new);
 
-    /**
-     * Creates a BindableSortRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public BindableSortRule(RelBuilderFactory relBuilderFactory) {
-      super(Sort.class, (Predicate<RelNode>) r -> true, Convention.NONE,
-          BindableConvention.INSTANCE, relBuilderFactory, "BindableSortRule");
+    /** Called from the Config. */
+    protected BindableSortRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final Sort sort = (Sort) rel;
       final RelTraitSet traitSet =
           sort.getTraitSet().replace(BindableConvention.INSTANCE);
@@ -458,21 +481,22 @@ public class Bindables {
   /**
    * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalJoin}
    * to a {@link BindableJoin}.
+   *
+   * @see #BINDABLE_JOIN_RULE
    */
   public static class BindableJoinRule extends ConverterRule {
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(LogicalJoin.class, Convention.NONE,
+            BindableConvention.INSTANCE, "BindableJoinRule")
+        .withRuleFactory(BindableJoinRule::new);
 
-    /**
-     * Creates a BindableJoinRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public BindableJoinRule(RelBuilderFactory relBuilderFactory) {
-      super(LogicalJoin.class, (Predicate<RelNode>) r -> true,
-          Convention.NONE, BindableConvention.INSTANCE, relBuilderFactory,
-          "BindableJoinRule");
+    /** Called from the Config. */
+    protected BindableJoinRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final LogicalJoin join = (LogicalJoin) rel;
       final BindableConvention out = BindableConvention.INSTANCE;
       final RelTraitSet traitSet = join.getTraitSet().replace(out);
@@ -529,21 +553,22 @@ public class Bindables {
   /**
    * Rule to convert an {@link SetOp} to a {@link BindableUnion}
    * or {@link BindableIntersect} or {@link BindableMinus}.
+   *
+   * @see #BINDABLE_SET_OP_RULE
    */
   public static class BindableSetOpRule extends ConverterRule {
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(SetOp.class, Convention.NONE,
+            BindableConvention.INSTANCE, "BindableSetOpRule")
+        .withRuleFactory(BindableSetOpRule::new);
 
-    /**
-     * Creates a BindableSetOpRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public BindableSetOpRule(RelBuilderFactory relBuilderFactory) {
-      super(SetOp.class, (Predicate<RelNode>) r -> true,
-          Convention.NONE, BindableConvention.INSTANCE, relBuilderFactory,
-          "BindableSetOpRule");
+    /** Called from the Config. */
+    protected BindableSetOpRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final SetOp setOp = (SetOp) rel;
       final BindableConvention out = BindableConvention.INSTANCE;
       final RelTraitSet traitSet = setOp.getTraitSet().replace(out);
@@ -662,18 +687,19 @@ public class Bindables {
     }
   }
 
-  /** Rule that converts a {@link Values} to bindable convention. */
+  /** Rule that converts a {@link Values} to bindable convention.
+   *
+   * @see #BINDABLE_VALUES_RULE */
   public static class BindableValuesRule extends ConverterRule {
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(LogicalValues.class, Convention.NONE,
+            BindableConvention.INSTANCE, "BindableValuesRule")
+        .withRuleFactory(BindableValuesRule::new);
 
-    /**
-     * Creates a BindableValuesRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public BindableValuesRule(RelBuilderFactory relBuilderFactory) {
-      super(LogicalValues.class, (Predicate<RelNode>) r -> true,
-          Convention.NONE, BindableConvention.INSTANCE, relBuilderFactory,
-          "BindableValuesRule");
+    /** Called from the Config. */
+    protected BindableValuesRule(Config config) {
+      super(config);
     }
 
     @Override public RelNode convert(RelNode rel) {
@@ -748,21 +774,22 @@ public class Bindables {
     }
   }
 
-  /** Rule that converts an {@link Aggregate} to bindable convention. */
+  /** Rule that converts an {@link Aggregate} to bindable convention.
+   *
+   * @see #BINDABLE_AGGREGATE_RULE */
   public static class BindableAggregateRule extends ConverterRule {
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(LogicalAggregate.class, Convention.NONE,
+            BindableConvention.INSTANCE, "BindableAggregateRule")
+        .withRuleFactory(BindableAggregateRule::new);
 
-    /**
-     * Creates a BindableAggregateRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public BindableAggregateRule(RelBuilderFactory relBuilderFactory) {
-      super(LogicalAggregate.class, (Predicate<RelNode>) r -> true,
-          Convention.NONE, BindableConvention.INSTANCE, relBuilderFactory,
-          "BindableAggregateRule");
+    /** Called from the Config. */
+    protected BindableAggregateRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final LogicalAggregate agg = (LogicalAggregate) rel;
       final RelTraitSet traitSet =
           agg.getTraitSet().replace(BindableConvention.INSTANCE);
@@ -810,24 +837,23 @@ public class Bindables {
     }
   }
 
-  /**
-   * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalWindow}
+  /** Rule to convert a {@link org.apache.calcite.rel.logical.LogicalWindow}
    * to a {@link BindableWindow}.
-   */
+   *
+   * @see #BINDABLE_WINDOW_RULE */
   public static class BindableWindowRule extends ConverterRule {
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(LogicalWindow.class, Convention.NONE,
+            BindableConvention.INSTANCE, "BindableWindowRule")
+        .withRuleFactory(BindableWindowRule::new);
 
-    /**
-     * Creates a BindableWindowRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public BindableWindowRule(RelBuilderFactory relBuilderFactory) {
-      super(LogicalWindow.class, (Predicate<RelNode>) r -> true,
-          Convention.NONE, BindableConvention.INSTANCE, relBuilderFactory,
-          "BindableWindowRule");
+    /** Called from the Config. */
+    protected BindableWindowRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final LogicalWindow winAgg = (LogicalWindow) rel;
       final RelTraitSet traitSet =
           winAgg.getTraitSet().replace(BindableConvention.INSTANCE);
@@ -843,7 +869,7 @@ public class Bindables {
   /** Implementation of {@link org.apache.calcite.rel.core.Match}
    * in bindable convention. */
   public static class BindableMatch extends Match implements BindableRel {
-    /** Creates a BindableMatch. */
+    /** Singleton instance of BindableMatch. */
     BindableMatch(RelOptCluster cluster, RelTraitSet traitSet, RelNode input,
         RelDataType rowType, RexNode pattern, boolean strictStart,
         boolean strictEnd, Map<String, RexNode> patternDefinitions,
@@ -878,21 +904,22 @@ public class Bindables {
   /**
    * Rule to convert a {@link org.apache.calcite.rel.logical.LogicalMatch}
    * to a {@link BindableMatch}.
+   *
+   * @see #BINDABLE_MATCH_RULE
    */
   public static class BindableMatchRule extends ConverterRule {
+    /** Default configuration. */
+    public static final Config DEFAULT_CONFIG = Config.INSTANCE
+        .withConversion(LogicalMatch.class, Convention.NONE,
+            BindableConvention.INSTANCE, "BindableMatchRule")
+        .withRuleFactory(BindableMatchRule::new);
 
-    /**
-     * Creates a BindableMatchRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
-    public BindableMatchRule(RelBuilderFactory relBuilderFactory) {
-      super(LogicalMatch.class, (Predicate<RelNode>) r -> true,
-          Convention.NONE, BindableConvention.INSTANCE, relBuilderFactory,
-          "BindableMatchRule");
+    /** Called from the Config. */
+    protected BindableMatchRule(Config config) {
+      super(config);
     }
 
-    public RelNode convert(RelNode rel) {
+    @Override public RelNode convert(RelNode rel) {
       final LogicalMatch match = (LogicalMatch) rel;
       final RelTraitSet traitSet =
           match.getTraitSet().replace(BindableConvention.INSTANCE);
diff --git a/core/src/main/java/org/apache/calcite/interpreter/NoneToBindableConverterRule.java b/core/src/main/java/org/apache/calcite/interpreter/NoneToBindableConverterRule.java
index 17dc359..bdf78ab 100644
--- a/core/src/main/java/org/apache/calcite/interpreter/NoneToBindableConverterRule.java
+++ b/core/src/main/java/org/apache/calcite/interpreter/NoneToBindableConverterRule.java
@@ -20,30 +20,25 @@ import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.convert.ConverterRule;
-import org.apache.calcite.tools.RelBuilderFactory;
 
-import java.util.function.Predicate;
 
 /**
  * Rule to convert a relational expression from
  * {@link org.apache.calcite.plan.Convention#NONE}
  * to {@link org.apache.calcite.interpreter.BindableConvention}.
+ *
+ * @see Bindables#FROM_NONE_RULE
  */
 public class NoneToBindableConverterRule extends ConverterRule {
-  /** @deprecated Use {@link Bindables#FROM_NONE_RULE}. */
-  @Deprecated // to be removed before 1.25
-  public static final ConverterRule INSTANCE =
-      Bindables.FROM_NONE_RULE;
+  /** Default configuration. */
+  public static final Config DEFAULT_CONFIG = Config.INSTANCE
+      .withConversion(RelNode.class, Convention.NONE,
+          BindableConvention.INSTANCE, "NoneToBindableConverterRule")
+      .withRuleFactory(NoneToBindableConverterRule::new);
 
-  /**
-   * Creates a NoneToBindableConverterRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
-  public NoneToBindableConverterRule(RelBuilderFactory relBuilderFactory) {
-    super(RelNode.class, (Predicate<RelNode>) r -> true, Convention.NONE,
-        BindableConvention.INSTANCE, relBuilderFactory,
-        "NoneToBindableConverterRule");
+  /** Called from the Config. */
+  protected NoneToBindableConverterRule(Config config) {
+    super(config);
   }
 
   @Override public RelNode convert(RelNode rel) {
diff --git a/core/src/main/java/org/apache/calcite/plan/CommonRelSubExprRule.java b/core/src/main/java/org/apache/calcite/plan/CommonRelSubExprRule.java
index e5ffda5..256f25e 100644
--- a/core/src/main/java/org/apache/calcite/plan/CommonRelSubExprRule.java
+++ b/core/src/main/java/org/apache/calcite/plan/CommonRelSubExprRule.java
@@ -22,15 +22,24 @@ package org.apache.calcite.plan;
  * that are fired only on relational expressions that appear more than once
  * in a query tree.
  */
-public abstract class CommonRelSubExprRule extends RelOptRule {
+
+// TODO: obsolete this?
+public abstract class CommonRelSubExprRule
+    extends RelRule<CommonRelSubExprRule.Config> {
   //~ Constructors -----------------------------------------------------------
 
-  /**
-   * Creates a <code>CommonRelSubExprRule</code>.
-   *
-   * @param operand root operand, must not be null
-   */
+  /** Creates a CommonRelSubExprRule. */
+  protected CommonRelSubExprRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public CommonRelSubExprRule(RelOptRuleOperand operand) {
-    super(operand);
+    this(Config.EMPTY.withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class));
+  }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptMaterialization.java b/core/src/main/java/org/apache/calcite/plan/RelOptMaterialization.java
index b0d84fa..a8823e3 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptMaterialization.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptMaterialization.java
@@ -195,8 +195,8 @@ public class RelOptMaterialization {
       return null;
     }
     final Program program = Programs.hep(
-        ImmutableList.of(CoreRules.PROJECT_FILTER_TRANSPOSE,
-            CoreRules.AGGREGATE_PROJECT_MERGE,
+        ImmutableList.of(
+            CoreRules.PROJECT_FILTER_TRANSPOSE, CoreRules.AGGREGATE_PROJECT_MERGE,
             CoreRules.AGGREGATE_FILTER_TRANSPOSE),
         false,
         DefaultRelMetadataProvider.INSTANCE);
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptRule.java b/core/src/main/java/org/apache/calcite/plan/RelOptRule.java
index 1a066c0..6eea4b8 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptRule.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptRule.java
@@ -29,6 +29,7 @@ import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
 import java.util.function.Predicate;
+import javax.annotation.Nonnull;
 
 /**
  * A <code>RelOptRule</code> transforms an expression into another. It has a
@@ -120,7 +121,10 @@ public abstract class RelOptRule {
    * @param <R> Class of relational expression to match
    * @return Operand that matches a relational expression that has no
    *   children
+   *
+   * @deprecated Use {@link RelRule.OperandBuilder#operand(Class)}
    */
+  @Deprecated // to be removed before 2.0
   public static <R extends RelNode> RelOptRuleOperand operand(
       Class<R> clazz,
       RelOptRuleOperandChildren operandList) {
@@ -138,7 +142,10 @@ public abstract class RelOptRule {
    * @param <R> Class of relational expression to match
    * @return Operand that matches a relational expression that has no
    *   children
+   *
+   * @deprecated Use {@link RelRule.OperandBuilder#operand(Class)}
    */
+  @Deprecated // to be removed before 2.0
   public static <R extends RelNode> RelOptRuleOperand operand(
       Class<R> clazz,
       RelTrait trait,
@@ -158,7 +165,10 @@ public abstract class RelOptRule {
    * @param <R> Class of relational expression to match
    * @return Operand that matches a relational expression that has a
    *   particular trait and predicate
+   *
+   * @deprecated Use {@link RelRule.OperandBuilder#operand(Class)}
    */
+  @Deprecated // to be removed before 2.0
   public static <R extends RelNode> RelOptRuleOperand operandJ(
       Class<R> clazz,
       RelTrait trait,
@@ -191,7 +201,10 @@ public abstract class RelOptRule {
    * @param rest Rest operands
    * @param <R> Class of relational expression to match
    * @return Operand
+   *
+   * @deprecated Use {@link RelRule.OperandBuilder#operand(Class)}
    */
+  @Deprecated // to be removed before 2.0
   public static <R extends RelNode> RelOptRuleOperand operandJ(
       Class<R> clazz,
       RelTrait trait,
@@ -231,7 +244,10 @@ public abstract class RelOptRule {
    * @param <R> Class of relational expression to match
    * @return Operand that matches a relational expression with a given
    *   list of children
+   *
+   * @deprecated Use {@link RelRule.OperandBuilder#operand(Class)}
    */
+  @Deprecated // to be removed before 2.0
   public static <R extends RelNode> RelOptRuleOperand operand(
       Class<R> clazz,
       RelOptRuleOperand first,
@@ -246,6 +262,7 @@ public abstract class RelOptRule {
    * @param trait    Trait to match, or null to match any trait
    * @param predicate Predicate to apply to relational expression
    */
+  @Deprecated // to be removed before 2.0
   protected static <R extends RelNode> ConverterRelOptRuleOperand
       convertOperand(Class<R> clazz, Predicate<? super R> predicate,
       RelTrait trait) {
@@ -272,7 +289,10 @@ public abstract class RelOptRule {
    * @param rest  Remaining child operands (may be empty)
    * @return List of child operands that matches child relational
    *   expressions in the order
+   *
+   * @deprecated Use {@link RelRule.OperandDetailBuilder#inputs}
    */
+  @Deprecated // to be removed before 2.0
   public static RelOptRuleOperandChildren some(
       RelOptRuleOperand first,
       RelOptRuleOperand... rest) {
@@ -307,6 +327,7 @@ public abstract class RelOptRule {
    * @return List of child operands that matches child relational
    *   expressions in any order
    */
+  @Deprecated // to be removed before 2.0
   public static RelOptRuleOperandChildren unordered(
       RelOptRuleOperand first,
       RelOptRuleOperand... rest) {
@@ -319,7 +340,10 @@ public abstract class RelOptRule {
    * Creates an empty list of child operands.
    *
    * @return Empty list of child operands
+   *
+   * @deprecated Use {@link RelRule.OperandDetailBuilder#noInputs()}
    */
+  @Deprecated // to be removed before 2.0
   public static RelOptRuleOperandChildren none() {
     return RelOptRuleOperandChildren.LEAF_CHILDREN;
   }
@@ -330,7 +354,10 @@ public abstract class RelOptRule {
    *
    * @return List of child operands that signifies that the operand matches
    *   any number of child relational expressions
+   *
+   * @deprecated Use {@link RelRule.OperandDetailBuilder#anyInputs()}
    */
+  @Deprecated // to be removed before 2.0
   public static RelOptRuleOperandChildren any() {
     return RelOptRuleOperandChildren.ANY_CHILDREN;
   }
@@ -447,10 +474,11 @@ public abstract class RelOptRule {
    * @param that Another rule
    * @return Whether this rule is equal to another rule
    */
-  protected boolean equals(RelOptRule that) {
+  protected boolean equals(@Nonnull RelOptRule that) {
     // Include operands and class in the equality criteria just in case
     // they have chosen a poor description.
-    return this == that || this.getClass() == that.getClass()
+    return this == that
+        || this.getClass() == that.getClass()
         && this.description.equals(that.description)
         && this.operand.equals(that.operand);
   }
@@ -640,7 +668,7 @@ public abstract class RelOptRule {
   /**
    * Operand to an instance of the converter rule.
    */
-  private static class ConverterRelOptRuleOperand extends RelOptRuleOperand {
+  protected static class ConverterRelOptRuleOperand extends RelOptRuleOperand {
     <R extends RelNode> ConverterRelOptRuleOperand(Class<R> clazz, RelTrait in,
         Predicate<? super R> predicate) {
       super(clazz, in, predicate, RelOptRuleOperandChildPolicy.ANY,
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java b/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java
index 861b658..fd5f50f 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptRuleCall.java
@@ -177,7 +177,7 @@ public abstract class RelOptRuleCall {
 
   /** Assigns the input relational expressions of a given relational expression,
    * as seen by this particular call. Is only called when the operand is
-   * {@link RelOptRule#any()}. */
+   * {@link RelRule.OperandDetailBuilder#anyInputs() any}. */
   protected void setChildRels(RelNode rel, List<RelNode> inputs) {
     if (nodeInputs.isEmpty()) {
       nodeInputs = new HashMap<>();
diff --git a/core/src/main/java/org/apache/calcite/plan/RelOptRuleOperandChildren.java b/core/src/main/java/org/apache/calcite/plan/RelOptRuleOperandChildren.java
index 98539a8..2ef1465 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptRuleOperandChildren.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptRuleOperandChildren.java
@@ -28,8 +28,11 @@ import java.util.List;
  * {@link RelOptRule#some},
  * {@link RelOptRule#none},
  * {@link RelOptRule#any},
- * {@link RelOptRule#unordered},</p>
+ * {@link RelOptRule#unordered}.
+ *
+ * @deprecated Use {@link RelRule.OperandBuilder}
  */
+@Deprecated // to be removed before 2.0
 public class RelOptRuleOperandChildren {
   static final RelOptRuleOperandChildren ANY_CHILDREN =
       new RelOptRuleOperandChildren(
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 2e634f8..f7c1ca7 100644
--- a/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
+++ b/core/src/main/java/org/apache/calcite/plan/RelOptUtil.java
@@ -144,8 +144,7 @@ public abstract class RelOptUtil {
   @SuppressWarnings("Guava")
   @Deprecated // to be removed before 2.0
   public static final com.google.common.base.Predicate<Filter>
-      FILTER_PREDICATE =
-      RelOptUtil::notContainsWindowedAgg;
+      FILTER_PREDICATE = f -> !f.containsOver();
 
   @SuppressWarnings("Guava")
   @Deprecated // to be removed before 2.0
@@ -2060,7 +2059,6 @@ public abstract class RelOptUtil {
     }
 
     planner.addRule(CoreRules.FILTER_REDUCE_EXPRESSIONS);
-
   }
 
   @Deprecated // to be removed before 1.25
@@ -2979,7 +2977,7 @@ public abstract class RelOptUtil {
       return null;
     }
     if (RexOver.containsOver(nodes, null)
-        && RexOver.containsOver(project.getProjects(), null)) {
+        && project.containsOver()) {
       // Is it valid relational algebra to apply windowed function to a windowed
       // function? Possibly. But it's invalid SQL, so don't go there.
       return null;
@@ -3427,17 +3425,17 @@ public abstract class RelOptUtil {
 
   /** Predicate for if a {@link Calc} does not contain windowed aggregates. */
   public static boolean notContainsWindowedAgg(Calc calc) {
-    return !calc.getProgram().containsAggs();
+    return !calc.containsOver();
   }
 
   /** Predicate for if a {@link Filter} does not windowed aggregates. */
   public static boolean notContainsWindowedAgg(Filter filter) {
-    return !RexOver.containsOver(filter.getCondition());
+    return !filter.containsOver();
   }
 
   /** Predicate for if a {@link Project} does not contain windowed aggregates. */
   public static boolean notContainsWindowedAgg(Project project) {
-    return !RexOver.containsOver(project.getProjects(), null);
+    return !project.containsOver();
   }
 
   /** Policies for handling two- and three-valued boolean logic. */
diff --git a/core/src/main/java/org/apache/calcite/plan/RelRule.java b/core/src/main/java/org/apache/calcite/plan/RelRule.java
new file mode 100644
index 0000000..edf62de
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/plan/RelRule.java
@@ -0,0 +1,324 @@
+/*
+ * 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;
+
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.RelFactories;
+import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import javax.annotation.Nonnull;
+
+/**
+ * Rule that is parameterized via a configuration.
+ *
+ * <p>Eventually (before Calcite version 2.0), this class will replace
+ * {@link RelOptRule}. Constructors of {@code RelOptRule} are deprecated, so new
+ * rule classes should extend {@code RelRule}, not {@code RelOptRule}.
+ * Next, we will deprecate {@code RelOptRule}, so that variables that reference
+ * rules will be of type {@code RelRule}.
+ *
+ * <p><b>Guidelines for writing rules</b>
+ *
+ * <p>1. If your rule is a sub-class of
+ * {@link org.apache.calcite.rel.convert.ConverterRule}
+ * and does not need any extra properties,
+ * there's no need to create an {@code interface Config} inside your class.
+ * In your class, create a constant
+ * {@code public static final Config DEFAULT_CONFIG}. Goto step 5.
+ *
+ * <p>2. If your rule is not a sub-class of
+ * {@link org.apache.calcite.rel.convert.ConverterRule},
+ * create an inner {@code interface Config extends RelRule.Config}.
+ * Implement {@link Config#toRule() toRule} using a {@code default} method:
+ *
+ * <blockquote>
+ * <code>
+ * &#x40;Override default CsvProjectTableScanRule toRule() {<br>
+ * &nbsp;&nbsp;return new CsvProjectTableScanRule(this);<br>
+ * }
+ * </code>
+ * </blockquote>
+ *
+ * <p>3. For each configuration property, create a pair of methods in your
+ * {@code Config} interface. For example, for a property {@code foo} of type
+ * {@code int}, create methods {@code foo} and {@code withFoo}:
+ *
+ * <blockquote><pre><code>
+ * &#x2f;** Returns foo. *&#x2f;
+ * &#x40;ImmutableBeans.Property
+ * int foo();
+ *
+ * &#x2f;** Sets {&#x40;link #foo}. *&#x2f;
+ * Config withFoo(int x);
+ * </code></pre></blockquote>
+ *
+ * <p>4. In your {@code Config} interface, create a {@code DEFAULT} constant
+ * that represents the most typical configuration of your rule. For example,
+ * {@code CsvProjectTableScanRule.Config} has the following:
+ *
+ * <blockquote><pre><code>
+ * Config DEFAULT = EMPTY
+ *     .withOperandSupplier(b0 -&gt;
+ *         b0.operand(LogicalProject.class).oneInput(b1 -&gt;
+ *             b1.operand(CsvTableScan.class).noInputs()))
+ *      .as(Config.class);
+ * </code></pre></blockquote>
+ *
+ * <p>5. Do not create an {@code INSTANCE} constant inside your rule.
+ * Instead, create a named instance of your rule, with default configuration,
+ * in a holder class. The holder class must not be a sub-class of
+ * {@code RelOptRule} (otherwise cyclic class-loading issues may arise).
+ * Generally it will be called <code><i>Xxx</i>Rules</code>, for example
+ * {@code CsvRules}. The rule instance is named after your rule, and is based
+ * on the default config ({@code Config.DEFAULT}, or {@code DEFAULT_CONFIG} for
+ * converter rules):
+ *
+ * <blockquote><pre><code>
+ * &#x2f;** Rule that matches a {&#x40;code Project} on a
+ *  * {&#x40;code CsvTableScan} and pushes down projects if possible. *&#x2f;
+ * public static final CsvProjectTableScanRule PROJECT_SCAN =
+ *     CsvProjectTableScanRule.Config.DEFAULT.toRule();
+ * </code></pre></blockquote>
+ *
+ * @param <C> Configuration type
+ */
+public abstract class RelRule<C extends RelRule.Config> extends RelOptRule {
+  public final C config;
+
+  /** Creates a RelRule. */
+  public RelRule(C config) {
+    super(OperandBuilderImpl.operand(config.operandSupplier()),
+        config.relBuilderFactory(), config.description());
+    this.config = config;
+  }
+
+  /** Rule configuration. */
+  public interface Config {
+    /** Empty configuration. */
+    RelRule.Config EMPTY = ImmutableBeans.create(Config.class)
+        .withRelBuilderFactory(RelFactories.LOGICAL_BUILDER)
+        .withOperandSupplier(b -> {
+          throw new IllegalArgumentException("Rules must have at least one "
+              + "operand. Call Config.withOperandSupplier to specify them.");
+        });
+
+    /** Creates a rule that uses this configuration. Sub-class must override. */
+    RelOptRule toRule();
+
+    /** Casts this configuration to another type, usually a sub-class. */
+    default <T> T as(Class<T> class_) {
+      return ImmutableBeans.copy(class_, this);
+    }
+
+    /** The factory that is used to create a
+     * {@link org.apache.calcite.tools.RelBuilder} during rule invocations. */
+    @ImmutableBeans.Property
+    RelBuilderFactory relBuilderFactory();
+
+    /** Sets {@link #relBuilderFactory()}. */
+    Config withRelBuilderFactory(RelBuilderFactory factory);
+
+    /** Description of the rule instance. */
+    @ImmutableBeans.Property
+    String description();
+
+    /** Sets {@link #description()}. */
+    Config withDescription(String description);
+
+    /** Creates the operands for the rule instance. */
+    @ImmutableBeans.Property
+    OperandTransform operandSupplier();
+
+    /** Sets {@link #operandSupplier()}. */
+    Config withOperandSupplier(OperandTransform transform);
+  }
+
+  /** Function that creates an operand.
+   *
+   * @see Config#withOperandSupplier(OperandTransform) */
+  @FunctionalInterface
+  public interface OperandTransform extends Function<OperandBuilder, Done> {
+  }
+
+  /** Callback to create an operand.
+   *
+   * @see OperandTransform */
+  public interface OperandBuilder {
+    /** Starts building an operand by specifying its class.
+     * Call further methods on the returned {@link OperandDetailBuilder} to
+     * complete the operand. */
+    <R extends RelNode> OperandDetailBuilder<R> operand(Class<R> relClass);
+
+    /** Supplies an operand that has been built manually. */
+    Done exactly(RelOptRuleOperand operand);
+  }
+
+  /** Indicates that an operand is complete.
+   *
+   * @see OperandTransform */
+  public interface Done {
+  }
+
+  /** Add details about an operand, such as its inputs.
+   *
+   * @param <R> Type of relational expression */
+  public interface OperandDetailBuilder<R extends RelNode> {
+    /** Sets a trait of this operand. */
+    OperandDetailBuilder<R> trait(@Nonnull RelTrait trait);
+
+    /** Sets the predicate of this operand. */
+    OperandDetailBuilder<R> predicate(Predicate<? super R> predicate);
+
+    /** Indicates that this operand has a single input. */
+    Done oneInput(OperandTransform transform);
+
+    /** Indicates that this operand has several inputs. */
+    Done inputs(OperandTransform... transforms);
+
+    /** Indicates that this operand has several inputs, unordered. */
+    Done unorderedInputs(OperandTransform... transforms);
+
+    /** Indicates that this operand takes any number or type of inputs. */
+    Done anyInputs();
+
+    /** Indicates that this operand takes no inputs. */
+    Done noInputs();
+
+    /** Indicates that this operand converts a relational expression to
+     * another trait. */
+    Done convert(RelTrait in);
+  }
+
+  /** Implementation of {@link OperandBuilder}. */
+  private static class OperandBuilderImpl implements OperandBuilder {
+    final List<RelOptRuleOperand> operands = new ArrayList<>();
+
+    static RelOptRuleOperand operand(OperandTransform transform) {
+      final OperandBuilderImpl b = new OperandBuilderImpl();
+      final Done done = transform.apply(b);
+      Objects.requireNonNull(done);
+      if (b.operands.size() != 1) {
+        throw new IllegalArgumentException("operand supplier must call one of "
+            + "the following methods: operand or exactly");
+      }
+      return b.operands.get(0);
+    }
+
+    public <R extends RelNode> OperandDetailBuilder<R> operand(Class<R> relClass) {
+      return new OperandDetailBuilderImpl<>(this, relClass);
+    }
+
+    public Done exactly(RelOptRuleOperand operand) {
+      operands.add(operand);
+      return DoneImpl.INSTANCE;
+    }
+  }
+
+  /** Implementation of {@link OperandDetailBuilder}.
+   *
+   * @param <R> Type of relational expression */
+  private static class OperandDetailBuilderImpl<R extends RelNode>
+      implements OperandDetailBuilder<R> {
+    private final OperandBuilderImpl parent;
+    private final Class<R> relClass;
+    final OperandBuilderImpl inputBuilder = new OperandBuilderImpl();
+    private RelTrait trait;
+    private Predicate<? super R> predicate = r -> true;
+
+    OperandDetailBuilderImpl(OperandBuilderImpl parent, Class<R> relClass) {
+      this.parent = Objects.requireNonNull(parent);
+      this.relClass = Objects.requireNonNull(relClass);
+    }
+
+    public OperandDetailBuilderImpl<R> trait(@Nonnull RelTrait trait) {
+      this.trait = Objects.requireNonNull(trait);
+      return this;
+    }
+
+    public OperandDetailBuilderImpl<R> predicate(Predicate<? super R> predicate) {
+      this.predicate = predicate;
+      return this;
+    }
+
+    /** Indicates that there are no more inputs. */
+    Done done(RelOptRuleOperandChildPolicy childPolicy) {
+      parent.operands.add(
+          new RelOptRuleOperand(relClass, trait, predicate, childPolicy,
+              ImmutableList.copyOf(inputBuilder.operands)));
+      return DoneImpl.INSTANCE;
+    }
+
+    public Done convert(RelTrait in) {
+      parent.operands.add(
+          new ConverterRelOptRuleOperand(relClass, in, predicate));
+      return DoneImpl.INSTANCE;
+    }
+
+    public Done noInputs() {
+      return done(RelOptRuleOperandChildPolicy.LEAF);
+    }
+
+    public Done anyInputs() {
+      return done(RelOptRuleOperandChildPolicy.ANY);
+    }
+
+    public Done oneInput(OperandTransform transform) {
+      final Done done = transform.apply(inputBuilder);
+      Objects.requireNonNull(done);
+      return done(RelOptRuleOperandChildPolicy.SOME);
+    }
+
+    public Done inputs(OperandTransform... transforms) {
+      for (OperandTransform transform : transforms) {
+        final Done done = transform.apply(inputBuilder);
+        Objects.requireNonNull(done);
+      }
+      return done(RelOptRuleOperandChildPolicy.SOME);
+    }
+
+    public Done unorderedInputs(OperandTransform... transforms) {
+      for (OperandTransform transform : transforms) {
+        final Done done = transform.apply(inputBuilder);
+        Objects.requireNonNull(done);
+      }
+      return done(RelOptRuleOperandChildPolicy.UNORDERED);
+    }
+  }
+
+  /** Singleton instance of {@link Done}. */
+  private enum DoneImpl implements Done {
+    INSTANCE
+  }
+
+  /** Callback interface that helps you avoid creating sub-classes of
+   * {@link RelRule} that differ only in implementations of
+   * {@link #onMatch(RelOptRuleCall)} method.
+   *
+   * @param <R> Rule type */
+  public interface MatchHandler<R extends RelOptRule>
+      extends BiConsumer<R, RelOptRuleCall> {
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java b/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
index 97cf205..77a1155 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepPlanner.java
@@ -300,7 +300,8 @@ public class HepPlanner extends AbstractRelOptPlanner {
         if (!instruction.guaranteed) {
           // Add a TraitMatchingRule to work bottom-up
           instruction.ruleSet.add(
-              new TraitMatchingRule(converter, RelFactories.LOGICAL_BUILDER));
+              TraitMatchingRule.config(converter, RelFactories.LOGICAL_BUILDER)
+                  .toRule());
         }
       }
     }
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepProgramBuilder.java b/core/src/main/java/org/apache/calcite/plan/hep/HepProgramBuilder.java
index f3ed082..f1bd874 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepProgramBuilder.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepProgramBuilder.java
@@ -23,6 +23,7 @@ import org.apache.calcite.plan.RelOptRule;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * HepProgramBuilder creates instances of {@link HepProgram}.
@@ -111,7 +112,7 @@ public class HepProgramBuilder {
   public HepProgramBuilder addRuleInstance(RelOptRule rule) {
     HepInstruction.RuleInstance instruction =
         new HepInstruction.RuleInstance();
-    instruction.rule = rule;
+    instruction.rule = Objects.requireNonNull(rule);
     instructions.add(instruction);
     return this;
   }
diff --git a/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java b/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java
index 629c791..e02d595 100644
--- a/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java
+++ b/core/src/main/java/org/apache/calcite/plan/hep/HepRuleCall.java
@@ -52,8 +52,7 @@ public class HepRuleCall extends RelOptRuleCall {
 
   //~ Methods ----------------------------------------------------------------
 
-  // implement RelOptRuleCall
-  public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
+  @Override public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
       RelHintsPropagator handler) {
     final RelNode rel0 = rels[0];
     RelOptUtil.verifyTypeEquivalence(rel0, rel, rel0);
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/AbstractConverter.java b/core/src/main/java/org/apache/calcite/plan/volcano/AbstractConverter.java
index f1ef0b9..40a50ac 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/AbstractConverter.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/AbstractConverter.java
@@ -19,15 +19,14 @@ package org.apache.calcite.plan.volcano;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptCost;
 import org.apache.calcite.plan.RelOptPlanner;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTrait;
 import org.apache.calcite.plan.RelTraitDef;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.RelWriter;
 import org.apache.calcite.rel.convert.ConverterImpl;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.tools.RelBuilderFactory;
 
@@ -88,36 +87,39 @@ public class AbstractConverter extends ConverterImpl {
   //~ Inner Classes ----------------------------------------------------------
 
   /**
-   * Rule which converts an {@link AbstractConverter} into a chain of
+   * Rule that converts an {@link AbstractConverter} into a chain of
    * converters from the source relation to the target traits.
    *
    * <p>The chain produced is minimal: we have previously built the transitive
-   * closure of the graph of conversions, so we choose the shortest chain.</p>
+   * closure of the graph of conversions, so we choose the shortest chain.
    *
    * <p>Unlike the {@link AbstractConverter} they are replacing, these
    * converters are guaranteed to be able to convert any relation of their
    * calling convention. Furthermore, because they introduce subsets of other
    * calling conventions along the way, these subsets may spawn more efficient
-   * conversions which are not generally applicable.</p>
+   * conversions which are not generally applicable.
    *
    * <p>AbstractConverters can be messy, so they restrain themselves: they
    * don't fire if the target subset already has an implementation (with less
-   * than infinite cost).</p>
+   * than infinite cost).
    */
-  public static class ExpandConversionRule extends RelOptRule {
+  public static class ExpandConversionRule
+      extends RelRule<ExpandConversionRule.Config> {
     public static final ExpandConversionRule INSTANCE =
-        new ExpandConversionRule(RelFactories.LOGICAL_BUILDER);
+        Config.DEFAULT.toRule();
 
-    /**
-     * Creates an ExpandConversionRule.
-     *
-     * @param relBuilderFactory Builder for relational expressions
-     */
+    /** Creates an ExpandConversionRule. */
+    protected ExpandConversionRule(Config config) {
+      super(config);
+    }
+
+    @Deprecated // to be removed before 2.0
     public ExpandConversionRule(RelBuilderFactory relBuilderFactory) {
-      super(operand(AbstractConverter.class, any()), relBuilderFactory, null);
+      this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+          .as(Config.class));
     }
 
-    public void onMatch(RelOptRuleCall call) {
+    @Override public void onMatch(RelOptRuleCall call) {
       final VolcanoPlanner planner = (VolcanoPlanner) call.getPlanner();
       AbstractConverter converter = call.rel(0);
       final RelNode child = converter.getInput();
@@ -129,5 +131,17 @@ public class AbstractConverter extends ConverterImpl {
         call.transformTo(converted);
       }
     }
+
+    /** Rule configuration. */
+    public interface Config extends RelRule.Config {
+      Config DEFAULT = EMPTY
+          .withOperandSupplier(b ->
+              b.operand(AbstractConverter.class).anyInputs())
+          .as(Config.class);
+
+      @Override default ExpandConversionRule toRule() {
+        return new ExpandConversionRule(this);
+      }
+    }
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java
index d4f3f03..b5e61e3 100644
--- a/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java
+++ b/core/src/main/java/org/apache/calcite/plan/volcano/VolcanoRuleCall.java
@@ -91,8 +91,7 @@ public class VolcanoRuleCall extends RelOptRuleCall {
 
   //~ Methods ----------------------------------------------------------------
 
-  // implement RelOptRuleCall
-  public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
+  @Override public void transformTo(RelNode rel, Map<RelNode, RelNode> equiv,
       RelHintsPropagator handler) {
     if (rel instanceof PhysicalNode
         && rule instanceof TransformationRule) {
diff --git a/core/src/main/java/org/apache/calcite/rel/convert/ConverterRule.java b/core/src/main/java/org/apache/calcite/rel/convert/ConverterRule.java
index 2da27fd..0d78259 100644
--- a/core/src/main/java/org/apache/calcite/rel/convert/ConverterRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/convert/ConverterRule.java
@@ -19,28 +19,47 @@ package org.apache.calcite.rel.convert;
 import org.apache.calcite.plan.Convention;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTrait;
 import org.apache.calcite.plan.RelTraitDef;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
 
 import java.util.Locale;
 import java.util.Objects;
+import java.util.function.Function;
 import java.util.function.Predicate;
 
 /**
  * Abstract base class for a rule which converts from one calling convention to
  * another without changing semantics.
  */
-public abstract class ConverterRule extends RelOptRule {
+public abstract class ConverterRule
+    extends RelRule<ConverterRule.Config> {
   //~ Instance fields --------------------------------------------------------
 
   private final RelTrait inTrait;
   private final RelTrait outTrait;
+  protected final Convention out;
 
   //~ Constructors -----------------------------------------------------------
 
+  /** Creates a <code>ConverterRule</code>. */
+  protected ConverterRule(Config config) {
+    super(config);
+    this.inTrait = Objects.requireNonNull(config.inTrait());
+    this.outTrait = Objects.requireNonNull(config.outTrait());
+
+    // Source and target traits must have same type
+    assert inTrait.getTraitDef() == outTrait.getTraitDef();
+
+    // Most sub-classes are concerned with converting one convention to
+    // another, and for them, the "out" field is a convenient short-cut.
+    this.out = outTrait instanceof Convention ? (Convention) outTrait
+        : null;
+  }
+
   /**
    * Creates a <code>ConverterRule</code>.
    *
@@ -48,11 +67,14 @@ public abstract class ConverterRule extends RelOptRule {
    * @param in          Trait of relational expression to consider converting
    * @param out         Trait which is converted to
    * @param descriptionPrefix Description prefix of rule
+   *
+   * @deprecated Use {@link #ConverterRule(Config)}
    */
+  @Deprecated // to be removed before 2.0
   public ConverterRule(Class<? extends RelNode> clazz, RelTrait in,
       RelTrait out, String descriptionPrefix) {
-    this(clazz, (Predicate<RelNode>) r -> true, in, out,
-        RelFactories.LOGICAL_BUILDER, descriptionPrefix);
+    this(Config.INSTANCE
+        .withConversion(clazz, in, out, descriptionPrefix));
   }
 
   @SuppressWarnings("Guava")
@@ -60,7 +82,9 @@ public abstract class ConverterRule extends RelOptRule {
   public <R extends RelNode> ConverterRule(Class<R> clazz,
       com.google.common.base.Predicate<? super R> predicate,
       RelTrait in, RelTrait out, String descriptionPrefix) {
-    this(clazz, predicate, in, out, RelFactories.LOGICAL_BUILDER, descriptionPrefix);
+    this(Config.INSTANCE
+        .withConversion(clazz, (Predicate<? super R>) predicate::apply,
+            in, out, descriptionPrefix));
   }
 
   /**
@@ -72,18 +96,17 @@ public abstract class ConverterRule extends RelOptRule {
    * @param out         Trait which is converted to
    * @param relBuilderFactory Builder for relational expressions
    * @param descriptionPrefix Description prefix of rule
+   *
+   * @deprecated Use {@link #ConverterRule(Config)}
    */
+  @Deprecated // to be removed before 2.0
   public <R extends RelNode> ConverterRule(Class<R> clazz,
       Predicate<? super R> predicate, RelTrait in, RelTrait out,
       RelBuilderFactory relBuilderFactory, String descriptionPrefix) {
-    super(convertOperand(clazz, predicate, in),
-        relBuilderFactory,
-        createDescription(descriptionPrefix, in, out));
-    this.inTrait = Objects.requireNonNull(in);
-    this.outTrait = Objects.requireNonNull(out);
-
-    // Source and target traits must have same type
-    assert in.getTraitDef() == out.getTraitDef();
+    this(Config.EMPTY
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withConversion(clazz, predicate, in, out, descriptionPrefix));
   }
 
   @SuppressWarnings("Guava")
@@ -97,7 +120,7 @@ public abstract class ConverterRule extends RelOptRule {
 
   //~ Methods ----------------------------------------------------------------
 
-  public Convention getOutConvention() {
+  @Override public Convention getOutConvention() {
     return (Convention) outTrait;
   }
 
@@ -138,7 +161,7 @@ public abstract class ConverterRule extends RelOptRule {
     return false;
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     RelNode rel = call.rel(0);
     if (rel.getTraitSet().contains(inTrait)) {
       final RelNode converted = convert(rel);
@@ -150,4 +173,51 @@ public abstract class ConverterRule extends RelOptRule {
 
   //~ Inner Classes ----------------------------------------------------------
 
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config INSTANCE = EMPTY.as(Config.class);
+
+    @ImmutableBeans.Property
+    RelTrait inTrait();
+
+    /** Sets {@link #inTrait}. */
+    Config withInTrait(RelTrait trait);
+
+    @ImmutableBeans.Property
+    RelTrait outTrait();
+
+    /** Sets {@link #outTrait}. */
+    Config withOutTrait(RelTrait trait);
+
+    @ImmutableBeans.Property
+    Function<Config, ConverterRule> ruleFactory();
+
+    /** Sets {@link #outTrait}. */
+    Config withRuleFactory(Function<Config, ConverterRule> factory);
+
+    default <R extends RelNode> Config withConversion(Class<R> clazz,
+        Predicate<? super R> predicate, RelTrait in, RelTrait out,
+        String descriptionPrefix) {
+      return withInTrait(in)
+          .withOutTrait(out)
+          .withOperandSupplier(b ->
+              b.operand(clazz).predicate(predicate).convert(in))
+          .withDescription(createDescription(descriptionPrefix, in, out))
+          .as(Config.class);
+    }
+
+    default Config withConversion(Class<? extends RelNode> clazz, RelTrait in,
+        RelTrait out, String descriptionPrefix) {
+      return withConversion(clazz, r -> true, in, out, descriptionPrefix);
+    }
+
+    @Override default RelOptRule toRule() {
+      return toRule(ConverterRule.class);
+    }
+
+    default <R extends ConverterRule> R toRule(Class<R> ruleClass) {
+      return ruleClass.cast(ruleFactory().apply(this));
+    }
+  }
+
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/convert/TraitMatchingRule.java b/core/src/main/java/org/apache/calcite/rel/convert/TraitMatchingRule.java
index c857693..affb186 100644
--- a/core/src/main/java/org/apache/calcite/rel/convert/TraitMatchingRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/convert/TraitMatchingRule.java
@@ -17,12 +17,14 @@
 package org.apache.calcite.rel.convert;
 
 import org.apache.calcite.plan.Convention;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptRuleOperand;
 import org.apache.calcite.plan.RelOptRuleOperandChildPolicy;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
 
 /**
  * TraitMatchingRule adapts a converter rule, restricting it to fire only when
@@ -30,48 +32,71 @@ import org.apache.calcite.tools.RelBuilderFactory;
  * {@link org.apache.calcite.plan.hep.HepPlanner} in cases where alternate
  * implementations are available and it is desirable to minimize converters.
  */
-public class TraitMatchingRule extends RelOptRule {
-  //~ Instance fields --------------------------------------------------------
-
-  private final ConverterRule converter;
+public class TraitMatchingRule extends RelRule<TraitMatchingRule.Config> {
+  /**
+   * Creates a configuration for a TraitMatchingRule.
+   *
+   * @param converterRule     Rule to be restricted; rule must take a single
+   *                          operand expecting a single input
+   * @param relBuilderFactory Builder for relational expressions
+   */
+  public static Config config(ConverterRule converterRule,
+      RelBuilderFactory relBuilderFactory) {
+    final RelOptRuleOperand operand = converterRule.getOperand();
+    assert operand.childPolicy == RelOptRuleOperandChildPolicy.ANY;
+    return Config.EMPTY.withRelBuilderFactory(relBuilderFactory)
+        .withDescription("TraitMatchingRule: " + converterRule)
+        .withOperandSupplier(b0 ->
+            b0.operand(operand.getMatchedClass()).oneInput(b1 ->
+                b1.operand(RelNode.class).anyInputs()))
+        .as(Config.class)
+        .withConverterRule(converterRule);
+  }
 
   //~ Constructors -----------------------------------------------------------
 
+  /** Creates a TraitMatchingRule. */
+  protected TraitMatchingRule(Config config) {
+    super(config);
+  }
+
   @Deprecated // to be removed before 2.0
   public TraitMatchingRule(ConverterRule converterRule) {
-    this(converterRule, RelFactories.LOGICAL_BUILDER);
+    this(config(converterRule, RelFactories.LOGICAL_BUILDER));
   }
 
-  /**
-   * Creates a TraitMatchingRule.
-   *
-   * @param converterRule     Rule to be restricted; rule must take a single
-   *                          operand expecting a single input
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public TraitMatchingRule(ConverterRule converterRule,
       RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(
-            converterRule.getOperand().getMatchedClass(),
-            operand(RelNode.class, any())),
-        relBuilderFactory,
-        "TraitMatchingRule: " + converterRule);
-    assert converterRule.getOperand().childPolicy
-        == RelOptRuleOperandChildPolicy.ANY;
-    this.converter = converterRule;
+    this(config(converterRule, relBuilderFactory));
   }
 
   //~ Methods ----------------------------------------------------------------
 
   @Override public Convention getOutConvention() {
-    return converter.getOutConvention();
+    return config.converterRule().getOutConvention();
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     RelNode input = call.rel(1);
-    if (input.getTraitSet().contains(converter.getOutTrait())) {
-      converter.onMatch(call);
+    final ConverterRule converterRule = config.converterRule();
+    if (input.getTraitSet().contains(converterRule.getOutTrait())) {
+      converterRule.onMatch(call);
     }
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    @Override default TraitMatchingRule toRule() {
+      return new TraitMatchingRule(this);
+    }
+
+    /** Returns the rule to be restricted; rule must take a single
+     * operand expecting a single input. */
+    @ImmutableBeans.Property
+    ConverterRule converterRule();
+
+    /** Sets {@link #converterRule()}. */
+    Config withConverterRule(ConverterRule converterRule);
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Calc.java b/core/src/main/java/org/apache/calcite/rel/core/Calc.java
index 8b2b738..4f1f208 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Calc.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Calc.java
@@ -33,6 +33,7 @@ import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexLocalRef;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexOver;
 import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.rex.RexProgramBuilder;
 import org.apache.calcite.rex.RexShuttle;
@@ -130,6 +131,11 @@ public abstract class Calc extends SingleRel implements Hintable {
     return copy(traitSet, child, program);
   }
 
+  /** Returns whether this Calc contains any windowed-aggregate functions. */
+  public final boolean containsOver() {
+    return RexOver.containsOver(program);
+  }
+
   public boolean isValid(Litmus litmus, Context context) {
     if (!RelOptUtil.equal(
         "program's input type",
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Filter.java b/core/src/main/java/org/apache/calcite/rel/core/Filter.java
index 7394ddf..91c435e 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Filter.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Filter.java
@@ -29,6 +29,7 @@ import org.apache.calcite.rel.metadata.RelMdUtil;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rex.RexChecker;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexOver;
 import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.rex.RexUtil;
@@ -114,6 +115,11 @@ public abstract class Filter extends SingleRel {
     return condition;
   }
 
+  /** Returns whether this Filter contains any windowed-aggregate functions. */
+  public final boolean containsOver() {
+    return RexOver.containsOver(condition);
+  }
+
   @Override public boolean isValid(Litmus litmus, Context context) {
     if (RexUtil.isNullabilityCast(getCluster().getTypeFactory(), condition)) {
       return litmus.fail("Cast for just nullability not allowed");
diff --git a/core/src/main/java/org/apache/calcite/rel/core/Project.java b/core/src/main/java/org/apache/calcite/rel/core/Project.java
index c24b7af..92c7d4f 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/Project.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/Project.java
@@ -33,6 +33,7 @@ import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rex.RexChecker;
 import org.apache.calcite.rex.RexInputRef;
 import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.rex.RexOver;
 import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.sql.SqlExplainLevel;
@@ -199,6 +200,11 @@ public abstract class Project extends SingleRel implements Hintable {
     return 1;
   }
 
+  /** Returns whether this Project contains any windowed-aggregate functions. */
+  public final boolean containsOver() {
+    return RexOver.containsOver(getProjects(), null);
+  }
+
   public boolean isValid(Litmus litmus, Context context) {
     if (!super.isValid(litmus, context)) {
       return litmus.fail(null);
diff --git a/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java b/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java
index 4d838e0..7b49647 100644
--- a/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java
+++ b/core/src/main/java/org/apache/calcite/rel/core/RelFactories.java
@@ -205,7 +205,7 @@ public class RelFactories {
    * of the appropriate type for a rule's calling convention.
    */
   public interface ExchangeFactory {
-    /** Creates a Exchange. */
+    /** Creates an Exchange. */
     RelNode createExchange(RelNode input, RelDistribution distribution);
   }
 
diff --git a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
index c02e8b0..8fe1fad 100644
--- a/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
+++ b/core/src/main/java/org/apache/calcite/rel/rel2sql/SqlImplementor.java
@@ -1537,7 +1537,7 @@ public abstract class SqlImplementor {
       }
 
       if (rel instanceof Project
-          && RexOver.containsOver(((Project) rel).getProjects(), null)
+          && ((Project) rel).containsOver()
           && maxClause == Clause.SELECT) {
         // Cannot merge a Project that contains windowed functions onto an
         // underlying Project
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AbstractJoinExtractFilterRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AbstractJoinExtractFilterRule.java
index 4582f63..7f40d28 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AbstractJoinExtractFilterRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AbstractJoinExtractFilterRule.java
@@ -16,9 +16,9 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinRelType;
@@ -38,15 +38,25 @@ import org.apache.calcite.tools.RelBuilderFactory;
  * <p>The constructor is parameterized to allow any sub-class of
  * {@link org.apache.calcite.rel.core.Join}.</p>
  */
-public abstract class AbstractJoinExtractFilterRule extends RelOptRule
+public abstract class AbstractJoinExtractFilterRule
+    extends RelRule<AbstractJoinExtractFilterRule.Config>
     implements TransformationRule {
   /** Creates an AbstractJoinExtractFilterRule. */
+  protected AbstractJoinExtractFilterRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   protected AbstractJoinExtractFilterRule(RelOptRuleOperand operand,
       RelBuilderFactory relBuilderFactory, String description) {
-    super(operand, relBuilderFactory, description);
+    this(Config.EMPTY
+        .withOperandSupplier(b -> b.exactly(operand))
+        .withRelBuilderFactory(relBuilderFactory)
+        .withDescription(description)
+        .as(Config.class));
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Join join = call.rel(0);
 
     if (join.getJoinType() != JoinRelType.INNER) {
@@ -81,4 +91,8 @@ public abstract class AbstractJoinExtractFilterRule extends RelOptRule
 
     call.transformTo(builder.build());
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AbstractMaterializedViewRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AbstractMaterializedViewRule.java
deleted file mode 100644
index c70ea16..0000000
--- a/core/src/main/java/org/apache/calcite/rel/rules/AbstractMaterializedViewRule.java
+++ /dev/null
@@ -1,48 +0,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.
- */
-package org.apache.calcite.rel.rules;
-
-import org.apache.calcite.plan.RelOptRule;
-import org.apache.calcite.rel.rules.materialize.MaterializedViewRules;
-
-/** For backwards compatibility.
- *
- * @deprecated Use equivalent fields in {@link MaterializedViewRules}.
- */
-@Deprecated // to be removed before 1.25
-public abstract class AbstractMaterializedViewRule {
-  private AbstractMaterializedViewRule() {
-  }
-
-  public static final RelOptRule INSTANCE_PROJECT_AGGREGATE =
-      MaterializedViewRules.PROJECT_AGGREGATE;
-
-  public static final RelOptRule INSTANCE_AGGREGATE =
-      MaterializedViewRules.AGGREGATE;
-
-  public static final RelOptRule INSTANCE_FILTER =
-      MaterializedViewRules.FILTER;
-
-  public static final RelOptRule INSTANCE_JOIN =
-      MaterializedViewRules.JOIN;
-
-  public static final RelOptRule INSTANCE_PROJECT_FILTER =
-      MaterializedViewRules.PROJECT_FILTER;
-
-  public static final RelOptRule INSTANCE_PROJECT_JOIN =
-      MaterializedViewRules.PROJECT_JOIN;
-}
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateCaseToFilterRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateCaseToFilterRule.java
index 60fe9dc..2c52c00 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateCaseToFilterRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateCaseToFilterRule.java
@@ -17,8 +17,8 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
@@ -60,19 +60,24 @@ import javax.annotation.Nullable;
  *   <code>SELECT SUM(salary) FILTER (WHERE gender = 'F')<br>
  *   FROM Emp</code>
  * </blockquote>
+ *
+ * @see CoreRules#AGGREGATE_CASE_TO_FILTER
  */
-public class AggregateCaseToFilterRule extends RelOptRule
+public class AggregateCaseToFilterRule
+    extends RelRule<AggregateCaseToFilterRule.Config>
     implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_CASE_TO_FILTER}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateCaseToFilterRule INSTANCE =
-      CoreRules.AGGREGATE_CASE_TO_FILTER;
 
   /** Creates an AggregateCaseToFilterRule. */
+  protected AggregateCaseToFilterRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   protected AggregateCaseToFilterRule(RelBuilderFactory relBuilderFactory,
       String description) {
-    super(operand(Aggregate.class, operand(Project.class, any())),
-        relBuilderFactory, description);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .withDescription(description)
+        .as(Config.class));
   }
 
   @Override public boolean matches(final RelOptRuleCall call) {
@@ -266,4 +271,17 @@ public class AggregateCaseToFilterRule extends RelOptRule
     return rexNode instanceof RexLiteral
         && SqlTypeName.INT_TYPES.contains(rexNode.getType().getSqlTypeName());
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b0 ->
+            b0.operand(Aggregate.class).oneInput(b1 ->
+                b1.operand(Project.class).anyInputs()))
+        .as(Config.class);
+
+    @Override default AggregateCaseToFilterRule toRule() {
+      return new AggregateCaseToFilterRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java
index 06ad022..b152c83 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateExpandDistinctAggregatesRule.java
@@ -17,8 +17,8 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.Contexts;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.Aggregate.Group;
@@ -36,6 +36,7 @@ import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.fun.SqlSumEmptyIsZeroAggFunction;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.calcite.util.Optionality;
@@ -76,32 +77,29 @@ import java.util.stream.Stream;
  * (e.g. {@code COUNT(DISTINCT x), COUNT(DISTINCT y)})
  * the rule creates separate {@code Aggregate}s and combines using a
  * {@link org.apache.calcite.rel.core.Join}.
+ *
+ * @see CoreRules#AGGREGATE_EXPAND_DISTINCT_AGGREGATES
+ * @see CoreRules#AGGREGATE_EXPAND_DISTINCT_AGGREGATES_TO_JOIN
  */
-public final class AggregateExpandDistinctAggregatesRule extends RelOptRule
+public final class AggregateExpandDistinctAggregatesRule
+    extends RelRule<AggregateExpandDistinctAggregatesRule.Config>
     implements TransformationRule {
-  //~ Static fields/initializers ---------------------------------------------
-
-  /** @deprecated Use {@link CoreRules#AGGREGATE_EXPAND_DISTINCT_AGGREGATES}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateExpandDistinctAggregatesRule INSTANCE =
-      CoreRules.AGGREGATE_EXPAND_DISTINCT_AGGREGATES;
-
-  /** @deprecated Use
-   * {@link CoreRules#AGGREGATE_EXPAND_DISTINCT_AGGREGATES_TO_JOIN}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateExpandDistinctAggregatesRule JOIN =
-      CoreRules.AGGREGATE_EXPAND_DISTINCT_AGGREGATES_TO_JOIN;
-
-  public final boolean useGroupingSets;
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates an AggregateExpandDistinctAggregatesRule. */
+  protected AggregateExpandDistinctAggregatesRule(Config config) {
+    super(config);
+  }
 
+  @Deprecated // to be removed before 2.0
   public AggregateExpandDistinctAggregatesRule(
       Class<? extends Aggregate> clazz,
       boolean useGroupingSets,
       RelBuilderFactory relBuilderFactory) {
-    super(operand(clazz, any()), relBuilderFactory, null);
-    this.useGroupingSets = useGroupingSets;
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b ->
+            b.operand(clazz).anyInputs())
+        .as(Config.class)
+        .withUsingGroupingSets(useGroupingSets));
   }
 
   @Deprecated // to be removed before 2.0
@@ -121,7 +119,7 @@ public final class AggregateExpandDistinctAggregatesRule extends RelOptRule
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Aggregate aggregate = call.rel(0);
     if (!aggregate.containsDistinctCall()) {
       return;
@@ -193,7 +191,7 @@ public final class AggregateExpandDistinctAggregatesRule extends RelOptRule
       return;
     }
 
-    if (useGroupingSets) {
+    if (((Config) config).isUsingGroupingSets()) {
       rewriteUsingGroupingSets(call, aggregate);
       return;
     }
@@ -899,4 +897,26 @@ public final class AggregateExpandDistinctAggregatesRule extends RelOptRule
             ImmutableBitSet.range(projects.size()), null, ImmutableList.of()));
     return relBuilder;
   }
+
+       /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b ->
+            b.operand(LogicalAggregate.class).anyInputs())
+        .as(Config.class);
+
+    Config JOIN = DEFAULT.withUsingGroupingSets(false);
+
+    @Override default AggregateExpandDistinctAggregatesRule toRule() {
+      return new AggregateExpandDistinctAggregatesRule(this);
+    }
+
+    /** Whether to use GROUPING SETS, default true. */
+    @ImmutableBeans.Property
+    @ImmutableBeans.BooleanDefault(true)
+    boolean isUsingGroupingSets();
+
+    /** Sets {@link #isUsingGroupingSets()}. */
+    Config withUsingGroupingSets(boolean usingGroupingSets);
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateExtractProjectRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateExtractProjectRule.java
index 1a81683..0452e73 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateExtractProjectRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateExtractProjectRule.java
@@ -16,13 +16,14 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.logical.LogicalTableScan;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
@@ -48,32 +49,38 @@ import java.util.List;
  * <p>To prevent cycles, this rule will not extract a {@code Project} if the
  * {@code Aggregate}s input is already a {@code Project}.
  */
-public class AggregateExtractProjectRule extends RelOptRule
+public class AggregateExtractProjectRule
+    extends RelRule<AggregateExtractProjectRule.Config>
     implements TransformationRule {
+  public static final AggregateExtractProjectRule SCAN =
+      Config.DEFAULT.toRule();
 
-  /**
-   * Creates an AggregateExtractProjectRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  /** Creates an AggregateExtractProjectRule. */
+  protected AggregateExtractProjectRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateExtractProjectRule(
       Class<? extends Aggregate> aggregateClass,
       Class<? extends RelNode> inputClass,
       RelBuilderFactory relBuilderFactory) {
-    // Predicate prevents matching against an Aggregate whose input
-    // is already a Project. Prevents this rule firing repeatedly.
-    this(
-        operand(aggregateClass,
-            operandJ(inputClass, null, r -> !(r instanceof Project), any())),
-        relBuilderFactory);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(aggregateClass, inputClass));
   }
 
+  @Deprecated // to be removed before 2.0
   public AggregateExtractProjectRule(RelOptRuleOperand operand,
       RelBuilderFactory builderFactory) {
-    super(operand, builderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(builderFactory)
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class));
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Aggregate aggregate = call.rel(0);
     final RelNode input = call.rel(1);
     // Compute which input fields are used.
@@ -130,4 +137,28 @@ public class AggregateExtractProjectRule extends RelOptRule
     relBuilder.aggregate(groupKey, newAggCallList);
     call.transformTo(relBuilder.build());
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .as(Config.class)
+        .withOperandFor(Aggregate.class, LogicalTableScan.class);
+
+    @Override default AggregateExtractProjectRule toRule() {
+      return new AggregateExtractProjectRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends RelNode> inputClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass).oneInput(b1 ->
+              b1.operand(inputClass)
+                  // Predicate prevents matching against an Aggregate whose
+                  // input is already a Project. Prevents this rule firing
+                  // repeatedly.
+                  .predicate(r -> !(r instanceof Project)).anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateFilterTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateFilterTransposeRule.java
index ae0b75a..56974af 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateFilterTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateFilterTransposeRule.java
@@ -16,17 +16,16 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.SubstitutionVisitor;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.Aggregate.Group;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.Filter;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
@@ -54,28 +53,27 @@ import java.util.List;
  * under an aggregate to an existing aggregate table.
  *
  * @see org.apache.calcite.rel.rules.FilterAggregateTransposeRule
+ * @see CoreRules#AGGREGATE_FILTER_TRANSPOSE
  */
-public class AggregateFilterTransposeRule extends RelOptRule
+public class AggregateFilterTransposeRule
+    extends RelRule<AggregateFilterTransposeRule.Config>
     implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_FILTER_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateFilterTransposeRule INSTANCE =
-      CoreRules.AGGREGATE_FILTER_TRANSPOSE;
 
-  AggregateFilterTransposeRule() {
-    this(
-        operand(Aggregate.class,
-            operand(Filter.class, any())),
-        RelFactories.LOGICAL_BUILDER);
+  /** Creates an AggregateFilterTransposeRule. */
+  protected AggregateFilterTransposeRule(Config config) {
+    super(config);
   }
 
-  /** Creates an AggregateFilterTransposeRule. */
+  @Deprecated // to be removed before 2.0
   public AggregateFilterTransposeRule(RelOptRuleOperand operand,
       RelBuilderFactory relBuilderFactory) {
-    super(operand, relBuilderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class));
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Aggregate aggregate = call.rel(0);
     final Filter filter = call.rel(1);
 
@@ -157,4 +155,34 @@ public class AggregateFilterTransposeRule extends RelOptRule
       call.transformTo(topAggregate);
     }
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Aggregate.class, Filter.class);
+
+    @Override default AggregateFilterTransposeRule toRule() {
+      return new AggregateFilterTransposeRule(this);
+    }
+
+    /** Defines an operand tree for the given 2 classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Filter> filterClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass).oneInput(b1 ->
+              b1.operand(filterClass).anyInputs()))
+          .as(Config.class);
+    }
+
+    /** Defines an operand tree for the given 3 classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Filter> filterClass,
+        Class<? extends RelNode> relClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass).oneInput(b1 ->
+              b1.operand(filterClass).oneInput(b2 ->
+                  b2.operand(relClass).anyInputs())))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinJoinRemoveRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinJoinRemoveRule.java
index 6a5735d..45034bc 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinJoinRemoveRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinJoinRemoveRule.java
@@ -16,14 +16,16 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalJoin;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.tools.RelBuilder;
@@ -44,43 +46,43 @@ import java.util.Set;
  * on a {@link org.apache.calcite.rel.core.Join} and removes the left input
  * of the join provided that the left input is also a left join if possible.
  *
- * <p>For instance,</p>
+ * <p>For instance,
  *
  * <blockquote>
- * <pre>select distinct s.product_id, pc.product_id from
- * sales as s
+ * <pre>select distinct s.product_id, pc.product_id
+ * from sales as s
  * left join product as p
- * on s.product_id = p.product_id
+ *   on s.product_id = p.product_id
  * left join product_class pc
- * on s.product_id = pc.product_id</pre></blockquote>
+ *   on s.product_id = pc.product_id</pre></blockquote>
  *
  * <p>becomes
  *
  * <blockquote>
- * <pre>select distinct s.product_id, pc.product_id from
- * sales as s
+ * <pre>select distinct s.product_id, pc.product_id
+ * from sales as s
  * left join product_class pc
- * on s.product_id = pc.product_id</pre></blockquote>
+ *   on s.product_id = pc.product_id</pre></blockquote>
  *
+ * @see CoreRules#AGGREGATE_JOIN_JOIN_REMOVE
  */
-public class AggregateJoinJoinRemoveRule extends RelOptRule
+public class AggregateJoinJoinRemoveRule
+    extends RelRule<AggregateJoinJoinRemoveRule.Config>
     implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_JOIN_JOIN_REMOVE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateJoinJoinRemoveRule INSTANCE =
-      CoreRules.AGGREGATE_JOIN_JOIN_REMOVE;
 
   /** Creates an AggregateJoinJoinRemoveRule. */
+  protected AggregateJoinJoinRemoveRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateJoinJoinRemoveRule(
       Class<? extends Aggregate> aggregateClass,
       Class<? extends Join> joinClass, RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(aggregateClass,
-            operandJ(joinClass, null,
-                join -> join.getJoinType() == JoinRelType.LEFT,
-                operandJ(joinClass, null,
-                    join -> join.getJoinType() == JoinRelType.LEFT, any()))),
-        relBuilderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(aggregateClass, joinClass));
   }
 
   @Override public void onMatch(RelOptRuleCall call) {
@@ -152,4 +154,26 @@ public class AggregateJoinJoinRemoveRule extends RelOptRule
 
     call.transformTo(newAggregate);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .as(Config.class)
+        .withOperandFor(LogicalAggregate.class, LogicalJoin.class);
+
+    @Override default AggregateJoinJoinRemoveRule toRule() {
+      return new AggregateJoinJoinRemoveRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Join> joinClass) {
+      return withOperandSupplier(b0 -> b0.operand(aggregateClass)
+          .oneInput(b1 -> b1.operand(joinClass)
+              .predicate(join -> join.getJoinType() == JoinRelType.LEFT)
+              .inputs(b2 -> b2.operand(joinClass)
+                  .predicate(join -> join.getJoinType() == JoinRelType.LEFT)
+                  .anyInputs()))).as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinRemoveRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinRemoveRule.java
index 032dfdb..41ca66a 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinRemoveRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinRemoveRule.java
@@ -16,14 +16,16 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinRelType;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalJoin;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
 import org.apache.calcite.util.ImmutableBitSet;
@@ -54,23 +56,25 @@ import java.util.Set;
  * <blockquote>
  * <pre>select distinct s.product_id from sales as s</pre></blockquote>
  *
+ * @see CoreRules#AGGREGATE_JOIN_REMOVE
  */
-public class AggregateJoinRemoveRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_JOIN_REMOVE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateJoinRemoveRule INSTANCE =
-      CoreRules.AGGREGATE_JOIN_REMOVE;
+public class AggregateJoinRemoveRule
+    extends RelRule<AggregateJoinRemoveRule.Config>
+    implements TransformationRule {
 
   /** Creates an AggregateJoinRemoveRule. */
+  protected AggregateJoinRemoveRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateJoinRemoveRule(
       Class<? extends Aggregate> aggregateClass,
       Class<? extends Join> joinClass, RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(aggregateClass,
-            operandJ(joinClass, null,
-                join -> join.getJoinType() == JoinRelType.LEFT
-                    || join.getJoinType() == JoinRelType.RIGHT, any())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(aggregateClass, joinClass));
   }
 
   @Override public void onMatch(RelOptRuleCall call) {
@@ -119,4 +123,27 @@ public class AggregateJoinRemoveRule extends RelOptRule implements Transformatio
     }
     call.transformTo(node);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(LogicalAggregate.class, LogicalJoin.class);
+
+    @Override default AggregateJoinRemoveRule toRule() {
+      return new AggregateJoinRemoveRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Join> joinClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass).oneInput(b1 ->
+              b1.operand(joinClass)
+                  .predicate(join ->
+                      join.getJoinType() == JoinRelType.LEFT
+                          || join.getJoinType() == JoinRelType.RIGHT)
+                  .anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinTransposeRule.java
index d5b6062..2e46153 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateJoinTransposeRule.java
@@ -17,15 +17,17 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.linq4j.Ord;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.core.RelFactories;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalJoin;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rex.RexBuilder;
@@ -38,6 +40,7 @@ import org.apache.calcite.sql.SqlSplittableAggFunction;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
 import org.apache.calcite.util.Bug;
+import org.apache.calcite.util.ImmutableBeans;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.Util;
 import org.apache.calcite.util.mapping.Mapping;
@@ -58,29 +61,27 @@ import java.util.TreeMap;
  * Planner rule that pushes an
  * {@link org.apache.calcite.rel.core.Aggregate}
  * past a {@link org.apache.calcite.rel.core.Join}.
+ *
+ * @see CoreRules#AGGREGATE_JOIN_TRANSPOSE
+ * @see CoreRules#AGGREGATE_JOIN_TRANSPOSE_EXTENDED
  */
-public class AggregateJoinTransposeRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_JOIN_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateJoinTransposeRule INSTANCE =
-      CoreRules.AGGREGATE_JOIN_TRANSPOSE;
-
-  /** @deprecated Use {@link CoreRules#AGGREGATE_JOIN_TRANSPOSE_EXTENDED}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateJoinTransposeRule EXTENDED =
-      CoreRules.AGGREGATE_JOIN_TRANSPOSE_EXTENDED;
-
-  private final boolean allowFunctions;
+public class AggregateJoinTransposeRule
+    extends RelRule<AggregateJoinTransposeRule.Config>
+    implements TransformationRule {
 
   /** Creates an AggregateJoinTransposeRule. */
+  protected AggregateJoinTransposeRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateJoinTransposeRule(Class<? extends Aggregate> aggregateClass,
       Class<? extends Join> joinClass, RelBuilderFactory relBuilderFactory,
       boolean allowFunctions) {
-    super(
-        operandJ(aggregateClass, null, agg -> isAggregateSupported(agg, allowFunctions),
-            operand(joinClass, null, any())),
-        relBuilderFactory, null);
-    this.allowFunctions = allowFunctions;
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(aggregateClass, joinClass, allowFunctions));
   }
 
   @Deprecated // to be removed before 2.0
@@ -124,7 +125,8 @@ public class AggregateJoinTransposeRule extends RelOptRule implements Transforma
         allowFunctions);
   }
 
-  private static boolean isAggregateSupported(Aggregate aggregate, boolean allowFunctions) {
+  private static boolean isAggregateSupported(Aggregate aggregate,
+      boolean allowFunctions) {
     if (!allowFunctions && !aggregate.getAggCallList().isEmpty()) {
       return false;
     }
@@ -152,7 +154,7 @@ public class AggregateJoinTransposeRule extends RelOptRule implements Transforma
     return join.getJoinType() == JoinRelType.INNER || aggregate.getAggCallList().isEmpty();
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Aggregate aggregate = call.rel(0);
     final Join join = call.rel(1);
     final RexBuilder rexBuilder = aggregate.getCluster().getRexBuilder();
@@ -213,7 +215,7 @@ public class AggregateJoinTransposeRule extends RelOptRule implements Transforma
       final ImmutableBitSet belowAggregateKey =
           belowAggregateKeyNotShifted.shift(-offset);
       final boolean unique;
-      if (!allowFunctions) {
+      if (!config.isAllowFunctions()) {
         assert aggregate.getAggCallList().isEmpty();
         // If there are no functions, it doesn't matter as much whether we
         // aggregate the inputs before the join, because there will not be
@@ -447,4 +449,39 @@ public class AggregateJoinTransposeRule extends RelOptRule implements Transforma
     RelNode newInput;
     boolean aggregate;
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(LogicalAggregate.class, LogicalJoin.class, false);
+
+    /** Extended instance that can push down aggregate functions. */
+    Config EXTENDED = EMPTY.as(Config.class)
+        .withOperandFor(LogicalAggregate.class, LogicalJoin.class, true);
+
+    @Override default AggregateJoinTransposeRule toRule() {
+      return new AggregateJoinTransposeRule(this);
+    }
+
+    /** Whether to push down aggregate functions, default false. */
+    @ImmutableBeans.Property
+    @ImmutableBeans.BooleanDefault(false)
+    boolean isAllowFunctions();
+
+    /** Sets {@link #isAllowFunctions()}. */
+    Config withAllowFunctions(boolean allowFunctions);
+
+    /** Defines an operand tree for the given classes, and also sets
+     * {@link #isAllowFunctions()}. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Join> joinClass, boolean allowFunctions) {
+      return withAllowFunctions(allowFunctions)
+          .withOperandSupplier(b0 ->
+              b0.operand(aggregateClass)
+                  .predicate(agg -> isAggregateSupported(agg, allowFunctions))
+              .oneInput(b1 ->
+                  b1.operand(joinClass).anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateMergeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateMergeRule.java
index f9bdc52..fa063fd 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateMergeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateMergeRule.java
@@ -16,13 +16,12 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.Aggregate.Group;
 import org.apache.calcite.rel.core.AggregateCall;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.sql.SqlSplittableAggFunction;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.tools.RelBuilderFactory;
@@ -45,25 +44,24 @@ import java.util.Objects;
  * <p>For example, SUM of SUM becomes SUM; SUM of COUNT becomes COUNT;
  * MAX of MAX becomes MAX; MIN of MIN becomes MIN. AVG of AVG would not
  * match, nor would COUNT of COUNT.
+ *
+ * @see CoreRules#AGGREGATE_MERGE
  */
-public class AggregateMergeRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_MERGE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateMergeRule INSTANCE =
-      CoreRules.AGGREGATE_MERGE;
-
-  AggregateMergeRule() {
-    this(
-        operand(Aggregate.class,
-            operandJ(Aggregate.class, null,
-                agg -> Aggregate.isSimple(agg), any())),
-        RelFactories.LOGICAL_BUILDER);
-  }
+public class AggregateMergeRule
+    extends RelRule<AggregateMergeRule.Config>
+    implements TransformationRule {
 
   /** Creates an AggregateMergeRule. */
+  protected AggregateMergeRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateMergeRule(RelOptRuleOperand operand,
       RelBuilderFactory relBuilderFactory) {
-    super(operand, relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class));
   }
 
   private boolean isAggregateSupported(AggregateCall aggCall) {
@@ -78,7 +76,7 @@ public class AggregateMergeRule extends RelOptRule implements TransformationRule
     return splitter != null;
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Aggregate topAgg = call.rel(0);
     final Aggregate bottomAgg = call.rel(1);
     if (topAgg.getGroupCount() > bottomAgg.getGroupCount()) {
@@ -148,4 +146,20 @@ public class AggregateMergeRule extends RelOptRule implements TransformationRule
             newGroupingSets, finalCalls);
     call.transformTo(finalAgg);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b0 ->
+            b0.operand(Aggregate.class)
+                .oneInput(b1 ->
+                    b1.operand(Aggregate.class)
+                        .predicate(Aggregate::isSimple)
+                        .anyInputs()))
+        .as(Config.class);
+
+    @Override default AggregateMergeRule toRule() {
+      return new AggregateMergeRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectMergeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectMergeRule.java
index 6ca4260..49d56e5 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectMergeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectMergeRule.java
@@ -16,9 +16,9 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.Aggregate.Group;
@@ -50,24 +50,30 @@ import java.util.Set;
  *
  * <p>In some cases, this rule has the effect of trimming: the aggregate will
  * use fewer columns than the project did.
+ *
+ * @see CoreRules#AGGREGATE_PROJECT_MERGE
  */
-public class AggregateProjectMergeRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_PROJECT_MERGE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateProjectMergeRule INSTANCE =
-      CoreRules.AGGREGATE_PROJECT_MERGE;
+public class AggregateProjectMergeRule
+    extends RelRule<AggregateProjectMergeRule.Config>
+    implements TransformationRule {
+
+  /** Creates an AggregateProjectMergeRule. */
+  protected AggregateProjectMergeRule(Config config) {
+    super(config);
+  }
 
+  @Deprecated // to be removed before 2.0
   public AggregateProjectMergeRule(
       Class<? extends Aggregate> aggregateClass,
       Class<? extends Project> projectClass,
       RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(aggregateClass,
-            operand(projectClass, any())),
-        relBuilderFactory, null);
+    this(CoreRules.AGGREGATE_PROJECT_MERGE.config
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(aggregateClass, projectClass));
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Aggregate aggregate = call.rel(0);
     final Project project = call.rel(1);
     RelNode x = apply(call, aggregate, project);
@@ -134,4 +140,22 @@ public class AggregateProjectMergeRule extends RelOptRule implements Transformat
 
     return relBuilder.build();
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Aggregate.class, Project.class);
+
+    @Override default AggregateProjectMergeRule toRule() {
+      return new AggregateProjectMergeRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Project> projectClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass).oneInput(b1 ->
+              b1.operand(projectClass).anyInputs())).as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectPullUpConstantsRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectPullUpConstantsRule.java
index 9484a0c..b044c6d 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectPullUpConstantsRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectPullUpConstantsRule.java
@@ -17,11 +17,12 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.RelOptPredicateList;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.logical.LogicalAggregate;
 import org.apache.calcite.rel.logical.LogicalProject;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
@@ -55,43 +56,30 @@ import java.util.TreeMap;
  * reduced aggregate. If those constants are not used, another rule will remove
  * them from the project.
  */
-public class AggregateProjectPullUpConstantsRule extends RelOptRule
+public class AggregateProjectPullUpConstantsRule
+    extends RelRule<AggregateProjectPullUpConstantsRule.Config>
     implements TransformationRule {
-  //~ Static fields/initializers ---------------------------------------------
-
-  /** @deprecated Use {@link CoreRules#AGGREGATE_PROJECT_PULL_UP_CONSTANTS}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateProjectPullUpConstantsRule INSTANCE =
-      CoreRules.AGGREGATE_PROJECT_PULL_UP_CONSTANTS;
-
-  /** @deprecated Use {@link CoreRules#AGGREGATE_ANY_PULL_UP_CONSTANTS}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateProjectPullUpConstantsRule INSTANCE2 =
-      CoreRules.AGGREGATE_ANY_PULL_UP_CONSTANTS;
-
-  //~ Constructors -----------------------------------------------------------
-
-  /**
-   * Creates an AggregateProjectPullUpConstantsRule.
-   *
-   * @param aggregateClass Aggregate class
-   * @param inputClass Input class, such as {@link LogicalProject}
-   * @param relBuilderFactory Builder for relational expressions
-   * @param description Description, or null to guess description
-   */
+
+  /** Creates an AggregateProjectPullUpConstantsRule. */
+  protected AggregateProjectPullUpConstantsRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateProjectPullUpConstantsRule(
       Class<? extends Aggregate> aggregateClass,
       Class<? extends RelNode> inputClass,
       RelBuilderFactory relBuilderFactory, String description) {
-    super(
-        operandJ(aggregateClass, null, Aggregate::isSimple,
-            operand(inputClass, any())),
-        relBuilderFactory, description);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .withDescription(description)
+        .as(Config.class)
+        .withOperandFor(aggregateClass, inputClass));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Aggregate aggregate = call.rel(0);
     final RelNode input = call.rel(1);
 
@@ -185,4 +173,29 @@ public class AggregateProjectPullUpConstantsRule extends RelOptRule
     call.transformTo(relBuilder.build());
   }
 
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(LogicalAggregate.class, LogicalProject.class);
+
+    @Override default AggregateProjectPullUpConstantsRule toRule() {
+      return new AggregateProjectPullUpConstantsRule(this);
+    }
+
+    /** Defines an operand tree for the given classes.
+     *
+     * @param aggregateClass Aggregate class
+     * @param inputClass Input class, such as {@link LogicalProject}
+     */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends RelNode> inputClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass)
+              .predicate(Aggregate::isSimple)
+              .oneInput(b1 ->
+                  b1.operand(inputClass).anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectStarTableRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectStarTableRule.java
new file mode 100644
index 0000000..f641521
--- /dev/null
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateProjectStarTableRule.java
@@ -0,0 +1,78 @@
+/*
+ * 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.rules;
+
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.schema.impl.StarTable;
+
+/** Variant of {@link AggregateStarTableRule} that accepts a {@link Project}
+ * between the {@link Aggregate} and its {@link StarTable.StarTableScan}
+ * input. */
+public class AggregateProjectStarTableRule extends AggregateStarTableRule {
+  /** Creates an AggregateProjectStarTableRule. */
+  protected AggregateProjectStarTableRule(Config config) {
+    super(config);
+  }
+
+  @Override public void onMatch(RelOptRuleCall call) {
+    final Aggregate aggregate = call.rel(0);
+    final Project project = call.rel(1);
+    final StarTable.StarTableScan scan = call.rel(2);
+    final RelNode rel =
+        AggregateProjectMergeRule.apply(call, aggregate, project);
+    final Aggregate aggregate2;
+    final Project project2;
+    if (rel instanceof Aggregate) {
+      project2 = null;
+      aggregate2 = (Aggregate) rel;
+    } else if (rel instanceof Project) {
+      project2 = (Project) rel;
+      aggregate2 = (Aggregate) project2.getInput();
+    } else {
+      return;
+    }
+    apply(call, project2, aggregate2, scan);
+  }
+
+  /** Rule configuration. */
+  public interface Config extends AggregateStarTableRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Aggregate.class, Project.class,
+            StarTable.StarTableScan.class);
+
+    @Override default AggregateProjectStarTableRule toRule() {
+      return new AggregateProjectStarTableRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Project> projectClass,
+        Class<StarTable.StarTableScan> scanClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass)
+              .predicate(Aggregate::isSimple)
+              .oneInput(b1 ->
+                  b1.operand(projectClass)
+                      .oneInput(b2 ->
+                          b2.operand(scanClass).noInputs())))
+          .as(Config.class);
+    }
+  }
+}
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateReduceFunctionsRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateReduceFunctionsRule.java
index d5ce21d..0ffbbcb 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateReduceFunctionsRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateReduceFunctionsRule.java
@@ -17,12 +17,13 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.logical.LogicalAggregate;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeFactory;
 import org.apache.calcite.rel.type.RelDataTypeField;
@@ -36,11 +37,13 @@ import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
 import org.apache.calcite.util.CompositeList;
+import org.apache.calcite.util.ImmutableBeans;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.ImmutableIntList;
 import org.apache.calcite.util.Util;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 
 import java.math.BigDecimal;
 import java.util.ArrayList;
@@ -50,6 +53,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
+import javax.annotation.Nonnull;
 
 /**
  * Planner rule that reduces aggregate functions in
@@ -89,69 +94,62 @@ import java.util.Objects;
  * <p>Since many of these rewrites introduce multiple occurrences of simpler
  * forms like {@code COUNT(x)}, the rule gathers common sub-expressions as it
  * goes.
+ *
+ * @see CoreRules#AGGREGATE_REDUCE_FUNCTIONS
  */
-public class AggregateReduceFunctionsRule extends RelOptRule
+public class AggregateReduceFunctionsRule
+    extends RelRule<AggregateReduceFunctionsRule.Config>
     implements TransformationRule {
   //~ Static fields/initializers ---------------------------------------------
 
-  /** @deprecated Use {@link CoreRules#AGGREGATE_REDUCE_FUNCTIONS}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateReduceFunctionsRule INSTANCE =
-      CoreRules.AGGREGATE_REDUCE_FUNCTIONS;
+  private static void validateFunction(SqlKind function) {
+    if (!isValid(function)) {
+      throw new IllegalArgumentException("AggregateReduceFunctionsRule doesn't "
+          + "support function: " + function.sql);
+    }
+  }
 
-  private final EnumSet<SqlKind> functionsToReduce;
+  private static boolean isValid(SqlKind function) {
+    return SqlKind.AVG_AGG_FUNCTIONS.contains(function)
+        || SqlKind.COVAR_AVG_AGG_FUNCTIONS.contains(function)
+        || function == SqlKind.SUM;
+  }
+
+  private final Set<SqlKind> functionsToReduce;
 
   //~ Constructors -----------------------------------------------------------
 
-  /**
-   * Creates an AggregateReduceFunctionsRule to reduce all functions
-   * handled by this rule
-   * @param operand operand to determine if rule can be applied
-   * @param relBuilderFactory builder for relational expressions
-   */
+  /** Creates an AggregateReduceFunctionsRule. */
+  protected AggregateReduceFunctionsRule(Config config) {
+    super(config);
+    this.functionsToReduce =
+        ImmutableSet.copyOf(config.actualFunctionsToReduce());
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateReduceFunctionsRule(RelOptRuleOperand operand,
       RelBuilderFactory relBuilderFactory) {
-    super(operand, relBuilderFactory, null);
-    functionsToReduce = EnumSet.noneOf(SqlKind.class);
-    addDefaultSetOfFunctionsToReduce();
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class)
+        // reduce all functions handled by this rule
+        .withFunctionsToReduce(null));
   }
 
-  /**
-   * Creates an AggregateReduceFunctionsRule with client
-   * provided information on which specific functions will
-   * be reduced by this rule
-   * @param aggregateClass aggregate class
-   * @param relBuilderFactory builder for relational expressions
-   * @param functionsToReduce client provided information
-   *                          on which specific functions
-   *                          will be reduced by this rule
-   */
+  @Deprecated // to be removed before 2.0
   public AggregateReduceFunctionsRule(Class<? extends Aggregate> aggregateClass,
       RelBuilderFactory relBuilderFactory, EnumSet<SqlKind> functionsToReduce) {
-    super(operand(aggregateClass, any()), relBuilderFactory, null);
-    Objects.requireNonNull(functionsToReduce,
-        "Expecting a valid handle for AggregateFunctionsToReduce");
-    this.functionsToReduce = EnumSet.noneOf(SqlKind.class);
-    for (SqlKind function : functionsToReduce) {
-      if (SqlKind.AVG_AGG_FUNCTIONS.contains(function)
-          || SqlKind.COVAR_AVG_AGG_FUNCTIONS.contains(function)
-          || function == SqlKind.SUM) {
-        this.functionsToReduce.add(function);
-      } else {
-        throw new IllegalArgumentException(
-          "AggregateReduceFunctionsRule doesn't support function: " + function.sql);
-      }
-    }
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(aggregateClass)
+        // reduce specific functions provided by the client
+        .withFunctionsToReduce(Objects.requireNonNull(functionsToReduce)));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  private void addDefaultSetOfFunctionsToReduce() {
-    functionsToReduce.addAll(SqlKind.AVG_AGG_FUNCTIONS);
-    functionsToReduce.addAll(SqlKind.COVAR_AVG_AGG_FUNCTIONS);
-    functionsToReduce.add(SqlKind.SUM);
-  }
-
   @Override public boolean matches(RelOptRuleCall call) {
     if (!super.matches(call)) {
       return false;
@@ -160,7 +158,7 @@ public class AggregateReduceFunctionsRule extends RelOptRule
     return containsAvgStddevVarCall(oldAggRel.getAggCallList());
   }
 
-  public void onMatch(RelOptRuleCall ruleCall) {
+  @Override public void onMatch(RelOptRuleCall ruleCall) {
     Aggregate oldAggRel = (Aggregate) ruleCall.rels[0];
     reduceAggs(ruleCall, oldAggRel);
   }
@@ -857,4 +855,42 @@ public class AggregateReduceFunctionsRule extends RelOptRule
         relNode.getRowType().getFieldList().get(i);
     return inputField.getType();
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(LogicalAggregate.class);
+
+    Set<SqlKind> DEFAULT_FUNCTIONS_TO_REDUCE =
+        ImmutableSet.<SqlKind>builder()
+            .addAll(SqlKind.AVG_AGG_FUNCTIONS)
+            .addAll(SqlKind.COVAR_AVG_AGG_FUNCTIONS)
+            .add(SqlKind.SUM)
+            .build();
+
+    @Override default AggregateReduceFunctionsRule toRule() {
+      return new AggregateReduceFunctionsRule(this);
+    }
+
+    @ImmutableBeans.Property
+    Set<SqlKind> functionsToReduce();
+
+    /** Sets {@link #functionsToReduce}. */
+    Config withFunctionsToReduce(Set<SqlKind> functionSet);
+
+    /** Returns the validated set of functions to reduce, or the default set
+     * if not specified. */
+    @Nonnull default Set<SqlKind> actualFunctionsToReduce() {
+      final Set<SqlKind> set =
+          Util.first(functionsToReduce(), DEFAULT_FUNCTIONS_TO_REDUCE);
+      set.forEach(AggregateReduceFunctionsRule::validateFunction);
+      return set;
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass) {
+      return withOperandSupplier(b -> b.operand(aggregateClass).anyInputs())
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateRemoveRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateRemoveRule.java
index 7ac5382..96017c6 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateRemoveRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateRemoveRule.java
@@ -16,12 +16,13 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.RelFactories;
+import org.apache.calcite.rel.logical.LogicalAggregate;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexNode;
@@ -43,28 +44,30 @@ import java.util.Objects;
  * (that is, it is implementing {@code SELECT DISTINCT}),
  * or all the aggregate functions are splittable,
  * and the underlying relational expression is already distinct.
+ *
+ * @see CoreRules#AGGREGATE_REMOVE
  */
-public class AggregateRemoveRule extends RelOptRule implements SubstitutionRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_REMOVE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateRemoveRule INSTANCE =
-      CoreRules.AGGREGATE_REMOVE;
+public class AggregateRemoveRule
+    extends RelRule<AggregateRemoveRule.Config>
+    implements SubstitutionRule {
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates an AggregateRemoveRule. */
+  protected AggregateRemoveRule(Config config) {
+    super(config);
+  }
 
   @Deprecated // to be removed before 2.0
   public AggregateRemoveRule(Class<? extends Aggregate> aggregateClass) {
     this(aggregateClass, RelFactories.LOGICAL_BUILDER);
   }
 
-  /**
-   * Creates an AggregateRemoveRule.
-   */
+  @Deprecated // to be removed before 2.0
   public AggregateRemoveRule(Class<? extends Aggregate> aggregateClass,
       RelBuilderFactory relBuilderFactory) {
-    super(
-        operandJ(aggregateClass, null, agg -> isAggregateSupported(agg),
-            any()), relBuilderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(aggregateClass));
   }
 
   private static boolean isAggregateSupported(Aggregate aggregate) {
@@ -85,7 +88,7 @@ public class AggregateRemoveRule extends RelOptRule implements SubstitutionRule
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Aggregate aggregate = call.rel(0);
     final RelNode input = aggregate.getInput();
     final RelMetadataQuery mq = call.getMetadataQuery();
@@ -126,4 +129,25 @@ public class AggregateRemoveRule extends RelOptRule implements SubstitutionRule
     call.getPlanner().prune(aggregate);
     call.transformTo(relBuilder.build());
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withRelBuilderFactory(RelFactories.LOGICAL_BUILDER)
+        .as(Config.class)
+        .withOperandFor(LogicalAggregate.class);
+
+    @Override default AggregateRemoveRule toRule() {
+      return new AggregateRemoveRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass) {
+      return withOperandSupplier(b ->
+          b.operand(aggregateClass)
+              .predicate(AggregateRemoveRule::isAggregateSupported)
+              .anyInputs())
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateStarTableRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateStarTableRule.java
index 2941d3a..859150c 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateStarTableRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateStarTableRule.java
@@ -24,10 +24,10 @@ import org.apache.calcite.materialize.TileKey;
 import org.apache.calcite.plan.RelOptCluster;
 import org.apache.calcite.plan.RelOptLattice;
 import org.apache.calcite.plan.RelOptPlanner;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
 import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.SubstitutionVisitor;
 import org.apache.calcite.plan.ViewExpanders;
 import org.apache.calcite.prepare.RelOptTableImpl;
@@ -56,29 +56,28 @@ import java.util.List;
  *
  * <p>This pattern indicates that an aggregate table may exist. The rule asks
  * the star table for an aggregate table at the required level of aggregation.
+ *
+ * @see AggregateProjectStarTableRule
+ * @see CoreRules#AGGREGATE_STAR_TABLE
+ * @see CoreRules#AGGREGATE_PROJECT_STAR_TABLE
  */
-public class AggregateStarTableRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_STAR_TABLE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateStarTableRule INSTANCE =
-      CoreRules.AGGREGATE_STAR_TABLE;
-
-  /** @deprecated This field is prone to issues during class-loading;
-   * use {@link CoreRules#AGGREGATE_PROJECT_STAR_TABLE} instead. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateStarTableRule INSTANCE2 =
-      CoreRules.AGGREGATE_PROJECT_STAR_TABLE;
-
-  /**
-   * Creates an AggregateStarTableRule.
-   *
-   * @param operand           root operand, must not be null
-   * @param description       Description, or null to guess description
-   * @param relBuilderFactory Builder for relational expressions
-   */
+public class AggregateStarTableRule
+    extends RelRule<AggregateStarTableRule.Config>
+    implements TransformationRule {
+
+  /** Creates an AggregateStarTableRule. */
+  protected AggregateStarTableRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateStarTableRule(RelOptRuleOperand operand,
       RelBuilderFactory relBuilderFactory, String description) {
-    super(operand, relBuilderFactory, description);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .withDescription(description)
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class));
   }
 
   @Override public void onMatch(RelOptRuleCall call) {
@@ -245,4 +244,25 @@ public class AggregateStarTableRule extends RelOptRule implements Transformation
     }
     return -1;
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Aggregate.class, StarTable.StarTableScan.class);
+
+    @Override default AggregateStarTableRule toRule() {
+      return new AggregateStarTableRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<StarTable.StarTableScan> scanClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass)
+              .predicate(Aggregate::isSimple)
+              .oneInput(b1 ->
+                  b1.operand(scanClass).noInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateUnionAggregateRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateUnionAggregateRule.java
index 977d6d3..8453d39 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateUnionAggregateRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateUnionAggregateRule.java
@@ -16,13 +16,15 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.Union;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalUnion;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
 
@@ -36,40 +38,33 @@ import org.apache.calcite.tools.RelBuilderFactory;
  * <p>This rule only handles cases where the
  * {@link org.apache.calcite.rel.core.Union}s
  * still have only two inputs.
+ *
+ * @see CoreRules#AGGREGATE_UNION_AGGREGATE
+ * @see CoreRules#AGGREGATE_UNION_AGGREGATE_FIRST
+ * @see CoreRules#AGGREGATE_UNION_AGGREGATE_SECOND
  */
-public class AggregateUnionAggregateRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_UNION_AGGREGATE_FIRST}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateUnionAggregateRule AGG_ON_FIRST_INPUT =
-      CoreRules.AGGREGATE_UNION_AGGREGATE_FIRST;
-
-  /** @deprecated Use {@link CoreRules#AGGREGATE_UNION_AGGREGATE_SECOND}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateUnionAggregateRule AGG_ON_SECOND_INPUT =
-      CoreRules.AGGREGATE_UNION_AGGREGATE_SECOND;
+public class AggregateUnionAggregateRule
+    extends RelRule<AggregateUnionAggregateRule.Config>
+    implements TransformationRule {
 
-  /** @deprecated Use {@link CoreRules#AGGREGATE_UNION_AGGREGATE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateUnionAggregateRule INSTANCE =
-      CoreRules.AGGREGATE_UNION_AGGREGATE;
-
-  //~ Constructors -----------------------------------------------------------
+  /** Creates an AggregateUnionAggregateRule. */
+  protected AggregateUnionAggregateRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a AggregateUnionAggregateRule.
-   */
+  @Deprecated // to be removed before 2.0
   public AggregateUnionAggregateRule(Class<? extends Aggregate> aggregateClass,
       Class<? extends Union> unionClass,
       Class<? extends RelNode> firstUnionInputClass,
       Class<? extends RelNode> secondUnionInputClass,
       RelBuilderFactory relBuilderFactory,
       String desc) {
-    super(
-        operandJ(aggregateClass, null, Aggregate::isSimple,
-            operand(unionClass,
-                operand(firstUnionInputClass, any()),
-                operand(secondUnionInputClass, any()))),
-        relBuilderFactory, desc);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .withDescription(desc)
+        .as(Config.class)
+        .withOperandFor(aggregateClass, unionClass, firstUnionInputClass,
+            secondUnionInputClass));
   }
 
   @Deprecated // to be removed before 2.0
@@ -101,7 +96,7 @@ public class AggregateUnionAggregateRule extends RelOptRule implements Transform
     }
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Aggregate topAggRel = call.rel(0);
     final Union union = call.rel(1);
 
@@ -138,4 +133,44 @@ public class AggregateUnionAggregateRule extends RelOptRule implements Transform
         topAggRel.getAggCallList());
     call.transformTo(relBuilder.build());
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withDescription("AggregateUnionAggregateRule")
+        .as(Config.class)
+        .withOperandFor(LogicalAggregate.class, LogicalUnion.class,
+            RelNode.class, RelNode.class);
+
+    Config AGG_FIRST = DEFAULT
+        .withDescription("AggregateUnionAggregateRule:first-input-agg")
+        .as(Config.class)
+        .withOperandFor(LogicalAggregate.class, LogicalUnion.class,
+            LogicalAggregate.class, RelNode.class);
+
+    Config AGG_SECOND = DEFAULT
+        .withDescription("AggregateUnionAggregateRule:second-input-agg")
+        .as(Config.class)
+        .withOperandFor(LogicalAggregate.class, LogicalUnion.class,
+            RelNode.class, LogicalAggregate.class);
+
+    @Override default AggregateUnionAggregateRule toRule() {
+      return new AggregateUnionAggregateRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Union> unionClass,
+        Class<? extends RelNode> firstUnionInputClass,
+        Class<? extends RelNode> secondUnionInputClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass)
+              .predicate(Aggregate::isSimple)
+              .oneInput(b1 ->
+                  b1.operand(unionClass).inputs(
+                      b2 -> b2.operand(firstUnionInputClass).anyInputs(),
+                      b3 -> b3.operand(secondUnionInputClass).anyInputs())))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateUnionTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateUnionTransposeRule.java
index 16c91d2..5bd165e 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateUnionTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateUnionTransposeRule.java
@@ -17,13 +17,15 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.linq4j.Ord;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.Union;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalUnion;
 import org.apache.calcite.rel.metadata.RelMdUtil;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rel.type.RelDataType;
@@ -50,12 +52,12 @@ import java.util.Map;
  * Planner rule that pushes an
  * {@link org.apache.calcite.rel.core.Aggregate}
  * past a non-distinct {@link org.apache.calcite.rel.core.Union}.
+ *
+ * @see CoreRules#AGGREGATE_UNION_TRANSPOSE
  */
-public class AggregateUnionTransposeRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_UNION_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateUnionTransposeRule INSTANCE =
-      CoreRules.AGGREGATE_UNION_TRANSPOSE;
+public class AggregateUnionTransposeRule
+    extends RelRule<AggregateUnionTransposeRule.Config>
+    implements TransformationRule {
 
   private static final Map<Class<? extends SqlAggFunction>, Boolean>
       SUPPORTED_AGGREGATES = new IdentityHashMap<>();
@@ -70,12 +72,17 @@ public class AggregateUnionTransposeRule extends RelOptRule implements Transform
   }
 
   /** Creates an AggregateUnionTransposeRule. */
+  protected AggregateUnionTransposeRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateUnionTransposeRule(Class<? extends Aggregate> aggregateClass,
       Class<? extends Union> unionClass, RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(aggregateClass,
-            operand(unionClass, any())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(aggregateClass, unionClass));
   }
 
   @Deprecated // to be removed before 2.0
@@ -87,7 +94,7 @@ public class AggregateUnionTransposeRule extends RelOptRule implements Transform
         RelBuilder.proto(aggregateFactory, setOpFactory));
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     Aggregate aggRel = call.rel(0);
     Union union = call.rel(1);
 
@@ -182,4 +189,23 @@ public class AggregateUnionTransposeRule extends RelOptRule implements Transform
     }
     return newCalls;
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(LogicalAggregate.class, LogicalUnion.class);
+
+    @Override default AggregateUnionTransposeRule toRule() {
+      return new AggregateUnionTransposeRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Union> unionClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass).oneInput(b1 ->
+              b1.operand(unionClass).anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/AggregateValuesRule.java b/core/src/main/java/org/apache/calcite/rel/rules/AggregateValuesRule.java
index a016e10..4c0bfa3 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/AggregateValuesRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/AggregateValuesRule.java
@@ -16,8 +16,8 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.AggregateCall;
 import org.apache.calcite.rel.core.Values;
@@ -53,24 +53,20 @@ import java.util.List;
  *
  * @see CoreRules#AGGREGATE_VALUES
  */
-public class AggregateValuesRule extends RelOptRule implements SubstitutionRule {
-  /** @deprecated Use {@link CoreRules#AGGREGATE_VALUES}. */
-  @Deprecated // to be removed before 1.25
-  public static final AggregateValuesRule INSTANCE =
-      CoreRules.AGGREGATE_VALUES;
+public class AggregateValuesRule
+    extends RelRule<AggregateValuesRule.Config>
+    implements SubstitutionRule {
 
-  /**
-   * Creates an AggregateValuesRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  /** Creates an AggregateValuesRule. */
+  protected AggregateValuesRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public AggregateValuesRule(RelBuilderFactory relBuilderFactory) {
-    super(
-        operandJ(Aggregate.class, null,
-            aggregate -> aggregate.getGroupCount() == 0,
-            operandJ(Values.class, null,
-                values -> values.getTuples().isEmpty(), none())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   @Override public void onMatch(RelOptRuleCall call) {
@@ -109,4 +105,27 @@ public class AggregateValuesRule extends RelOptRule implements SubstitutionRule
     // New plan is absolutely better than old plan.
     call.getPlanner().prune(aggregate);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Aggregate.class, Values.class);
+
+    @Override default AggregateValuesRule toRule() {
+      return new AggregateValuesRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Aggregate> aggregateClass,
+        Class<? extends Values> valuesClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(aggregateClass)
+              .predicate(aggregate -> aggregate.getGroupCount() == 0)
+              .oneInput(b1 ->
+                  b1.operand(valuesClass)
+                      .predicate(values -> values.getTuples().isEmpty())
+                      .noInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CalcMergeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/CalcMergeRule.java
index 1b4fbd8..bb4c56d 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CalcMergeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CalcMergeRule.java
@@ -16,8 +16,8 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.core.Calc;
 import org.apache.calcite.rex.RexOver;
 import org.apache.calcite.rex.RexProgram;
@@ -33,33 +33,26 @@ import org.apache.calcite.tools.RelBuilderFactory;
  * same project list as the upper
  * {@link org.apache.calcite.rel.logical.LogicalCalc}, but expressed in terms of
  * the lower {@link org.apache.calcite.rel.logical.LogicalCalc}'s inputs.
+ *
+ * @see CoreRules#CALC_MERGE
  */
-public class CalcMergeRule extends RelOptRule implements TransformationRule {
-  //~ Static fields/initializers ---------------------------------------------
-
-  /** @deprecated Use {@link CoreRules#CALC_MERGE}. */
-  @Deprecated // to be removed before 1.25
-  public static final CalcMergeRule INSTANCE =
-      CoreRules.CALC_MERGE;
+public class CalcMergeRule extends RelRule<CalcMergeRule.Config>
+    implements TransformationRule {
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a CalcMergeRule. */
+  protected CalcMergeRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a CalcMergeRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public CalcMergeRule(RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(
-            Calc.class,
-            operand(Calc.class, any())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Calc topCalc = call.rel(0);
     final Calc bottomCalc = call.rel(1);
 
@@ -95,4 +88,17 @@ public class CalcMergeRule extends RelOptRule implements TransformationRule {
 
     call.transformTo(newCalc);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = Config.EMPTY
+        .withOperandSupplier(b0 ->
+            b0.operand(Calc.class).oneInput(b1 ->
+                b1.operand(Calc.class).anyInputs()))
+        .as(Config.class);
+
+    @Override default CalcMergeRule toRule() {
+      return new CalcMergeRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CalcRemoveRule.java b/core/src/main/java/org/apache/calcite/rel/rules/CalcRemoveRule.java
index 320d364..737be2b 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CalcRemoveRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CalcRemoveRule.java
@@ -16,11 +16,11 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Calc;
 import org.apache.calcite.rel.logical.LogicalCalc;
-import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.tools.RelBuilderFactory;
 
 /**
@@ -33,33 +33,25 @@ import org.apache.calcite.tools.RelBuilderFactory;
  *
  * @see ProjectRemoveRule
  */
-public class CalcRemoveRule extends RelOptRule implements SubstitutionRule {
-  //~ Static fields/initializers ---------------------------------------------
+public class CalcRemoveRule extends RelRule<CalcRemoveRule.Config>
+    implements SubstitutionRule {
 
-  /** @deprecated Use {@link CoreRules#CALC_REMOVE}. */
-  @Deprecated // to be removed before 1.25
-  public static final CalcRemoveRule INSTANCE =
-      CoreRules.CALC_REMOVE;
-
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a CalcRemoveRule. */
+  protected CalcRemoveRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a CalcRemoveRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public CalcRemoveRule(RelBuilderFactory relBuilderFactory) {
-    super(operand(LogicalCalc.class, any()), relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
-    LogicalCalc calc = call.rel(0);
-    RexProgram program = calc.getProgram();
-    if (!program.isTrivial()) {
-      return;
-    }
+  @Override public void onMatch(RelOptRuleCall call) {
+    final Calc calc = call.rel(0);
+    assert calc.getProgram().isTrivial() : "rule predicate";
     RelNode input = calc.getInput();
     input = call.getPlanner().register(input, calc);
     call.transformTo(
@@ -67,4 +59,18 @@ public class CalcRemoveRule extends RelOptRule implements SubstitutionRule {
             input,
             calc.getTraitSet()));
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b ->
+            b.operand(LogicalCalc.class)
+                .predicate(calc -> calc.getProgram().isTrivial())
+                .anyInputs())
+        .as(Config.class);
+
+    @Override default CalcRemoveRule toRule() {
+      return new CalcRemoveRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CalcSplitRule.java b/core/src/main/java/org/apache/calcite/rel/rules/CalcSplitRule.java
index 72e24c7..160af8e 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CalcSplitRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CalcSplitRule.java
@@ -16,8 +16,8 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.core.Calc;
 import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rex.RexNode;
@@ -36,20 +36,21 @@ import com.google.common.collect.ImmutableList;
  * convert {@code Project} and {@code Filter} to {@code Calc}. But useful for
  * specific tasks, such as optimizing before calling an
  * {@link org.apache.calcite.interpreter.Interpreter}.
+ *
+ * @see CoreRules#CALC_SPLIT
  */
-public class CalcSplitRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#CALC_SPLIT}. */
-  @Deprecated // to be removed before 1.25
-  public static final CalcSplitRule INSTANCE =
-      CoreRules.CALC_SPLIT;
+public class CalcSplitRule extends RelRule<CalcSplitRule.Config>
+    implements TransformationRule {
+
+  /** Creates a CalcSplitRule. */
+  protected CalcSplitRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a CalcSplitRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public CalcSplitRule(RelBuilderFactory relBuilderFactory) {
-    super(operand(Calc.class, any()), relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   @Override public void onMatch(RelOptRuleCall call) {
@@ -62,4 +63,15 @@ public class CalcSplitRule extends RelOptRule implements TransformationRule {
     relBuilder.project(projectFilter.left, calc.getRowType().getFieldNames());
     call.transformTo(relBuilder.build());
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b -> b.operand(Calc.class).anyInputs())
+        .as(Config.class);
+
+    @Override default CalcSplitRule toRule() {
+      return new CalcSplitRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CoerceInputsRule.java b/core/src/main/java/org/apache/calcite/rel/rules/CoerceInputsRule.java
index 7e477a7..ea5b5ca 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CoerceInputsRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CoerceInputsRule.java
@@ -17,13 +17,13 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.Convention;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -32,39 +32,36 @@ import java.util.List;
  * CoerceInputsRule pre-casts inputs to a particular type. This can be used to
  * assist operator implementations which impose requirements on their input
  * types.
+ *
+ * @see CoreRules#COERCE_INPUTS
  */
-public class CoerceInputsRule extends RelOptRule implements TransformationRule {
-  //~ Instance fields --------------------------------------------------------
-
-  private final Class consumerRelClass;
-
-  private final boolean coerceNames;
-
+public class CoerceInputsRule
+    extends RelRule<CoerceInputsRule.Config>
+    implements TransformationRule {
   //~ Constructors -----------------------------------------------------------
 
+  /** Creates a CoerceInputsRule. */
+  protected CoerceInputsRule(Config config) {
+    super(config);
+  }
+
   @Deprecated // to be removed before 2.0
   public CoerceInputsRule(
       Class<? extends RelNode> consumerRelClass,
       boolean coerceNames) {
-    this(consumerRelClass, coerceNames, RelFactories.LOGICAL_BUILDER);
+    this(Config.DEFAULT
+        .withCoerceNames(coerceNames)
+        .withOperandFor(consumerRelClass));
   }
 
-  /**
-   * Creates a CoerceInputsRule.
-   *
-   * @param consumerRelClass  Class of RelNode that will consume the inputs
-   * @param coerceNames       If true, coerce names and types; if false, coerce
-   *                          type only
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public CoerceInputsRule(Class<? extends RelNode> consumerRelClass,
       boolean coerceNames, RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(consumerRelClass, any()),
-        relBuilderFactory,
-        "CoerceInputsRule:" + consumerRelClass.getName());
-    this.consumerRelClass = consumerRelClass;
-    this.coerceNames = coerceNames;
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withCoerceNames(coerceNames)
+        .withConsumerRelClass(consumerRelClass));
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -73,9 +70,9 @@ public class CoerceInputsRule extends RelOptRule implements TransformationRule {
     return Convention.NONE;
   }
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     RelNode consumerRel = call.rel(0);
-    if (consumerRel.getClass() != consumerRelClass) {
+    if (consumerRel.getClass() != config.consumerRelClass()) {
       // require exact match on type
       return;
     }
@@ -89,7 +86,7 @@ public class CoerceInputsRule extends RelOptRule implements TransformationRule {
           RelOptUtil.createCastRel(
               input,
               expectedType,
-              coerceNames);
+              config.isCoerceNames());
       if (newInput != input) {
         newInputs.set(i, newInput);
         coerce = true;
@@ -97,7 +94,7 @@ public class CoerceInputsRule extends RelOptRule implements TransformationRule {
       assert RelOptUtil.areRowTypesEqual(
           newInputs.get(i).getRowType(),
           expectedType,
-          coerceNames);
+          config.isCoerceNames());
     }
     if (!coerce) {
       return;
@@ -108,4 +105,38 @@ public class CoerceInputsRule extends RelOptRule implements TransformationRule {
             newInputs);
     call.transformTo(newConsumerRel);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withCoerceNames(false)
+        .withOperandFor(RelNode.class);
+
+    @Override default CoerceInputsRule toRule() {
+      return new CoerceInputsRule(this);
+    }
+
+    /** Whether to coerce names. */
+    @ImmutableBeans.Property
+    @ImmutableBeans.BooleanDefault(false)
+    boolean isCoerceNames();
+
+    /** Sets {@link #isCoerceNames()}. */
+    Config withCoerceNames(boolean coerceNames);
+
+    /** Class of {@link RelNode} to coerce to. */
+    @ImmutableBeans.Property
+    Class<? extends RelNode> consumerRelClass();
+
+    /** Sets {@link #consumerRelClass()}. */
+    Config withConsumerRelClass(Class<? extends RelNode> relClass);
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends RelNode> consumerRelClass) {
+      return withConsumerRelClass(consumerRelClass)
+          .withOperandSupplier(b -> b.operand(consumerRelClass).anyInputs())
+          .withDescription("CoerceInputsRule:" + consumerRelClass.getName())
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
index 6052dc4..2861cd3 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/CoreRules.java
@@ -16,8 +16,6 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.adapter.enumerable.EnumerableInterpreter;
-import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.Calc;
@@ -27,7 +25,6 @@ import org.apache.calcite.rel.core.Intersect;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.Minus;
 import org.apache.calcite.rel.core.Project;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.SetOp;
 import org.apache.calcite.rel.core.Sort;
 import org.apache.calcite.rel.core.TableScan;
@@ -38,30 +35,17 @@ import org.apache.calcite.rel.logical.LogicalCalc;
 import org.apache.calcite.rel.logical.LogicalCorrelate;
 import org.apache.calcite.rel.logical.LogicalExchange;
 import org.apache.calcite.rel.logical.LogicalFilter;
-import org.apache.calcite.rel.logical.LogicalIntersect;
 import org.apache.calcite.rel.logical.LogicalJoin;
 import org.apache.calcite.rel.logical.LogicalMatch;
-import org.apache.calcite.rel.logical.LogicalMinus;
 import org.apache.calcite.rel.logical.LogicalProject;
-import org.apache.calcite.rel.logical.LogicalSort;
 import org.apache.calcite.rel.logical.LogicalSortExchange;
 import org.apache.calcite.rel.logical.LogicalTableFunctionScan;
-import org.apache.calcite.rel.logical.LogicalUnion;
-import org.apache.calcite.rel.logical.LogicalValues;
 import org.apache.calcite.rel.logical.LogicalWindow;
 import org.apache.calcite.rel.rules.materialize.MaterializedViewRules;
 import org.apache.calcite.rex.RexInputRef;
-import org.apache.calcite.rex.RexOver;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.schema.impl.StarTable;
 
-import static org.apache.calcite.plan.RelOptRule.any;
-import static org.apache.calcite.plan.RelOptRule.none;
-import static org.apache.calcite.plan.RelOptRule.operand;
-import static org.apache.calcite.plan.RelOptRule.operandJ;
-import static org.apache.calcite.plan.RelOptRule.some;
-import static org.apache.calcite.rel.rules.ProjectMergeRule.DEFAULT_BLOAT;
-
 /** Rules that perform logical transformations on relational expressions.
  *
  * @see MaterializedViewRules */
@@ -73,75 +57,43 @@ public class CoreRules {
    * on top of a {@link Project} and if possible
    * aggregates through the Project or removes the Project. */
   public static final AggregateProjectMergeRule AGGREGATE_PROJECT_MERGE =
-      new AggregateProjectMergeRule(Aggregate.class, Project.class, RelFactories.LOGICAL_BUILDER);
+      AggregateProjectMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that removes constant keys from an {@link Aggregate}. */
   public static final AggregateProjectPullUpConstantsRule
       AGGREGATE_PROJECT_PULL_UP_CONSTANTS =
-      new AggregateProjectPullUpConstantsRule(LogicalAggregate.class,
-          LogicalProject.class, RelFactories.LOGICAL_BUILDER,
-          "AggregateProjectPullUpConstantsRule");
+      AggregateProjectPullUpConstantsRule.Config.DEFAULT.toRule();
 
   /** More general form of {@link #AGGREGATE_PROJECT_PULL_UP_CONSTANTS}
    * that matches any relational expression. */
   public static final AggregateProjectPullUpConstantsRule
       AGGREGATE_ANY_PULL_UP_CONSTANTS =
-      new AggregateProjectPullUpConstantsRule(LogicalAggregate.class,
-          RelNode.class, RelFactories.LOGICAL_BUILDER,
-          "AggregatePullUpConstantsRule");
+      AggregateProjectPullUpConstantsRule.Config.DEFAULT
+          .withOperandFor(LogicalAggregate.class, RelNode.class)
+          .toRule();
 
   /** Rule that matches an {@link Aggregate} on
    * a {@link StarTable.StarTableScan}. */
   public static final AggregateStarTableRule AGGREGATE_STAR_TABLE =
-      new AggregateStarTableRule(
-          operandJ(Aggregate.class, null, Aggregate::isSimple,
-              some(operand(StarTable.StarTableScan.class, none()))),
-          RelFactories.LOGICAL_BUILDER,
-          "AggregateStarTableRule");
+      AggregateStarTableRule.Config.DEFAULT.toRule();
 
   /** Variant of {@link #AGGREGATE_STAR_TABLE} that accepts a {@link Project}
    * between the {@link Aggregate} and its {@link StarTable.StarTableScan}
    * input. */
-  public static final AggregateStarTableRule AGGREGATE_PROJECT_STAR_TABLE =
-      new AggregateStarTableRule(
-          operandJ(Aggregate.class, null, Aggregate::isSimple,
-              operand(Project.class,
-                  operand(StarTable.StarTableScan.class, none()))),
-          RelFactories.LOGICAL_BUILDER,
-          "AggregateStarTableRule:project") {
-        @Override public void onMatch(RelOptRuleCall call) {
-          final Aggregate aggregate = call.rel(0);
-          final Project project = call.rel(1);
-          final StarTable.StarTableScan scan = call.rel(2);
-          final RelNode rel =
-              AggregateProjectMergeRule.apply(call, aggregate, project);
-          final Aggregate aggregate2;
-          final Project project2;
-          if (rel instanceof Aggregate) {
-            project2 = null;
-            aggregate2 = (Aggregate) rel;
-          } else if (rel instanceof Project) {
-            project2 = (Project) rel;
-            aggregate2 = (Aggregate) project2.getInput();
-          } else {
-            return;
-          }
-          apply(call, project2, aggregate2, scan);
-        }
-      };
+  public static final AggregateProjectStarTableRule AGGREGATE_PROJECT_STAR_TABLE =
+      AggregateProjectStarTableRule.Config.DEFAULT.toRule();
 
   /** Rule that reduces aggregate functions in
    * an {@link Aggregate} to simpler forms. */
   public static final AggregateReduceFunctionsRule AGGREGATE_REDUCE_FUNCTIONS =
-      new AggregateReduceFunctionsRule(operand(LogicalAggregate.class, any()),
-          RelFactories.LOGICAL_BUILDER);
+      AggregateReduceFunctionsRule.Config.DEFAULT.toRule();
 
   /** Rule that matches an {@link Aggregate} on an {@link Aggregate},
    * and merges into a single Aggregate if the top aggregate's group key is a
    * subset of the lower aggregate's group key, and the aggregates are
    * expansions of rollups. */
   public static final AggregateMergeRule AGGREGATE_MERGE =
-      new AggregateMergeRule();
+      AggregateMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that removes an {@link Aggregate}
    * if it computes no aggregate functions
@@ -149,8 +101,7 @@ public class CoreRules {
    * or all the aggregate functions are splittable,
    * and the underlying relational expression is already distinct. */
   public static final AggregateRemoveRule AGGREGATE_REMOVE =
-      new AggregateRemoveRule(LogicalAggregate.class,
-          RelFactories.LOGICAL_BUILDER);
+      AggregateRemoveRule.Config.DEFAULT.toRule();
 
   /** Rule that expands distinct aggregates
    * (such as {@code COUNT(DISTINCT x)}) from a
@@ -158,54 +109,47 @@ public class CoreRules {
    * This instance operates only on logical expressions. */
   public static final AggregateExpandDistinctAggregatesRule
       AGGREGATE_EXPAND_DISTINCT_AGGREGATES =
-      new AggregateExpandDistinctAggregatesRule(LogicalAggregate.class, true,
-          RelFactories.LOGICAL_BUILDER);
+      AggregateExpandDistinctAggregatesRule.Config.DEFAULT.toRule();
 
   /** As {@link #AGGREGATE_EXPAND_DISTINCT_AGGREGATES} but generates a Join. */
   public static final AggregateExpandDistinctAggregatesRule
       AGGREGATE_EXPAND_DISTINCT_AGGREGATES_TO_JOIN =
-      new AggregateExpandDistinctAggregatesRule(LogicalAggregate.class, false,
-          RelFactories.LOGICAL_BUILDER);
+      AggregateExpandDistinctAggregatesRule.Config.JOIN.toRule();
 
   /** Rule that matches an {@link Aggregate}
    * on a {@link Filter} and transposes them,
    * pushing the aggregate below the filter. */
   public static final AggregateFilterTransposeRule AGGREGATE_FILTER_TRANSPOSE =
-      new AggregateFilterTransposeRule();
+      AggregateFilterTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that matches an {@link Aggregate}
    * on a {@link Join} and removes the left input
    * of the join provided that the left input is also a left join if
    * possible. */
   public static final AggregateJoinJoinRemoveRule AGGREGATE_JOIN_JOIN_REMOVE =
-      new AggregateJoinJoinRemoveRule(LogicalAggregate.class,
-          LogicalJoin.class, RelFactories.LOGICAL_BUILDER);
+      AggregateJoinJoinRemoveRule.Config.DEFAULT.toRule();
 
   /** Rule that matches an {@link Aggregate}
    * on a {@link Join} and removes the join
    * provided that the join is a left join or right join and it computes no
    * aggregate functions or all the aggregate calls have distinct. */
   public static final AggregateJoinRemoveRule AGGREGATE_JOIN_REMOVE =
-      new AggregateJoinRemoveRule(LogicalAggregate.class, LogicalJoin.class,
-          RelFactories.LOGICAL_BUILDER);
+      AggregateJoinRemoveRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes an {@link Aggregate}
    * past a {@link Join}. */
   public static final AggregateJoinTransposeRule AGGREGATE_JOIN_TRANSPOSE =
-      new AggregateJoinTransposeRule(LogicalAggregate.class, LogicalJoin.class,
-          RelFactories.LOGICAL_BUILDER, false);
+      AggregateJoinTransposeRule.Config.DEFAULT.toRule();
 
   /** As {@link #AGGREGATE_JOIN_TRANSPOSE}, but extended to push down aggregate
    * functions. */
   public static final AggregateJoinTransposeRule AGGREGATE_JOIN_TRANSPOSE_EXTENDED =
-      new AggregateJoinTransposeRule(LogicalAggregate.class, LogicalJoin.class,
-          RelFactories.LOGICAL_BUILDER, true);
+      AggregateJoinTransposeRule.Config.EXTENDED.toRule();
 
   /** Rule that pushes an {@link Aggregate}
    * past a non-distinct {@link Union}. */
   public static final AggregateUnionTransposeRule AGGREGATE_UNION_TRANSPOSE =
-      new AggregateUnionTransposeRule(LogicalAggregate.class,
-          LogicalUnion.class, RelFactories.LOGICAL_BUILDER);
+      AggregateUnionTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that matches an {@link Aggregate} whose input is a {@link Union}
    * one of whose inputs is an {@code Aggregate}.
@@ -216,95 +160,85 @@ public class CoreRules {
    * use {@link #AGGREGATE_UNION_AGGREGATE_FIRST}
    * and {@link #AGGREGATE_UNION_AGGREGATE_SECOND} instead. */
   public static final AggregateUnionAggregateRule AGGREGATE_UNION_AGGREGATE =
-      new AggregateUnionAggregateRule(LogicalAggregate.class,
-          LogicalUnion.class, RelNode.class, RelNode.class,
-          RelFactories.LOGICAL_BUILDER, "AggregateUnionAggregateRule");
+      AggregateUnionAggregateRule.Config.DEFAULT.toRule();
 
   /** As {@link #AGGREGATE_UNION_AGGREGATE}, but matches an {@code Aggregate}
    * only as the left input of the {@code Union}. */
   public static final AggregateUnionAggregateRule AGGREGATE_UNION_AGGREGATE_FIRST =
-      new AggregateUnionAggregateRule(LogicalAggregate.class, LogicalUnion.class,
-          LogicalAggregate.class, RelNode.class, RelFactories.LOGICAL_BUILDER,
-          "AggregateUnionAggregateRule:first-input-agg");
+      AggregateUnionAggregateRule.Config.AGG_FIRST.toRule();
 
   /** As {@link #AGGREGATE_UNION_AGGREGATE}, but matches an {@code Aggregate}
    * only as the right input of the {@code Union}. */
   public static final AggregateUnionAggregateRule AGGREGATE_UNION_AGGREGATE_SECOND =
-      new AggregateUnionAggregateRule(LogicalAggregate.class, LogicalUnion.class,
-          RelNode.class, LogicalAggregate.class, RelFactories.LOGICAL_BUILDER,
-          "AggregateUnionAggregateRule:second-input-agg");
+      AggregateUnionAggregateRule.Config.AGG_SECOND.toRule();
 
   /** Rule that converts CASE-style filtered aggregates into true filtered
    * aggregates. */
   public static final AggregateCaseToFilterRule AGGREGATE_CASE_TO_FILTER =
-      new AggregateCaseToFilterRule(RelFactories.LOGICAL_BUILDER, null);
+      AggregateCaseToFilterRule.Config.DEFAULT.toRule();
 
   /** Rule that merges a {@link Calc} onto a {@code Calc}. */
   public static final CalcMergeRule CALC_MERGE =
-      new CalcMergeRule(RelFactories.LOGICAL_BUILDER);
+      CalcMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that removes a trivial {@link LogicalCalc}. */
   public static final CalcRemoveRule CALC_REMOVE =
-      new CalcRemoveRule(RelFactories.LOGICAL_BUILDER);
+      CalcRemoveRule.Config.DEFAULT.toRule();
 
   /** Rule that reduces operations on the DECIMAL type, such as casts or
    * arithmetic, into operations involving more primitive types such as BIGINT
    * and DOUBLE. */
   public static final ReduceDecimalsRule CALC_REDUCE_DECIMALS =
-      new ReduceDecimalsRule(RelFactories.LOGICAL_BUILDER);
+      ReduceDecimalsRule.Config.DEFAULT.toRule();
 
   /** Rule that reduces constants inside a {@link LogicalCalc}.
    *
    * @see #FILTER_REDUCE_EXPRESSIONS */
   public static final ReduceExpressionsRule.CalcReduceExpressionsRule
       CALC_REDUCE_EXPRESSIONS =
-      new ReduceExpressionsRule.CalcReduceExpressionsRule(LogicalCalc.class,
-          true, RelFactories.LOGICAL_BUILDER);
+      ReduceExpressionsRule.CalcReduceExpressionsRule.Config.DEFAULT.toRule();
 
   /** Rule that converts a {@link Calc} to a {@link Project} and
    * {@link Filter}. */
   public static final CalcSplitRule CALC_SPLIT =
-      new CalcSplitRule(RelFactories.LOGICAL_BUILDER);
+      CalcSplitRule.Config.DEFAULT.toRule();
 
   /** Rule that transforms a {@link Calc}
    * that contains windowed aggregates to a mixture of
    * {@link LogicalWindow} and {@code Calc}. */
   public static final ProjectToWindowRule.CalcToWindowRule CALC_TO_WINDOW =
-      new ProjectToWindowRule.CalcToWindowRule(RelFactories.LOGICAL_BUILDER);
+      ProjectToWindowRule.CalcToWindowRule.Config.DEFAULT.toRule();
 
   /** Rule that pre-casts inputs to a particular type. This can assist operator
    * implementations that impose requirements on their input types. */
   public static final CoerceInputsRule COERCE_INPUTS =
-      new CoerceInputsRule(RelNode.class, false, RelFactories.LOGICAL_BUILDER);
+      CoerceInputsRule.Config.DEFAULT.toRule();
 
   /** Rule that removes constants inside a {@link LogicalExchange}. */
-  @SuppressWarnings("deprecation")
   public static final ExchangeRemoveConstantKeysRule EXCHANGE_REMOVE_CONSTANT_KEYS =
-      new ExchangeRemoveConstantKeysRule(LogicalExchange.class,
-          "ExchangeRemoveConstantKeysRule");
+      ExchangeRemoveConstantKeysRule.Config.DEFAULT.toRule();
 
   /** Rule that removes constants inside a {@link LogicalSortExchange}. */
-  @SuppressWarnings("deprecation")
   public static final ExchangeRemoveConstantKeysRule SORT_EXCHANGE_REMOVE_CONSTANT_KEYS =
-      new ExchangeRemoveConstantKeysRule.SortExchangeRemoveConstantKeysRule(
-          LogicalSortExchange.class, "SortExchangeRemoveConstantKeysRule");
+      ExchangeRemoveConstantKeysRule.Config.SORT.toRule();
 
   /** Rule that tries to push filter expressions into a join
    * condition and into the inputs of the join. */
   public static final FilterJoinRule.FilterIntoJoinRule FILTER_INTO_JOIN =
-      new FilterJoinRule.FilterIntoJoinRule(true, RelFactories.LOGICAL_BUILDER,
-          (join, joinType, exp) -> true);
+      FilterJoinRule.FilterIntoJoinRule.Config.DEFAULT.toRule();
 
   /** Dumber version of {@link #FILTER_INTO_JOIN}. Not intended for production
    * use, but keeps some tests working for which {@code FILTER_INTO_JOIN} is too
    * smart. */
   public static final FilterJoinRule.FilterIntoJoinRule FILTER_INTO_JOIN_DUMB =
-      new FilterJoinRule.FilterIntoJoinRule(false, RelFactories.LOGICAL_BUILDER,
-          (join, joinType, exp) -> true);
+      FILTER_INTO_JOIN.config
+          .withSmart(false)
+          .as(FilterJoinRule.FilterIntoJoinRule.Config.class)
+          .toRule();
 
   /** Rule that combines two {@link LogicalFilter}s. */
   public static final FilterMergeRule FILTER_MERGE =
-      new FilterMergeRule(RelFactories.LOGICAL_BUILDER);
+      FilterMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that merges a {@link Filter} and a {@link LogicalCalc}. The
    * result is a {@link LogicalCalc} whose filter condition is the logical AND
@@ -312,20 +246,19 @@ public class CoreRules {
    *
    * @see #PROJECT_CALC_MERGE */
   public static final FilterCalcMergeRule FILTER_CALC_MERGE =
-      new FilterCalcMergeRule(RelFactories.LOGICAL_BUILDER);
+      FilterCalcMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that converts a {@link LogicalFilter} to a {@link LogicalCalc}.
    *
    * @see #PROJECT_TO_CALC */
   public static final FilterToCalcRule FILTER_TO_CALC =
-      new FilterToCalcRule(RelFactories.LOGICAL_BUILDER);
+      FilterToCalcRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Filter} past an {@link Aggregate}.
    *
    * @see #AGGREGATE_FILTER_TRANSPOSE */
   public static final FilterAggregateTransposeRule FILTER_AGGREGATE_TRANSPOSE =
-      new FilterAggregateTransposeRule(Filter.class,
-          RelFactories.LOGICAL_BUILDER, Aggregate.class);
+      FilterAggregateTransposeRule.Config.DEFAULT.toRule();
 
   /** The default instance of
    * {@link org.apache.calcite.rel.rules.FilterProjectTransposeRule}.
@@ -336,70 +269,45 @@ public class CoreRules {
    * {@link Correlate} from being de-correlated.
    */
   public static final FilterProjectTransposeRule FILTER_PROJECT_TRANSPOSE =
-      new FilterProjectTransposeRule(Filter.class, Project.class, true, true,
-          RelFactories.LOGICAL_BUILDER);
+      FilterProjectTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link LogicalFilter}
    * past a {@link LogicalTableFunctionScan}. */
   public static final FilterTableFunctionTransposeRule
       FILTER_TABLE_FUNCTION_TRANSPOSE =
-      new FilterTableFunctionTransposeRule(RelFactories.LOGICAL_BUILDER);
+      FilterTableFunctionTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that matches a {@link Filter} on a {@link TableScan}. */
   public static final FilterTableScanRule FILTER_SCAN =
-      new FilterTableScanRule(
-          operand(Filter.class,
-              operandJ(TableScan.class, null, FilterTableScanRule::test,
-                  none())),
-          RelFactories.LOGICAL_BUILDER,
-          "FilterTableScanRule") {
-        public void onMatch(RelOptRuleCall call) {
-          final Filter filter = call.rel(0);
-          final TableScan scan = call.rel(1);
-          apply(call, filter, scan);
-        }
-      };
+      FilterTableScanRule.Config.DEFAULT.toRule();
 
   /** Rule that matches a {@link Filter} on an
    * {@link org.apache.calcite.adapter.enumerable.EnumerableInterpreter} on a
    * {@link TableScan}. */
   public static final FilterTableScanRule FILTER_INTERPRETER_SCAN =
-      new FilterTableScanRule(
-          operand(Filter.class,
-              operand(
-                  EnumerableInterpreter.class,
-                  operandJ(TableScan.class, null, FilterTableScanRule::test,
-                      none()))),
-          RelFactories.LOGICAL_BUILDER,
-          "FilterTableScanRule:interpreter") {
-        public void onMatch(RelOptRuleCall call) {
-          final Filter filter = call.rel(0);
-          final TableScan scan = call.rel(2);
-          apply(call, filter, scan);
-        }
-      };
+      FilterTableScanRule.Config.INTERPRETER.toRule();
 
   /** Rule that pushes a {@link Filter} above a {@link Correlate} into the
    * inputs of the {@code Correlate}. */
   public static final FilterCorrelateRule FILTER_CORRELATE =
-      new FilterCorrelateRule(RelFactories.LOGICAL_BUILDER);
+      FilterCorrelateRule.Config.DEFAULT.toRule();
 
   /** Rule that merges a {@link Filter} into a {@link MultiJoin},
    * creating a richer {@code MultiJoin}.
    *
    * @see #PROJECT_MULTI_JOIN_MERGE */
   public static final FilterMultiJoinMergeRule FILTER_MULTI_JOIN_MERGE =
-      new FilterMultiJoinMergeRule(RelFactories.LOGICAL_BUILDER);
+      FilterMultiJoinMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that replaces {@code IS NOT DISTINCT FROM}
    * in a {@link Filter} with logically equivalent operations. */
   public static final FilterRemoveIsNotDistinctFromRule
       FILTER_EXPAND_IS_NOT_DISTINCT_FROM =
-      new FilterRemoveIsNotDistinctFromRule(RelFactories.LOGICAL_BUILDER);
+      FilterRemoveIsNotDistinctFromRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Filter} past a {@link SetOp}. */
   public static final FilterSetOpTransposeRule FILTER_SET_OP_TRANSPOSE =
-      new FilterSetOpTransposeRule(RelFactories.LOGICAL_BUILDER);
+      FilterSetOpTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that reduces constants inside a {@link LogicalFilter}.
    *
@@ -410,73 +318,65 @@ public class CoreRules {
    */
   public static final ReduceExpressionsRule.FilterReduceExpressionsRule
       FILTER_REDUCE_EXPRESSIONS =
-      new ReduceExpressionsRule.FilterReduceExpressionsRule(LogicalFilter.class,
-          false, RelFactories.LOGICAL_BUILDER);
+      ReduceExpressionsRule.FilterReduceExpressionsRule.Config.DEFAULT.toRule();
 
   /** Rule that flattens an {@link Intersect} on an {@code Intersect}
    * into a single {@code Intersect}. */
   public static final UnionMergeRule INTERSECT_MERGE =
-      new UnionMergeRule(LogicalIntersect.class, "IntersectMergeRule",
-          RelFactories.LOGICAL_BUILDER);
+      UnionMergeRule.Config.INTERSECT.toRule();
 
   /** Rule that translates a distinct
    * {@link Intersect} into a group of operators
    * composed of {@link Union}, {@link Aggregate}, etc. */
   public static final IntersectToDistinctRule INTERSECT_TO_DISTINCT =
-      new IntersectToDistinctRule(LogicalIntersect.class, RelFactories.LOGICAL_BUILDER);
+      IntersectToDistinctRule.Config.DEFAULT.toRule();
 
   /** Rule that converts a {@link LogicalMatch} to the result of calling
    * {@link LogicalMatch#copy}. */
-  public static final MatchRule MATCH = new MatchRule();
+  public static final MatchRule MATCH = MatchRule.Config.DEFAULT.toRule();
 
   /** Rule that flattens a {@link Minus} on a {@code Minus}
    * into a single {@code Minus}. */
   public static final UnionMergeRule MINUS_MERGE =
-      new UnionMergeRule(LogicalMinus.class, "MinusMergeRule",
-          RelFactories.LOGICAL_BUILDER);
+      UnionMergeRule.Config.MINUS.toRule();
 
   /** Rule that merges a {@link LogicalProject} and a {@link LogicalCalc}.
    *
    * @see #FILTER_CALC_MERGE */
   public static final ProjectCalcMergeRule PROJECT_CALC_MERGE =
-      new ProjectCalcMergeRule(RelFactories.LOGICAL_BUILDER);
+      ProjectCalcMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that matches a {@link Project} on a {@link Correlate} and
    * pushes the projections to the Correlate's left and right inputs. */
   public static final ProjectCorrelateTransposeRule PROJECT_CORRELATE_TRANSPOSE =
-      new ProjectCorrelateTransposeRule(expr -> !(expr instanceof RexOver),
-          RelFactories.LOGICAL_BUILDER);
+      ProjectCorrelateTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Project} past a {@link Filter}.
    *
    * @see #PROJECT_FILTER_TRANSPOSE_WHOLE_PROJECT_EXPRESSIONS
    * @see #PROJECT_FILTER_TRANSPOSE_WHOLE_EXPRESSIONS */
   public static final ProjectFilterTransposeRule PROJECT_FILTER_TRANSPOSE =
-      new ProjectFilterTransposeRule(LogicalProject.class, LogicalFilter.class,
-          RelFactories.LOGICAL_BUILDER, expr -> false, false, false);
+      ProjectFilterTransposeRule.Config.DEFAULT.toRule();
 
   /** As {@link #PROJECT_FILTER_TRANSPOSE}, but pushes down project and filter
    * expressions whole, not field references. */
   public static final ProjectFilterTransposeRule
       PROJECT_FILTER_TRANSPOSE_WHOLE_EXPRESSIONS =
-      new ProjectFilterTransposeRule(LogicalProject.class, LogicalFilter.class,
-          RelFactories.LOGICAL_BUILDER, expr -> false, true, true);
+      ProjectFilterTransposeRule.Config.PROJECT_FILTER.toRule();
 
   /** As {@link #PROJECT_FILTER_TRANSPOSE},
    * pushes down field references for filters,
    * but pushes down project expressions whole. */
   public static final ProjectFilterTransposeRule
       PROJECT_FILTER_TRANSPOSE_WHOLE_PROJECT_EXPRESSIONS =
-      new ProjectFilterTransposeRule(LogicalProject.class, LogicalFilter.class,
-          RelFactories.LOGICAL_BUILDER, expr -> false, true, false);
+      ProjectFilterTransposeRule.Config.PROJECT.toRule();
 
   /** Rule that reduces constants inside a {@link LogicalProject}.
    *
    * @see #FILTER_REDUCE_EXPRESSIONS */
   public static final ReduceExpressionsRule.ProjectReduceExpressionsRule
       PROJECT_REDUCE_EXPRESSIONS =
-      new ReduceExpressionsRule.ProjectReduceExpressionsRule(
-          LogicalProject.class, true, RelFactories.LOGICAL_BUILDER);
+      ReduceExpressionsRule.ProjectReduceExpressionsRule.Config.DEFAULT.toRule();
 
   /** Rule that converts sub-queries from project expressions into
    * {@link Correlate} instances.
@@ -484,7 +384,7 @@ public class CoreRules {
    * @see #FILTER_SUB_QUERY_TO_CORRELATE
    * @see #JOIN_SUB_QUERY_TO_CORRELATE */
   public static final SubQueryRemoveRule PROJECT_SUB_QUERY_TO_CORRELATE =
-      new SubQueryRemoveRule.SubQueryProjectRemoveRule(RelFactories.LOGICAL_BUILDER);
+      SubQueryRemoveRule.Config.PROJECT.toRule();
 
   /** Rule that converts a sub-queries from filter expressions into
    * {@link Correlate} instances.
@@ -492,7 +392,7 @@ public class CoreRules {
    * @see #PROJECT_SUB_QUERY_TO_CORRELATE
    * @see #JOIN_SUB_QUERY_TO_CORRELATE */
   public static final SubQueryRemoveRule FILTER_SUB_QUERY_TO_CORRELATE =
-      new SubQueryRemoveRule.SubQueryFilterRemoveRule(RelFactories.LOGICAL_BUILDER);
+      SubQueryRemoveRule.Config.FILTER.toRule();
 
   /** Rule that converts sub-queries from join expressions into
    * {@link Correlate} instances.
@@ -500,15 +400,15 @@ public class CoreRules {
    * @see #PROJECT_SUB_QUERY_TO_CORRELATE
    * @see #FILTER_SUB_QUERY_TO_CORRELATE */
   public static final SubQueryRemoveRule JOIN_SUB_QUERY_TO_CORRELATE =
-      new SubQueryRemoveRule.SubQueryJoinRemoveRule(RelFactories.LOGICAL_BUILDER);
+      SubQueryRemoveRule.Config.JOIN.toRule();
 
   /** Rule that transforms a {@link Project}
    *  into a mixture of {@code LogicalProject}
    * and {@link LogicalWindow}. */
   public static final ProjectToWindowRule.ProjectToLogicalProjectAndWindowRule
       PROJECT_TO_LOGICAL_PROJECT_AND_WINDOW =
-      new ProjectToWindowRule.ProjectToLogicalProjectAndWindowRule(
-          RelFactories.LOGICAL_BUILDER);
+      ProjectToWindowRule.ProjectToLogicalProjectAndWindowRule.Config.DEFAULT
+          .toRule();
 
   /** Rule that creates a {@link Join#isSemiJoin semi-join} from a
    * {@link Project} on top of a {@link Join} with an {@link Aggregate} as its
@@ -516,36 +416,30 @@ public class CoreRules {
    *
    * @see #JOIN_TO_SEMI_JOIN */
   public static final SemiJoinRule.ProjectToSemiJoinRule PROJECT_TO_SEMI_JOIN =
-      new SemiJoinRule.ProjectToSemiJoinRule(Project.class, Join.class, Aggregate.class,
-          RelFactories.LOGICAL_BUILDER, "SemiJoinRule:project");
+      SemiJoinRule.ProjectToSemiJoinRule.Config.DEFAULT.toRule();
 
   /** Rule that matches an {@link Project} on a {@link Join} and removes the
    * left input of the join provided that the left input is also a left join. */
   public static final ProjectJoinJoinRemoveRule PROJECT_JOIN_JOIN_REMOVE =
-      new ProjectJoinJoinRemoveRule(LogicalProject.class,
-          LogicalJoin.class, RelFactories.LOGICAL_BUILDER);
+      ProjectJoinJoinRemoveRule.Config.DEFAULT.toRule();
 
   /** Rule that matches an {@link Project} on a {@link Join} and removes the
    * join provided that the join is a left join or right join and the join keys
    * are unique. */
   public static final ProjectJoinRemoveRule PROJECT_JOIN_REMOVE =
-      new ProjectJoinRemoveRule(LogicalProject.class,
-          LogicalJoin.class, RelFactories.LOGICAL_BUILDER);
+      ProjectJoinRemoveRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link LogicalProject} past a {@link LogicalJoin}
    * by splitting the projection into a projection on top of each child of
    * the join. */
   public static final ProjectJoinTransposeRule PROJECT_JOIN_TRANSPOSE =
-      new ProjectJoinTransposeRule(
-          LogicalProject.class, LogicalJoin.class,
-          expr -> !(expr instanceof RexOver),
-          RelFactories.LOGICAL_BUILDER);
+      ProjectJoinTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that merges a {@link Project} into another {@link Project},
    * provided the projects are not projecting identical sets of input
    * references. */
   public static final ProjectMergeRule PROJECT_MERGE =
-      new ProjectMergeRule(true, DEFAULT_BLOAT, RelFactories.LOGICAL_BUILDER);
+      ProjectMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link LogicalProject} past a {@link SetOp}.
    *
@@ -553,20 +447,19 @@ public class CoreRules {
    * only the {@link RexInputRef}s referenced in the original
    * {@code LogicalProject}. */
   public static final ProjectSetOpTransposeRule PROJECT_SET_OP_TRANSPOSE =
-      new ProjectSetOpTransposeRule(expr -> !(expr instanceof RexOver),
-          RelFactories.LOGICAL_BUILDER);
+      ProjectSetOpTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Project} into a {@link MultiJoin},
    * creating a richer {@code MultiJoin}.
    *
    * @see #FILTER_MULTI_JOIN_MERGE */
   public static final ProjectMultiJoinMergeRule PROJECT_MULTI_JOIN_MERGE =
-      new ProjectMultiJoinMergeRule(RelFactories.LOGICAL_BUILDER);
+      ProjectMultiJoinMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that, given a {@link Project} node that merely returns its input,
    *  converts the node into its input. */
   public static final ProjectRemoveRule PROJECT_REMOVE =
-      new ProjectRemoveRule(RelFactories.LOGICAL_BUILDER);
+      ProjectRemoveRule.Config.DEFAULT.toRule();
 
   /** Rule that converts a {@link Project} on a {@link TableScan}
    * of a {@link org.apache.calcite.schema.ProjectableFilterableTable}
@@ -574,148 +467,94 @@ public class CoreRules {
    *
    * @see #PROJECT_INTERPRETER_TABLE_SCAN */
   public static final ProjectTableScanRule PROJECT_TABLE_SCAN =
-      new ProjectTableScanRule(
-          operand(Project.class,
-              operandJ(TableScan.class, null, ProjectTableScanRule::test,
-                  none())),
-          RelFactories.LOGICAL_BUILDER,
-          "ProjectScanRule") {
-        @Override public void onMatch(RelOptRuleCall call) {
-          final Project project = call.rel(0);
-          final TableScan scan = call.rel(1);
-          apply(call, project, scan);
-        }
-      };
+      ProjectTableScanRule.Config.DEFAULT.toRule();
 
   /** As {@link #PROJECT_TABLE_SCAN}, but with an intervening
    * {@link org.apache.calcite.adapter.enumerable.EnumerableInterpreter}. */
   public static final ProjectTableScanRule PROJECT_INTERPRETER_TABLE_SCAN =
-      new ProjectTableScanRule(
-          operand(Project.class,
-              operand(EnumerableInterpreter.class,
-                  operandJ(TableScan.class, null, ProjectTableScanRule::test,
-                      none()))),
-          RelFactories.LOGICAL_BUILDER,
-          "ProjectScanRule:interpreter") {
-        @Override public void onMatch(RelOptRuleCall call) {
-          final Project project = call.rel(0);
-          final TableScan scan = call.rel(2);
-          apply(call, project, scan);
-        }
-      };
+      ProjectTableScanRule.Config.INTERPRETER.toRule();
 
   /** Rule that converts a {@link LogicalProject} to a {@link LogicalCalc}.
    *
    * @see #FILTER_TO_CALC */
   public static final ProjectToCalcRule PROJECT_TO_CALC =
-      new ProjectToCalcRule(RelFactories.LOGICAL_BUILDER);
+      ProjectToCalcRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link LogicalProject} past a {@link LogicalWindow}. */
   public static final ProjectWindowTransposeRule PROJECT_WINDOW_TRANSPOSE =
-      new ProjectWindowTransposeRule(RelFactories.LOGICAL_BUILDER);
+      ProjectWindowTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes predicates in a Join into the inputs to the join. */
   public static final FilterJoinRule.JoinConditionPushRule JOIN_CONDITION_PUSH =
-      new FilterJoinRule.JoinConditionPushRule(RelFactories.LOGICAL_BUILDER,
-          (join, joinType, exp) -> true);
+      FilterJoinRule.JoinConditionPushRule.Config.DEFAULT.toRule();
 
   /** Rule to add a semi-join into a {@link Join}. */
   public static final JoinAddRedundantSemiJoinRule JOIN_ADD_REDUNDANT_SEMI_JOIN =
-      new JoinAddRedundantSemiJoinRule(LogicalJoin.class,
-          RelFactories.LOGICAL_BUILDER);
+      JoinAddRedundantSemiJoinRule.Config.DEFAULT.toRule();
 
   /** Rule that changes a join based on the associativity rule,
    * ((a JOIN b) JOIN c) &rarr; (a JOIN (b JOIN c)). */
   public static final JoinAssociateRule JOIN_ASSOCIATE =
-      new JoinAssociateRule(RelFactories.LOGICAL_BUILDER);
+      JoinAssociateRule.Config.DEFAULT.toRule();
 
   /** Rule that permutes the inputs to an inner {@link Join}. */
   public static final JoinCommuteRule JOIN_COMMUTE =
-      new JoinCommuteRule(false);
+      JoinCommuteRule.Config.DEFAULT.toRule();
 
   /** As {@link #JOIN_COMMUTE} but swaps outer joins as well as inner joins. */
   public static final JoinCommuteRule JOIN_COMMUTE_OUTER =
-      new JoinCommuteRule(true);
+      JoinCommuteRule.Config.DEFAULT.withSwapOuter(true).toRule();
 
   /** Rule to convert an
    * {@link LogicalJoin inner join} to a
    * {@link LogicalFilter filter} on top of a
    * {@link LogicalJoin cartesian inner join}. */
   public static final JoinExtractFilterRule JOIN_EXTRACT_FILTER =
-      new JoinExtractFilterRule(LogicalJoin.class,
-          RelFactories.LOGICAL_BUILDER);
+      JoinExtractFilterRule.Config.DEFAULT.toRule();
 
   /** Rule that matches a {@link LogicalJoin} whose inputs are
    * {@link LogicalProject}s, and pulls the project expressions up. */
   public static final JoinProjectTransposeRule JOIN_PROJECT_BOTH_TRANSPOSE =
-      new JoinProjectTransposeRule(
-          operand(LogicalJoin.class,
-              operand(LogicalProject.class, any()),
-              operand(LogicalProject.class, any())),
-          "JoinProjectTransposeRule(Project-Project)");
+      JoinProjectTransposeRule.Config.DEFAULT.toRule();
 
   /** As {@link #JOIN_PROJECT_BOTH_TRANSPOSE} but only the left input is
    * a {@link LogicalProject}. */
   public static final JoinProjectTransposeRule JOIN_PROJECT_LEFT_TRANSPOSE =
-      new JoinProjectTransposeRule(
-          operand(LogicalJoin.class,
-              some(operand(LogicalProject.class, any()))),
-          "JoinProjectTransposeRule(Project-Other)");
+      JoinProjectTransposeRule.Config.LEFT.toRule();
 
   /** As {@link #JOIN_PROJECT_BOTH_TRANSPOSE} but only the right input is
    * a {@link LogicalProject}. */
   public static final JoinProjectTransposeRule JOIN_PROJECT_RIGHT_TRANSPOSE =
-      new JoinProjectTransposeRule(
-          operand(
-              LogicalJoin.class,
-              operand(RelNode.class, any()),
-              operand(LogicalProject.class, any())),
-          "JoinProjectTransposeRule(Other-Project)");
+      JoinProjectTransposeRule.Config.RIGHT.toRule();
 
   /** As {@link #JOIN_PROJECT_BOTH_TRANSPOSE} but match outer as well as
    * inner join. */
   public static final JoinProjectTransposeRule
       JOIN_PROJECT_BOTH_TRANSPOSE_INCLUDE_OUTER =
-      new JoinProjectTransposeRule(
-          operand(LogicalJoin.class,
-              operand(LogicalProject.class, any()),
-              operand(LogicalProject.class, any())),
-          "Join(IncludingOuter)ProjectTransposeRule(Project-Project)",
-          true, RelFactories.LOGICAL_BUILDER);
+      JoinProjectTransposeRule.Config.OUTER.toRule();
 
   /** As {@link #JOIN_PROJECT_LEFT_TRANSPOSE} but match outer as well as
    * inner join. */
   public static final JoinProjectTransposeRule
       JOIN_PROJECT_LEFT_TRANSPOSE_INCLUDE_OUTER =
-      new JoinProjectTransposeRule(
-          operand(LogicalJoin.class,
-              some(operand(LogicalProject.class, any()))),
-          "Join(IncludingOuter)ProjectTransposeRule(Project-Other)",
-          true, RelFactories.LOGICAL_BUILDER);
+      JoinProjectTransposeRule.Config.LEFT_OUTER.toRule();
 
   /** As {@link #JOIN_PROJECT_RIGHT_TRANSPOSE} but match outer as well as
    * inner join. */
   public static final JoinProjectTransposeRule
       JOIN_PROJECT_RIGHT_TRANSPOSE_INCLUDE_OUTER =
-      new JoinProjectTransposeRule(
-          operand(
-              LogicalJoin.class,
-              operand(RelNode.class, any()),
-              operand(LogicalProject.class, any())),
-          "Join(IncludingOuter)ProjectTransposeRule(Other-Project)",
-          true, RelFactories.LOGICAL_BUILDER);
+      JoinProjectTransposeRule.Config.RIGHT_OUTER.toRule();
 
   /** Rule that matches a {@link Join} and pushes down expressions on either
    * side of "equal" conditions. */
   public static final JoinPushExpressionsRule JOIN_PUSH_EXPRESSIONS =
-      new JoinPushExpressionsRule(Join.class, RelFactories.LOGICAL_BUILDER);
+      JoinPushExpressionsRule.Config.DEFAULT.toRule();
 
   /** Rule that infers predicates from on a {@link Join} and creates
    * {@link Filter}s if those predicates can be pushed to its inputs. */
   public static final JoinPushTransitivePredicatesRule
       JOIN_PUSH_TRANSITIVE_PREDICATES =
-      new JoinPushTransitivePredicatesRule(Join.class,
-          RelFactories.LOGICAL_BUILDER);
+      JoinPushTransitivePredicatesRule.Config.DEFAULT.toRule();
 
   /** Rule that reduces constants inside a {@link Join}.
    *
@@ -723,47 +562,34 @@ public class CoreRules {
    * @see #PROJECT_REDUCE_EXPRESSIONS */
   public static final ReduceExpressionsRule.JoinReduceExpressionsRule
       JOIN_REDUCE_EXPRESSIONS =
-      new ReduceExpressionsRule.JoinReduceExpressionsRule(Join.class, false,
-          RelFactories.LOGICAL_BUILDER);
+      ReduceExpressionsRule.JoinReduceExpressionsRule.Config.DEFAULT.toRule();
 
   /** Rule that converts a {@link LogicalJoin}
    * into a {@link LogicalCorrelate}. */
   public static final JoinToCorrelateRule JOIN_TO_CORRELATE =
-      new JoinToCorrelateRule(LogicalJoin.class, RelFactories.LOGICAL_BUILDER,
-          "JoinToCorrelateRule");
+      JoinToCorrelateRule.Config.DEFAULT.toRule();
 
   /** Rule that flattens a tree of {@link LogicalJoin}s
    * into a single {@link MultiJoin} with N inputs. */
   public static final JoinToMultiJoinRule JOIN_TO_MULTI_JOIN =
-      new JoinToMultiJoinRule(LogicalJoin.class, RelFactories.LOGICAL_BUILDER);
+      JoinToMultiJoinRule.Config.DEFAULT.toRule();
 
   /** Rule that creates a {@link Join#isSemiJoin semi-join} from a
    * {@link Join} with an empty {@link Aggregate} as its right input.
    *
    * @see #PROJECT_TO_SEMI_JOIN */
   public static final SemiJoinRule.JoinToSemiJoinRule JOIN_TO_SEMI_JOIN =
-      new SemiJoinRule.JoinToSemiJoinRule(Join.class, Aggregate.class,
-          RelFactories.LOGICAL_BUILDER, "SemiJoinRule:join");
+      SemiJoinRule.JoinToSemiJoinRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Join}
    * past a non-distinct {@link Union} as its left input. */
   public static final JoinUnionTransposeRule JOIN_LEFT_UNION_TRANSPOSE =
-      new JoinUnionTransposeRule(
-          operand(Join.class,
-              operand(Union.class, any()),
-              operand(RelNode.class, any())),
-          RelFactories.LOGICAL_BUILDER,
-          "JoinUnionTransposeRule(Union-Other)");
+      JoinUnionTransposeRule.Config.LEFT.toRule();
 
   /** Rule that pushes a {@link Join}
    * past a non-distinct {@link Union} as its right input. */
   public static final JoinUnionTransposeRule JOIN_RIGHT_UNION_TRANSPOSE =
-      new JoinUnionTransposeRule(
-          operand(Join.class,
-              operand(RelNode.class, any()),
-              operand(Union.class, any())),
-          RelFactories.LOGICAL_BUILDER,
-          "JoinUnionTransposeRule(Other-Union)");
+      JoinUnionTransposeRule.Config.RIGHT.toRule();
 
   /** Rule that re-orders a {@link Join} using a heuristic planner.
    *
@@ -773,7 +599,7 @@ public class CoreRules {
    * @see #JOIN_TO_MULTI_JOIN
    * @see #MULTI_JOIN_OPTIMIZE_BUSHY */
   public static final LoptOptimizeJoinRule MULTI_JOIN_OPTIMIZE =
-      new LoptOptimizeJoinRule(RelFactories.LOGICAL_BUILDER);
+      LoptOptimizeJoinRule.Config.DEFAULT.toRule();
 
   /** Rule that finds an approximately optimal ordering for join operators
    * using a heuristic algorithm and can handle bushy joins.
@@ -784,7 +610,23 @@ public class CoreRules {
    * @see #MULTI_JOIN_OPTIMIZE
    */
   public static final MultiJoinOptimizeBushyRule MULTI_JOIN_OPTIMIZE_BUSHY =
-      new MultiJoinOptimizeBushyRule(RelFactories.LOGICAL_BUILDER);
+      MultiJoinOptimizeBushyRule.Config.DEFAULT.toRule();
+
+  /** Rule that matches a {@link LogicalJoin} whose inputs are both a
+   * {@link MultiJoin} with intervening {@link LogicalProject}s,
+   * and pulls the Projects up above the Join. */
+  public static final MultiJoinProjectTransposeRule MULTI_JOIN_BOTH_PROJECT =
+      MultiJoinProjectTransposeRule.Config.BOTH_PROJECT.toRule();
+
+  /** As {@link #MULTI_JOIN_BOTH_PROJECT} but only the left input has
+   * an intervening Project. */
+  public static final MultiJoinProjectTransposeRule MULTI_JOIN_LEFT_PROJECT =
+      MultiJoinProjectTransposeRule.Config.LEFT_PROJECT.toRule();
+
+  /** As {@link #MULTI_JOIN_BOTH_PROJECT} but only the right input has
+   * an intervening Project. */
+  public static final MultiJoinProjectTransposeRule MULTI_JOIN_RIGHT_PROJECT =
+      MultiJoinProjectTransposeRule.Config.RIGHT_PROJECT.toRule();
 
   /** Rule that pushes a {@link Join#isSemiJoin semi-join} down in a tree past
    * a {@link Filter}.
@@ -792,16 +634,15 @@ public class CoreRules {
    * @see #SEMI_JOIN_PROJECT_TRANSPOSE
    * @see #SEMI_JOIN_JOIN_TRANSPOSE */
   public static final SemiJoinFilterTransposeRule SEMI_JOIN_FILTER_TRANSPOSE =
-      new SemiJoinFilterTransposeRule(RelFactories.LOGICAL_BUILDER);
+      SemiJoinFilterTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Join#isSemiJoin semi-join} down in a tree past
    * a {@link Project}.
    *
    * @see #SEMI_JOIN_FILTER_TRANSPOSE
    * @see #SEMI_JOIN_JOIN_TRANSPOSE */
-  @SuppressWarnings("deprecation")
   public static final SemiJoinProjectTransposeRule SEMI_JOIN_PROJECT_TRANSPOSE =
-      new SemiJoinProjectTransposeRule(RelFactories.LOGICAL_BUILDER);
+      SemiJoinProjectTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Join#isSemiJoin semi-join} down in a tree past a
    * {@link Join}.
@@ -809,11 +650,11 @@ public class CoreRules {
    * @see #SEMI_JOIN_FILTER_TRANSPOSE
    * @see #SEMI_JOIN_PROJECT_TRANSPOSE */
   public static final SemiJoinJoinTransposeRule SEMI_JOIN_JOIN_TRANSPOSE =
-      new SemiJoinJoinTransposeRule(RelFactories.LOGICAL_BUILDER);
+      SemiJoinJoinTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that removes a {@link Join#isSemiJoin semi-join} from a join tree. */
   public static final SemiJoinRemoveRule SEMI_JOIN_REMOVE =
-      new SemiJoinRemoveRule(RelFactories.LOGICAL_BUILDER);
+      SemiJoinRemoveRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Sort} past a {@link Union}.
    *
@@ -822,105 +663,78 @@ public class CoreRules {
    * if the Sort does not have a limit, i.e., {@link Sort#fetch} is null.
    *
    * @see #SORT_UNION_TRANSPOSE_MATCH_NULL_FETCH */
-  @SuppressWarnings("deprecation")
   public static final SortUnionTransposeRule SORT_UNION_TRANSPOSE =
-      new SortUnionTransposeRule(false);
+      SortUnionTransposeRule.Config.DEFAULT.toRule();
 
   /** As {@link #SORT_UNION_TRANSPOSE}, but for a Union implementation that
    * preserves the ordering of its inputs. It is still worth applying this rule
    * even if the Sort does not have a limit, for the merge of already sorted
    * inputs that the Union can do is usually cheap. */
-  @SuppressWarnings("deprecation")
   public static final SortUnionTransposeRule SORT_UNION_TRANSPOSE_MATCH_NULL_FETCH =
-      new SortUnionTransposeRule(true);
+      SortUnionTransposeRule.Config.DEFAULT.withMatchNullFetch(true).toRule();
 
   /** Rule that copies a {@link Sort} past a {@link Join} without its limit and
    * offset. The original {@link Sort} is preserved but can potentially be
    * removed by {@link #SORT_REMOVE} if redundant. */
   public static final SortJoinCopyRule SORT_JOIN_COPY =
-      new SortJoinCopyRule(LogicalSort.class,
-          LogicalJoin.class, RelFactories.LOGICAL_BUILDER);
+      SortJoinCopyRule.Config.DEFAULT.toRule();
 
   /** Rule that removes a {@link Sort} if its input is already sorted. */
   public static final SortRemoveRule SORT_REMOVE =
-      new SortRemoveRule(RelFactories.LOGICAL_BUILDER);
+      SortRemoveRule.Config.DEFAULT.toRule();
 
   /** Rule that removes keys from a {@link Sort}
    * if those keys are known to be constant, or removes the entire Sort if all
    * keys are constant. */
-  @SuppressWarnings("deprecation")
   public static final SortRemoveConstantKeysRule SORT_REMOVE_CONSTANT_KEYS =
-      new SortRemoveConstantKeysRule();
+      SortRemoveConstantKeysRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Sort} past a {@link Join}. */
   public static final SortJoinTransposeRule SORT_JOIN_TRANSPOSE =
-      new SortJoinTransposeRule(LogicalSort.class,
-          LogicalJoin.class, RelFactories.LOGICAL_BUILDER);
+      SortJoinTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that pushes a {@link Sort} past a {@link Project}. */
   public static final SortProjectTransposeRule SORT_PROJECT_TRANSPOSE =
-      new SortProjectTransposeRule(Sort.class, LogicalProject.class,
-          RelFactories.LOGICAL_BUILDER, null);
+      SortProjectTransposeRule.Config.DEFAULT.toRule();
 
   /** Rule that flattens a {@link Union} on a {@code Union}
    * into a single {@code Union}. */
   public static final UnionMergeRule UNION_MERGE =
-      new UnionMergeRule(LogicalUnion.class, "UnionMergeRule",
-          RelFactories.LOGICAL_BUILDER);
+      UnionMergeRule.Config.DEFAULT.toRule();
 
   /** Rule that removes a {@link Union} if it has only one input.
    *
    * @see PruneEmptyRules#UNION_INSTANCE */
   public static final UnionEliminatorRule UNION_REMOVE =
-      new UnionEliminatorRule(LogicalUnion.class, RelFactories.LOGICAL_BUILDER);
+      UnionEliminatorRule.Config.DEFAULT.toRule();
 
   /** Rule that pulls up constants through a Union operator. */
   public static final UnionPullUpConstantsRule UNION_PULL_UP_CONSTANTS =
-      new UnionPullUpConstantsRule(Union.class, RelFactories.LOGICAL_BUILDER);
+       UnionPullUpConstantsRule.Config.DEFAULT.toRule();
 
   /** Rule that translates a distinct {@link Union}
    * (<code>all</code> = <code>false</code>)
    * into an {@link Aggregate} on top of a non-distinct {@link Union}
    * (<code>all</code> = <code>true</code>). */
   public static final UnionToDistinctRule UNION_TO_DISTINCT =
-      new UnionToDistinctRule(LogicalUnion.class, RelFactories.LOGICAL_BUILDER);
+      UnionToDistinctRule.Config.DEFAULT.toRule();
 
   /** Rule that applies an {@link Aggregate} to a {@link Values} (currently just
    * an empty {@code Values}). */
   public static final AggregateValuesRule AGGREGATE_VALUES =
-      new AggregateValuesRule(RelFactories.LOGICAL_BUILDER);
+      AggregateValuesRule.Config.DEFAULT.toRule();
 
   /** Rule that merges a {@link Filter} onto an underlying
    * {@link org.apache.calcite.rel.logical.LogicalValues},
    * resulting in a {@code Values} with potentially fewer rows. */
   public static final ValuesReduceRule FILTER_VALUES_MERGE =
-      new ValuesReduceRule(
-          operand(LogicalFilter.class,
-              operandJ(LogicalValues.class, null, Values::isNotEmpty, none())),
-          RelFactories.LOGICAL_BUILDER,
-          "ValuesReduceRule(Filter)") {
-        public void onMatch(RelOptRuleCall call) {
-          LogicalFilter filter = call.rel(0);
-          LogicalValues values = call.rel(1);
-          apply(call, null, filter, values);
-        }
-      };
+      ValuesReduceRule.Config.FILTER.toRule();
 
   /** Rule that merges a {@link Project} onto an underlying
    * {@link org.apache.calcite.rel.logical.LogicalValues},
    * resulting in a {@code Values} with different columns. */
   public static final ValuesReduceRule PROJECT_VALUES_MERGE =
-      new ValuesReduceRule(
-          operand(LogicalProject.class,
-              operandJ(LogicalValues.class, null, Values::isNotEmpty, none())),
-          RelFactories.LOGICAL_BUILDER,
-          "ValuesReduceRule(Project)") {
-        public void onMatch(RelOptRuleCall call) {
-          LogicalProject project = call.rel(0);
-          LogicalValues values = call.rel(1);
-          apply(call, project, null, values);
-        }
-      };
+      ValuesReduceRule.Config.PROJECT.toRule();
 
   /** Rule that merges a {@link Project}
    * on top of a {@link Filter} onto an underlying
@@ -928,26 +742,12 @@ public class CoreRules {
    * resulting in a {@code Values} with different columns
    * and potentially fewer rows. */
   public static final ValuesReduceRule PROJECT_FILTER_VALUES_MERGE =
-      new ValuesReduceRule(
-          operand(LogicalProject.class,
-              operand(LogicalFilter.class,
-                  operandJ(LogicalValues.class, null, Values::isNotEmpty,
-                      none()))),
-          RelFactories.LOGICAL_BUILDER,
-          "ValuesReduceRule(Project-Filter)") {
-        public void onMatch(RelOptRuleCall call) {
-          LogicalProject project = call.rel(0);
-          LogicalFilter filter = call.rel(1);
-          LogicalValues values = call.rel(2);
-          apply(call, project, filter, values);
-        }
-      };
+      ValuesReduceRule.Config.PROJECT_FILTER.toRule();
 
   /** Rule that reduces constants inside a {@link LogicalWindow}.
    *
    * @see #FILTER_REDUCE_EXPRESSIONS */
   public static final ReduceExpressionsRule.WindowReduceExpressionsRule
       WINDOW_REDUCE_EXPRESSIONS =
-      new ReduceExpressionsRule.WindowReduceExpressionsRule(LogicalWindow.class,
-          true, RelFactories.LOGICAL_BUILDER);
+      ReduceExpressionsRule.WindowReduceExpressionsRule.Config.DEFAULT.toRule();
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/DateRangeRules.java b/core/src/main/java/org/apache/calcite/rel/rules/DateRangeRules.java
index 9e1dac4..ba2a777 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/DateRangeRules.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/DateRangeRules.java
@@ -21,8 +21,8 @@ import org.apache.calcite.avatica.util.TimeUnitRange;
 import org.apache.calcite.config.CalciteConnectionConfig;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.core.Filter;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexCall;
 import org.apache.calcite.rex.RexLiteral;
@@ -63,7 +63,6 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TimeZone;
-import java.util.function.Predicate;
 import javax.annotation.Nonnull;
 
 /**
@@ -90,21 +89,13 @@ public abstract class DateRangeRules {
 
   private DateRangeRules() {}
 
-  private static final Predicate<Filter> FILTER_PREDICATE =
-      filter -> {
-        try (ExtractFinder finder = ExtractFinder.THREAD_INSTANCES.get()) {
-          assert finder.timeUnits.isEmpty() && finder.opKinds.isEmpty()
-              : "previous user did not clean up";
-          filter.getCondition().accept(finder);
-          // bail out if there is no EXTRACT of YEAR, or call to FLOOR or CEIL
-          return finder.timeUnits.contains(TimeUnitRange.YEAR)
-              || finder.opKinds.contains(SqlKind.FLOOR)
-              || finder.opKinds.contains(SqlKind.CEIL);
-        }
-      };
-
+  /** Rule that matches a {@link Filter} and converts calls to {@code EXTRACT},
+   * {@code FLOOR} and {@code CEIL} functions to date ranges (typically using
+   * the {@code BETWEEN} operator). */
   public static final RelOptRule FILTER_INSTANCE =
-      new FilterDateRangeRule(RelFactories.LOGICAL_BUILDER);
+      FilterDateRangeRule.Config.DEFAULT
+          .as(FilterDateRangeRule.Config.class)
+          .toRule();
 
   private static final Map<TimeUnitRange, Integer> TIME_UNIT_CODES =
       ImmutableMap.<TimeUnitRange, Integer>builder()
@@ -167,12 +158,35 @@ public abstract class DateRangeRules {
   }
 
   /** Rule that converts EXTRACT, FLOOR and CEIL in a {@link Filter} into a date
-   * range. */
+   * range.
+   *
+   * @see DateRangeRules#FILTER_INSTANCE */
   @SuppressWarnings("WeakerAccess")
-  public static class FilterDateRangeRule extends RelOptRule implements TransformationRule {
+  public static class FilterDateRangeRule
+      extends RelRule<FilterDateRangeRule.Config>
+      implements TransformationRule {
+    /** Creates a FilterDateRangeRule. */
+    protected FilterDateRangeRule(Config config) {
+      super(config);
+    }
+
+    @Deprecated // to be removed before 2.0
     public FilterDateRangeRule(RelBuilderFactory relBuilderFactory) {
-      super(operandJ(Filter.class, null, FILTER_PREDICATE, any()),
-          relBuilderFactory, "FilterDateRangeRule");
+      this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+          .as(Config.class));
+    }
+
+    /** Whether this an EXTRACT of YEAR, or a call to FLOOR or CEIL.
+     * If none of these, we cannot apply the rule. */
+    private static boolean containsRoundingExpression(Filter filter) {
+      try (ExtractFinder finder = ExtractFinder.THREAD_INSTANCES.get()) {
+        assert finder.timeUnits.isEmpty() && finder.opKinds.isEmpty()
+            : "previous user did not clean up";
+        filter.getCondition().accept(finder);
+        return finder.timeUnits.contains(TimeUnitRange.YEAR)
+            || finder.opKinds.contains(SqlKind.FLOOR)
+            || finder.opKinds.contains(SqlKind.CEIL);
+      }
     }
 
     @Override public void onMatch(RelOptRuleCall call) {
@@ -191,6 +205,20 @@ public abstract class DateRangeRules {
           .filter(condition);
       call.transformTo(relBuilder.build());
     }
+
+    /** Rule configuration. */
+    public interface Config extends RelRule.Config {
+      Config DEFAULT = EMPTY
+          .withOperandSupplier(b ->
+              b.operand(Filter.class)
+                  .predicate(FilterDateRangeRule::containsRoundingExpression)
+                  .anyInputs())
+          .as(Config.class);
+
+      @Override default FilterDateRangeRule toRule() {
+        return new FilterDateRangeRule(this);
+      }
+    }
   }
 
   /** Visitor that searches for calls to {@code EXTRACT}, {@code FLOOR} or
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/ExchangeRemoveConstantKeysRule.java b/core/src/main/java/org/apache/calcite/rel/rules/ExchangeRemoveConstantKeysRule.java
index efafd85..096162f 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/ExchangeRemoveConstantKeysRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/ExchangeRemoveConstantKeysRule.java
@@ -19,6 +19,7 @@ package org.apache.calcite.rel.rules;
 import org.apache.calcite.plan.RelOptPredicateList;
 import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelCollation;
 import org.apache.calcite.rel.RelCollations;
 import org.apache.calcite.rel.RelDistribution;
@@ -26,15 +27,18 @@ import org.apache.calcite.rel.RelDistributions;
 import org.apache.calcite.rel.RelFieldCollation;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Exchange;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.SortExchange;
+import org.apache.calcite.rel.logical.LogicalExchange;
+import org.apache.calcite.rel.logical.LogicalSortExchange;
 import org.apache.calcite.rel.metadata.RelMetadataQuery;
 import org.apache.calcite.rex.RexInputRef;
+import org.apache.calcite.util.ImmutableBeans;
 
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 /**
@@ -44,26 +48,18 @@ import java.util.stream.Collectors;
  * <p>For example,
  * <code>SELECT key,value FROM (SELECT 1 AS key, value FROM src) r DISTRIBUTE
  * BY key</code> can be reduced to
- * <code>SELECT 1 AS key, value FROM src</code>.</p>
+ * <code>SELECT 1 AS key, value FROM src</code>.
  *
+ * @see CoreRules#EXCHANGE_REMOVE_CONSTANT_KEYS
+ * @see CoreRules#SORT_EXCHANGE_REMOVE_CONSTANT_KEYS
  */
-public class ExchangeRemoveConstantKeysRule extends RelOptRule
+public class ExchangeRemoveConstantKeysRule
+    extends RelRule<ExchangeRemoveConstantKeysRule.Config>
     implements SubstitutionRule {
-  /** @deprecated Use {@link CoreRules#EXCHANGE_REMOVE_CONSTANT_KEYS}. */
-  @Deprecated // to be removed before 1.25
-  public static final ExchangeRemoveConstantKeysRule EXCHANGE_INSTANCE =
-      CoreRules.EXCHANGE_REMOVE_CONSTANT_KEYS;
-
-  /** @deprecated Use {@link CoreRules#SORT_EXCHANGE_REMOVE_CONSTANT_KEYS}. */
-  @Deprecated // to be removed before 1.25
-  public static final ExchangeRemoveConstantKeysRule SORT_EXCHANGE_INSTANCE =
-      CoreRules.SORT_EXCHANGE_REMOVE_CONSTANT_KEYS;
-
-  @SuppressWarnings("DeprecatedIsStillUsed")
-  @Deprecated // to be removed before 1.25
-  ExchangeRemoveConstantKeysRule(Class<? extends RelNode> clazz,
-      String description) {
-    super(operand(clazz, any()), RelFactories.LOGICAL_BUILDER, description);
+
+  /** Creates an ExchangeRemoveConstantKeysRule. */
+  protected ExchangeRemoveConstantKeysRule(Config config) {
+    super(config);
   }
 
   /** Removes constant in distribution keys. */
@@ -74,13 +70,12 @@ public class ExchangeRemoveConstantKeysRule extends RelOptRule
         .collect(Collectors.toList());
   }
 
-  @Override public boolean matches(RelOptRuleCall call) {
-    final Exchange exchange = call.rel(0);
-    return exchange.getDistribution().getType()
-        == RelDistribution.Type.HASH_DISTRIBUTED;
+  @Override public void onMatch(RelOptRuleCall call) {
+    config.matchHandler().accept(this, call);
   }
 
-  @Override public void onMatch(RelOptRuleCall call) {
+  private static void matchExchange(ExchangeRemoveConstantKeysRule rule,
+      RelOptRuleCall call) {
     final Exchange exchange = call.rel(0);
     final RelMetadataQuery mq = call.getMetadataQuery();
     final RelNode input = exchange.getInput();
@@ -114,83 +109,103 @@ public class ExchangeRemoveConstantKeysRule extends RelOptRule
     }
   }
 
-  /**
-   * Rule that reduces constants inside a {@link SortExchange}.
-   */
-  public static class SortExchangeRemoveConstantKeysRule
-      extends ExchangeRemoveConstantKeysRule {
-
-    @SuppressWarnings("DeprecatedIsStillUsed")
-    @Deprecated // to be removed before 1.25
-    SortExchangeRemoveConstantKeysRule(Class<? extends RelNode> clazz,
-        String description) {
-      super(clazz, description);
+  private static void matchSortExchange(ExchangeRemoveConstantKeysRule rule,
+      RelOptRuleCall call) {
+    final SortExchange sortExchange = call.rel(0);
+    final RelMetadataQuery mq = call.getMetadataQuery();
+    final RelNode input = sortExchange.getInput();
+    final RelOptPredicateList predicates = mq.getPulledUpPredicates(input);
+    if (predicates == null) {
+      return;
     }
 
-    @Override public boolean matches(RelOptRuleCall call) {
-      final SortExchange sortExchange = call.rel(0);
-      return  sortExchange.getDistribution().getType()
-          == RelDistribution.Type.HASH_DISTRIBUTED
-          || !sortExchange.getCollation().getFieldCollations().isEmpty();
+    final Set<Integer> constants = new HashSet<>();
+    predicates.constantMap.keySet().forEach(key -> {
+      if (key instanceof RexInputRef) {
+        constants.add(((RexInputRef) key).getIndex());
+      }
+    });
+
+    if (constants.isEmpty()) {
+      return;
     }
 
-    @Override public void onMatch(RelOptRuleCall call) {
-      final SortExchange sortExchange = call.rel(0);
-      final RelMetadataQuery mq = call.getMetadataQuery();
-      final RelNode input = sortExchange.getInput();
-      final RelOptPredicateList predicates = mq.getPulledUpPredicates(input);
-      if (predicates == null) {
-        return;
-      }
+    List<Integer> distributionKeys = new ArrayList<>();
+    boolean distributionSimplified = false;
+    boolean hashDistribution = sortExchange.getDistribution().getType()
+        == RelDistribution.Type.HASH_DISTRIBUTED;
+    if (hashDistribution) {
+      distributionKeys = simplifyDistributionKeys(
+          sortExchange.getDistribution(), constants);
+      distributionSimplified =
+          distributionKeys.size() != sortExchange.getDistribution().getKeys()
+              .size();
+    }
 
-      final Set<Integer> constants = new HashSet<>();
-      predicates.constantMap.keySet().forEach(key -> {
-        if (key instanceof RexInputRef) {
-          constants.add(((RexInputRef) key).getIndex());
-        }
-      });
+    final List<RelFieldCollation> fieldCollations = sortExchange
+        .getCollation().getFieldCollations().stream().filter(
+            fc -> !constants.contains(fc.getFieldIndex()))
+        .collect(Collectors.toList());
 
-      if (constants.isEmpty()) {
-        return;
-      }
+    boolean collationSimplified =
+        fieldCollations.size() != sortExchange.getCollation()
+            .getFieldCollations().size();
+    if (distributionSimplified
+        || collationSimplified) {
+      RelDistribution distribution = distributionSimplified
+          ? (distributionKeys.isEmpty()
+          ? RelDistributions.SINGLETON
+          : RelDistributions.hash(distributionKeys))
+          : sortExchange.getDistribution();
+      RelCollation collation = collationSimplified
+          ? RelCollations.of(fieldCollations)
+          : sortExchange.getCollation();
 
-      List<Integer> distributionKeys = new ArrayList<>();
-      boolean distributionSimplified = false;
-      boolean hashDistribution = sortExchange.getDistribution().getType()
-          == RelDistribution.Type.HASH_DISTRIBUTED;
-      if (hashDistribution) {
-        distributionKeys = simplifyDistributionKeys(
-            sortExchange.getDistribution(), constants);
-        distributionSimplified =
-            distributionKeys.size() != sortExchange.getDistribution().getKeys()
-                .size();
-      }
+      call.transformTo(call.builder()
+          .push(sortExchange.getInput())
+          .sortExchange(distribution, collation)
+          .build());
+      call.getPlanner().prune(sortExchange);
+    }
+  }
 
-      final List<RelFieldCollation> fieldCollations = sortExchange
-          .getCollation().getFieldCollations().stream().filter(
-              fc -> !constants.contains(fc.getFieldIndex()))
-           .collect(Collectors.toList());
-
-      boolean collationSimplified =
-           fieldCollations.size() != sortExchange.getCollation()
-               .getFieldCollations().size();
-      if (distributionSimplified
-           || collationSimplified) {
-        RelDistribution distribution = distributionSimplified
-            ? (distributionKeys.isEmpty()
-                ? RelDistributions.SINGLETON
-                : RelDistributions.hash(distributionKeys))
-            : sortExchange.getDistribution();
-        RelCollation collation = collationSimplified
-            ? RelCollations.of(fieldCollations)
-            : sortExchange.getCollation();
-
-        call.transformTo(call.builder()
-            .push(sortExchange.getInput())
-            .sortExchange(distribution, collation)
-            .build());
-        call.getPlanner().prune(sortExchange);
-      }
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .as(Config.class)
+        .withOperandFor(LogicalExchange.class,
+            exchange -> exchange.getDistribution().getType()
+                == RelDistribution.Type.HASH_DISTRIBUTED)
+        .withMatchHandler(ExchangeRemoveConstantKeysRule::matchExchange);
+
+    Config SORT = EMPTY
+        .withDescription("SortExchangeRemoveConstantKeysRule")
+        .as(Config.class)
+        .withOperandFor(LogicalSortExchange.class,
+            sortExchange -> sortExchange.getDistribution().getType()
+                == RelDistribution.Type.HASH_DISTRIBUTED
+                || !sortExchange.getCollation().getFieldCollations()
+                .isEmpty())
+        .withMatchHandler(ExchangeRemoveConstantKeysRule::matchSortExchange);
+
+    @Override default ExchangeRemoveConstantKeysRule toRule() {
+      return new ExchangeRemoveConstantKeysRule(this);
+    }
+
+    /** Forwards a call to {@link #onMatch(RelOptRuleCall)}. */
+    @ImmutableBeans.Property
+    <R extends RelOptRule> MatchHandler<R> matchHandler();
+
+    /** Sets {@link #matchHandler()}. */
+    <R extends RelOptRule> Config withMatchHandler(MatchHandler<R> matchHandler);
+
+    /** Defines an operand tree for the given classes. */
+    default <R extends Exchange> Config withOperandFor(Class<R> exchangeClass,
+        Predicate<R> predicate) {
+      return withOperandSupplier(b ->
+          b.operand(exchangeClass).predicate(predicate)
+              .anyInputs())
+          .as(Config.class);
     }
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterAggregateTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterAggregateTransposeRule.java
index 752a305..ca6576d 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterAggregateTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterAggregateTransposeRule.java
@@ -17,10 +17,10 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.Contexts;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Aggregate;
 import org.apache.calcite.rel.core.Aggregate.Group;
@@ -43,35 +43,33 @@ import java.util.List;
  * past a {@link org.apache.calcite.rel.core.Aggregate}.
  *
  * @see org.apache.calcite.rel.rules.AggregateFilterTransposeRule
+ * @see CoreRules#FILTER_AGGREGATE_TRANSPOSE
  */
-public class FilterAggregateTransposeRule extends RelOptRule implements TransformationRule {
+public class FilterAggregateTransposeRule
+    extends RelRule<FilterAggregateTransposeRule.Config>
+    implements TransformationRule {
 
-  /** @deprecated Use {@link CoreRules#FILTER_AGGREGATE_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterAggregateTransposeRule INSTANCE =
-      CoreRules.FILTER_AGGREGATE_TRANSPOSE;
-
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterAggregateTransposeRule. */
+  protected FilterAggregateTransposeRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a FilterAggregateTransposeRule.
-   *
-   * <p>If {@code filterFactory} is null, creates the same kind of filter as
-   * matched in the rule. Similarly {@code aggregateFactory}.</p>
-   */
+  @Deprecated // to be removed before 2.0
   public FilterAggregateTransposeRule(
       Class<? extends Filter> filterClass,
-      RelBuilderFactory builderFactory,
+      RelBuilderFactory relBuilderFactory,
       Class<? extends Aggregate> aggregateClass) {
-    this(
-        operand(filterClass,
-            operand(aggregateClass, any())),
-        builderFactory);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(filterClass, aggregateClass));
   }
 
+  @Deprecated // to be removed before 2.0
   protected FilterAggregateTransposeRule(RelOptRuleOperand operand,
-      RelBuilderFactory builderFactory) {
-    super(operand, builderFactory, null);
+      RelBuilderFactory relBuilderFactory) {
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class));
   }
 
   @Deprecated // to be removed before 2.0
@@ -85,7 +83,7 @@ public class FilterAggregateTransposeRule extends RelOptRule implements Transfor
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Filter filterRel = call.rel(0);
     final Aggregate aggRel = call.rel(1);
 
@@ -147,4 +145,34 @@ public class FilterAggregateTransposeRule extends RelOptRule implements Transfor
     }
     return true;
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Filter.class, Aggregate.class);
+
+    @Override default FilterAggregateTransposeRule toRule() {
+      return new FilterAggregateTransposeRule(this);
+    }
+
+    /** Defines an operand tree for the given 2 classes. */
+    default Config withOperandFor(Class<? extends Filter> filterClass,
+        Class<? extends Aggregate> aggregateClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(filterClass).oneInput(b1 ->
+              b1.operand(aggregateClass).anyInputs()))
+          .as(Config.class);
+    }
+
+    /** Defines an operand tree for the given 3 classes. */
+    default Config withOperandFor(Class<? extends Filter> filterClass,
+        Class<? extends Aggregate> aggregateClass,
+        Class<? extends RelNode> relClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(filterClass).oneInput(b1 ->
+              b1.operand(aggregateClass).oneInput(b2 ->
+                  b2.operand(relClass).anyInputs())))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterCalcMergeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterCalcMergeRule.java
index e958cc9..c98e327 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterCalcMergeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterCalcMergeRule.java
@@ -16,8 +16,9 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
+import org.apache.calcite.rel.core.Calc;
 import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.logical.LogicalCalc;
 import org.apache.calcite.rel.logical.LogicalFilter;
@@ -34,40 +35,35 @@ import org.apache.calcite.tools.RelBuilderFactory;
  * whose filter condition is the logical AND of the two.
  *
  * @see FilterMergeRule
+ * @see ProjectCalcMergeRule
+ * @see CoreRules#FILTER_CALC_MERGE
  */
-public class FilterCalcMergeRule extends RelOptRule implements TransformationRule {
-  //~ Static fields/initializers ---------------------------------------------
+public class FilterCalcMergeRule
+    extends RelRule<FilterCalcMergeRule.Config>
+    implements TransformationRule {
 
-  /** @deprecated Use {@link CoreRules#FILTER_CALC_MERGE}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterCalcMergeRule INSTANCE =
-      CoreRules.FILTER_CALC_MERGE;
-
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterCalcMergeRule. */
+  protected FilterCalcMergeRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a FilterCalcMergeRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public FilterCalcMergeRule(RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(
-            Filter.class,
-            operand(LogicalCalc.class, any())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final LogicalFilter filter = call.rel(0);
     final LogicalCalc calc = call.rel(1);
 
     // Don't merge a filter onto a calc which contains windowed aggregates.
     // That would effectively be pushing a multiset down through a filter.
     // We'll have chance to merge later, when the over is expanded.
-    if (calc.getProgram().containsAggs()) {
+    if (calc.containsOver()) {
       return;
     }
 
@@ -92,4 +88,23 @@ public class FilterCalcMergeRule extends RelOptRule implements TransformationRul
         LogicalCalc.create(calc.getInput(), mergedProgram);
     call.transformTo(newCalc);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Filter.class, LogicalCalc.class);
+
+    @Override default FilterCalcMergeRule toRule() {
+      return new FilterCalcMergeRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Filter> filterClass,
+        Class<? extends Calc> calcClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(filterClass).oneInput(b1 ->
+              b1.operand(calcClass).anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterCorrelateRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterCorrelateRule.java
index abca2be..71ed11a 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterCorrelateRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterCorrelateRule.java
@@ -16,9 +16,9 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Correlate;
 import org.apache.calcite.rel.core.Filter;
@@ -37,30 +37,24 @@ import java.util.List;
 /**
  * Planner rule that pushes a {@link Filter} above a {@link Correlate} into the
  * inputs of the Correlate.
+ *
+ * @see CoreRules#FILTER_CORRELATE
  */
-public class FilterCorrelateRule extends RelOptRule implements TransformationRule {
-
-  /** @deprecated Use {@link CoreRules#FILTER_CORRELATE}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterCorrelateRule INSTANCE =
-      CoreRules.FILTER_CORRELATE;
-
-  //~ Constructors -----------------------------------------------------------
-
-  /**
-   * Creates a FilterCorrelateRule.
-   */
-  public FilterCorrelateRule(RelBuilderFactory builderFactory) {
-    super(
-        operand(Filter.class,
-            operand(Correlate.class, RelOptRule.any())),
-        builderFactory, "FilterCorrelateRule");
+public class FilterCorrelateRule
+    extends RelRule<FilterCorrelateRule.Config>
+    implements TransformationRule {
+
+  /** Creates a FilterCorrelateRule. */
+  protected FilterCorrelateRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
+  public FilterCorrelateRule(RelBuilderFactory relBuilderFactory) {
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
-  /**
-   * Creates a FilterCorrelateRule with an explicit root operand and
-   * factories.
-   */
   @Deprecated // to be removed before 2.0
   public FilterCorrelateRule(RelFactories.FilterFactory filterFactory,
       RelFactories.ProjectFactory projectFactory) {
@@ -69,7 +63,7 @@ public class FilterCorrelateRule extends RelOptRule implements TransformationRul
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Filter filter = call.rel(0);
     final Correlate corr = call.rel(1);
 
@@ -129,4 +123,23 @@ public class FilterCorrelateRule extends RelOptRule implements TransformationRul
 
     call.transformTo(relBuilder.build());
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Filter.class, Correlate.class);
+
+    @Override default FilterCorrelateRule toRule() {
+      return new FilterCorrelateRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Filter> filterClass,
+        Class<? extends Correlate> correlateClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(filterClass).oneInput(b1 ->
+              b1.operand(correlateClass).anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterJoinRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterJoinRule.java
index 72a6dd3..77804cd 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterJoinRule.java
@@ -16,10 +16,9 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
-import org.apache.calcite.plan.RelOptRuleOperand;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.core.Join;
@@ -32,6 +31,7 @@ import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
 
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
@@ -39,81 +39,26 @@ import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Objects;
 
 import static org.apache.calcite.plan.RelOptUtil.conjunctions;
 
 /**
  * Planner rule that pushes filters above and
  * within a join node into the join node and/or its children nodes.
+ *
+ * @param <C> Configuration type
  */
-public abstract class FilterJoinRule extends RelOptRule implements TransformationRule {
+public abstract class FilterJoinRule<C extends FilterJoinRule.Config>
+    extends RelRule<C>
+    implements TransformationRule {
   /** Predicate that always returns true. With this predicate, every filter
    * will be pushed into the ON clause. */
+  @Deprecated // to be removed before 2.0
   public static final Predicate TRUE_PREDICATE = (join, joinType, exp) -> true;
 
-  /** @deprecated Use {@link CoreRules#FILTER_INTO_JOIN}. */
-  @SuppressWarnings("StaticInitializerReferencesSubClass")
-  @Deprecated // to be removed before 1.25
-  public static final FilterJoinRule FILTER_ON_JOIN =
-      CoreRules.FILTER_INTO_JOIN;
-
-  /** @deprecated Use {@link CoreRules#FILTER_INTO_JOIN_DUMB}. */
-  @SuppressWarnings("StaticInitializerReferencesSubClass")
-  @Deprecated // to be removed before 1.25
-  public static final FilterJoinRule DUMB_FILTER_ON_JOIN =
-      CoreRules.FILTER_INTO_JOIN_DUMB;
-
-  /** @deprecated Use {@link CoreRules#JOIN_CONDITION_PUSH}. */
-  @SuppressWarnings("StaticInitializerReferencesSubClass")
-  @Deprecated // to be removed before 1.25
-  public static final FilterJoinRule JOIN =
-      CoreRules.JOIN_CONDITION_PUSH;
-
-  /** Whether to try to strengthen join-type. */
-  private final boolean smart;
-
-  /** Predicate that returns whether a filter is valid in the ON clause of a
-   * join for this particular kind of join. If not, Calcite will push it back to
-   * above the join. */
-  private final Predicate predicate;
-
-  //~ Constructors -----------------------------------------------------------
-
-  /**
-   * Creates a FilterJoinRule with an explicit root operand and
-   * factories.
-   */
-  protected FilterJoinRule(RelOptRuleOperand operand, String id,
-      boolean smart, RelBuilderFactory relBuilderFactory, Predicate predicate) {
-    super(operand, relBuilderFactory, "FilterJoinRule:" + id);
-    this.smart = smart;
-    this.predicate = Objects.requireNonNull(predicate);
-  }
-
-  /**
-   * Creates a FilterJoinRule with an explicit root operand and
-   * factories.
-   */
-  @Deprecated // to be removed before 2.0
-  protected FilterJoinRule(RelOptRuleOperand operand, String id,
-      boolean smart, RelFactories.FilterFactory filterFactory,
-      RelFactories.ProjectFactory projectFactory) {
-    this(operand, id, smart, RelBuilder.proto(filterFactory, projectFactory),
-        TRUE_PREDICATE);
-  }
-
-  /**
-   * Creates a FilterJoinRule with an explicit root operand and
-   * factories.
-   */
-  @Deprecated // to be removed before 2.0
-  protected FilterJoinRule(RelOptRuleOperand operand, String id,
-      boolean smart, RelFactories.FilterFactory filterFactory,
-      RelFactories.ProjectFactory projectFactory,
-      Predicate predicate) {
-    this(operand, id, smart, RelBuilder.proto(filterFactory, projectFactory),
-        predicate);
+  /** Creates a FilterJoinRule. */
+  protected FilterJoinRule(C config) {
+    super(config);
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -141,7 +86,7 @@ public abstract class FilterJoinRule extends RelOptRule implements Transformatio
 
     // Simplify Outer Joins
     JoinRelType joinType = join.getJoinType();
-    if (smart
+    if (config.isSmart()
         && !origAboveFilters.isEmpty()
         && join.getJoinType() != JoinRelType.INNER) {
       joinType = RelOptUtil.simplifyJoin(join, origAboveFilters, joinType);
@@ -325,7 +270,8 @@ public abstract class FilterJoinRule extends RelOptRule implements Transformatio
     while (filterIter.hasNext()) {
       RexNode exp = filterIter.next();
       // Do not pull up filter conditions for semi/anti join.
-      if (!predicate.apply(join, joinType, exp) && joinType.projectsRight()) {
+      if (!config.getPredicate().apply(join, joinType, exp)
+          && joinType.projectsRight()) {
         aboveFilters.add(exp);
         filterIter.remove();
       }
@@ -333,12 +279,25 @@ public abstract class FilterJoinRule extends RelOptRule implements Transformatio
   }
 
   /** Rule that pushes parts of the join condition to its inputs. */
-  public static class JoinConditionPushRule extends FilterJoinRule {
+  public static class JoinConditionPushRule
+      extends FilterJoinRule<JoinConditionPushRule.Config> {
+    /** Creates a JoinConditionPushRule. */
+    protected JoinConditionPushRule(Config config) {
+      super(config);
+    }
+
+    @Deprecated // to be removed before 2.0
     public JoinConditionPushRule(RelBuilderFactory relBuilderFactory,
         Predicate predicate) {
-      super(RelOptRule.operand(Join.class, RelOptRule.any()),
-          "FilterJoinRule:no-filter", true, relBuilderFactory,
-          predicate);
+      this(Config.EMPTY
+          .withRelBuilderFactory(relBuilderFactory)
+          .withOperandSupplier(b ->
+              b.operand(Join.class).anyInputs())
+          .withDescription("FilterJoinRule:no-filter")
+          .as(Config.class)
+          .withSmart(true)
+          .withPredicate(predicate)
+          .as(Config.class));
     }
 
     @Deprecated // to be removed before 2.0
@@ -351,18 +310,47 @@ public abstract class FilterJoinRule extends RelOptRule implements Transformatio
       Join join = call.rel(0);
       perform(call, null, join);
     }
+
+    /** Rule configuration. */
+    public interface Config extends FilterJoinRule.Config {
+      Config DEFAULT = EMPTY
+          .withOperandSupplier(b ->
+              b.operand(Join.class).anyInputs())
+          .as(JoinConditionPushRule.Config.class)
+          .withSmart(true)
+          .withPredicate((join, joinType, exp) -> true)
+          .as(JoinConditionPushRule.Config.class);
+
+      @Override default JoinConditionPushRule toRule() {
+        return new JoinConditionPushRule(this);
+      }
+    }
   }
 
   /** Rule that tries to push filter expressions into a join
-   * condition and into the inputs of the join. */
-  public static class FilterIntoJoinRule extends FilterJoinRule {
+   * condition and into the inputs of the join.
+   *
+   * @see CoreRules#FILTER_INTO_JOIN */
+  public static class FilterIntoJoinRule
+      extends FilterJoinRule<FilterIntoJoinRule.Config> {
+    /** Creates a FilterIntoJoinRule. */
+    protected FilterIntoJoinRule(Config config) {
+      super(config);
+    }
+
+    @Deprecated // to be removed before 2.0
     public FilterIntoJoinRule(boolean smart,
         RelBuilderFactory relBuilderFactory, Predicate predicate) {
-      super(
-          operand(Filter.class,
-              operand(Join.class, RelOptRule.any())),
-          "FilterJoinRule:filter", smart, relBuilderFactory,
-          predicate);
+      this(Config.EMPTY
+          .withRelBuilderFactory(relBuilderFactory)
+          .withOperandSupplier(b0 ->
+              b0.operand(Filter.class).oneInput(b1 ->
+                  b1.operand(Join.class).anyInputs()))
+          .withDescription("FilterJoinRule:filter")
+          .as(Config.class)
+          .withSmart(smart)
+          .withPredicate(predicate)
+          .as(Config.class));
     }
 
     @Deprecated // to be removed before 2.0
@@ -370,7 +358,17 @@ public abstract class FilterJoinRule extends RelOptRule implements Transformatio
         RelFactories.FilterFactory filterFactory,
         RelFactories.ProjectFactory projectFactory,
         Predicate predicate) {
-      this(smart, RelBuilder.proto(filterFactory, projectFactory), predicate);
+      this(Config.EMPTY
+          .withRelBuilderFactory(
+              RelBuilder.proto(filterFactory, projectFactory))
+          .withOperandSupplier(b0 ->
+              b0.operand(Filter.class).oneInput(b1 ->
+                  b1.operand(Join.class).anyInputs()))
+          .withDescription("FilterJoinRule:filter")
+          .as(Config.class)
+          .withSmart(smart)
+          .withPredicate(predicate)
+          .as(Config.class));
     }
 
     @Override public void onMatch(RelOptRuleCall call) {
@@ -378,12 +376,49 @@ public abstract class FilterJoinRule extends RelOptRule implements Transformatio
       Join join = call.rel(1);
       perform(call, filter, join);
     }
+
+    /** Rule configuration. */
+    public interface Config extends FilterJoinRule.Config {
+      Config DEFAULT = EMPTY
+          .withOperandSupplier(b0 ->
+              b0.operand(Filter.class).oneInput(b1 ->
+                  b1.operand(Join.class).anyInputs()))
+          .as(FilterIntoJoinRule.Config.class)
+          .withSmart(true)
+          .withPredicate((join, joinType, exp) -> true)
+          .as(FilterIntoJoinRule.Config.class);
+
+      @Override default FilterIntoJoinRule toRule() {
+        return new FilterIntoJoinRule(this);
+      }
+    }
   }
 
   /** Predicate that returns whether a filter is valid in the ON clause of a
    * join for this particular kind of join. If not, Calcite will push it back to
    * above the join. */
+  @FunctionalInterface
   public interface Predicate {
     boolean apply(Join join, JoinRelType joinType, RexNode exp);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    /** Whether to try to strengthen join-type, default false. */
+    @ImmutableBeans.Property
+    @ImmutableBeans.BooleanDefault(false)
+    boolean isSmart();
+
+    /** Sets {@link #isSmart()}. */
+    Config withSmart(boolean smart);
+
+    /** Predicate that returns whether a filter is valid in the ON clause of a
+     * join for this particular kind of join. If not, Calcite will push it back to
+     * above the join. */
+    @ImmutableBeans.Property
+    Predicate getPredicate();
+
+    /** Sets {@link #getPredicate()} ()}. */
+    Config withPredicate(Predicate predicate);
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterMergeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterMergeRule.java
index d7d4c99..f25a56c 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterMergeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterMergeRule.java
@@ -17,8 +17,8 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.Contexts;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.tools.RelBuilder;
@@ -28,22 +28,19 @@ import org.apache.calcite.tools.RelBuilderFactory;
  * Planner rule that combines two
  * {@link org.apache.calcite.rel.logical.LogicalFilter}s.
  */
-public class FilterMergeRule extends RelOptRule implements SubstitutionRule {
-  /** @deprecated Use {@link CoreRules#FILTER_MERGE}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterMergeRule INSTANCE =
-      CoreRules.FILTER_MERGE;
+public class FilterMergeRule extends RelRule<FilterMergeRule.Config>
+    implements SubstitutionRule {
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterMergeRule. */
+  protected FilterMergeRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a FilterMergeRule.
-   */
+  @Deprecated // to be removed before 2.0
   public FilterMergeRule(RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(Filter.class,
-            operand(Filter.class, any())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   @Deprecated // to be removed before 2.0
@@ -53,7 +50,7 @@ public class FilterMergeRule extends RelOptRule implements SubstitutionRule {
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Filter topFilter = call.rel(0);
     final Filter bottomFilter = call.rel(1);
 
@@ -64,4 +61,21 @@ public class FilterMergeRule extends RelOptRule implements SubstitutionRule {
     call.transformTo(relBuilder.build());
   }
 
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Filter.class);
+
+    @Override default FilterMergeRule toRule() {
+      return new FilterMergeRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Filter> filterClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(filterClass).oneInput(b1 ->
+              b1.operand(filterClass).anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterMultiJoinMergeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterMultiJoinMergeRule.java
index f585d17..b92881e 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterMultiJoinMergeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterMultiJoinMergeRule.java
@@ -16,10 +16,9 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.core.Filter;
-import org.apache.calcite.rel.logical.LogicalFilter;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
@@ -34,41 +33,34 @@ import java.util.List;
  * creating a richer {@code MultiJoin}.
  *
  * @see org.apache.calcite.rel.rules.ProjectMultiJoinMergeRule
+ * @see CoreRules#FILTER_MULTI_JOIN_MERGE
  */
-public class FilterMultiJoinMergeRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#FILTER_MULTI_JOIN_MERGE}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterMultiJoinMergeRule INSTANCE =
-      CoreRules.FILTER_MULTI_JOIN_MERGE;
+public class FilterMultiJoinMergeRule
+    extends RelRule<FilterMultiJoinMergeRule.Config>
+    implements TransformationRule {
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterMultiJoinMergeRule. */
+  protected FilterMultiJoinMergeRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a FilterMultiJoinMergeRule that uses {@link Filter}
-   * of type {@link LogicalFilter}
-   * @param relBuilderFactory builder factory for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public FilterMultiJoinMergeRule(RelBuilderFactory relBuilderFactory) {
-    this(LogicalFilter.class, relBuilderFactory);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
-  /**
-   * Creates a FilterMultiJoinMergeRule that uses a generic
-   * {@link Filter}
-   * @param filterClass filter class
-   * @param relBuilderFactory builder factory for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public FilterMultiJoinMergeRule(Class<? extends Filter> filterClass,
       RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(filterClass,
-          operand(MultiJoin.class, any())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(filterClass, MultiJoin.class));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     Filter filter = call.rel(0);
     MultiJoin multiJoin = call.rel(1);
 
@@ -94,4 +86,23 @@ public class FilterMultiJoinMergeRule extends RelOptRule implements Transformati
 
     call.transformTo(newMultiJoin);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Filter.class, MultiJoin.class);
+
+    @Override default FilterMultiJoinMergeRule toRule() {
+      return new FilterMultiJoinMergeRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Filter> filterClass,
+        Class<? extends MultiJoin> multiJoinClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(filterClass).oneInput(b1 ->
+              b1.operand(multiJoinClass).anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterProjectTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterProjectTransposeRule.java
index 192ba81..5d94844 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterProjectTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterProjectTransposeRule.java
@@ -16,10 +16,10 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.rel.RelDistributionTraitDef;
@@ -28,10 +28,10 @@ import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.core.Project;
 import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.rex.RexOver;
 import org.apache.calcite.rex.RexUtil;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
 
 import java.util.Collections;
 import java.util.function.Predicate;
@@ -40,17 +40,17 @@ import java.util.function.Predicate;
  * Planner rule that pushes
  * a {@link org.apache.calcite.rel.core.Filter}
  * past a {@link org.apache.calcite.rel.core.Project}.
+ *
+ * @see CoreRules#FILTER_PROJECT_TRANSPOSE
  */
-public class FilterProjectTransposeRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#FILTER_PROJECT_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterProjectTransposeRule INSTANCE =
-      CoreRules.FILTER_PROJECT_TRANSPOSE;
-
-  private final boolean copyFilter;
-  private final boolean copyProject;
+public class FilterProjectTransposeRule
+    extends RelRule<FilterProjectTransposeRule.Config>
+    implements TransformationRule {
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterProjectTransposeRule. */
+  protected FilterProjectTransposeRule(Config config) {
+    super(config);
+  }
 
   /**
    * Creates a FilterProjectTransposeRule.
@@ -62,15 +62,20 @@ public class FilterProjectTransposeRule extends RelOptRule implements Transforma
    * filter (since in some cases it can prevent a
    * {@link org.apache.calcite.rel.core.Correlate} from being de-correlated).
    */
+  @Deprecated // to be removed before 2.0
   public FilterProjectTransposeRule(
       Class<? extends Filter> filterClass,
       Class<? extends Project> projectClass,
       boolean copyFilter, boolean copyProject,
       RelBuilderFactory relBuilderFactory) {
-    this(filterClass,
-        filter -> !RexUtil.containsCorrelation(filter.getCondition()),
-        projectClass, project -> true,
-        copyFilter, copyProject, relBuilderFactory);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(filterClass,
+            f -> !RexUtil.containsCorrelation(f.getCondition()),
+            projectClass, project -> true)
+        .withCopyFilter(copyFilter)
+        .withCopyProject(copyProject));
   }
 
   /**
@@ -85,6 +90,7 @@ public class FilterProjectTransposeRule extends RelOptRule implements Transforma
    * and/or the Project (using {@code projectPredicate} allows making the rule
    * more restrictive.
    */
+  @Deprecated // to be removed before 2.0
   public <F extends Filter, P extends Project> FilterProjectTransposeRule(
       Class<F> filterClass,
       Predicate<? super F> filterPredicate,
@@ -92,10 +98,16 @@ public class FilterProjectTransposeRule extends RelOptRule implements Transforma
       Predicate<? super P> projectPredicate,
       boolean copyFilter, boolean copyProject,
       RelBuilderFactory relBuilderFactory) {
-    this(
-        operandJ(filterClass, null, filterPredicate,
-            operandJ(projectClass, null, projectPredicate, any())),
-        copyFilter, copyProject, relBuilderFactory);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b0 ->
+            b0.operand(filterClass).predicate(filterPredicate)
+                .oneInput(b1 ->
+                    b1.operand(projectClass).predicate(projectPredicate)
+                        .anyInputs()))
+            .as(Config.class)
+            .withCopyFilter(copyFilter)
+            .withCopyProject(copyProject));
   }
 
   @Deprecated // to be removed before 2.0
@@ -104,30 +116,42 @@ public class FilterProjectTransposeRule extends RelOptRule implements Transforma
       RelFactories.FilterFactory filterFactory,
       Class<? extends Project> projectClass,
       RelFactories.ProjectFactory projectFactory) {
-    this(filterClass, filter -> !RexUtil.containsCorrelation(filter.getCondition()),
-        projectClass, project -> true,
-        filterFactory == null,
-        projectFactory == null,
-        RelBuilder.proto(filterFactory, projectFactory));
+    this(Config.DEFAULT
+        .withRelBuilderFactory(RelBuilder.proto(filterFactory, projectFactory))
+        .withOperandSupplier(b0 ->
+            b0.operand(filterClass)
+                .predicate(filter ->
+                    !RexUtil.containsCorrelation(filter.getCondition()))
+                .oneInput(b2 ->
+                    b2.operand(projectClass)
+                        .predicate(project -> true)
+                        .anyInputs()))
+        .as(Config.class)
+        .withCopyFilter(filterFactory == null)
+        .withCopyProject(projectFactory == null));
   }
 
+  @Deprecated // to be removed before 2.0
   protected FilterProjectTransposeRule(
       RelOptRuleOperand operand,
       boolean copyFilter,
       boolean copyProject,
       RelBuilderFactory relBuilderFactory) {
-    super(operand, relBuilderFactory, null);
-    this.copyFilter = copyFilter;
-    this.copyProject = copyProject;
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class)
+        .withCopyFilter(copyFilter)
+        .withCopyProject(copyProject));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Filter filter = call.rel(0);
     final Project project = call.rel(1);
 
-    if (RexOver.containsOver(project.getProjects(), null)) {
+    if (project.containsOver()) {
       // In general a filter cannot be pushed below a windowing calculation.
       // Applying the filter before the aggregation function changes
       // the results of the windowing invocation.
@@ -142,7 +166,7 @@ public class FilterProjectTransposeRule extends RelOptRule implements Transforma
 
     final RelBuilder relBuilder = call.builder();
     RelNode newFilterRel;
-    if (copyFilter) {
+    if (config.isCopyFilter()) {
       final RelNode input = project.getInput();
       final RelTraitSet traitSet = filter.getTraitSet()
           .replaceIfs(RelCollationTraitDef.INSTANCE,
@@ -158,14 +182,77 @@ public class FilterProjectTransposeRule extends RelOptRule implements Transforma
           relBuilder.push(project.getInput()).filter(newCondition).build();
     }
 
-    RelNode newProjRel =
-        copyProject
+    RelNode newProject =
+        config.isCopyProject()
             ? project.copy(project.getTraitSet(), newFilterRel,
                 project.getProjects(), project.getRowType())
             : relBuilder.push(newFilterRel)
                 .project(project.getProjects(), project.getRowType().getFieldNames())
                 .build();
 
-    call.transformTo(newProjRel);
+    call.transformTo(newProject);
+  }
+
+  /** Rule configuration.
+   *
+   * <p>If {@code copyFilter} is true, creates the same kind of Filter as
+   * matched in the rule, otherwise it creates a Filter using the RelBuilder
+   * obtained by the {@code relBuilderFactory}.
+   * Similarly for {@code copyProject}.
+   *
+   * <p>Defining predicates for the Filter (using {@code filterPredicate})
+   * and/or the Project (using {@code projectPredicate} allows making the rule
+   * more restrictive. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Filter.class,
+            f -> !RexUtil.containsCorrelation(f.getCondition()),
+            Project.class, p -> true)
+        .withCopyFilter(true)
+        .withCopyProject(true);
+
+    @Override default FilterProjectTransposeRule toRule() {
+      return new FilterProjectTransposeRule(this);
+    }
+
+    /** Whether to create a {@link Filter} of the same convention as the
+     * matched Filter. */
+    @ImmutableBeans.Property
+    @ImmutableBeans.BooleanDefault(true)
+    boolean isCopyFilter();
+
+    /** Sets {@link #isCopyFilter()}. */
+    Config withCopyFilter(boolean copyFilter);
+
+    /** Whether to create a {@link Project} of the same convention as the
+     * matched Project. */
+    @ImmutableBeans.Property
+    @ImmutableBeans.BooleanDefault(true)
+    boolean isCopyProject();
+
+    /** Sets {@link #isCopyProject()}. */
+    Config withCopyProject(boolean copyProject);
+
+    /** Defines an operand tree for the given 2 classes. */
+    default Config withOperandFor(Class<? extends Filter> filterClass,
+        Predicate<Filter> filterPredicate,
+        Class<? extends Project> projectClass,
+        Predicate<Project> projectPredicate) {
+      return withOperandSupplier(b0 ->
+          b0.operand(filterClass).predicate(filterPredicate).oneInput(b1 ->
+              b1.operand(projectClass).predicate(projectPredicate).anyInputs()))
+          .as(Config.class);
+    }
+
+    /** Defines an operand tree for the given 3 classes. */
+    default Config withOperandFor(Class<? extends Filter> filterClass,
+        Class<? extends Project> projectClass,
+        Class<? extends RelNode> relClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(filterClass).oneInput(b1 ->
+              b1.operand(projectClass).oneInput(b2 ->
+                  b2.operand(relClass).anyInputs())))
+          .as(Config.class);
+    }
   }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterRemoveIsNotDistinctFromRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterRemoveIsNotDistinctFromRule.java
index 070b142..8a908fa 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterRemoveIsNotDistinctFromRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterRemoveIsNotDistinctFromRule.java
@@ -16,9 +16,9 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rex.RexBuilder;
@@ -35,31 +35,27 @@ import org.apache.calcite.tools.RelBuilderFactory;
  * in a {@link Filter} with logically equivalent operations.
  *
  * @see org.apache.calcite.sql.fun.SqlStdOperatorTable#IS_NOT_DISTINCT_FROM
+ * @see CoreRules#FILTER_EXPAND_IS_NOT_DISTINCT_FROM
  */
-public final class FilterRemoveIsNotDistinctFromRule extends RelOptRule
+public final class FilterRemoveIsNotDistinctFromRule
+    extends RelRule<FilterRemoveIsNotDistinctFromRule.Config>
     implements TransformationRule {
-  //~ Static fields/initializers ---------------------------------------------
 
-  /** @deprecated Use {@link CoreRules#FILTER_EXPAND_IS_NOT_DISTINCT_FROM}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterRemoveIsNotDistinctFromRule INSTANCE =
-      CoreRules.FILTER_EXPAND_IS_NOT_DISTINCT_FROM;
-
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterRemoveIsNotDistinctFromRule. */
+  protected FilterRemoveIsNotDistinctFromRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a FilterRemoveIsNotDistinctFromRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public FilterRemoveIsNotDistinctFromRule(
       RelBuilderFactory relBuilderFactory) {
-    super(operand(Filter.class, any()), relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     Filter oldFilter = call.rel(0);
     RexNode oldFilterCond = oldFilter.getCondition();
 
@@ -92,16 +88,15 @@ public final class FilterRemoveIsNotDistinctFromRule extends RelOptRule
   /** Shuttle that removes 'x IS NOT DISTINCT FROM y' and converts it
    * to 'CASE WHEN x IS NULL THEN y IS NULL WHEN y IS NULL THEN x IS
    * NULL ELSE x = y END'. */
-  private class RemoveIsNotDistinctFromRexShuttle extends RexShuttle {
-    RexBuilder rexBuilder;
+  private static class RemoveIsNotDistinctFromRexShuttle extends RexShuttle {
+    final RexBuilder rexBuilder;
 
     RemoveIsNotDistinctFromRexShuttle(
         RexBuilder rexBuilder) {
       this.rexBuilder = rexBuilder;
     }
 
-    // override RexShuttle
-    public RexNode visitCall(RexCall call) {
+    @Override public RexNode visitCall(RexCall call) {
       RexNode newCall = super.visitCall(call);
 
       if (call.getOperator()
@@ -117,4 +112,15 @@ public final class FilterRemoveIsNotDistinctFromRule extends RelOptRule
       return newCall;
     }
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b -> b.operand(Filter.class).anyInputs())
+        .as(Config.class);
+
+    @Override default FilterRemoveIsNotDistinctFromRule toRule() {
+      return new FilterRemoveIsNotDistinctFromRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterSetOpTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterSetOpTransposeRule.java
index 6e3b7d9..d4daea9 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterSetOpTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterSetOpTransposeRule.java
@@ -17,9 +17,9 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.Contexts;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Filter;
 import org.apache.calcite.rel.core.RelFactories;
@@ -36,34 +36,34 @@ import java.util.List;
 /**
  * Planner rule that pushes a {@link org.apache.calcite.rel.core.Filter}
  * past a {@link org.apache.calcite.rel.core.SetOp}.
+ *
+ * @see CoreRules#FILTER_SET_OP_TRANSPOSE
  */
-public class FilterSetOpTransposeRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#FILTER_SET_OP_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterSetOpTransposeRule INSTANCE =
-      CoreRules.FILTER_SET_OP_TRANSPOSE;
+public class FilterSetOpTransposeRule
+    extends RelRule<FilterSetOpTransposeRule.Config>
+    implements TransformationRule {
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterSetOpTransposeRule. */
+  protected FilterSetOpTransposeRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a FilterSetOpTransposeRule.
-   */
+  @Deprecated // to be removed before 2.0
   public FilterSetOpTransposeRule(RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(Filter.class,
-            operand(SetOp.class, any())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   @Deprecated // to  be removed before 2.0
   public FilterSetOpTransposeRule(RelFactories.FilterFactory filterFactory) {
-    this(RelBuilder.proto(Contexts.of(filterFactory)));
+    this(Config.DEFAULT
+        .withRelBuilderFactory(RelBuilder.proto(Contexts.of(filterFactory)))
+        .as(Config.class));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  // implement RelOptRule
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     Filter filterRel = call.rel(0);
     SetOp setOp = call.rel(1);
 
@@ -94,4 +94,17 @@ public class FilterSetOpTransposeRule extends RelOptRule implements Transformati
 
     call.transformTo(newSetOp);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b0 ->
+            b0.operand(Filter.class).oneInput(b1 ->
+                b1.operand(SetOp.class).anyInputs()))
+        .as(Config.class);
+
+    @Override default FilterSetOpTransposeRule toRule() {
+      return new FilterSetOpTransposeRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterTableFunctionTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterTableFunctionTransposeRule.java
index 00e3833..7042055 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterTableFunctionTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterTableFunctionTransposeRule.java
@@ -17,9 +17,9 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.logical.LogicalFilter;
 import org.apache.calcite.rel.logical.LogicalTableFunctionScan;
@@ -37,30 +37,27 @@ import java.util.Set;
  * Planner rule that pushes
  * a {@link org.apache.calcite.rel.logical.LogicalFilter}
  * past a {@link org.apache.calcite.rel.logical.LogicalTableFunctionScan}.
+ *
+ * @see CoreRules#FILTER_TABLE_FUNCTION_TRANSPOSE
  */
-public class FilterTableFunctionTransposeRule extends RelOptRule
+public class FilterTableFunctionTransposeRule
+    extends RelRule<FilterTableFunctionTransposeRule.Config>
     implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#FILTER_TABLE_FUNCTION_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterTableFunctionTransposeRule INSTANCE =
-      CoreRules.FILTER_TABLE_FUNCTION_TRANSPOSE;
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterTableFunctionTransposeRule. */
+  protected FilterTableFunctionTransposeRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a FilterTableFunctionTransposeRule.
-   */
+  @Deprecated // to be removed before 2.0
   public FilterTableFunctionTransposeRule(RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(LogicalFilter.class,
-            operand(LogicalTableFunctionScan.class, any())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  // implement RelOptRule
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     LogicalFilter filter = call.rel(0);
     LogicalTableFunctionScan funcRel = call.rel(1);
     Set<RelColumnMapping> columnMappings = funcRel.getColumnMappings();
@@ -119,4 +116,17 @@ public class FilterTableFunctionTransposeRule extends RelOptRule
             columnMappings);
     call.transformTo(newFuncRel);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b0 ->
+            b0.operand(LogicalFilter.class).oneInput(b1 ->
+                b1.operand(LogicalTableFunctionScan.class).anyInputs()))
+        .as(Config.class);
+
+    @Override default FilterTableFunctionTransposeRule toRule() {
+      return new FilterTableFunctionTransposeRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterTableScanRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterTableScanRule.java
index 2f11cee..9065386 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterTableScanRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterTableScanRule.java
@@ -16,13 +16,13 @@
  */
 package org.apache.calcite.rel.rules;
 
+import org.apache.calcite.adapter.enumerable.EnumerableInterpreter;
 import org.apache.calcite.interpreter.Bindables;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
 import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.core.Filter;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.TableScan;
 import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexUtil;
@@ -43,38 +43,37 @@ import com.google.common.collect.ImmutableList;
  * or a {@link org.apache.calcite.schema.ProjectableFilterableTable}
  * to a {@link org.apache.calcite.interpreter.Bindables.BindableTableScan}.
  *
- * <p>The {@link #INTERPRETER} variant allows an intervening
+ * <p>The {@link CoreRules#FILTER_INTERPRETER_SCAN} variant allows an
+ * intervening
  * {@link org.apache.calcite.adapter.enumerable.EnumerableInterpreter}.
  *
  * @see org.apache.calcite.rel.rules.ProjectTableScanRule
+ * @see CoreRules#FILTER_SCAN
+ * @see CoreRules#FILTER_INTERPRETER_SCAN
  */
-public abstract class FilterTableScanRule extends RelOptRule {
+public class FilterTableScanRule
+    extends RelRule<FilterTableScanRule.Config> {
   @SuppressWarnings("Guava")
   @Deprecated // to be removed before 2.0
   public static final com.google.common.base.Predicate<TableScan> PREDICATE =
       FilterTableScanRule::test;
 
-  /** @deprecated Use {@link CoreRules#FILTER_SCAN}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterTableScanRule INSTANCE =
-      CoreRules.FILTER_SCAN;
-
-  /** @deprecated Use {@link CoreRules#FILTER_INTERPRETER_SCAN}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterTableScanRule INTERPRETER =
-      CoreRules.FILTER_INTERPRETER_SCAN;
-
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterTableScanRule. */
+  protected FilterTableScanRule(Config config) {
+    super(config);
+  }
 
   @Deprecated // to be removed before 2.0
   protected FilterTableScanRule(RelOptRuleOperand operand, String description) {
-    this(operand, RelFactories.LOGICAL_BUILDER, description);
+    this(Config.EMPTY.as(Config.class));
+    throw new UnsupportedOperationException();
   }
 
-  /** Creates a FilterTableScanRule. */
+  @Deprecated // to be removed before 2.0
   protected FilterTableScanRule(RelOptRuleOperand operand,
       RelBuilderFactory relBuilderFactory, String description) {
-    super(operand, relBuilderFactory, description);
+    this(Config.EMPTY.as(Config.class));
+    throw new UnsupportedOperationException();
   }
 
   //~ Methods ----------------------------------------------------------------
@@ -87,6 +86,22 @@ public abstract class FilterTableScanRule extends RelOptRule {
         || table.unwrap(ProjectableFilterableTable.class) != null;
   }
 
+  @Override public void onMatch(RelOptRuleCall call) {
+    if (call.rels.length == 2) {
+      // the ordinary variant
+      final Filter filter = call.rel(0);
+      final TableScan scan = call.rel(1);
+      apply(call, filter, scan);
+    } else if (call.rels.length == 3) {
+      // the variant with intervening EnumerableInterpreter
+      final Filter filter = call.rel(0);
+      final TableScan scan = call.rel(2);
+      apply(call, filter, scan);
+    } else {
+      throw new AssertionError();
+    }
+  }
+
   protected void apply(RelOptRuleCall call, Filter filter, TableScan scan) {
     final ImmutableIntList projects;
     final ImmutableList.Builder<RexNode> filters = ImmutableList.builder();
@@ -108,4 +123,27 @@ public abstract class FilterTableScanRule extends RelOptRule {
         Bindables.BindableTableScan.create(scan.getCluster(), scan.getTable(),
             filters.build(), projects));
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b0 ->
+            b0.operand(Filter.class).oneInput(b1 ->
+                b1.operand(TableScan.class)
+                    .predicate(FilterTableScanRule::test).noInputs()))
+        .as(Config.class);
+
+    Config INTERPRETER = EMPTY
+        .withOperandSupplier(b0 ->
+            b0.operand(Filter.class).oneInput(b1 ->
+                b1.operand(EnumerableInterpreter.class).oneInput(b2 ->
+                    b2.operand(TableScan.class)
+                        .predicate(FilterTableScanRule::test).noInputs())))
+        .withDescription("FilterTableScanRule:interpreter")
+        .as(Config.class);
+
+    @Override default FilterTableScanRule toRule() {
+      return new FilterTableScanRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/FilterToCalcRule.java b/core/src/main/java/org/apache/calcite/rel/rules/FilterToCalcRule.java
index a6c1d3c..98afcd3 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/FilterToCalcRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/FilterToCalcRule.java
@@ -16,8 +16,8 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.logical.LogicalCalc;
 import org.apache.calcite.rel.logical.LogicalFilter;
@@ -40,29 +40,27 @@ import org.apache.calcite.tools.RelBuilderFactory;
  * {@link org.apache.calcite.rel.logical.LogicalCalc}. This
  * {@link org.apache.calcite.rel.logical.LogicalFilter} will eventually be
  * converted by {@link FilterCalcMergeRule}.
+ *
+ * @see CoreRules#FILTER_TO_CALC
  */
-public class FilterToCalcRule extends RelOptRule implements TransformationRule {
-  //~ Static fields/initializers ---------------------------------------------
-
-  /** @deprecated Use {@link CoreRules#FILTER_TO_CALC}. */
-  @Deprecated // to be removed before 1.25
-  public static final FilterToCalcRule INSTANCE =
-      CoreRules.FILTER_TO_CALC;
+public class FilterToCalcRule
+    extends RelRule<FilterToCalcRule.Config>
+    implements TransformationRule {
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a FilterToCalcRule. */
+  protected FilterToCalcRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a FilterToCalcRule.
-   *
-   * @param relBuilderFactory Builder for relational expressions
-   */
+  @Deprecated // to be removed before 2.0
   public FilterToCalcRule(RelBuilderFactory relBuilderFactory) {
-    super(operand(LogicalFilter.class, any()), relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final LogicalFilter filter = call.rel(0);
     final RelNode rel = filter.getInput();
 
@@ -78,4 +76,16 @@ public class FilterToCalcRule extends RelOptRule implements TransformationRule {
     final LogicalCalc calc = LogicalCalc.create(rel, program);
     call.transformTo(calc);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b ->
+            b.operand(LogicalFilter.class).anyInputs())
+        .as(Config.class);
+
+    @Override default FilterToCalcRule toRule() {
+      return new FilterToCalcRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/IntersectToDistinctRule.java b/core/src/main/java/org/apache/calcite/rel/rules/IntersectToDistinctRule.java
index 7772285..bc74cf4 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/IntersectToDistinctRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/IntersectToDistinctRule.java
@@ -17,10 +17,11 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Intersect;
+import org.apache.calcite.rel.logical.LogicalIntersect;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
@@ -63,24 +64,28 @@ import java.math.BigDecimal;
  * <p><code>R6 = Proj(R5 on all attributes)</code>
  *
  * @see org.apache.calcite.rel.rules.UnionToDistinctRule
+ * @see CoreRules#INTERSECT_TO_DISTINCT
  */
-public class IntersectToDistinctRule extends RelOptRule implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#INTERSECT_TO_DISTINCT}. */
-  @Deprecated // to be removed before 1.25
-  public static final IntersectToDistinctRule INSTANCE =
-      CoreRules.INTERSECT_TO_DISTINCT;
-
-  //~ Constructors -----------------------------------------------------------
+public class IntersectToDistinctRule
+    extends RelRule<IntersectToDistinctRule.Config>
+    implements TransformationRule {
 
   /** Creates an IntersectToDistinctRule. */
-  public IntersectToDistinctRule(Class<? extends Intersect> intersectClazz,
+  protected IntersectToDistinctRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
+  public IntersectToDistinctRule(Class<? extends Intersect> intersectClass,
       RelBuilderFactory relBuilderFactory) {
-    super(operand(intersectClazz, any()), relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(intersectClass));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     final Intersect intersect = call.rel(0);
     if (intersect.all) {
       return; // nothing we can do
@@ -123,4 +128,20 @@ public class IntersectToDistinctRule extends RelOptRule implements Transformatio
     // finally add a project to project out the last column
     call.transformTo(relBuilder.build());
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(LogicalIntersect.class);
+
+    @Override default IntersectToDistinctRule toRule() {
+      return new IntersectToDistinctRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Intersect> intersectClass) {
+      return withOperandSupplier(b -> b.operand(intersectClass).anyInputs())
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/JoinAddRedundantSemiJoinRule.java b/core/src/main/java/org/apache/calcite/rel/rules/JoinAddRedundantSemiJoinRule.java
index 4a9d30f..9c427a5 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/JoinAddRedundantSemiJoinRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/JoinAddRedundantSemiJoinRule.java
@@ -16,8 +16,8 @@
  */
 package org.apache.calcite.rel.rules;
 
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinInfo;
@@ -33,36 +33,38 @@ import com.google.common.collect.ImmutableSet;
  *
  * <p>LogicalJoin(X, Y) &rarr; LogicalJoin(SemiJoin(X, Y), Y)
  *
- * <p>The constructor is parameterized to allow any sub-class of
+ * <p>Can be configured to match any sub-class of
  * {@link org.apache.calcite.rel.core.Join}, not just
  * {@link org.apache.calcite.rel.logical.LogicalJoin}.
+ *
+ * @see CoreRules#JOIN_ADD_REDUNDANT_SEMI_JOIN
  */
-public class JoinAddRedundantSemiJoinRule extends RelOptRule
+public class JoinAddRedundantSemiJoinRule
+    extends RelRule<JoinAddRedundantSemiJoinRule.Config>
     implements TransformationRule {
-  /** @deprecated Use {@link CoreRules#JOIN_ADD_REDUNDANT_SEMI_JOIN}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinAddRedundantSemiJoinRule INSTANCE =
-      CoreRules.JOIN_ADD_REDUNDANT_SEMI_JOIN;
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a JoinAddRedundantSemiJoinRule. */
+  protected JoinAddRedundantSemiJoinRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates an JoinAddRedundantSemiJoinRule.
-   */
+  @Deprecated // to be removed before 2.0
   public JoinAddRedundantSemiJoinRule(Class<? extends Join> clazz,
       RelBuilderFactory relBuilderFactory) {
-    super(operand(clazz, any()), relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(clazz));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(RelOptRuleCall call) {
+  @Override public void onMatch(RelOptRuleCall call) {
     Join origJoinRel = call.rel(0);
     if (origJoinRel.isSemiJoinDone()) {
       return;
     }
 
-    // can't process outer joins using semijoins
+    // can't process outer joins using semi-joins
     if (origJoinRel.getJoinType() != JoinRelType.INNER) {
       return;
     }
@@ -92,4 +94,20 @@ public class JoinAddRedundantSemiJoinRule extends RelOptRule
 
     call.transformTo(newJoinRel);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(LogicalJoin.class);
+
+    @Override default JoinAddRedundantSemiJoinRule toRule() {
+      return new JoinAddRedundantSemiJoinRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Join> joinClass) {
+      return withOperandSupplier(b -> b.operand(joinClass).anyInputs())
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/JoinAssociateRule.java b/core/src/main/java/org/apache/calcite/rel/rules/JoinAssociateRule.java
index 2eb7c38..e1964b0 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/JoinAssociateRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/JoinAssociateRule.java
@@ -17,8 +17,8 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.RelOptCluster;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.volcano.RelSubset;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Join;
@@ -44,31 +44,26 @@ import java.util.List;
  * {@link JoinCommuteRule}.
  *
  * @see JoinCommuteRule
+ * @see CoreRules#JOIN_ASSOCIATE
  */
-public class JoinAssociateRule extends RelOptRule implements TransformationRule {
-  //~ Static fields/initializers ---------------------------------------------
+public class JoinAssociateRule
+    extends RelRule<JoinAssociateRule.Config>
+    implements TransformationRule {
 
-  /** @deprecated Use {@link CoreRules#JOIN_ASSOCIATE}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinAssociateRule INSTANCE =
-      CoreRules.JOIN_ASSOCIATE;
-
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a JoinAssociateRule. */
+  protected JoinAssociateRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a JoinAssociateRule.
-   */
+  @Deprecated // to be removed before 2.0
   public JoinAssociateRule(RelBuilderFactory relBuilderFactory) {
-    super(
-        operand(Join.class,
-            operand(Join.class, any()),
-            operand(RelSubset.class, any())),
-        relBuilderFactory, null);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  public void onMatch(final RelOptRuleCall call) {
+  @Override public void onMatch(final RelOptRuleCall call) {
     final Join topJoin = call.rel(0);
     final Join bottomJoin = call.rel(1);
     final RelNode relA = bottomJoin.getLeft();
@@ -153,4 +148,24 @@ public class JoinAssociateRule extends RelOptRule implements TransformationRule
 
     call.transformTo(newTopJoin);
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(Join.class, RelSubset.class);
+
+    @Override default JoinAssociateRule toRule() {
+      return new JoinAssociateRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Join> joinClass,
+        Class<? extends RelSubset> relSubsetClass) {
+      return withOperandSupplier(b0 ->
+          b0.operand(joinClass).inputs(
+              b1 -> b1.operand(joinClass).anyInputs(),
+              b2 -> b2.operand(relSubsetClass).anyInputs()))
+          .as(Config.class);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/JoinCommuteRule.java b/core/src/main/java/org/apache/calcite/rel/rules/JoinCommuteRule.java
index 8b66d05..bec08e0 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/JoinCommuteRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/JoinCommuteRule.java
@@ -17,9 +17,9 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.Contexts;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinRelType;
@@ -34,9 +34,9 @@ import org.apache.calcite.rex.RexNode;
 import org.apache.calcite.rex.RexShuttle;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
 
 import java.util.List;
-import java.util.function.Predicate;
 
 /**
  * Planner rule that permutes the inputs to a
@@ -47,39 +47,26 @@ import java.util.function.Predicate;
  *
  * <p>To preserve the order of columns in the output row, the rule adds a
  * {@link org.apache.calcite.rel.core.Project}.
+ *
+ * @see CoreRules#JOIN_COMMUTE
+ * @see CoreRules#JOIN_COMMUTE_OUTER
  */
-public class JoinCommuteRule extends RelOptRule implements TransformationRule {
-  //~ Static fields/initializers ---------------------------------------------
-
-  /** @deprecated Use {@link CoreRules#JOIN_COMMUTE}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinCommuteRule INSTANCE = CoreRules.JOIN_COMMUTE;
-
-  /** @deprecated Use {@link CoreRules#JOIN_COMMUTE_OUTER}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinCommuteRule SWAP_OUTER = CoreRules.JOIN_COMMUTE_OUTER;
+public class JoinCommuteRule
+    extends RelRule<JoinCommuteRule.Config>
+    implements TransformationRule {
 
-  private final boolean swapOuter;
-
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a JoinCommuteRule. */
+  protected JoinCommuteRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a JoinCommuteRule.
-   */
+  @Deprecated // to be removed before 2.0
   public JoinCommuteRule(Class<? extends Join> clazz,
       RelBuilderFactory relBuilderFactory, boolean swapOuter) {
-    // FIXME Enable this rule for joins with system fields
-    super(
-        operandJ(clazz, null,
-            (Predicate<Join>) j -> j.getLeft().getId() != j.getRight().getId()
-                && j.getSystemFieldList().isEmpty(),
-            any()),
-        relBuilderFactory, null);
-    this.swapOuter = swapOuter;
-  }
-
-  JoinCommuteRule(boolean swapOuter) {
-    this(LogicalJoin.class, RelFactories.LOGICAL_BUILDER, swapOuter);
+    this(Config.DEFAULT.withRelBuilderFactory(relBuilderFactory)
+        .as(Config.class)
+        .withOperandFor(clazz)
+        .withSwapOuter(swapOuter));
   }
 
   @Deprecated // to be removed before 2.0
@@ -136,7 +123,7 @@ public class JoinCommuteRule extends RelOptRule implements TransformationRule {
     // swap.  This way, we will generate one semijoin for the original
     // join, and one for the swapped join, and no more.  This
     // doesn't prevent us from seeing any new combinations assuming
-    // that the planner tries the desired order (semijoins after swaps).
+    // that the planner tries the desired order (semi-joins after swaps).
     Join newJoin =
         join.copy(join.getTraitSet(), condition, join.getRight(),
             join.getLeft(), joinType.swap(), join.isSemiJoinDone());
@@ -147,16 +134,16 @@ public class JoinCommuteRule extends RelOptRule implements TransformationRule {
         .build();
   }
 
-  public boolean matches(RelOptRuleCall call) {
+  @Override public boolean matches(RelOptRuleCall call) {
     Join join = call.rel(0);
     // SEMI and ANTI join cannot be swapped.
     return join.getJoinType().projectsRight();
   }
 
-  public void onMatch(final RelOptRuleCall call) {
+  @Override public void onMatch(final RelOptRuleCall call) {
     Join join = call.rel(0);
 
-    final RelNode swapped = swap(join, this.swapOuter, call.builder());
+    final RelNode swapped = swap(join, config.isSwapOuter(), call.builder());
     if (swapped == null) {
       return;
     }
@@ -229,4 +216,35 @@ public class JoinCommuteRule extends RelOptRule implements TransformationRule {
           + ", rightFieldCount=" + rightFields.size());
     }
   }
+
+  /** Rule configuration. */
+  public interface Config extends RelRule.Config {
+    Config DEFAULT = EMPTY.as(Config.class)
+        .withOperandFor(LogicalJoin.class)
+        .withSwapOuter(false);
+
+    @Override default JoinCommuteRule toRule() {
+      return new JoinCommuteRule(this);
+    }
+
+    /** Defines an operand tree for the given classes. */
+    default Config withOperandFor(Class<? extends Join> joinClass) {
+      return withOperandSupplier(b ->
+          b.operand(joinClass)
+              // FIXME Enable this rule for joins with system fields
+              .predicate(j ->
+                  j.getLeft().getId() != j.getRight().getId()
+                      && j.getSystemFieldList().isEmpty())
+              .anyInputs())
+          .as(Config.class);
+    }
+
+    /** Whether to swap outer joins. */
+    @ImmutableBeans.Property
+    @ImmutableBeans.BooleanDefault(false)
+    boolean isSwapOuter();
+
+    /** Sets {@link #isSwapOuter()}. */
+    Config withSwapOuter(boolean swapOuter);
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/JoinExtractFilterRule.java b/core/src/main/java/org/apache/calcite/rel/rules/JoinExtractFilterRule.java
index bf6b916..8cdb6fe 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/JoinExtractFilterRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/JoinExtractFilterRule.java
@@ -17,6 +17,7 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.rel.core.Join;
+import org.apache.calcite.rel.logical.LogicalJoin;
 import org.apache.calcite.tools.RelBuilderFactory;
 
 /**
@@ -29,28 +30,37 @@ import org.apache.calcite.tools.RelBuilderFactory;
  * can be combined with conditions and expressions above the join. It also makes
  * the <code>FennelCartesianJoinRule</code> applicable.
  *
- * <p>The constructor is parameterized to allow any sub-class of
+ * <p>Can be configured to match any sub-class of
  * {@link org.apache.calcite.rel.core.Join}, not just
- * {@link org.apache.calcite.rel.logical.LogicalJoin}.</p>
+ * {@link org.apache.calcite.rel.logical.LogicalJoin}.
+ *
+ * @see CoreRules#JOIN_EXTRACT_FILTER
  */
 public final class JoinExtractFilterRule extends AbstractJoinExtractFilterRule {
-  //~ Static fields/initializers ---------------------------------------------
-
-  /** @deprecated Use {@link CoreRules#JOIN_EXTRACT_FILTER}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinExtractFilterRule INSTANCE =
-      CoreRules.JOIN_EXTRACT_FILTER;
 
-  //~ Constructors -----------------------------------------------------------
+  /** Creates a JoinExtractFilterRule. */
+  protected JoinExtractFilterRule(Config config) {
+    super(config);
+  }
 
-  /**
-   * Creates a JoinExtractFilterRule.
-   */
+  @Deprecated // to be removed before 2.0
   public JoinExtractFilterRule(Class<? extends Join> clazz,
       RelBuilderFactory relBuilderFactory) {
-    super(operand(clazz, any()), relBuilderFactory, null);
+    this(Config.DEFAULT
+        .withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b ->
+            b.operand(clazz).anyInputs())
+        .as(Config.class));
   }
 
-  //~ Methods ----------------------------------------------------------------
+  /** Rule configuration. */
+  public interface Config extends AbstractJoinExtractFilterRule.Config {
+    Config DEFAULT = EMPTY
+        .withOperandSupplier(b -> b.operand(LogicalJoin.class).anyInputs())
+        .as(Config.class);
 
+    @Override default JoinExtractFilterRule toRule() {
+      return new JoinExtractFilterRule(this);
+    }
+  }
 }
diff --git a/core/src/main/java/org/apache/calcite/rel/rules/JoinProjectTransposeRule.java b/core/src/main/java/org/apache/calcite/rel/rules/JoinProjectTransposeRule.java
index 47bd17b..c790a37 100644
--- a/core/src/main/java/org/apache/calcite/rel/rules/JoinProjectTransposeRule.java
+++ b/core/src/main/java/org/apache/calcite/rel/rules/JoinProjectTransposeRule.java
@@ -17,28 +17,29 @@
 package org.apache.calcite.rel.rules;
 
 import org.apache.calcite.plan.Contexts;
-import org.apache.calcite.plan.RelOptRule;
 import org.apache.calcite.plan.RelOptRuleCall;
 import org.apache.calcite.plan.RelOptRuleOperand;
 import org.apache.calcite.plan.RelOptUtil;
+import org.apache.calcite.plan.RelRule;
 import org.apache.calcite.plan.Strong;
 import org.apache.calcite.rel.RelNode;
 import org.apache.calcite.rel.core.Join;
 import org.apache.calcite.rel.core.JoinRelType;
 import org.apache.calcite.rel.core.Project;
-import org.apache.calcite.rel.core.RelFactories;
 import org.apache.calcite.rel.core.RelFactories.ProjectFactory;
+import org.apache.calcite.rel.logical.LogicalJoin;
+import org.apache.calcite.rel.logical.LogicalProject;
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.rel.type.RelDataTypeField;
 import org.apache.calcite.rex.RexBuilder;
 import org.apache.calcite.rex.RexLocalRef;
 import org.apache.calcite.rex.RexNode;
-import org.apache.calcite.rex.RexOver;
 import org.apache.calcite.rex.RexProgram;
 import org.apache.calcite.rex.RexProgramBuilder;
 import org.apache.calcite.sql.validate.SqlValidatorUtil;
 import org.apache.calcite.tools.RelBuilder;
 import org.apache.calcite.tools.RelBuilderFactory;
+import org.apache.calcite.util.ImmutableBeans;
 import org.apache.calcite.util.Pair;
 
 import java.util.ArrayList;
@@ -55,129 +56,108 @@ import java.util.List;
  * {@link org.apache.calcite.rel.logical.LogicalProject} doesn't originate from
  * a null generating input in an outer join.
  */
-public class JoinProjectTransposeRule extends RelOptRule implements TransformationRule {
-  //~ Static fields/initializers ---------------------------------------------
-
-  /** @deprecated Use {@link CoreRules#JOIN_PROJECT_BOTH_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinProjectTransposeRule BOTH_PROJECT =
-      CoreRules.JOIN_PROJECT_BOTH_TRANSPOSE;
-
-  /** @deprecated Use {@link CoreRules#JOIN_PROJECT_LEFT_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinProjectTransposeRule LEFT_PROJECT =
-      CoreRules.JOIN_PROJECT_LEFT_TRANSPOSE;
-
-  /** @deprecated Use {@link CoreRules#JOIN_PROJECT_RIGHT_TRANSPOSE}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinProjectTransposeRule RIGHT_PROJECT =
-      CoreRules.JOIN_PROJECT_RIGHT_TRANSPOSE;
-
-  /** @deprecated Use
-   * {@link CoreRules#JOIN_PROJECT_BOTH_TRANSPOSE_INCLUDE_OUTER}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinProjectTransposeRule BOTH_PROJECT_INCLUDE_OUTER =
-      CoreRules.JOIN_PROJECT_BOTH_TRANSPOSE_INCLUDE_OUTER;
-
-  /** @deprecated Use
-   * {@link CoreRules#JOIN_PROJECT_LEFT_TRANSPOSE_INCLUDE_OUTER}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinProjectTransposeRule LEFT_PROJECT_INCLUDE_OUTER =
-      CoreRules.JOIN_PROJECT_LEFT_TRANSPOSE_INCLUDE_OUTER;
-
-  /** @deprecated Use
-   * {@link CoreRules#JOIN_PROJECT_RIGHT_TRANSPOSE_INCLUDE_OUTER}. */
-  @Deprecated // to be removed before 1.25
-  public static final JoinProjectTransposeRule RIGHT_PROJECT_INCLUDE_OUTER =
-      CoreRules.JOIN_PROJECT_RIGHT_TRANSPOSE_INCLUDE_OUTER;
-
-  private final boolean includeOuter;
-
-  //~ Constructors -----------------------------------------------------------
+public class JoinProjectTransposeRule
+    extends RelRule<JoinProjectTransposeRule.Config>
+    implements TransformationRule {
 
   /** Creates a JoinProjectTransposeRule. */
+  protected JoinProjectTransposeRule(Config config) {
+    super(config);
+  }
+
+  @Deprecated // to be removed before 2.0
   public JoinProjectTransposeRule(RelOptRuleOperand operand,
       String description, boolean includeOuter,
       RelBuilderFactory relBuilderFactory) {
-    super(operand, relBuilderFactory, description);
-    this.includeOuter = includeOuter;
+    this(Config.DEFAULT.withDescription(description)
+        .withRelBuilderFactory(relBuilderFactory)
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class)
+        .withIncludeOuter(includeOuter));
   }
 
-  /** Creates a JoinProjectTransposeRule with default factory. */
+  @Deprecated // to be removed before 2.0
   public JoinProjectTransposeRule(
       RelOptRuleOperand operand,
       String description) {
-    this(operand, description, false, RelFactories.LOGICAL_BUILDER);
+    this(Config.DEFAULT.withDescription(description)
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class));
   }
 
   @Deprecated // to be removed before 2.0
   public JoinProjectTransposeRule(RelOptRuleOperand operand,
       String description, ProjectFactory projectFactory) {
-    this(operand, description, false,
-        RelBuilder.proto(Contexts.of(projectFactory)));
+    this(Config.DEFAULT.withDescription(description)
+        .withRelBuilderFactory(RelBuilder.proto(Contexts.of(projectFactory)))
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class));
   }
 
   @Deprecated // to be removed before 2.0
   public JoinProjectTransposeRule(RelOptRuleOperand operand,
       String description, boolean includeOuter,
       ProjectFactory projectFactory) {
-    this(operand, description, includeOuter,
-        RelBuilder.proto(Contexts.of(projectFactory)));
+    this(Config.DEFAULT.withDescription(description)
+        .withRelBuilderFactory(RelBuilder.proto(Contexts.of(projectFactory)))
+        .withOperandSupplier(b -> b.exactly(operand))
+        .as(Config.class)
+        .withIncludeOuter(includeOuter));
   }
 
   //~ Methods ----------------------------------------------------------------
 
-  // implement RelOptRule
-  public void onMatch(RelOptRuleCall call) {
-    Join joinRel = call.rel(0);
-    JoinRelType joinType = joinRel.getJoinType();
+  @Override public void onMatch(RelOptRuleCall call) {
+    final Join join = call.rel(0);
+    final JoinRelType joinType = join.getJoinType();
 
-    Project leftProj;
-    Project rightProj;
+    Project leftProject;
+    Project rightProject;
     RelNode leftJoinChild;
     RelNode rightJoinChild;
 
     // If 1) the rule works on outer joins, or
     //    2) input's projection doesn't generate nulls
+    final boolean includeOuter = config.isIncludeOuter();
     if (hasLeftChild(call)
-            && (includeOuter || !joinType.generatesNullsOnLeft())) {
-      leftProj = call.rel(1);
-      leftJoinChild = getProjectChild(call, leftProj, true);
+        && (includeOuter || !joinType.generatesNullsOnLeft())) {
+      leftProject = call.rel(1);
+      leftJoinChild = getProjectChild(call, leftProject, true);
     } else {
-      leftProj = null;
+      leftProject = null;
       leftJoinChild = call.rel(1);
     }
     if (hasRightChild(call)
-            && (includeOuter || !joinType.generatesNullsOnRight())) {
-      rightProj = getRightChild(call);
-      rightJoinChild = getProjectChild(call, rightProj, false);
+        && (includeOuter || !joinType.generatesNullsOnRight())) {
+      rightProject = getRightChild(call);
+      rightJoinChild = getProjectChild(call, rightProject, false);
     } else {
-      rightProj = null;
-      rightJoinChild = joinRel.getRight();
+      rightProject = null;
+      rightJoinChild = join.getRight();
     }
 
     // Skip projects containing over clause
-    if (leftProj != null && RexOver.containsOver(leftProj.getProjects(), null)) {
-      leftProj = null;
-      leftJoinChild = joinRel.getLeft();
+    if (leftProject != null && leftProject.containsOver()) {
+      leftProject = null;
+      leftJoinChild = join.getLeft();
     }
-    if (rightProj != null && RexOver.containsOver(rightProj.getProjects(), null)) {
-      rightProj = null;
-      rightJoinChild = joinRel.getRight();
+    if (rightProject != null && rightProject.containsOver()) {
+      rightProject = null;
+      rightJoinChild = join.getRight();
     }
 
-    if ((leftProj == null) && (rightProj == null)) {
+    if ((leftProject == null) && (rightProject == null)) {
       return;
     }
 
     if (includeOuter) {
-      if (leftProj != null && joinType.generatesNullsOnLeft()
-          && !Strong.allStrong(leftProj.getProjects())) {
+      if (leftProject != null && joinType.generatesNullsOnLeft()
+          && !Strong.allStrong(leftProject.getProjects())) {
         return;
       }
 
-      if (rightProj != null && joinType.generatesNullsOnRight()
-          && !Strong.allStrong(rightProj.getProjects())) {
+      if (rightProject != null && joinType.generatesNullsOnRight()
+          && !Strong.allStrong(rightProject.getProjects())) {
         return;
       }
     }
@@ -191,12 +171,12 @@ public class JoinProjectTransposeRule extends RelOptRule implements Transformati
     // underneath the projects that feed into the join.  This is the input
     // into the bottom RexProgram.  Note that the join type is an inner
     // join because the inputs haven't actually been joined yet.
-    RelDataType joinChildrenRowType =
+    final RelDataType joinChildrenRowType =
         SqlValidatorUtil.deriveJoinRowType(
             leftJoinChild.getRowType(),
             rightJoinChild.getRowType(),
             JoinRelType.INNER,
-            joinRel.getCluster().getTypeFactory(),
+            join.getCluster().getTypeFactory(),
             null,
             Collections.emptyList());
 
@@ -205,23 +185,23 @@ public class JoinProjectTransposeRule extends RelOptRule implements Transformati
     // expressions, shift them to the right by the number of fields on
     // the LHS.  If the join input was not a projection, simply create
     // references to the inputs.
-    int nProjExprs = joinRel.getRowType().getFieldCount();
+    final int nProjExprs = join.getRowType().getFieldCount();
     final List<Pair<RexNode, String>> projects = new ArrayList<>();
-    final RexBuilder rexBuilder = joinRel.getCluster().getRexBuilder();
+    final RexBuilder rexBuilder = join.getCluster().getRexBuilder();
 
     createProjectExprs(
-        leftProj,
+        leftProject,
         leftJoinChild,
         0,
         rexBuilder,
         joinChildrenRowType.getFieldList(),
         projects);
 
-    List<RelDataTypeField> leftFields =
+    final List<RelDataTypeField> leftFields =
         leftJoinChild.getRowType().getFieldList();
-    int nFieldsLeft = leftFields.size();
+    final int nFieldsLeft = leftFields.size();
     createProjectExprs(
-        rightProj,
+        rightProject,
         rightJoinChild,
         nFieldsLeft,
         rexBuilder,
@@ -238,21 +218,21 @@ public class JoinProjectTransposeRule extends RelOptRule implements Transformati
             Pair.right(projects));
 
     // create the RexPrograms and merge them
-    RexProgram bottomProgram =
+    final RexProgram bottomProgram =
         RexProgram.create(
             joinChildrenRowType,
             Pair.left(projects),
             null,
             projRowType,
             rexBuilder);
-    RexProgramBuilder topProgramBuilder =
+    final RexProgramBuilder topProgramBuilder =
         new RexProgramBuilder(
             projRowType,
             rexBuilder);
     topProgramBuilder.addIdentity();
-    topProgramBuilder.addCondition(joinRel.getCondition());
-    RexProgram topProgram = topProgramBuilder.getProgram();
-    RexProgram mergedProgram =
+    topProgramBuilder.addCondition(join.getCondition());
+    final RexProgram topProgram = topProgramBuilder.getProgram();
+    final RexProgram mergedProgram =
         RexProgramBuilder.mergePrograms(
             topProgram,
             bottomProgram,
... 15637 lines suppressed ...