@Override
  protected AbstractPlanNode recursivelyApply(AbstractPlanNode planNode) {
    assert (planNode != null);

    // breadth first:
    //     find AggregatePlanNode with exactly one child
    //     where that child is an AbstractScanPlanNode.
    //     Inline any qualifying AggregatePlanNode to its AbstractScanPlanNode.

    Queue<AbstractPlanNode> children = new LinkedList<AbstractPlanNode>();
    children.add(planNode);

    while (!children.isEmpty()) {
      AbstractPlanNode plan = children.remove();
      AbstractPlanNode newPlan = inlineAggregationApply(plan);
      if (newPlan != plan) {
        if (plan == planNode) {
          planNode = newPlan;
        } else {
          planNode.replaceChild(plan, newPlan);
        }
      }

      for (int i = 0; i < newPlan.getChildCount(); i++) {
        children.add(newPlan.getChild(i));
      }
    }

    return planNode;
  }
  AbstractPlanNode inlineAggregationApply(AbstractPlanNode plan) {
    // check for an aggregation of the right form
    if ((plan instanceof AggregatePlanNode) == false || (plan instanceof WindowFunctionPlanNode)) {
      return plan;
    }
    assert (plan.getChildCount() == 1);
    AggregatePlanNode aggplan = (AggregatePlanNode) plan;

    // Assuming all AggregatePlanNode has not been inlined before this microoptimization
    AbstractPlanNode child = aggplan.getChild(0);

    // EE Currently support: seqscan + indexscan
    if (child.getPlanNodeType() != PlanNodeType.SEQSCAN
        && child.getPlanNodeType() != PlanNodeType.INDEXSCAN
        && child.getPlanNodeType() != PlanNodeType.NESTLOOP
        && child.getPlanNodeType() != PlanNodeType.NESTLOOPINDEX) {
      return plan;
    }

    if (child.getPlanNodeType() == PlanNodeType.INDEXSCAN) {
      // Currently do not conflict with the optimized MIN/MAX
      // because of the big amount of tests changed.

      IndexScanPlanNode isp = (IndexScanPlanNode) child;
      LimitPlanNode limit = (LimitPlanNode) isp.getInlinePlanNode(PlanNodeType.LIMIT);
      if (limit != null && (aggplan.isTableMin() || aggplan.isTableMax())) {
        // Optimized MIN/MAX
        if (limit.getLimit() == 1 && limit.getOffset() == 0) {
          return plan;
        }
      }
    }

    // Inline aggregate node
    AbstractPlanNode parent = null;
    if (aggplan.getParentCount() == 1) {
      parent = aggplan.getParent(0);
    }
    child.addInlinePlanNode(aggplan);
    child.clearParents();
    if (parent != null) {
      parent.replaceChild(aggplan, child);
    }
    return child;
  }
  @Override
  protected AbstractPlanNode recursivelyApply(AbstractPlanNode plan) {
    assert (plan != null);

    // depth first:
    //     find AggregatePlanNode with exactly one child
    //     where that child is an AbstractScanPlanNode.
    //     Replace any qualifying AggregatePlanNode / AbstractScanPlanNode pair
    //     with an IndexCountPlanNode or TableCountPlanNode

    ArrayList<AbstractPlanNode> children = new ArrayList<AbstractPlanNode>();

    for (int i = 0; i < plan.getChildCount(); i++) children.add(plan.getChild(i));

    for (AbstractPlanNode child : children) {
      // TODO this will break when children feed multiple parents
      AbstractPlanNode newChild = recursivelyApply(child);
      // Do a graft into the (parent) plan only if a replacement for a child was found.
      if (newChild == child) {
        continue;
      }
      boolean replaced = plan.replaceChild(child, newChild);
      assert (true == replaced);
    }

    // check for an aggregation of the right form
    if ((plan instanceof AggregatePlanNode) == false) return plan;
    assert (plan.getChildCount() == 1);
    AggregatePlanNode aggplan = (AggregatePlanNode) plan;
    // ENG-6131 fixed here.
    if (!(aggplan.isTableCountStar()
        || aggplan.isTableNonDistinctCountConstant()
        || aggplan.isTableCountNonDistinctNullableColumn())) {
      return plan;
    }

    AbstractPlanNode child = plan.getChild(0);

    // A table count can replace a seq scan only if it has no predicates.
    if (child instanceof SeqScanPlanNode) {
      if (((SeqScanPlanNode) child).getPredicate() != null) {
        return plan;
      }

      AbstractExpression postPredicate = aggplan.getPostPredicate();
      if (postPredicate != null) {
        List<AbstractExpression> aggList =
            postPredicate.findAllSubexpressionsOfClass(AggregateExpression.class);

        boolean allCountStar = true;
        for (AbstractExpression expr : aggList) {
          if (expr.getExpressionType() != ExpressionType.AGGREGATE_COUNT_STAR) {
            allCountStar = false;
            break;
          }
        }
        if (allCountStar) {
          return plan;
        }
      }

      if (hasInlineLimit(aggplan)) {
        // table count EE executor does not handle inline limit stuff
        return plan;
      }

      return new TableCountPlanNode((AbstractScanPlanNode) child, aggplan);
    }

    // Otherwise, optimized counts only replace particular cases of index scan.
    if ((child instanceof IndexScanPlanNode) == false) return plan;

    IndexScanPlanNode isp = (IndexScanPlanNode) child;

    // Guard against (possible future?) cases of indexable subquery.
    if (((IndexScanPlanNode) child).isSubQuery()) {
      return plan;
    }

    // An index count or table count can replace an index scan only if it has no (post-)predicates
    // except those (post-)predicates are artifact predicates we added for reverse scan purpose only
    if (isp.getPredicate() != null && !isp.isPredicatesOptimizableForAggregate()) {
      return plan;
    }

    // With no start or end keys, there's not much a counting index can do.
    if (isp.getEndExpression() == null && isp.getSearchKeyExpressions().size() == 0) {
      // An indexed query without a where clause can fall back to a plain old table count.
      // This can only happen when a confused query like
      // "select count(*) from table order by index_key;"
      // meets a naive planner that doesn't just cull the no-op ORDER BY. Who, us?

      if (hasInlineLimit(aggplan)) {
        return plan;
      }

      return new TableCountPlanNode(isp, aggplan);
    }

    // check for the index's support for counting
    Index idx = isp.getCatalogIndex();
    if (!idx.getCountable()) {
      return plan;
    }

    // The core idea is that counting index needs to know the start key and end key to
    // jump to to get counts instead of actually doing any scanning.
    // Options to be determined are:
    // - whether each of the start/end keys is missing, partial (a prefix of a compund key), or
    // complete,
    // - whether the count should include or exclude entries exactly matching each of the start/end
    // keys.
    // Not all combinations of these options are supported;
    // unsupportable cases cause the factory method to return null.
    IndexCountPlanNode countingPlan = IndexCountPlanNode.createOrNull(isp, aggplan);
    if (countingPlan == null) {
      return plan;
    }
    return countingPlan;
  }