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

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


The following commit(s) were added to refs/heads/master by this push:
     new 09d6c5a45e Decouple logical planning and native query generation in 
SQL planning (#14232)
09d6c5a45e is described below

commit 09d6c5a45ed9c737e711403a864903dcbbf22830
Author: Rohan Garg <[email protected]>
AuthorDate: Tue Jun 20 04:30:40 2023 +0530

    Decouple logical planning and native query generation in SQL planning 
(#14232)
    
    Add a new planning strategy that explicitly decouples the DAG from building 
the native query.
    
    With this mode, it is Calcite's job to generate a "logical DAG" which is 
all of the various
    DruidProject, DruidFilter, etc. nodes.  We then take those nodes and use 
them to build a native
    query.  The current commit doesn't pass all tests, but it does work for 
some things and is a
    decent starting baseline.
---
 .../calcite/plan/volcano/DruidVolcanoCost.java     | 288 ++++++++++++++
 .../sql/calcite/planner/CalciteRulesManager.java   |  65 +++-
 .../sql/calcite/planner/DruidQueryGenerator.java   | 336 ++++++++++++++++
 .../druid/sql/calcite/planner/PlannerConfig.java   |  32 +-
 .../druid/sql/calcite/planner/PlannerFactory.java  |  15 +-
 .../druid/sql/calcite/planner/QueryHandler.java    | 119 ++++--
 .../druid/sql/calcite/rel/CostEstimates.java       |  22 +-
 .../druid/sql/calcite/rel/PartialDruidQuery.java   |  56 ++-
 .../sql/calcite/rel/logical/DruidAggregate.java    | 101 +++++
 .../druid/sql/calcite/rel/logical/DruidFilter.java |  54 +++
 .../rel/logical/DruidLogicalConvention.java        |  93 +++++
 .../sql/calcite/rel/logical/DruidLogicalNode.java  |  30 ++
 .../sql/calcite/rel/logical/DruidProject.java      | 101 +++++
 .../druid/sql/calcite/rel/logical/DruidSort.java   |  97 +++++
 .../sql/calcite/rel/logical/DruidTableScan.java    | 111 ++++++
 .../druid/sql/calcite/rel/logical/DruidValues.java |  64 ++++
 .../sql/calcite/rule/DruidLogicalValuesRule.java   |   2 +-
 .../logical/DruidAggregateCaseToFilterRule.java    | 339 ++++++++++++++++
 .../calcite/rule/logical/DruidAggregateRule.java   |  88 +++++
 .../sql/calcite/rule/logical/DruidFilterRule.java  |  62 +++
 .../calcite/rule/logical/DruidLogicalRules.java    |  90 +++++
 .../sql/calcite/rule/logical/DruidProjectRule.java |  59 +++
 .../sql/calcite/rule/logical/DruidSortRule.java    |  60 +++
 .../calcite/rule/logical/DruidTableScanRule.java   |  54 +++
 .../sql/calcite/rule/logical/DruidValuesRule.java  |  58 +++
 .../druid/sql/calcite/BaseCalciteQueryTest.java    |  12 +
 .../calcite/DecoupledPlanningCalciteQueryTest.java | 425 +++++++++++++++++++++
 .../apache/druid/sql/calcite/QueryTestBuilder.java |  13 +-
 28 files changed, 2777 insertions(+), 69 deletions(-)

diff --git 
a/sql/src/main/java/org/apache/calcite/plan/volcano/DruidVolcanoCost.java 
b/sql/src/main/java/org/apache/calcite/plan/volcano/DruidVolcanoCost.java
new file mode 100644
index 0000000000..c26fe21c69
--- /dev/null
+++ b/sql/src/main/java/org/apache/calcite/plan/volcano/DruidVolcanoCost.java
@@ -0,0 +1,288 @@
+/*
+ * 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.
+ */
+
+//CHECKSTYLE.OFF: PackageName - Must be in Calcite
+
+package org.apache.calcite.plan.volcano;
+
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptCostFactory;
+import org.apache.calcite.plan.RelOptUtil;
+
+import java.util.Objects;
+
+/**
+ * Druid's extension to {@link VolcanoCost}. The difference between the two is 
in
+ * comparing two costs. Druid's cost model gives most weightage to rowCount, 
then to cpuCost and then lastly ioCost.
+ */
+public class DruidVolcanoCost implements RelOptCost
+{
+
+  static final DruidVolcanoCost INFINITY = new DruidVolcanoCost(
+      Double.POSITIVE_INFINITY,
+      Double.POSITIVE_INFINITY,
+      Double.POSITIVE_INFINITY
+  )
+  {
+    @Override
+    public String toString()
+    {
+      return "{inf}";
+    }
+  };
+
+  //CHECKSTYLE.OFF: Regexp
+  static final DruidVolcanoCost HUGE = new DruidVolcanoCost(Double.MAX_VALUE, 
Double.MAX_VALUE, Double.MAX_VALUE) {
+    @Override
+    public String toString()
+    {
+      return "{huge}";
+    }
+  };
+  //CHECKSTYLE.ON: Regexp
+
+  static final DruidVolcanoCost ZERO =
+      new DruidVolcanoCost(0.0, 0.0, 0.0)
+      {
+        @Override
+        public String toString()
+        {
+          return "{0}";
+        }
+      };
+
+  static final DruidVolcanoCost TINY =
+      new DruidVolcanoCost(1.0, 1.0, 0.0)
+      {
+        @Override
+        public String toString()
+        {
+          return "{tiny}";
+        }
+      };
+
+  public static final RelOptCostFactory FACTORY = new Factory();
+
+  final double cpu;
+  final double io;
+  final double rowCount;
+
+  DruidVolcanoCost(double rowCount, double cpu, double io)
+  {
+    this.rowCount = rowCount;
+    this.cpu = cpu;
+    this.io = io;
+  }
+
+  @Override
+  public double getCpu()
+  {
+    return cpu;
+  }
+
+  @Override
+  public boolean isInfinite()
+  {
+    return (this == INFINITY)
+           || (this.rowCount == Double.POSITIVE_INFINITY)
+           || (this.cpu == Double.POSITIVE_INFINITY)
+           || (this.io == Double.POSITIVE_INFINITY);
+  }
+
+  @Override
+  public double getIo()
+  {
+    return io;
+  }
+
+  @Override
+  public boolean isLe(RelOptCost other)
+  {
+    DruidVolcanoCost that = (DruidVolcanoCost) other;
+    return (this == that)
+           || ((this.rowCount < that.rowCount)
+               || (this.rowCount == that.rowCount && this.cpu < that.cpu)
+               || (this.rowCount == that.rowCount && this.cpu == that.cpu && 
this.io <= that.io));
+  }
+
+  @Override
+  public boolean isLt(RelOptCost other)
+  {
+    return isLe(other) && !equals(other);
+  }
+
+  @Override
+  public double getRows()
+  {
+    return rowCount;
+  }
+
+  @Override
+  public int hashCode()
+  {
+    return Objects.hash(rowCount, cpu, io);
+  }
+
+  @Override
+  public boolean equals(RelOptCost other)
+  {
+    return this == other
+           || other instanceof DruidVolcanoCost
+              && (this.rowCount == ((DruidVolcanoCost) other).rowCount)
+              && (this.cpu == ((DruidVolcanoCost) other).cpu)
+              && (this.io == ((DruidVolcanoCost) other).io);
+  }
+
+  @Override
+  public boolean equals(Object obj)
+  {
+    if (obj instanceof DruidVolcanoCost) {
+      return equals((DruidVolcanoCost) obj);
+    }
+    return false;
+  }
+
+  @Override
+  public boolean isEqWithEpsilon(RelOptCost other)
+  {
+    if (!(other instanceof DruidVolcanoCost)) {
+      return false;
+    }
+    DruidVolcanoCost that = (DruidVolcanoCost) other;
+    return (this == that)
+           || ((Math.abs(this.rowCount - that.rowCount) < RelOptUtil.EPSILON)
+               && (Math.abs(this.cpu - that.cpu) < RelOptUtil.EPSILON)
+               && (Math.abs(this.io - that.io) < RelOptUtil.EPSILON));
+  }
+
+  @Override
+  public RelOptCost minus(RelOptCost other)
+  {
+    if (this == INFINITY) {
+      return this;
+    }
+    DruidVolcanoCost that = (DruidVolcanoCost) other;
+    return new DruidVolcanoCost(
+        this.rowCount - that.rowCount,
+        this.cpu - that.cpu,
+        this.io - that.io
+    );
+  }
+
+  @Override
+  public RelOptCost multiplyBy(double factor)
+  {
+    if (this == INFINITY) {
+      return this;
+    }
+    return new DruidVolcanoCost(rowCount * factor, cpu * factor, io * factor);
+  }
+
+  @Override
+  public double divideBy(RelOptCost cost)
+  {
+    // Compute the geometric average of the ratios of all of the factors
+    // which are non-zero and finite.
+    DruidVolcanoCost that = (DruidVolcanoCost) cost;
+    double d = 1;
+    double n = 0;
+    if ((this.rowCount != 0)
+        && !Double.isInfinite(this.rowCount)
+        && (that.rowCount != 0)
+        && !Double.isInfinite(that.rowCount)) {
+      d *= this.rowCount / that.rowCount;
+      ++n;
+    }
+    if ((this.cpu != 0)
+        && !Double.isInfinite(this.cpu)
+        && (that.cpu != 0)
+        && !Double.isInfinite(that.cpu)) {
+      d *= this.cpu / that.cpu;
+      ++n;
+    }
+    if ((this.io != 0)
+        && !Double.isInfinite(this.io)
+        && (that.io != 0)
+        && !Double.isInfinite(that.io)) {
+      d *= this.io / that.io;
+      ++n;
+    }
+    if (n == 0) {
+      return 1.0;
+    }
+    return Math.pow(d, 1 / n);
+  }
+
+  @Override
+  public RelOptCost plus(RelOptCost other)
+  {
+    DruidVolcanoCost that = (DruidVolcanoCost) other;
+    if ((this == INFINITY) || (that == INFINITY)) {
+      return INFINITY;
+    }
+    return new DruidVolcanoCost(
+        this.rowCount + that.rowCount,
+        this.cpu + that.cpu,
+        this.io + that.io
+    );
+  }
+
+  @Override
+  public String toString()
+  {
+    return "{" + rowCount + " rows, " + cpu + " cpu, " + io + " io}";
+  }
+
+  /**
+   * Implementation of {@link RelOptCostFactory}
+   * that creates {@link DruidVolcanoCost}s.
+   */
+  public static class Factory implements RelOptCostFactory
+  {
+    @Override
+    public RelOptCost makeCost(double dRows, double dCpu, double dIo)
+    {
+      return new DruidVolcanoCost(dRows, dCpu, dIo);
+    }
+
+    @Override
+    public RelOptCost makeHugeCost()
+    {
+      return DruidVolcanoCost.HUGE;
+    }
+
+    @Override
+    public RelOptCost makeInfiniteCost()
+    {
+      return DruidVolcanoCost.INFINITY;
+    }
+
+    @Override
+    public RelOptCost makeTinyCost()
+    {
+      return DruidVolcanoCost.TINY;
+    }
+
+    @Override
+    public RelOptCost makeZeroCost()
+    {
+      return DruidVolcanoCost.ZERO;
+    }
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalciteRulesManager.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalciteRulesManager.java
index 5cc56f7e4c..5bf4bee733 100644
--- 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalciteRulesManager.java
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/CalciteRulesManager.java
@@ -26,6 +26,7 @@ import org.apache.calcite.plan.RelOptLattice;
 import org.apache.calcite.plan.RelOptMaterialization;
 import org.apache.calcite.plan.RelOptPlanner;
 import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptUtil;
 import org.apache.calcite.plan.RelTraitSet;
 import org.apache.calcite.plan.hep.HepProgram;
 import org.apache.calcite.plan.hep.HepProgramBuilder;
@@ -74,11 +75,14 @@ import org.apache.calcite.rel.rules.TableScanRule;
 import org.apache.calcite.rel.rules.UnionPullUpConstantsRule;
 import org.apache.calcite.rel.rules.UnionToDistinctRule;
 import org.apache.calcite.rel.rules.ValuesReduceRule;
+import org.apache.calcite.sql.SqlExplainFormat;
+import org.apache.calcite.sql.SqlExplainLevel;
 import org.apache.calcite.sql2rel.RelDecorrelator;
 import org.apache.calcite.sql2rel.RelFieldTrimmer;
 import org.apache.calcite.tools.Program;
 import org.apache.calcite.tools.Programs;
 import org.apache.calcite.tools.RelBuilder;
+import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.sql.calcite.external.ExternalTableScanRule;
 import org.apache.druid.sql.calcite.rule.DruidLogicalValuesRule;
 import org.apache.druid.sql.calcite.rule.DruidRelToDruidRule;
@@ -88,6 +92,8 @@ import 
org.apache.druid.sql.calcite.rule.ExtensionCalciteRuleProvider;
 import org.apache.druid.sql.calcite.rule.FilterJoinExcludePushToChildRule;
 import org.apache.druid.sql.calcite.rule.ProjectAggregatePruneUnusedCallRule;
 import org.apache.druid.sql.calcite.rule.SortCollapseRule;
+import 
org.apache.druid.sql.calcite.rule.logical.DruidAggregateCaseToFilterRule;
+import org.apache.druid.sql.calcite.rule.logical.DruidLogicalRules;
 import org.apache.druid.sql.calcite.run.EngineFeature;
 
 import java.util.List;
@@ -95,8 +101,11 @@ import java.util.Set;
 
 public class CalciteRulesManager
 {
+  private static final Logger log = new Logger(CalciteRulesManager.class);
+
   public static final int DRUID_CONVENTION_RULES = 0;
   public static final int BINDABLE_CONVENTION_RULES = 1;
+  public static final int DRUID_DAG_CONVENTION_RULES = 2;
 
   // Due to Calcite bug (CALCITE-3845), ReduceExpressionsRule can considered 
expression which is the same as the
   // previous input expression as reduced. Basically, the expression is 
actually not reduced but is still considered as
@@ -249,12 +258,56 @@ public class CalciteRulesManager
             buildHepProgram(REDUCTION_RULES, true, 
DefaultRelMetadataProvider.INSTANCE, HEP_DEFAULT_MATCH_LIMIT)
         );
 
+    boolean isDebug = plannerContext.queryContext().isDebug();
     return ImmutableList.of(
         Programs.sequence(preProgram, 
Programs.ofRules(druidConventionRuleSet(plannerContext))),
-        Programs.sequence(preProgram, 
Programs.ofRules(bindableConventionRuleSet(plannerContext)))
+        Programs.sequence(preProgram, 
Programs.ofRules(bindableConventionRuleSet(plannerContext))),
+        Programs.sequence(
+            // currently, adding logging program after every stage for easier 
debugging
+            new LoggingProgram("Start", isDebug),
+            Programs.subQuery(DefaultRelMetadataProvider.INSTANCE),
+            new LoggingProgram("After subquery program", isDebug),
+            DecorrelateAndTrimFieldsProgram.INSTANCE,
+            new LoggingProgram("After trim fields and decorelate program", 
isDebug),
+            buildHepProgram(REDUCTION_RULES, true, 
DefaultRelMetadataProvider.INSTANCE, HEP_DEFAULT_MATCH_LIMIT),
+            new LoggingProgram("After hep planner program", isDebug),
+            Programs.ofRules(logicalConventionRuleSet(plannerContext)),
+            new LoggingProgram("After volcano planner program", isDebug)
+        )
     );
   }
 
+  private static class LoggingProgram implements Program
+  {
+    private final String stage;
+    private final boolean isDebug;
+
+    public LoggingProgram(String stage, boolean isDebug)
+    {
+      this.stage = stage;
+      this.isDebug = isDebug;
+    }
+
+    @Override
+    public RelNode run(
+        RelOptPlanner planner,
+        RelNode rel,
+        RelTraitSet requiredOutputTraits,
+        List<RelOptMaterialization> materializations,
+        List<RelOptLattice> lattices
+    )
+    {
+      if (isDebug) {
+        log.info(
+            "%s%n%s",
+            stage,
+            RelOptUtil.dumpPlan("", rel, SqlExplainFormat.TEXT, 
SqlExplainLevel.ALL_ATTRIBUTES)
+        );
+      }
+      return rel;
+    }
+  }
+
   public Program buildHepProgram(
       final Iterable<? extends RelOptRule> rules,
       final boolean noDag,
@@ -287,6 +340,16 @@ public class CalciteRulesManager
     return retVal.build();
   }
 
+  public List<RelOptRule> logicalConventionRuleSet(final PlannerContext 
plannerContext)
+  {
+    final ImmutableList.Builder<RelOptRule> retVal = ImmutableList
+        .<RelOptRule>builder()
+        .addAll(baseRuleSet(plannerContext))
+        .add(DruidAggregateCaseToFilterRule.INSTANCE)
+        .add(new DruidLogicalRules(plannerContext).rules().toArray(new 
RelOptRule[0]));
+    return retVal.build();
+  }
+
   public List<RelOptRule> bindableConventionRuleSet(final PlannerContext 
plannerContext)
   {
     return ImmutableList.<RelOptRule>builder()
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidQueryGenerator.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidQueryGenerator.java
new file mode 100644
index 0000000000..87d46809cd
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidQueryGenerator.java
@@ -0,0 +1,336 @@
+/*
+ * 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.druid.sql.calcite.planner;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelShuttleImpl;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.core.TableFunctionScan;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+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.LogicalUnion;
+import org.apache.calcite.rel.logical.LogicalValues;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.druid.java.util.common.ISE;
+import org.apache.druid.query.InlineDataSource;
+import org.apache.druid.segment.column.RowSignature;
+import org.apache.druid.sql.calcite.rel.PartialDruidQuery;
+import org.apache.druid.sql.calcite.rel.logical.DruidTableScan;
+import org.apache.druid.sql.calcite.rule.DruidLogicalValuesRule;
+import org.apache.druid.sql.calcite.table.DruidTable;
+import org.apache.druid.sql.calcite.table.InlineTable;
+import org.apache.druid.sql.calcite.table.RowSignatures;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+
+/**
+ * Converts a DAG of {@link 
org.apache.druid.sql.calcite.rel.logical.DruidLogicalNode} convention to a 
native
+ * Druid query for execution. The convertion is done via a {@link 
org.apache.calcite.rel.RelShuttle} visitor
+ * implementation.
+ */
+public class DruidQueryGenerator extends RelShuttleImpl
+{
+  private final List<PartialDruidQuery> queryList = new ArrayList<>();
+  private final List<DruidTable> queryTables = new ArrayList<>();
+  private final PlannerContext plannerContext;
+  private PartialDruidQuery partialDruidQuery;
+  private PartialDruidQuery.Stage currentStage = null;
+  private DruidTable currentTable = null;
+  private boolean isRoot = true;
+
+  public DruidQueryGenerator(PlannerContext plannerContext)
+  {
+    this.plannerContext = plannerContext;
+  }
+
+  @Override
+  public RelNode visit(TableScan scan)
+  {
+    if (!(scan instanceof DruidTableScan)) {
+      throw new ISE("Planning hasn't converted logical table scan to druid 
convention");
+    }
+    DruidTableScan druidTableScan = (DruidTableScan) scan;
+    isRoot = false;
+    RelNode result = super.visit(scan);
+    partialDruidQuery = PartialDruidQuery.create(scan);
+    currentStage = PartialDruidQuery.Stage.SCAN;
+    final RelOptTable table = scan.getTable();
+    final DruidTable druidTable = table.unwrap(DruidTable.class);
+    if (druidTable != null) {
+      currentTable = druidTable;
+    }
+    if (druidTableScan.getProject() != null) {
+      partialDruidQuery = 
partialDruidQuery.withSelectProject(druidTableScan.getProject());
+      currentStage = PartialDruidQuery.Stage.SELECT_PROJECT;
+    }
+    return result;
+  }
+
+  @Override
+  public RelNode visit(TableFunctionScan scan)
+  {
+    return null;
+  }
+
+  @Override
+  public RelNode visit(LogicalValues values)
+  {
+    isRoot = false;
+    RelNode result = super.visit(values);
+    final List<ImmutableList<RexLiteral>> tuples = values.getTuples();
+    final List<Object[]> objectTuples = tuples
+        .stream()
+        .map(tuple -> tuple
+            .stream()
+            .map(v -> DruidLogicalValuesRule.getValueFromLiteral(v, 
plannerContext))
+            .collect(Collectors.toList())
+            .toArray(new Object[0])
+        )
+        .collect(Collectors.toList());
+    RowSignature rowSignature = RowSignatures.fromRelDataType(
+        values.getRowType().getFieldNames(),
+        values.getRowType()
+    );
+    currentTable = new InlineTable(InlineDataSource.fromIterable(objectTuples, 
rowSignature));
+    if (currentStage == null) {
+      partialDruidQuery = PartialDruidQuery.create(values);
+      currentStage = PartialDruidQuery.Stage.SCAN;
+    } else {
+      throw new ISE("Values node found at non leaf node in the plan");
+    }
+    return result;
+  }
+
+  @Override
+  public RelNode visit(LogicalFilter filter)
+  {
+    return visitFilter(filter);
+  }
+
+  public RelNode visitFilter(Filter filter)
+  {
+    isRoot = false;
+    RelNode result = super.visit(filter);
+    if (currentStage == PartialDruidQuery.Stage.AGGREGATE) {
+      partialDruidQuery = partialDruidQuery.withHavingFilter(filter);
+      currentStage = PartialDruidQuery.Stage.HAVING_FILTER;
+    } else if (currentStage == PartialDruidQuery.Stage.SCAN) {
+      partialDruidQuery = partialDruidQuery.withWhereFilter(filter);
+      currentStage = PartialDruidQuery.Stage.WHERE_FILTER;
+    } else if (currentStage == PartialDruidQuery.Stage.SELECT_PROJECT) {
+      PartialDruidQuery old = partialDruidQuery;
+      partialDruidQuery = PartialDruidQuery.create(old.getScan());
+      partialDruidQuery = partialDruidQuery.withWhereFilter(filter);
+      partialDruidQuery = 
partialDruidQuery.withSelectProject(old.getSelectProject());
+      currentStage = PartialDruidQuery.Stage.SELECT_PROJECT;
+    } else {
+      queryList.add(partialDruidQuery);
+      queryTables.add(currentTable);
+      partialDruidQuery = 
PartialDruidQuery.createOuterQuery(partialDruidQuery).withWhereFilter(filter);
+      currentStage = PartialDruidQuery.Stage.WHERE_FILTER;
+    }
+    return result;
+  }
+
+  @Override
+  public RelNode visit(LogicalProject project)
+  {
+    return visitProject(project);
+  }
+
+  @Override
+  public RelNode visit(LogicalJoin join)
+  {
+    throw new UnsupportedOperationException("Found join");
+  }
+
+  @Override
+  public RelNode visit(LogicalCorrelate correlate)
+  {
+    return null;
+  }
+
+  @Override
+  public RelNode visit(LogicalUnion union)
+  {
+    throw new UnsupportedOperationException("Found union");
+  }
+
+  @Override
+  public RelNode visit(LogicalIntersect intersect)
+  {
+    return null;
+  }
+
+  @Override
+  public RelNode visit(LogicalMinus minus)
+  {
+    return null;
+  }
+
+  @Override
+  public RelNode visit(LogicalAggregate aggregate)
+  {
+    isRoot = false;
+    RelNode result = super.visit(aggregate);
+    if (PartialDruidQuery.Stage.AGGREGATE.canFollow(currentStage)) {
+      partialDruidQuery = partialDruidQuery.withAggregate(aggregate);
+    } else {
+      queryList.add(partialDruidQuery);
+      queryTables.add(currentTable);
+      partialDruidQuery = 
PartialDruidQuery.createOuterQuery(partialDruidQuery).withAggregate(aggregate);
+    }
+    currentStage = PartialDruidQuery.Stage.AGGREGATE;
+    return result;
+  }
+
+  @Override
+  public RelNode visit(LogicalMatch match)
+  {
+    return null;
+  }
+
+  @Override
+  public RelNode visit(LogicalSort sort)
+  {
+    return visitSort(sort);
+  }
+
+  @Override
+  public RelNode visit(LogicalExchange exchange)
+  {
+    return null;
+  }
+
+  private RelNode visitProject(Project project)
+  {
+    boolean rootForReal = isRoot;
+    isRoot = false;
+    RelNode result = super.visit(project);
+    if (rootForReal && (currentStage == PartialDruidQuery.Stage.AGGREGATE
+                        || currentStage == 
PartialDruidQuery.Stage.HAVING_FILTER)) {
+      partialDruidQuery = partialDruidQuery.withAggregateProject(project);
+      currentStage = PartialDruidQuery.Stage.AGGREGATE_PROJECT;
+    } else if (currentStage == PartialDruidQuery.Stage.SCAN || currentStage == 
PartialDruidQuery.Stage.WHERE_FILTER) {
+      partialDruidQuery = partialDruidQuery.withSelectProject(project);
+      currentStage = PartialDruidQuery.Stage.SELECT_PROJECT;
+    } else if (currentStage == PartialDruidQuery.Stage.SELECT_PROJECT) {
+      partialDruidQuery = partialDruidQuery.mergeProject(project);
+      currentStage = PartialDruidQuery.Stage.SELECT_PROJECT;
+    } else if (currentStage == PartialDruidQuery.Stage.SORT) {
+      partialDruidQuery = partialDruidQuery.withSortProject(project);
+      currentStage = PartialDruidQuery.Stage.SORT_PROJECT;
+    } else {
+      queryList.add(partialDruidQuery);
+      queryTables.add(currentTable);
+      partialDruidQuery = 
PartialDruidQuery.createOuterQuery(partialDruidQuery).withSelectProject(project);
+      currentStage = PartialDruidQuery.Stage.SELECT_PROJECT;
+    }
+    return result;
+  }
+
+  private RelNode visitSort(Sort sort)
+  {
+    isRoot = false;
+    RelNode result = super.visit(sort);
+    if (PartialDruidQuery.Stage.SORT.canFollow(currentStage)) {
+      partialDruidQuery = partialDruidQuery.withSort(sort);
+    } else {
+      queryList.add(partialDruidQuery);
+      queryTables.add(currentTable);
+      partialDruidQuery = 
PartialDruidQuery.createOuterQuery(partialDruidQuery).withSort(sort);
+    }
+    currentStage = PartialDruidQuery.Stage.SORT;
+    return result;
+  }
+
+  private RelNode visitAggregate(Aggregate aggregate)
+  {
+    isRoot = false;
+    RelNode result = super.visit(aggregate);
+    if (PartialDruidQuery.Stage.AGGREGATE.canFollow(currentStage)) {
+      partialDruidQuery = partialDruidQuery.withAggregate(aggregate);
+    } else {
+      queryList.add(partialDruidQuery);
+      queryTables.add(currentTable);
+      partialDruidQuery = 
PartialDruidQuery.createOuterQuery(partialDruidQuery).withAggregate(aggregate);
+    }
+    currentStage = PartialDruidQuery.Stage.AGGREGATE;
+    return result;
+  }
+
+  @Override
+  public RelNode visit(RelNode other)
+  {
+    if (other instanceof TableScan) {
+      return visit((TableScan) other);
+    } else if (other instanceof Project) {
+      return visitProject((Project) other);
+    } else if (other instanceof Sort) {
+      return visitSort((Sort) other);
+    } else if (other instanceof Aggregate) {
+      return visitAggregate((Aggregate) other);
+    } else if (other instanceof Filter) {
+      return visitFilter((Filter) other);
+    } else if (other instanceof LogicalValues) {
+      return visit((LogicalValues) other);
+    }
+
+    return super.visit(other);
+  }
+
+  public PartialDruidQuery getPartialDruidQuery()
+  {
+    return partialDruidQuery;
+  }
+
+  public List<PartialDruidQuery> getQueryList()
+  {
+    return queryList;
+  }
+
+  public List<DruidTable> getQueryTables()
+  {
+    return queryTables;
+  }
+
+  public DruidTable getCurrentTable()
+  {
+    return currentTable;
+  }
+
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java
index 0a1c6bb68f..75887bcbec 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerConfig.java
@@ -36,6 +36,7 @@ public class PlannerConfig
   public static final String CTX_KEY_USE_NATIVE_QUERY_EXPLAIN = 
"useNativeQueryExplain";
   public static final String CTX_KEY_FORCE_EXPRESSION_VIRTUAL_COLUMNS = 
"forceExpressionVirtualColumns";
   public static final String CTX_MAX_NUMERIC_IN_FILTERS = 
"maxNumericInFilters";
+  public static final String CTX_NATIVE_QUERY_SQL_PLANNING_MODE = 
"plannerStrategy";
   public static final int NUM_FILTER_NOT_USED = -1;
 
   @JsonProperty
@@ -71,6 +72,11 @@ public class PlannerConfig
   @JsonProperty
   private int maxNumericInFilters = NUM_FILTER_NOT_USED;
 
+  @JsonProperty
+  private String nativeQuerySqlPlanningMode = 
NATIVE_QUERY_SQL_PLANNING_MODE_COUPLED; // can be COUPLED or DECOUPLED
+  public static final String NATIVE_QUERY_SQL_PLANNING_MODE_COUPLED = 
"COUPLED";
+  public static final String NATIVE_QUERY_SQL_PLANNING_MODE_DECOUPLED = 
"DECOUPLED";
+
   private boolean serializeComplexValues = true;
 
   public int getMaxNumericInFilters()
@@ -137,6 +143,11 @@ public class PlannerConfig
     return forceExpressionVirtualColumns;
   }
 
+  public String getNativeQuerySqlPlanningMode()
+  {
+    return nativeQuerySqlPlanningMode;
+  }
+
   public PlannerConfig withOverrides(final Map<String, Object> queryContext)
   {
     if (queryContext.isEmpty()) {
@@ -168,7 +179,8 @@ public class PlannerConfig
            useGroupingSetForExactDistinct == 
that.useGroupingSetForExactDistinct &&
            computeInnerJoinCostAsFilter == that.computeInnerJoinCostAsFilter &&
            authorizeSystemTablesDirectly == that.authorizeSystemTablesDirectly 
&&
-           maxNumericInFilters == that.maxNumericInFilters;
+           maxNumericInFilters == that.maxNumericInFilters &&
+           nativeQuerySqlPlanningMode.equals(that.nativeQuerySqlPlanningMode);
   }
 
   @Override
@@ -183,7 +195,8 @@ public class PlannerConfig
         sqlTimeZone,
         serializeComplexValues,
         useNativeQueryExplain,
-        forceExpressionVirtualColumns
+        forceExpressionVirtualColumns,
+        nativeQuerySqlPlanningMode
     );
   }
 
@@ -198,6 +211,7 @@ public class PlannerConfig
            ", sqlTimeZone=" + sqlTimeZone +
            ", serializeComplexValues=" + serializeComplexValues +
            ", useNativeQueryExplain=" + useNativeQueryExplain +
+           ", nativeQuerySqlPlanningMode=" + nativeQuerySqlPlanningMode +
            '}';
   }
 
@@ -231,6 +245,7 @@ public class PlannerConfig
     private boolean forceExpressionVirtualColumns;
     private int maxNumericInFilters;
     private boolean serializeComplexValues;
+    private String nativeQuerySqlPlanningMode;
 
     public Builder(PlannerConfig base)
     {
@@ -249,6 +264,7 @@ public class PlannerConfig
       forceExpressionVirtualColumns = base.isForceExpressionVirtualColumns();
       maxNumericInFilters = base.getMaxNumericInFilters();
       serializeComplexValues = base.shouldSerializeComplexValues();
+      nativeQuerySqlPlanningMode = base.getNativeQuerySqlPlanningMode();
     }
 
     public Builder requireTimeCondition(boolean option)
@@ -317,6 +333,12 @@ public class PlannerConfig
       return this;
     }
 
+    public Builder nativeQuerySqlPlanningMode(String mode)
+    {
+      this.nativeQuerySqlPlanningMode = mode;
+      return this;
+    }
+
     public Builder withOverrides(final Map<String, Object> queryContext)
     {
       useApproximateCountDistinct = QueryContexts.parseBoolean(
@@ -357,6 +379,11 @@ public class PlannerConfig
       maxNumericInFilters = validateMaxNumericInFilters(
           queryContextMaxNumericInFilters,
           maxNumericInFilters);
+      nativeQuerySqlPlanningMode = QueryContexts.parseString(
+          queryContext,
+          CTX_NATIVE_QUERY_SQL_PLANNING_MODE,
+          nativeQuerySqlPlanningMode
+      );
       return this;
     }
 
@@ -397,6 +424,7 @@ public class PlannerConfig
       config.maxNumericInFilters = maxNumericInFilters;
       config.forceExpressionVirtualColumns = forceExpressionVirtualColumns;
       config.serializeComplexValues = serializeComplexValues;
+      config.nativeQuerySqlPlanningMode = nativeQuerySqlPlanningMode;
       return config;
     }
   }
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java
index 691c33567a..9780eaa082 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/PlannerFactory.java
@@ -29,6 +29,7 @@ import org.apache.calcite.config.CalciteConnectionConfig;
 import org.apache.calcite.config.CalciteConnectionConfigImpl;
 import org.apache.calcite.plan.Context;
 import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.volcano.DruidVolcanoCost;
 import org.apache.calcite.rel.RelCollationTraitDef;
 import org.apache.calcite.sql.parser.SqlParser;
 import org.apache.calcite.sql.validate.SqlConformance;
@@ -145,7 +146,7 @@ public class PlannerFactory extends PlannerToolbox
             plannerContext.queryContext().getInSubQueryThreshold()
         )
         .build();
-    return Frameworks
+    Frameworks.ConfigBuilder frameworkConfigBuilder = Frameworks
         .newConfigBuilder()
         .parserConfig(PARSER_CONFIG)
         .traitDefs(ConventionTraitDef.INSTANCE, RelCollationTraitDef.INSTANCE)
@@ -184,7 +185,15 @@ public class PlannerFactory extends PlannerToolbox
               return null;
             }
           }
-        })
-        .build();
+        });
+
+    if (PlannerConfig.NATIVE_QUERY_SQL_PLANNING_MODE_DECOUPLED
+        .equals(plannerConfig().getNativeQuerySqlPlanningMode())
+    ) {
+      frameworkConfigBuilder.costFactory(new DruidVolcanoCost.Factory());
+    }
+
+    return frameworkConfigBuilder.build();
+
   }
 }
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
index c11d600e26..ac3c6faff1 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/planner/QueryHandler.java
@@ -56,10 +56,12 @@ import org.apache.calcite.tools.ValidationException;
 import org.apache.calcite.util.Pair;
 import org.apache.druid.error.DruidException;
 import org.apache.druid.error.InvalidSqlInput;
+import org.apache.druid.jackson.DefaultObjectMapper;
 import org.apache.druid.java.util.common.guava.BaseSequence;
 import org.apache.druid.java.util.common.guava.Sequences;
 import org.apache.druid.java.util.emitter.EmittingLogger;
 import org.apache.druid.query.Query;
+import org.apache.druid.query.QueryDataSource;
 import org.apache.druid.server.QueryResponse;
 import org.apache.druid.server.security.Action;
 import org.apache.druid.server.security.Resource;
@@ -68,6 +70,7 @@ import org.apache.druid.sql.calcite.rel.DruidConvention;
 import org.apache.druid.sql.calcite.rel.DruidQuery;
 import org.apache.druid.sql.calcite.rel.DruidRel;
 import org.apache.druid.sql.calcite.rel.DruidUnionRel;
+import org.apache.druid.sql.calcite.rel.logical.DruidLogicalConvention;
 import org.apache.druid.sql.calcite.run.EngineFeature;
 import org.apache.druid.sql.calcite.run.QueryMaker;
 import org.apache.druid.sql.calcite.table.DruidTable;
@@ -531,42 +534,94 @@ public abstract class QueryHandler extends 
SqlStatementHandler.BaseStatementHand
     );
     
QueryValidations.validateLogicalQueryForDruid(handlerContext.plannerContext(), 
parameterized);
     CalcitePlanner planner = handlerContext.planner();
-    final DruidRel<?> druidRel = (DruidRel<?>) planner.transform(
-        CalciteRulesManager.DRUID_CONVENTION_RULES,
-        planner.getEmptyTraitSet()
-               .replace(DruidConvention.instance())
-               .plus(rootQueryRel.collation),
-        parameterized
-    );
-    handlerContext.hook().captureDruidRel(druidRel);
 
-    if (explain != null) {
-      return planExplanation(possiblyLimitedRoot, druidRel, true);
-    } else {
-      // Compute row type.
-      final RelDataType rowType = prepareResult.getReturnedRowType();
-
-      // Start the query.
-      final Supplier<QueryResponse<Object[]>> resultsSupplier = () -> {
-        // sanity check
-        final Set<ResourceAction> readResourceActions =
-            plannerContext.getResourceActions()
-                          .stream()
-                          .filter(action -> action.getAction() == Action.READ)
-                          .collect(Collectors.toSet());
-        Preconditions.checkState(
-            readResourceActions.isEmpty() == 
druidRel.getDataSourceNames().isEmpty()
-            // The resources found in the plannerContext can be less than the 
datasources in
-            // the query plan, because the query planner can eliminate empty 
tables by replacing
-            // them with InlineDataSource of empty rows.
-            || readResourceActions.size() >= 
druidRel.getDataSourceNames().size(),
-            "Authorization sanity check failed"
+    if (plannerContext.getPlannerConfig()
+                      .getNativeQuerySqlPlanningMode()
+                      
.equals(PlannerConfig.NATIVE_QUERY_SQL_PLANNING_MODE_DECOUPLED)
+    ) {
+      RelNode newRoot = parameterized;
+      newRoot = planner.transform(
+          CalciteRulesManager.DRUID_DAG_CONVENTION_RULES,
+          planner.getEmptyTraitSet()
+                 .plus(rootQueryRel.collation)
+                 .plus(DruidLogicalConvention.instance()),
+          newRoot
+      );
+      DruidQueryGenerator shuttle = new DruidQueryGenerator(plannerContext);
+      newRoot.accept(shuttle);
+      log.info("PartialDruidQuery : " + shuttle.getPartialDruidQuery());
+      shuttle.getQueryList().add(shuttle.getPartialDruidQuery()); // add 
topmost query to the list
+      shuttle.getQueryTables().add(shuttle.getCurrentTable());
+      assert !shuttle.getQueryList().isEmpty();
+      log.info("query list size " + shuttle.getQueryList().size());
+      log.info("query tables size " + shuttle.getQueryTables().size());
+      // build bottom-most query
+      DruidQuery baseQuery = shuttle.getQueryList().get(0).build(
+          shuttle.getQueryTables().get(0).getDataSource(),
+          shuttle.getQueryTables().get(0).getRowSignature(),
+          plannerContext,
+          rexBuilder,
+          shuttle.getQueryList().size() != 1,
+          null
+      );
+      // build outer queries
+      for (int i = 1; i < shuttle.getQueryList().size(); i++) {
+        baseQuery = shuttle.getQueryList().get(i).build(
+            new QueryDataSource(baseQuery.getQuery()),
+            baseQuery.getOutputRowSignature(),
+            plannerContext,
+            rexBuilder,
+            false
         );
+      }
+      try {
+        log.info("final query : " +
+                 new 
DefaultObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(baseQuery.getQuery()));
+      }
+      catch (JsonProcessingException e) {
+        throw new RuntimeException(e);
+      }
+      DruidQuery finalBaseQuery = baseQuery;
+      final Supplier<QueryResponse<Object[]>> resultsSupplier = () -> 
plannerContext.getQueryMaker().runQuery(finalBaseQuery);
 
-        return druidRel.runQuery();
-      };
+      return new PlannerResult(resultsSupplier, 
finalBaseQuery.getOutputRowType());
+    } else {
+      final DruidRel<?> druidRel = (DruidRel<?>) planner.transform(
+          CalciteRulesManager.DRUID_CONVENTION_RULES,
+          planner.getEmptyTraitSet()
+                 .replace(DruidConvention.instance())
+                 .plus(rootQueryRel.collation),
+          parameterized
+      );
+      handlerContext.hook().captureDruidRel(druidRel);
+      if (explain != null) {
+        return planExplanation(possiblyLimitedRoot, druidRel, true);
+      } else {
+        // Compute row type.
+        final RelDataType rowType = prepareResult.getReturnedRowType();
+
+        // Start the query.
+        final Supplier<QueryResponse<Object[]>> resultsSupplier = () -> {
+          // sanity check
+          final Set<ResourceAction> readResourceActions =
+              plannerContext.getResourceActions()
+                            .stream()
+                            .filter(action -> action.getAction() == 
Action.READ)
+                            .collect(Collectors.toSet());
+          Preconditions.checkState(
+              readResourceActions.isEmpty() == 
druidRel.getDataSourceNames().isEmpty()
+              // The resources found in the plannerContext can be less than 
the datasources in
+              // the query plan, because the query planner can eliminate empty 
tables by replacing
+              // them with InlineDataSource of empty rows.
+              || readResourceActions.size() >= 
druidRel.getDataSourceNames().size(),
+              "Authorization sanity check failed"
+          );
 
-      return new PlannerResult(resultsSupplier, rowType);
+          return druidRel.runQuery();
+        };
+
+        return new PlannerResult(resultsSupplier, rowType);
+      }
     }
   }
 
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/CostEstimates.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/CostEstimates.java
index 28f7c21182..7de0bcc56b 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/rel/CostEstimates.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/rel/CostEstimates.java
@@ -32,61 +32,61 @@ public class CostEstimates
    * Per-row base cost. This represents the cost of walking through every row, 
but not actually reading anything
    * from them or computing any aggregations.
    */
-  static final double COST_BASE = 1;
+  public static final double COST_BASE = 1;
 
   /**
    * Cost to include a column in query output.
    */
-  static final double COST_OUTPUT_COLUMN = 0.05;
+  public static final double COST_OUTPUT_COLUMN = 0.05;
 
   /**
    * Cost to compute and read an expression.
    */
-  static final double COST_EXPRESSION = 0.25;
+  public static final double COST_EXPRESSION = 0.25;
 
   /**
    * Cost to compute an aggregation.
    */
-  static final double COST_AGGREGATION = 0.05;
+  public static final double COST_AGGREGATION = 0.05;
 
   /**
    * Cost per GROUP BY dimension.
    */
-  static final double COST_DIMENSION = 0.25;
+  public static final double COST_DIMENSION = 0.25;
 
   /**
    * Multiplier to apply when there is a WHERE filter. Encourages pushing down 
filters and limits through joins and
    * subqueries when possible.
    */
-  static final double MULTIPLIER_FILTER = 0.1;
+  public static final double MULTIPLIER_FILTER = 0.1;
 
   /**
    * Multiplier to apply when there is an ORDER BY. Encourages avoiding them 
when possible.
    */
-  static final double MULTIPLIER_ORDER_BY = 10;
+  public static final double MULTIPLIER_ORDER_BY = 10;
 
   /**
    * Multiplier to apply when there is a LIMIT. Encourages pushing down limits 
when possible.
    */
-  static final double MULTIPLIER_LIMIT = 0.5;
+  public static final double MULTIPLIER_LIMIT = 0.5;
 
   /**
    * Multiplier to apply to an outer query via {@link DruidOuterQueryRel}. 
Encourages pushing down time-saving
    * operations to the lowest level of the query stack, because they'll have 
bigger impact there.
    */
-  static final double MULTIPLIER_OUTER_QUERY = .1;
+  public static final double MULTIPLIER_OUTER_QUERY = .1;
 
   /**
    * Cost to add to a subquery. Strongly encourages avoiding subqueries, since 
they must be inlined and then the join
    * must run on the Broker.
    */
-  static final double COST_SUBQUERY = 1e5;
+  public static final double COST_SUBQUERY = 1e5;
 
   /**
    * Cost to perform a cross join. Strongly encourages pushing down filters 
into join conditions, even if it means
    * we need to add a subquery (this is higher than {@link #COST_SUBQUERY}).
    */
-  static final double COST_JOIN_CROSS = 1e8;
+  public static final double COST_JOIN_CROSS = 1e8;
 
   private CostEstimates()
   {
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/PartialDruidQuery.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/PartialDruidQuery.java
index 068ff49308..f5d3e5ac8e 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/rel/PartialDruidQuery.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/rel/PartialDruidQuery.java
@@ -260,26 +260,48 @@ public class PartialDruidQuery
     if (selectProject == null) {
       theProject = newSelectProject;
     } else {
-      final List<RexNode> newProjectRexNodes = RelOptUtil.pushPastProject(
-          newSelectProject.getProjects(),
-          selectProject
-      );
+      return mergeProject(newSelectProject);
+    }
 
-      if (RexUtil.isIdentity(newProjectRexNodes, 
selectProject.getInput().getRowType())) {
-        // The projection is gone.
-        theProject = null;
-      } else {
-        final RelBuilder relBuilder = builderSupplier.get();
-        relBuilder.push(selectProject.getInput());
-        relBuilder.project(
-            newProjectRexNodes,
-            newSelectProject.getRowType().getFieldNames(),
-            true
-        );
-        theProject = (Project) relBuilder.build();
-      }
+    return new PartialDruidQuery(
+        builderSupplier,
+        scan,
+        whereFilter,
+        theProject,
+        aggregate,
+        aggregateProject,
+        havingFilter,
+        sort,
+        sortProject,
+        window,
+        windowProject
+    );
+  }
+
+  public PartialDruidQuery mergeProject(Project newSelectProject)
+  {
+    if (stage() != Stage.SELECT_PROJECT) {
+      throw new ISE("Expected partial query state to be [%s], but found [%s]", 
Stage.SELECT_PROJECT, stage());
     }
+    Project theProject;
+    final List<RexNode> newProjectRexNodes = RelOptUtil.pushPastProject(
+        newSelectProject.getProjects(),
+        selectProject
+    );
 
+    if (RexUtil.isIdentity(newProjectRexNodes, 
selectProject.getInput().getRowType())) {
+      // The projection is gone.
+      theProject = null;
+    } else {
+      final RelBuilder relBuilder = builderSupplier.get();
+      relBuilder.push(selectProject.getInput());
+      relBuilder.project(
+          newProjectRexNodes,
+          newSelectProject.getRowType().getFieldNames(),
+          true
+      );
+      theProject = (Project) relBuilder.build();
+    }
     return new PartialDruidQuery(
         builderSupplier,
         scan,
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidAggregate.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidAggregate.java
new file mode 100644
index 0000000000..711ba26ca6
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidAggregate.java
@@ -0,0 +1,101 @@
+/*
+ * 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.druid.sql.calcite.rel.logical;
+
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.core.Aggregate;
+import org.apache.calcite.rel.core.AggregateCall;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.util.ImmutableBitSet;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.sql.calcite.rel.CostEstimates;
+
+import java.util.List;
+
+/**
+ * {@link DruidLogicalNode} convention node for {@link Aggregate} plan node.
+ */
+public class DruidAggregate extends Aggregate implements DruidLogicalNode
+{
+  private final PlannerContext plannerContext;
+
+  public DruidAggregate(
+      RelOptCluster cluster,
+      RelTraitSet traitSet,
+      RelNode input,
+      ImmutableBitSet groupSet,
+      List<ImmutableBitSet> groupSets,
+      List<AggregateCall> aggCalls,
+      PlannerContext plannerContext
+  )
+  {
+    super(cluster, traitSet, input, groupSet, groupSets, aggCalls);
+    assert getConvention() instanceof DruidLogicalConvention;
+    this.plannerContext = plannerContext;
+  }
+
+  @Override
+  public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq)
+  {
+    double rowCount = mq.getRowCount(this);
+    double cost = CostEstimates.COST_DIMENSION * getGroupSet().size();
+    for (AggregateCall aggregateCall : getAggCallList()) {
+      if (aggregateCall.hasFilter()) {
+        cost += CostEstimates.COST_AGGREGATION * 
CostEstimates.MULTIPLIER_FILTER;
+      } else {
+        cost += CostEstimates.COST_AGGREGATION;
+      }
+    }
+    if (!plannerContext.getPlannerConfig().isUseApproximateCountDistinct() &&
+        getAggCallList().stream().anyMatch(AggregateCall::isDistinct)) {
+      return planner.getCostFactory().makeInfiniteCost();
+    }
+    return planner.getCostFactory().makeCost(rowCount, cost, 0);
+  }
+
+  @Override
+  public final Aggregate copy(
+      RelTraitSet traitSet,
+      RelNode input,
+      ImmutableBitSet groupSet,
+      List<ImmutableBitSet> groupSets,
+      List<AggregateCall> aggCalls
+  )
+  {
+    return new DruidAggregate(getCluster(), traitSet, input, groupSet, 
groupSets, aggCalls, plannerContext);
+  }
+
+  @Override
+  public RelWriter explainTerms(RelWriter pw)
+  {
+    return super.explainTerms(pw).item("druid", "logical");
+  }
+
+  @Override
+  public double estimateRowCount(RelMetadataQuery mq)
+  {
+    return mq.getRowCount(this);
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidFilter.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidFilter.java
new file mode 100644
index 0000000000..00886fe630
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidFilter.java
@@ -0,0 +1,54 @@
+/*
+ * 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.druid.sql.calcite.rel.logical;
+
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.core.Filter;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rex.RexNode;
+
+/**
+ * {@link DruidLogicalNode} convention node for {@link Filter} plan node.
+ */
+public class DruidFilter extends Filter implements DruidLogicalNode
+{
+
+  public DruidFilter(RelOptCluster cluster, RelTraitSet traits, RelNode child, 
RexNode condition)
+  {
+    super(cluster, traits, child, condition);
+    assert getConvention() instanceof DruidLogicalConvention;
+  }
+
+  @Override
+  public Filter copy(RelTraitSet traitSet, RelNode input, RexNode condition)
+  {
+    return new DruidFilter(getCluster(), getTraitSet(), input, condition);
+  }
+
+  @Override
+  public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq)
+  {
+    return planner.getCostFactory().makeCost(mq.getRowCount(this), 0, 0);
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidLogicalConvention.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidLogicalConvention.java
new file mode 100644
index 0000000000..0ac8b042ce
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidLogicalConvention.java
@@ -0,0 +1,93 @@
+/*
+ * 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.druid.sql.calcite.rel.logical;
+
+import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.ConventionTraitDef;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.plan.RelTraitDef;
+import org.apache.calcite.plan.RelTraitSet;
+
+/**
+ * A Calcite convention to produce {@link DruidLogicalNode} based DAG.
+ */
+public class DruidLogicalConvention implements Convention
+{
+
+  private static final DruidLogicalConvention INSTANCE = new 
DruidLogicalConvention();
+  private static final String NAME = "DRUID_LOGICAL";
+
+  private DruidLogicalConvention()
+  {
+  }
+
+  public static DruidLogicalConvention instance()
+  {
+    return INSTANCE;
+  }
+
+  @Override
+  public Class getInterface()
+  {
+    return DruidLogicalNode.class;
+  }
+
+  @Override
+  public String getName()
+  {
+    return NAME;
+  }
+
+  @Override
+  public boolean canConvertConvention(Convention toConvention)
+  {
+    return false;
+  }
+
+  @Override
+  public boolean useAbstractConvertersForConversion(RelTraitSet fromTraits, 
RelTraitSet toTraits)
+  {
+    return false;
+  }
+
+  @Override
+  public RelTraitDef getTraitDef()
+  {
+    return ConventionTraitDef.INSTANCE;
+  }
+
+  @Override
+  public boolean satisfies(RelTrait trait)
+  {
+    return trait.equals(this);
+  }
+
+  @Override
+  public void register(RelOptPlanner planner)
+  {
+  }
+
+  @Override
+  public String toString()
+  {
+    return NAME;
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidLogicalNode.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidLogicalNode.java
new file mode 100644
index 0000000000..75029eab1a
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidLogicalNode.java
@@ -0,0 +1,30 @@
+/*
+ * 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.druid.sql.calcite.rel.logical;
+
+import org.apache.calcite.rel.RelNode;
+
+/**
+ * An interface to mark {@link RelNode} as Druid physical nodes. These 
physical nodes look a lot same as their logical
+ * counterparts in Calcite, but they do follow a different costing model.
+ */
+public interface DruidLogicalNode extends RelNode
+{
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidProject.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidProject.java
new file mode 100644
index 0000000000..83e437514a
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidProject.java
@@ -0,0 +1,101 @@
+/*
+ * 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.druid.sql.calcite.rel.logical;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelCollationTraitDef;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.metadata.RelMdCollation;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.druid.sql.calcite.rel.CostEstimates;
+
+import java.util.List;
+
+/**
+ * {@link DruidLogicalNode} convention node for {@link Project} plan node.
+ */
+public class DruidProject extends Project implements DruidLogicalNode
+{
+  public DruidProject(
+      RelOptCluster cluster,
+      RelTraitSet traitSet,
+      RelNode input,
+      List<? extends RexNode> projects,
+      RelDataType rowType
+  )
+  {
+    super(cluster, traitSet, input, projects, rowType);
+    assert getConvention() instanceof DruidLogicalConvention;
+  }
+
+  @Override
+  public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq)
+  {
+    double cost = 0;
+    double rowCount = mq.getRowCount(getInput());
+    for (final RexNode rexNode : getProjects()) {
+      if (rexNode.isA(SqlKind.INPUT_REF)) {
+        cost += 0;
+      }
+      if (rexNode.getType().getSqlTypeName() == SqlTypeName.BOOLEAN || 
rexNode.isA(SqlKind.CAST)) {
+        cost += 0;
+      } else if (!rexNode.isA(ImmutableSet.of(SqlKind.INPUT_REF, 
SqlKind.LITERAL))) {
+        cost += CostEstimates.COST_EXPRESSION;
+      }
+    }
+    // adding atleast 1e-6 cost since zero cost is converted to a tiny cost by 
the planner which is (1 row, 1 cpu, 0 io)
+    // that becomes a significant cost in some cases.
+    return planner.getCostFactory().makeCost(0, Math.max(cost * rowCount, 
1e-6), 0);
+  }
+
+  @Override
+  public Project copy(RelTraitSet traitSet, RelNode input, List<RexNode> 
projects, RelDataType rowType)
+  {
+    return new DruidProject(getCluster(), traitSet, input, exps, rowType);
+  }
+
+  @Override
+  public RelWriter explainTerms(RelWriter pw)
+  {
+    return super.explainTerms(pw).item("druid", "logical");
+  }
+
+  public static DruidProject create(final RelNode input, final List<? extends 
RexNode> projects, RelDataType rowType)
+  {
+    final RelOptCluster cluster = input.getCluster();
+    final RelMetadataQuery mq = cluster.getMetadataQuery();
+    final RelTraitSet traitSet =
+        input.getTraitSet().replaceIfs(
+            RelCollationTraitDef.INSTANCE,
+            () -> RelMdCollation.project(mq, input, projects)
+        );
+    return new DruidProject(cluster, traitSet, input, projects, rowType);
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidSort.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidSort.java
new file mode 100644
index 0000000000..4ad6091ad1
--- /dev/null
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidSort.java
@@ -0,0 +1,97 @@
+/*
+ * 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.druid.sql.calcite.rel.logical;
+
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollationTraitDef;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rex.RexNode;
+import org.apache.druid.sql.calcite.planner.OffsetLimit;
+import org.apache.druid.sql.calcite.rel.CostEstimates;
+
+/**
+ * {@link DruidLogicalNode} convention node for {@link Sort} plan node.
+ */
+public class DruidSort extends Sort implements DruidLogicalNode
+{
+  private DruidSort(
+      RelOptCluster cluster,
+      RelTraitSet traits,
+      RelNode input,
+      RelCollation collation,
+      RexNode offset,
+      RexNode fetch
+  )
+  {
+    super(cluster, traits, input, collation, offset, fetch);
+    assert getConvention() instanceof DruidLogicalConvention;
+  }
+
+  public static DruidSort create(RelNode input, RelCollation collation, 
RexNode offset, RexNode fetch)
+  {
+    RelOptCluster cluster = input.getCluster();
+    collation = RelCollationTraitDef.INSTANCE.canonize(collation);
+    RelTraitSet traitSet =
+        
input.getTraitSet().replace(DruidLogicalConvention.instance()).replace(collation);
+    return new DruidSort(cluster, traitSet, input, collation, offset, fetch);
+  }
+
+  @Override
+  public Sort copy(
+      RelTraitSet traitSet,
+      RelNode newInput,
+      RelCollation newCollation,
+      RexNode offset,
+      RexNode fetch
+  )
+  {
+    return new DruidSort(getCluster(), traitSet, newInput, newCollation, 
offset, fetch);
+  }
+
+  @Override
+  public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq)
+  {
+    double cost = 0;
+    double rowCount = mq.getRowCount(this);
+
+    if (fetch != null) {
+      OffsetLimit offsetLimit = OffsetLimit.fromSort(this);
+      rowCount = Math.min(rowCount, offsetLimit.getLimit() - 
offsetLimit.getOffset());
+    }
+
+    if (!getCollation().getFieldCollations().isEmpty() && fetch == null) {
+      cost = rowCount * CostEstimates.MULTIPLIER_ORDER_BY;
+    }
+    return planner.getCostFactory().makeCost(rowCount, cost, 0);
+  }
+
+  @Override
+  public RelWriter explainTerms(RelWriter pw)
+  {
+    return super.explainTerms(pw).item("druid", "logical");
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidTableScan.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidTableScan.java
new file mode 100644
index 0000000000..d601aa4d2f
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidTableScan.java
@@ -0,0 +1,111 @@
+/*
+ * 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.druid.sql.calcite.rel.logical;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelOptTable;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelCollationTraitDef;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.RelWriter;
+import org.apache.calcite.rel.core.Project;
+import org.apache.calcite.rel.core.TableScan;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.schema.Table;
+
+import java.util.List;
+
+/**
+ * {@link DruidLogicalNode} convention node for {@link TableScan} plan node.
+ */
+public class DruidTableScan extends TableScan implements DruidLogicalNode
+{
+  private final Project project;
+
+  public DruidTableScan(
+      RelOptCluster cluster,
+      RelTraitSet traitSet,
+      RelOptTable table,
+      Project project
+  )
+  {
+    super(cluster, traitSet, table);
+    this.project = project;
+    assert getConvention() instanceof DruidLogicalConvention;
+  }
+
+  @Override
+  public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs)
+  {
+    return new DruidTableScan(getCluster(), traitSet, table, project);
+  }
+
+  @Override
+  public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq)
+  {
+    return planner.getCostFactory().makeTinyCost();
+  }
+
+  @Override
+  public double estimateRowCount(RelMetadataQuery mq)
+  {
+    return 1_000;
+  }
+
+  @Override
+  public RelWriter explainTerms(RelWriter pw)
+  {
+    if (project != null) {
+      project.explainTerms(pw);
+    }
+    return super.explainTerms(pw).item("druid", "logical");
+  }
+
+  @Override
+  public RelDataType deriveRowType()
+  {
+    if (project != null) {
+      return project.getRowType();
+    }
+    return super.deriveRowType();
+  }
+
+  public Project getProject()
+  {
+    return project;
+  }
+
+  public static DruidTableScan create(RelOptCluster cluster, final RelOptTable 
relOptTable)
+  {
+    final Table table = relOptTable.unwrap(Table.class);
+    final RelTraitSet traitSet =
+        cluster.traitSet().replaceIfs(RelCollationTraitDef.INSTANCE, () -> {
+          if (table != null) {
+            return table.getStatistic().getCollations();
+          }
+          return ImmutableList.of();
+        });
+    return new DruidTableScan(cluster, traitSet, relOptTable, null);
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidValues.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidValues.java
new file mode 100644
index 0000000000..d6a8ca98a2
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rel/logical/DruidValues.java
@@ -0,0 +1,64 @@
+/*
+ * 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.druid.sql.calcite.rel.logical;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptCost;
+import org.apache.calcite.plan.RelOptPlanner;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.logical.LogicalValues;
+import org.apache.calcite.rel.metadata.RelMetadataQuery;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.druid.sql.calcite.rel.CostEstimates;
+
+import java.util.List;
+
+/**
+ * {@link DruidLogicalNode} convention node for {@link LogicalValues} plan 
node.
+ */
+public class DruidValues extends LogicalValues implements DruidLogicalNode
+{
+
+  public DruidValues(
+      RelOptCluster cluster,
+      RelTraitSet traitSet,
+      RelDataType rowType,
+      ImmutableList<ImmutableList<RexLiteral>> tuples
+  )
+  {
+    super(cluster, traitSet, rowType, tuples);
+    assert getConvention() instanceof DruidLogicalConvention;
+  }
+
+  @Override
+  public RelNode copy(RelTraitSet traitSet, List<RelNode> inputs)
+  {
+    return new DruidValues(getCluster(), traitSet, getRowType(), tuples);
+  }
+
+  @Override
+  public RelOptCost computeSelfCost(RelOptPlanner planner, RelMetadataQuery mq)
+  {
+    return planner.getCostFactory().makeCost(CostEstimates.COST_BASE, 0, 0);
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRule.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRule.java
index ea71dfd909..614ffddf56 100644
--- 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRule.java
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/DruidLogicalValuesRule.java
@@ -92,7 +92,7 @@ public class DruidLogicalValuesRule extends RelOptRule
    */
   @Nullable
   @VisibleForTesting
-  static Object getValueFromLiteral(RexLiteral literal, PlannerContext 
plannerContext)
+  public static Object getValueFromLiteral(RexLiteral literal, PlannerContext 
plannerContext)
   {
     switch (literal.getType().getSqlTypeName()) {
       case CHAR:
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidAggregateCaseToFilterRule.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidAggregateCaseToFilterRule.java
new file mode 100644
index 0000000000..b620905f13
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidAggregateCaseToFilterRule.java
@@ -0,0 +1,339 @@
+/*
+ * 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.druid.sql.calcite.rule.logical;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.calcite.plan.RelOptCluster;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.rel.RelCollations;
+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.core.RelFactories;
+import org.apache.calcite.rel.type.RelDataType;
+import org.apache.calcite.rel.type.RelDataTypeFactory;
+import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
+import org.apache.calcite.rex.RexLiteral;
+import org.apache.calcite.rex.RexNode;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlPostfixOperator;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
+import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.tools.RelBuilder;
+import org.apache.calcite.tools.RelBuilderFactory;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A copy of {@link org.apache.calcite.rel.rules.AggregateCaseToFilterRule} 
except that it fixes a bug to eliminate
+ * left-over projects for converted aggregates to filter-aggregates. The 
elimination of left-over projects is necessary
+ * with the new planning since it determines the cost of the plan and hence 
determines which plan is going to get picked
+ * as the cheapest one.
+ * This fix will also be contributed upstream to Calcite project, and we can 
remove this rule once the fix is a part of
+ * the Calcite version we use.
+ */
+public class DruidAggregateCaseToFilterRule extends RelOptRule
+{
+  public static final DruidAggregateCaseToFilterRule INSTANCE =
+      new DruidAggregateCaseToFilterRule(RelFactories.LOGICAL_BUILDER, null);
+
+  /**
+   * Creates an AggregateCaseToFilterRule.
+   */
+  protected DruidAggregateCaseToFilterRule(
+      RelBuilderFactory relBuilderFactory,
+      String description
+  )
+  {
+    super(operand(Aggregate.class, operand(Project.class, any())),
+          relBuilderFactory, description
+    );
+  }
+
+  @Override
+  public boolean matches(final RelOptRuleCall call)
+  {
+    final Aggregate aggregate = call.rel(0);
+    final Project project = call.rel(1);
+
+    for (AggregateCall aggregateCall : aggregate.getAggCallList()) {
+      final int singleArg = soleArgument(aggregateCall);
+      if (singleArg >= 0
+          && isThreeArgCase(project.getProjects().get(singleArg))) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  @Override
+  public void onMatch(RelOptRuleCall call)
+  {
+    final Aggregate aggregate = call.rel(0);
+    final Project project = call.rel(1);
+    final List<AggregateCall> newCalls =
+        new ArrayList<>(aggregate.getAggCallList().size());
+    List<RexNode> newProjects;
+
+    // TODO : fix grouping columns
+    Set<Integer> groupUsedFields = new HashSet<>();
+    for (int fieldNumber : aggregate.getGroupSet()) {
+      groupUsedFields.add(fieldNumber);
+    }
+
+    List<RexNode> updatedProjects = new ArrayList<>();
+    for (int i = 0; i < project.getProjects().size(); i++) {
+      if (groupUsedFields.contains(i)) {
+        updatedProjects.add(project.getProjects().get(i));
+      }
+    }
+    newProjects = updatedProjects;
+
+    for (AggregateCall aggregateCall : aggregate.getAggCallList()) {
+      AggregateCall newCall =
+          transform(aggregateCall, project, newProjects);
+
+      // Possibly CAST the new aggregator to an appropriate type.
+      newCalls.add(newCall);
+    }
+    final RelBuilder relBuilder = call.builder()
+                                      .push(project.getInput())
+                                      .project(newProjects);
+
+    final RelBuilder.GroupKey groupKey =
+        relBuilder.groupKey(
+            aggregate.getGroupSet(),
+            aggregate.getGroupSets()
+        );
+
+    relBuilder.aggregate(groupKey, newCalls)
+              .convert(aggregate.getRowType(), false);
+
+    call.transformTo(relBuilder.build());
+    call.getPlanner().setImportance(aggregate, 0.0);
+  }
+
+  private AggregateCall transform(AggregateCall aggregateCall, Project 
project, List<RexNode> newProjects)
+  {
+    final int singleArg = soleArgument(aggregateCall);
+    if (singleArg < 0) {
+      Set<Integer> newFields = new HashSet<>();
+      for (int fieldNumber : aggregateCall.getArgList()) {
+        newProjects.add(project.getProjects().get(fieldNumber));
+        newFields.add(newProjects.size() - 1);
+      }
+      int newFilterArg = -1;
+      if (aggregateCall.hasFilter()) {
+        newProjects.add(project.getProjects().get(aggregateCall.filterArg));
+        newFilterArg = newProjects.size() - 1;
+      }
+      return AggregateCall.create(aggregateCall.getAggregation(),
+                                  aggregateCall.isDistinct(),
+                                  aggregateCall.isApproximate(),
+                                  aggregateCall.ignoreNulls(),
+                                  new ArrayList<>(newFields),
+                                  newFilterArg,
+                                  aggregateCall.getCollation(),
+                                  aggregateCall.getType(),
+                                  aggregateCall.getName()
+      );
+    }
+
+    final RexNode rexNode = project.getProjects().get(singleArg);
+    if (!isThreeArgCase(rexNode)) {
+      newProjects.add(rexNode);
+      int callArg = newProjects.size() - 1;
+      int newFilterArg = -1;
+      if (aggregateCall.hasFilter()) {
+        newProjects.add(project.getProjects().get(aggregateCall.filterArg));
+        newFilterArg = newProjects.size() - 1;
+      }
+      return AggregateCall.create(aggregateCall.getAggregation(),
+                                  aggregateCall.isDistinct(),
+                                  aggregateCall.isApproximate(),
+                                  aggregateCall.ignoreNulls(),
+                                  ImmutableList.of(callArg),
+                                  newFilterArg,
+                                  aggregateCall.getCollation(),
+                                  aggregateCall.getType(),
+                                  aggregateCall.getName()
+      );
+    }
+
+    final RelOptCluster cluster = project.getCluster();
+    final RexBuilder rexBuilder = cluster.getRexBuilder();
+    final RexCall caseCall = (RexCall) rexNode;
+
+    // If one arg is null and the other is not, reverse them and set "flip",
+    // which negates the filter.
+    final boolean flip = RexLiteral.isNullLiteral(caseCall.operands.get(1))
+                         && 
!RexLiteral.isNullLiteral(caseCall.operands.get(2));
+    final RexNode arg1 = caseCall.operands.get(flip ? 2 : 1);
+    final RexNode arg2 = caseCall.operands.get(flip ? 1 : 2);
+
+    // Operand 1: Filter
+    final SqlPostfixOperator op =
+        flip ? SqlStdOperatorTable.IS_FALSE : SqlStdOperatorTable.IS_TRUE;
+    final RexNode filterFromCase =
+        rexBuilder.makeCall(op, caseCall.operands.get(0));
+
+    // Combine the CASE filter with an honest-to-goodness SQL FILTER, if the
+    // latter is present.
+    final RexNode filter;
+    if (aggregateCall.filterArg >= 0) {
+      filter = rexBuilder.makeCall(SqlStdOperatorTable.AND,
+                                   
project.getProjects().get(aggregateCall.filterArg), filterFromCase
+      );
+    } else {
+      filter = filterFromCase;
+    }
+
+    final SqlKind kind = aggregateCall.getAggregation().getKind();
+    if (aggregateCall.isDistinct()) {
+      // Just one style supported:
+      //   COUNT(DISTINCT CASE WHEN x = 'foo' THEN y END)
+      // =>
+      //   COUNT(DISTINCT y) FILTER(WHERE x = 'foo')
+
+      if (kind == SqlKind.COUNT
+          && RexLiteral.isNullLiteral(arg2)) {
+        newProjects.add(arg1);
+        newProjects.add(filter);
+        return AggregateCall.create(SqlStdOperatorTable.COUNT, true, false,
+                                    false, ImmutableList.of(newProjects.size() 
- 2),
+                                    newProjects.size() - 1, 
RelCollations.EMPTY,
+                                    aggregateCall.getType(), 
aggregateCall.getName()
+        );
+      }
+      newProjects.add(rexNode);
+      int callArg = newProjects.size() - 1;
+      int newFilterArg = -1;
+      if (aggregateCall.hasFilter()) {
+        newProjects.add(project.getProjects().get(aggregateCall.filterArg));
+        newFilterArg = newProjects.size() - 1;
+      }
+      return AggregateCall.create(aggregateCall.getAggregation(),
+                                  aggregateCall.isDistinct(),
+                                  aggregateCall.isApproximate(),
+                                  aggregateCall.ignoreNulls(),
+                                  ImmutableList.of(callArg),
+                                  newFilterArg,
+                                  aggregateCall.getCollation(),
+                                  aggregateCall.getType(),
+                                  aggregateCall.getName()
+      );
+    }
+
+    // Four styles supported:
+    //
+    // A1: AGG(CASE WHEN x = 'foo' THEN cnt END)
+    //   => operands (x = 'foo', cnt, null)
+    // A2: SUM(CASE WHEN x = 'foo' THEN cnt ELSE 0 END)
+    //   => operands (x = 'foo', cnt, 0); must be SUM
+    // B: SUM(CASE WHEN x = 'foo' THEN 1 ELSE 0 END)
+    //   => operands (x = 'foo', 1, 0); must be SUM
+    // C: COUNT(CASE WHEN x = 'foo' THEN 'dummy' END)
+    //   => operands (x = 'foo', 'dummy', null)
+
+    if (kind == SqlKind.COUNT // Case C
+        && arg1.isA(SqlKind.LITERAL)
+        && !RexLiteral.isNullLiteral(arg1)
+        && RexLiteral.isNullLiteral(arg2)) {
+      newProjects.add(filter);
+      return AggregateCall.create(SqlStdOperatorTable.COUNT, false, false,
+                                  false, ImmutableList.of(), 
newProjects.size() - 1,
+                                  RelCollations.EMPTY, aggregateCall.getType(),
+                                  aggregateCall.getName()
+      );
+    } else if (kind == SqlKind.SUM // Case B
+               && isIntLiteral(arg1) && RexLiteral.intValue(arg1) == 1
+               && isIntLiteral(arg2) && RexLiteral.intValue(arg2) == 0) {
+
+      newProjects.add(filter);
+      final RelDataTypeFactory typeFactory = cluster.getTypeFactory();
+      final RelDataType dataType =
+          typeFactory.createTypeWithNullability(
+              typeFactory.createSqlType(SqlTypeName.BIGINT), false);
+      return AggregateCall.create(SqlStdOperatorTable.COUNT, false, false,
+                                  false, ImmutableList.of(), 
newProjects.size() - 1,
+                                  RelCollations.EMPTY, dataType, 
aggregateCall.getName()
+      );
+    } else if ((RexLiteral.isNullLiteral(arg2) // Case A1
+                && aggregateCall.getAggregation().allowsFilter())
+               || (kind == SqlKind.SUM // Case A2
+                   && isIntLiteral(arg2)
+                   && RexLiteral.intValue(arg2) == 0)) {
+      newProjects.add(arg1);
+      newProjects.add(filter);
+      return AggregateCall.create(aggregateCall.getAggregation(), false,
+                                  false, false, 
ImmutableList.of(newProjects.size() - 2),
+                                  newProjects.size() - 1, RelCollations.EMPTY,
+                                  aggregateCall.getType(), 
aggregateCall.getName()
+      );
+    } else {
+      newProjects.add(rexNode);
+      int callArg = newProjects.size() - 1;
+      int newFilterArg = -1;
+      if (aggregateCall.hasFilter()) {
+        newProjects.add(project.getProjects().get(aggregateCall.filterArg));
+        newFilterArg = newProjects.size() - 1;
+      }
+      return AggregateCall.create(aggregateCall.getAggregation(),
+                                  aggregateCall.isDistinct(),
+                                  aggregateCall.isApproximate(),
+                                  aggregateCall.ignoreNulls(),
+                                  ImmutableList.of(callArg),
+                                  newFilterArg,
+                                  aggregateCall.getCollation(),
+                                  aggregateCall.getType(),
+                                  aggregateCall.getName()
+      );
+    }
+  }
+
+  /**
+   * Returns the argument, if an aggregate call has a single argument,
+   * otherwise -1.
+   */
+  private static int soleArgument(AggregateCall aggregateCall)
+  {
+    return aggregateCall.getArgList().size() == 1
+           ? aggregateCall.getArgList().get(0)
+           : -1;
+  }
+
+  private static boolean isThreeArgCase(final RexNode rexNode)
+  {
+    return rexNode.getKind() == SqlKind.CASE
+           && ((RexCall) rexNode).operands.size() == 3;
+  }
+
+  private static boolean isIntLiteral(final RexNode rexNode)
+  {
+    return rexNode instanceof RexLiteral
+           && 
SqlTypeName.INT_TYPES.contains(rexNode.getType().getSqlTypeName());
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidAggregateRule.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidAggregateRule.java
new file mode 100644
index 0000000000..07ff9cd57b
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidAggregateRule.java
@@ -0,0 +1,88 @@
+/*
+ * 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.druid.sql.calcite.rule.logical;
+
+import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelCollation;
+import org.apache.calcite.rel.RelCollationTraitDef;
+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.Aggregate;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.sql.calcite.rel.logical.DruidAggregate;
+import org.apache.druid.sql.calcite.rel.logical.DruidLogicalConvention;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link ConverterRule} to convert {@link Aggregate} to {@link DruidAggregate}
+ */
+public class DruidAggregateRule extends ConverterRule
+{
+  private final PlannerContext plannerContext;
+
+  public DruidAggregateRule(
+      Class<? extends RelNode> clazz,
+      RelTrait in,
+      RelTrait out,
+      String descriptionPrefix,
+      PlannerContext plannerContext
+  )
+  {
+    super(clazz, in, out, descriptionPrefix);
+    this.plannerContext = plannerContext;
+  }
+
+  @Override
+  public RelNode convert(RelNode rel)
+  {
+    LogicalAggregate aggregate = (LogicalAggregate) rel;
+    RelTraitSet newTrait = deriveTraits(aggregate, aggregate.getTraitSet());
+    return new DruidAggregate(
+        aggregate.getCluster(),
+        newTrait,
+        convert(aggregate.getInput(), 
aggregate.getInput().getTraitSet().replace(DruidLogicalConvention.instance())),
+        aggregate.getGroupSet(),
+        aggregate.getGroupSets(),
+        aggregate.getAggCallList(),
+        plannerContext
+    );
+  }
+
+  private RelTraitSet deriveTraits(Aggregate aggregate, RelTraitSet traits)
+  {
+    final RelCollation collation = 
traits.getTrait(RelCollationTraitDef.INSTANCE);
+    if ((collation == null || collation.getFieldCollations().isEmpty()) && 
aggregate.getGroupSets().size() == 1) {
+      // Druid sorts by grouping keys when grouping. Add the collation.
+      // Note: [aggregate.getGroupSets().size() == 1] above means that 
collation isn't added for GROUPING SETS.
+      final List<RelFieldCollation> sortFields = new ArrayList<>();
+      for (int i = 0; i < aggregate.getGroupCount(); i++) {
+        sortFields.add(new RelFieldCollation(i));
+      }
+      return 
traits.replace(DruidLogicalConvention.instance()).replace(RelCollations.of(sortFields));
+    }
+    return traits.replace(DruidLogicalConvention.instance());
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidFilterRule.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidFilterRule.java
new file mode 100644
index 0000000000..d67cd17927
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidFilterRule.java
@@ -0,0 +1,62 @@
+/*
+ * 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.druid.sql.calcite.rule.logical;
+
+import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.druid.sql.calcite.rel.logical.DruidFilter;
+import org.apache.druid.sql.calcite.rel.logical.DruidLogicalConvention;
+
+/**
+ * {@link ConverterRule} to convert {@link org.apache.calcite.rel.core.Filter} 
to {@link DruidFilter}
+ */
+public class DruidFilterRule extends ConverterRule
+{
+
+  public DruidFilterRule(
+      Class<? extends RelNode> clazz,
+      RelTrait in,
+      RelTrait out,
+      String descriptionPrefix
+  )
+  {
+    super(clazz, in, out, descriptionPrefix);
+  }
+
+  @Override
+  public RelNode convert(RelNode rel)
+  {
+    LogicalFilter filter = (LogicalFilter) rel;
+    RelTraitSet newTrait = 
filter.getTraitSet().replace(DruidLogicalConvention.instance());
+    return new DruidFilter(
+        filter.getCluster(),
+        newTrait,
+        convert(
+            filter.getInput(),
+            filter.getInput().getTraitSet()
+                  .replace(DruidLogicalConvention.instance())
+        ),
+        filter.getCondition()
+    );
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidLogicalRules.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidLogicalRules.java
new file mode 100644
index 0000000000..d99cdce3d6
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidLogicalRules.java
@@ -0,0 +1,90 @@
+/*
+ * 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.druid.sql.calcite.rule.logical;
+
+import com.google.common.collect.ImmutableList;
+import org.apache.calcite.plan.Convention;
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.rel.logical.LogicalAggregate;
+import org.apache.calcite.rel.logical.LogicalFilter;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.calcite.rel.logical.LogicalSort;
+import org.apache.calcite.rel.logical.LogicalTableScan;
+import org.apache.calcite.rel.logical.LogicalValues;
+import org.apache.druid.java.util.common.StringUtils;
+import org.apache.druid.sql.calcite.planner.PlannerContext;
+import org.apache.druid.sql.calcite.rel.logical.DruidLogicalConvention;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+public class DruidLogicalRules
+{
+  private final PlannerContext plannerContext;
+
+  public DruidLogicalRules(PlannerContext plannerContext)
+  {
+    this.plannerContext = plannerContext;
+  }
+
+  public List<RelOptRule> rules()
+  {
+    return new ArrayList<>(
+        ImmutableList.of(
+            new DruidTableScanRule(
+                RelOptRule.operand(LogicalTableScan.class, null, 
RelOptRule.any()),
+                StringUtils.format("%s", 
DruidTableScanRule.class.getSimpleName())
+            ),
+            new DruidAggregateRule(
+                LogicalAggregate.class,
+                Convention.NONE,
+                DruidLogicalConvention.instance(),
+                DruidAggregateRule.class.getSimpleName(),
+                plannerContext
+            ),
+            new DruidSortRule(
+                LogicalSort.class,
+                Convention.NONE,
+                DruidLogicalConvention.instance(),
+                DruidSortRule.class.getSimpleName()
+            ),
+            new DruidProjectRule(
+                LogicalProject.class,
+                Convention.NONE,
+                DruidLogicalConvention.instance(),
+                DruidProjectRule.class.getSimpleName()
+            ),
+            new DruidFilterRule(
+                LogicalFilter.class,
+                Convention.NONE,
+                DruidLogicalConvention.instance(),
+                DruidFilterRule.class.getSimpleName()
+            ),
+            new DruidValuesRule(
+                LogicalValues.class,
+                Convention.NONE,
+                DruidLogicalConvention.instance(),
+                DruidValuesRule.class.getSimpleName()
+            )
+        )
+    );
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidProjectRule.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidProjectRule.java
new file mode 100644
index 0000000000..00863bee2a
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidProjectRule.java
@@ -0,0 +1,59 @@
+/*
+ * 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.druid.sql.calcite.rule.logical;
+
+import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.logical.LogicalProject;
+import org.apache.druid.sql.calcite.rel.logical.DruidLogicalConvention;
+import org.apache.druid.sql.calcite.rel.logical.DruidProject;
+
+/**
+ * {@link ConverterRule} to convert {@link 
org.apache.calcite.rel.core.Project} to {@link DruidProject}
+ */
+public class DruidProjectRule extends ConverterRule
+{
+
+  public DruidProjectRule(
+      Class<? extends RelNode> clazz,
+      RelTrait in,
+      RelTrait out,
+      String descriptionPrefix
+  )
+  {
+    super(clazz, in, out, descriptionPrefix);
+  }
+
+  @Override
+  public RelNode convert(RelNode rel)
+  {
+    LogicalProject project = (LogicalProject) rel;
+    return DruidProject.create(
+        convert(
+            project.getInput(),
+            project.getInput().getTraitSet()
+                   .replace(DruidLogicalConvention.instance())
+        ),
+        project.getProjects(),
+        project.getRowType()
+    );
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidSortRule.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidSortRule.java
new file mode 100644
index 0000000000..271dd83d74
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidSortRule.java
@@ -0,0 +1,60 @@
+/*
+ * 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.druid.sql.calcite.rule.logical;
+
+import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.core.Sort;
+import org.apache.calcite.rel.logical.LogicalSort;
+import org.apache.druid.sql.calcite.rel.logical.DruidLogicalConvention;
+import org.apache.druid.sql.calcite.rel.logical.DruidSort;
+
+/**
+ * {@link ConverterRule} to convert {@link Sort} to {@link DruidSort}
+ */
+public class DruidSortRule extends ConverterRule
+{
+
+  public DruidSortRule(
+      Class<? extends RelNode> clazz,
+      RelTrait in,
+      RelTrait out,
+      String descriptionPrefix
+  )
+  {
+    super(clazz, in, out, descriptionPrefix);
+  }
+
+  @Override
+  public RelNode convert(RelNode rel)
+  {
+    LogicalSort sort = (LogicalSort) rel;
+    return DruidSort.create(
+        convert(
+            sort.getInput(),
+            
sort.getInput().getTraitSet().replace(DruidLogicalConvention.instance())
+        ),
+        sort.getCollation(),
+        sort.offset,
+        sort.fetch
+    );
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidTableScanRule.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidTableScanRule.java
new file mode 100644
index 0000000000..517e93f2dc
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidTableScanRule.java
@@ -0,0 +1,54 @@
+/*
+ * 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.druid.sql.calcite.rule.logical;
+
+import org.apache.calcite.plan.RelOptRule;
+import org.apache.calcite.plan.RelOptRuleCall;
+import org.apache.calcite.plan.RelOptRuleOperand;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.logical.LogicalTableScan;
+import org.apache.druid.sql.calcite.rel.logical.DruidLogicalConvention;
+import org.apache.druid.sql.calcite.rel.logical.DruidTableScan;
+
+/**
+ * {@link ConverterRule} to convert {@link 
org.apache.calcite.rel.core.TableScan} to {@link DruidTableScan}
+ */
+public class DruidTableScanRule extends RelOptRule
+{
+  public DruidTableScanRule(RelOptRuleOperand operand, String description)
+  {
+    super(operand, description);
+  }
+
+  @Override
+  public void onMatch(RelOptRuleCall call)
+  {
+    LogicalTableScan tableScan = call.rel(0);
+    RelTraitSet newTrait = 
tableScan.getTraitSet().replace(DruidLogicalConvention.instance());
+    DruidTableScan druidTableScan = new DruidTableScan(
+        tableScan.getCluster(),
+        newTrait,
+        tableScan.getTable(),
+        null
+    );
+    call.transformTo(druidTableScan);
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidValuesRule.java
 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidValuesRule.java
new file mode 100644
index 0000000000..5fca4a2296
--- /dev/null
+++ 
b/sql/src/main/java/org/apache/druid/sql/calcite/rule/logical/DruidValuesRule.java
@@ -0,0 +1,58 @@
+/*
+ * 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.druid.sql.calcite.rule.logical;
+
+import org.apache.calcite.plan.RelTrait;
+import org.apache.calcite.plan.RelTraitSet;
+import org.apache.calcite.rel.RelNode;
+import org.apache.calcite.rel.convert.ConverterRule;
+import org.apache.calcite.rel.logical.LogicalValues;
+import org.apache.druid.sql.calcite.rel.logical.DruidLogicalConvention;
+import org.apache.druid.sql.calcite.rel.logical.DruidValues;
+
+/**
+ * {@link ConverterRule} to convert {@link org.apache.calcite.rel.core.Values} 
to {@link DruidValues}
+ */
+public class DruidValuesRule extends ConverterRule
+{
+
+  public DruidValuesRule(
+      Class<? extends RelNode> clazz,
+      RelTrait in,
+      RelTrait out,
+      String descriptionPrefix
+  )
+  {
+    super(clazz, in, out, descriptionPrefix);
+  }
+
+  @Override
+  public RelNode convert(RelNode rel)
+  {
+    LogicalValues values = (LogicalValues) rel;
+    RelTraitSet newTrait = 
values.getTraitSet().replace(DruidLogicalConvention.instance());
+    return new DruidValues(
+        values.getCluster(),
+        newTrait,
+        values.getRowType(),
+        values.getTuples()
+    );
+  }
+}
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java
index 428e1d8200..b93228e076 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/BaseCalciteQueryTest.java
@@ -858,6 +858,7 @@ public class BaseCalciteQueryTest extends CalciteTestBase
   public class CalciteTestConfig implements QueryTestBuilder.QueryTestConfig
   {
     private boolean isRunningMSQ = false;
+    private Map<String, Object> baseQueryContext;
 
     public CalciteTestConfig()
     {
@@ -868,6 +869,11 @@ public class BaseCalciteQueryTest extends CalciteTestBase
       this.isRunningMSQ = isRunningMSQ;
     }
 
+    public CalciteTestConfig(Map<String, Object> baseQueryContext)
+    {
+      this.baseQueryContext = baseQueryContext;
+    }
+
     @Override
     public QueryLogHook queryLogHook()
     {
@@ -909,6 +915,12 @@ public class BaseCalciteQueryTest extends CalciteTestBase
     {
       return isRunningMSQ;
     }
+
+    @Override
+    public Map<String, Object> baseQueryContext()
+    {
+      return baseQueryContext;
+    }
   }
 
   public void assertResultsEquals(String sql, List<Object[]> expectedResults, 
List<Object[]> results)
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/DecoupledPlanningCalciteQueryTest.java
 
b/sql/src/test/java/org/apache/druid/sql/calcite/DecoupledPlanningCalciteQueryTest.java
new file mode 100644
index 0000000000..dfd1acad0c
--- /dev/null
+++ 
b/sql/src/test/java/org/apache/druid/sql/calcite/DecoupledPlanningCalciteQueryTest.java
@@ -0,0 +1,425 @@
+/*
+ * 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.druid.sql.calcite;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.druid.query.QueryContexts;
+import org.apache.druid.server.security.AuthConfig;
+import org.apache.druid.sql.calcite.planner.PlannerConfig;
+import org.apache.druid.sql.calcite.util.SqlTestFramework;
+import org.junit.Ignore;
+import org.junit.Test;
+
+public class DecoupledPlanningCalciteQueryTest extends CalciteQueryTest
+{
+  private static final ImmutableMap<String, Object> CONTEXT_OVERRIDES = 
ImmutableMap.of(
+      PlannerConfig.CTX_NATIVE_QUERY_SQL_PLANNING_MODE, 
PlannerConfig.NATIVE_QUERY_SQL_PLANNING_MODE_DECOUPLED,
+      QueryContexts.ENABLE_DEBUG, true
+  );
+
+  @Override
+  protected QueryTestBuilder testBuilder()
+  {
+    return new QueryTestBuilder(
+        new CalciteTestConfig(CONTEXT_OVERRIDES)
+        {
+          @Override
+          public SqlTestFramework.PlannerFixture plannerFixture(PlannerConfig 
plannerConfig, AuthConfig authConfig)
+          {
+            plannerConfig = plannerConfig.withOverrides(CONTEXT_OVERRIDES);
+            return 
queryFramework().plannerFixture(DecoupledPlanningCalciteQueryTest.this, 
plannerConfig, authConfig);
+          }
+        })
+        .cannotVectorize(cannotVectorize)
+        .skipVectorize(skipVectorize);
+  }
+
+
+  @Override
+  @Ignore
+  public void testGroupByWithSelectAndOrderByProjections()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testTopNWithSelectAndOrderByProjections()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllQueries()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllQueriesWithLimit()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllDifferentTablesWithMapping()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testJoinUnionAllDifferentTablesWithMapping()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllTablesColumnTypeMismatchFloatLong()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllTablesColumnTypeMismatchStringLong()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllTablesWhenMappingIsRequired()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionIsUnplannable()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllTablesWhenCastAndMappingIsRequired()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllSameTableTwice()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllSameTableTwiceWithSameMapping()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllSameTableTwiceWithDifferentMapping()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllSameTableThreeTimes()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnionAllSameTableThreeTimesWithSameMapping()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testSelfJoin()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testTwoExactCountDistincts()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testViewAndJoin()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testGroupByWithSortOnPostAggregationDefault()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testGroupByWithSortOnPostAggregationNoTopNConfig()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testGroupByWithSortOnPostAggregationNoTopNContext()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnplannableQueries()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnplannableTwoExactCountDistincts()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUnplannableExactCountDistinctOnSketch()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testExactCountDistinctUsingSubqueryOnUnionAllTables()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUseTimeFloorInsteadOfGranularityOnJoinResult()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testMinMaxAvgDailyCountWithLimit()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testExactCountDistinctOfSemiJoinResult()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testMaxSubqueryRows()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testExactCountDistinctUsingSubqueryWithWherePushDown()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testGroupByTimeFloorAndDimOnGroupByTimeFloorAndDim()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUsingSubqueryAsFilterOnTwoColumns()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUsingSubqueryAsFilterWithInnerSort()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testUsingSubqueryWithLimit()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testPostAggWithTimeseries()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testPostAggWithTopN()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testRequireTimeConditionPositive()
+  {
+
+  }
+
+  @Override
+  public void testRequireTimeConditionSemiJoinNegative()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testEmptyGroupWithOffsetDoesntInfiniteLoop()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testJoinWithTimeDimension()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testSubqueryTypeMismatchWithLiterals()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testTimeseriesQueryWithEmptyInlineDatasourceAndGranularity()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testGroupBySortPushDown()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testGroupingWithNullInFilter()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  @Test
+  public void testStringAggExpressionNonConstantSeparator()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testOrderByAlongWithInternalScanQuery()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testSortProjectAfterNestedGroupBy()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testOrderByAlongWithInternalScanQueryNoDistinct()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testNestedGroupBy()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testQueryWithSelectProjectAndIdentityProjectDoesNotRename()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testFilterOnCurrentTimestampWithIntervalArithmetic()
+  {
+
+  }
+
+  @Override
+  @Ignore
+  public void testFilterOnCurrentTimestampOnView()
+  {
+
+  }
+}
diff --git 
a/sql/src/test/java/org/apache/druid/sql/calcite/QueryTestBuilder.java 
b/sql/src/test/java/org/apache/druid/sql/calcite/QueryTestBuilder.java
index 187beab91d..d5e20043ad 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/QueryTestBuilder.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/QueryTestBuilder.java
@@ -21,6 +21,7 @@ package org.apache.druid.sql.calcite;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.druid.query.Query;
+import org.apache.druid.query.QueryContexts;
 import org.apache.druid.segment.column.RowSignature;
 import org.apache.druid.server.security.AuthConfig;
 import org.apache.druid.server.security.AuthenticationResult;
@@ -79,11 +80,13 @@ public class QueryTestBuilder
     ResultsVerifier defaultResultsVerifier(List<Object[]> expectedResults, 
RowSignature expectedResultSignature);
 
     boolean isRunningMSQ();
+
+    Map<String, Object> baseQueryContext();
   }
 
   protected final QueryTestConfig config;
   protected PlannerConfig plannerConfig = 
BaseCalciteQueryTest.PLANNER_CONFIG_DEFAULT;
-  protected Map<String, Object> queryContext = 
BaseCalciteQueryTest.QUERY_CONTEXT_DEFAULT;
+  protected Map<String, Object> queryContext;
   protected List<SqlParameter> parameters = CalciteTestBase.DEFAULT_PARAMETERS;
   protected String sql;
   protected AuthenticationResult authenticationResult = 
CalciteTests.REGULAR_USER_AUTH_RESULT;
@@ -108,6 +111,12 @@ public class QueryTestBuilder
   public QueryTestBuilder(final QueryTestConfig config)
   {
     this.config = config;
+    // Done to maintain backwards compat. So,
+    // 1. If no base context is provided in config, the queryContext is set to 
the default one
+    // 2. If some base context is provided in config, we set that context as 
the queryContext
+    // 3. If someone overrides the context, we merge the context with the 
empty/non-empty base context provided in the config
+    this.queryContext =
+        config.baseQueryContext() == null ? 
BaseCalciteQueryTest.QUERY_CONTEXT_DEFAULT : config.baseQueryContext();
   }
 
   public QueryTestBuilder plannerConfig(PlannerConfig plannerConfig)
@@ -118,7 +127,7 @@ public class QueryTestBuilder
 
   public QueryTestBuilder queryContext(Map<String, Object> queryContext)
   {
-    this.queryContext = queryContext;
+    this.queryContext = QueryContexts.override(config.baseQueryContext(), 
queryContext);
     return this;
   }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to