/**
  * Creates a group-by plan for the underlying query. The grouping is determined by the specified
  * collection of group fields, and the aggregation is computed by the specified collection of
  * aggregation functions.
  *
  * @param p a plan for the underlying query
  * @param groupFlds the fields to group by. Can be empty, which means that all records are in a
  *     single group.
  * @param aggFns the aggregation functions. Optional, can be null.
  * @param tx the calling transaction
  */
 public GroupByPlan(Plan p, Set<String> groupFlds, Set<AggregationFn> aggFns, Transaction tx) {
   schema = new Schema();
   this.groupFlds = groupFlds;
   if (!this.groupFlds.isEmpty()) {
     for (String fld : groupFlds) schema.add(fld, p.schema());
     // sort records by group-by fields with default direction
     sp = new SortPlan(p, new ArrayList<String>(groupFlds), tx);
   } else
     // all records are in a single group, so p is already sorted
     sp = p;
   this.aggFns = aggFns;
   if (aggFns != null)
     for (AggregationFn fn : aggFns) {
       Type t =
           fn.isArgumentTypeDependent() ? p.schema().type(fn.argumentFieldName()) : fn.fieldType();
       schema.addField(fn.fieldName(), t);
     }
   hist = groupByHistogram(p.histogram(), this.groupFlds, aggFns);
 }
 /**
  * Returns the number of blocks required to compute the aggregation, which is one pass through the
  * sorted table. It does <em>not</em> include the one-time cost of materializing and sorting the
  * records.
  *
  * @see Plan#blocksAccessed()
  */
 @Override
 public long blocksAccessed() {
   return sp.blocksAccessed();
 }
 /**
  * This method opens a sort plan for the specified plan. The sort plan ensures that the underlying
  * records will be appropriately grouped.
  *
  * @see Plan#open()
  */
 @Override
 public Scan open() {
   Scan ss = sp.open();
   return new GroupByScan(ss, groupFlds, aggFns);
 }