org.apache.druid.query.groupby.GroupByQuery.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.druid.query.groupby.GroupByQuery.java

Source

/*
 * 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.query.groupby;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.primitives.Longs;
import org.apache.druid.data.input.Row;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.granularity.Granularities;
import org.apache.druid.java.util.common.granularity.Granularity;
import org.apache.druid.java.util.common.guava.Comparators;
import org.apache.druid.java.util.common.guava.Sequence;
import org.apache.druid.java.util.common.guava.Sequences;
import org.apache.druid.query.BaseQuery;
import org.apache.druid.query.DataSource;
import org.apache.druid.query.Queries;
import org.apache.druid.query.Query;
import org.apache.druid.query.QueryDataSource;
import org.apache.druid.query.TableDataSource;
import org.apache.druid.query.aggregation.AggregatorFactory;
import org.apache.druid.query.aggregation.PostAggregator;
import org.apache.druid.query.dimension.DefaultDimensionSpec;
import org.apache.druid.query.dimension.DimensionSpec;
import org.apache.druid.query.filter.DimFilter;
import org.apache.druid.query.groupby.having.HavingSpec;
import org.apache.druid.query.groupby.orderby.DefaultLimitSpec;
import org.apache.druid.query.groupby.orderby.LimitSpec;
import org.apache.druid.query.groupby.orderby.NoopLimitSpec;
import org.apache.druid.query.groupby.orderby.OrderByColumnSpec;
import org.apache.druid.query.ordering.StringComparator;
import org.apache.druid.query.ordering.StringComparators;
import org.apache.druid.query.spec.LegacySegmentSpec;
import org.apache.druid.query.spec.QuerySegmentSpec;
import org.apache.druid.segment.DimensionHandlerUtils;
import org.apache.druid.segment.VirtualColumn;
import org.apache.druid.segment.VirtualColumns;
import org.apache.druid.segment.column.ColumnHolder;
import org.apache.druid.segment.column.ValueType;
import org.joda.time.Interval;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
 */
public class GroupByQuery extends BaseQuery<Row> {
    public static final String CTX_KEY_SORT_BY_DIMS_FIRST = "sortByDimsFirst";

    private static final Comparator<Row> NON_GRANULAR_TIME_COMP = (Row lhs, Row rhs) -> Longs
            .compare(lhs.getTimestampFromEpoch(), rhs.getTimestampFromEpoch());

    public static Builder builder() {
        return new Builder();
    }

    private final VirtualColumns virtualColumns;
    private final LimitSpec limitSpec;
    private final HavingSpec havingSpec;
    private final DimFilter dimFilter;
    private final List<DimensionSpec> dimensions;
    private final List<AggregatorFactory> aggregatorSpecs;
    private final List<PostAggregator> postAggregatorSpecs;
    private final List<List<String>> subtotalsSpec;

    private final boolean applyLimitPushDown;
    private final Function<Sequence<Row>, Sequence<Row>> postProcessingFn;

    @JsonCreator
    public GroupByQuery(@JsonProperty("dataSource") DataSource dataSource,
            @JsonProperty("intervals") QuerySegmentSpec querySegmentSpec,
            @JsonProperty("virtualColumns") VirtualColumns virtualColumns,
            @JsonProperty("filter") DimFilter dimFilter, @JsonProperty("granularity") Granularity granularity,
            @JsonProperty("dimensions") List<DimensionSpec> dimensions,
            @JsonProperty("aggregations") List<AggregatorFactory> aggregatorSpecs,
            @JsonProperty("postAggregations") List<PostAggregator> postAggregatorSpecs,
            @JsonProperty("having") HavingSpec havingSpec, @JsonProperty("limitSpec") LimitSpec limitSpec,
            @JsonProperty("subtotalsSpec") List<List<String>> subtotalsSpec,
            @JsonProperty("context") Map<String, Object> context) {
        this(dataSource, querySegmentSpec, virtualColumns, dimFilter, granularity, dimensions, aggregatorSpecs,
                postAggregatorSpecs, havingSpec, limitSpec, subtotalsSpec, null, context);
    }

    private Function<Sequence<Row>, Sequence<Row>> makePostProcessingFn() {
        Function<Sequence<Row>, Sequence<Row>> postProcessingFn = limitSpec.build(dimensions, aggregatorSpecs,
                postAggregatorSpecs, getGranularity(), getContextSortByDimsFirst());

        if (havingSpec != null) {
            postProcessingFn = Functions.compose(postProcessingFn, (Sequence<Row> input) -> {
                havingSpec.setRowSignature(GroupByQueryHelper.rowSignatureFor(GroupByQuery.this));
                havingSpec.setAggregators(getAggregatorsMap(aggregatorSpecs));
                return Sequences.filter(input, havingSpec::eval);
            });
        }
        return postProcessingFn;
    }

    /**
     * A private constructor that avoids recomputing postProcessingFn.
     */
    private GroupByQuery(final DataSource dataSource, final QuerySegmentSpec querySegmentSpec,
            final VirtualColumns virtualColumns, final DimFilter dimFilter, final Granularity granularity,
            final List<DimensionSpec> dimensions, final List<AggregatorFactory> aggregatorSpecs,
            final List<PostAggregator> postAggregatorSpecs, final HavingSpec havingSpec, final LimitSpec limitSpec,
            final @Nullable List<List<String>> subtotalsSpec,
            final @Nullable Function<Sequence<Row>, Sequence<Row>> postProcessingFn,
            final Map<String, Object> context) {
        super(dataSource, querySegmentSpec, false, context, granularity);

        this.virtualColumns = VirtualColumns.nullToEmpty(virtualColumns);
        this.dimFilter = dimFilter;
        this.dimensions = dimensions == null ? ImmutableList.of() : dimensions;
        for (DimensionSpec spec : this.dimensions) {
            Preconditions.checkArgument(spec != null, "dimensions has null DimensionSpec");
        }

        this.aggregatorSpecs = aggregatorSpecs == null ? ImmutableList.of() : aggregatorSpecs;
        this.postAggregatorSpecs = Queries.prepareAggregations(
                this.dimensions.stream().map(DimensionSpec::getOutputName).collect(Collectors.toList()),
                this.aggregatorSpecs, postAggregatorSpecs == null ? ImmutableList.of() : postAggregatorSpecs);
        this.havingSpec = havingSpec;
        this.limitSpec = LimitSpec.nullToNoopLimitSpec(limitSpec);

        this.subtotalsSpec = verifySubtotalsSpec(subtotalsSpec, dimensions);

        // Verify no duplicate names between dimensions, aggregators, and postAggregators.
        // They will all end up in the same namespace in the returned Rows and we can't have them clobbering each other.
        // We're not counting __time, even though that name is problematic. See: https://github.com/apache/incubator-druid/pull/3684
        verifyOutputNames(this.dimensions, this.aggregatorSpecs, this.postAggregatorSpecs);

        this.postProcessingFn = postProcessingFn != null ? postProcessingFn : makePostProcessingFn();

        // Check if limit push down configuration is valid and check if limit push down will be applied
        this.applyLimitPushDown = determineApplyLimitPushDown();
    }

    private List<List<String>> verifySubtotalsSpec(List<List<String>> subtotalsSpec,
            List<DimensionSpec> dimensions) {
        // if subtotalsSpec exists then validate that all are subsets of dimensions spec and are in same order.
        // For example if we had {D1, D2, D3} in dimensions spec then
        // {D2}, {D1, D2}, {D1, D3}, {D2, D3} etc are valid in subtotalsSpec while
        // {D2, D1} is not as it is not in same order.
        // {D4} is not as its not a subset.
        // This restriction as enforced because implementation does sort merge on the results of top-level query
        // results and expects that ordering of events does not change when dimension columns are removed from
        // results of top level query.
        if (subtotalsSpec != null) {
            for (List<String> subtotalSpec : subtotalsSpec) {
                int i = 0;
                for (String s : subtotalSpec) {
                    boolean found = false;
                    for (; i < dimensions.size(); i++) {
                        if (s.equals(dimensions.get(i).getOutputName())) {
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        throw new IAE(
                                "Subtotal spec %s is either not a subset or items are in different order than in dimensions.",
                                subtotalSpec);
                    }
                }
            }
        }

        return subtotalsSpec;
    }

    @JsonProperty
    public VirtualColumns getVirtualColumns() {
        return virtualColumns;
    }

    @Nullable
    @JsonProperty("filter")
    public DimFilter getDimFilter() {
        return dimFilter;
    }

    @JsonProperty
    public List<DimensionSpec> getDimensions() {
        return dimensions;
    }

    @JsonProperty("aggregations")
    public List<AggregatorFactory> getAggregatorSpecs() {
        return aggregatorSpecs;
    }

    @JsonProperty("postAggregations")
    public List<PostAggregator> getPostAggregatorSpecs() {
        return postAggregatorSpecs;
    }

    @JsonProperty("having")
    public HavingSpec getHavingSpec() {
        return havingSpec;
    }

    @JsonProperty
    public LimitSpec getLimitSpec() {
        return limitSpec;
    }

    @JsonInclude(JsonInclude.Include.NON_NULL)
    @JsonProperty("subtotalsSpec")
    public List<List<String>> getSubtotalsSpec() {
        return subtotalsSpec;
    }

    @Override
    public boolean hasFilters() {
        return dimFilter != null;
    }

    @Override
    public DimFilter getFilter() {
        return dimFilter;
    }

    @Override
    public String getType() {
        return GROUP_BY;
    }

    @JsonIgnore
    public boolean getContextSortByDimsFirst() {
        return getContextBoolean(CTX_KEY_SORT_BY_DIMS_FIRST, false);
    }

    @JsonIgnore
    public boolean isApplyLimitPushDown() {
        return applyLimitPushDown;
    }

    @JsonIgnore
    public boolean getApplyLimitPushDownFromContext() {
        return getContextBoolean(GroupByQueryConfig.CTX_KEY_APPLY_LIMIT_PUSH_DOWN, true);
    }

    @Override
    public Ordering getResultOrdering() {
        final Ordering<Row> rowOrdering = getRowOrdering(false);

        return Ordering.from((lhs, rhs) -> {
            if (lhs instanceof Row) {
                return rowOrdering.compare((Row) lhs, (Row) rhs);
            } else {
                // Probably bySegment queries
                return ((Ordering) Comparators.naturalNullsFirst()).compare(lhs, rhs);
            }
        });
    }

    private boolean validateAndGetForceLimitPushDown() {
        final boolean forcePushDown = getContextBoolean(GroupByQueryConfig.CTX_KEY_FORCE_LIMIT_PUSH_DOWN, false);
        if (forcePushDown) {
            if (!(limitSpec instanceof DefaultLimitSpec)) {
                throw new IAE("When forcing limit push down, a limit spec must be provided.");
            }

            if (!((DefaultLimitSpec) limitSpec).isLimited()) {
                throw new IAE("When forcing limit push down, the provided limit spec must have a limit.");
            }

            if (havingSpec != null) {
                throw new IAE("Cannot force limit push down when a having spec is present.");
            }

            for (OrderByColumnSpec orderBySpec : ((DefaultLimitSpec) limitSpec).getColumns()) {
                if (OrderByColumnSpec.getPostAggIndexForOrderBy(orderBySpec, postAggregatorSpecs) > -1) {
                    throw new UnsupportedOperationException(
                            "Limit push down when sorting by a post aggregator is not supported.");
                }
            }
        }
        return forcePushDown;
    }

    public boolean determineApplyLimitPushDown() {
        if (subtotalsSpec != null) {
            return false;
        }

        final boolean forceLimitPushDown = validateAndGetForceLimitPushDown();

        if (limitSpec instanceof DefaultLimitSpec) {
            DefaultLimitSpec defaultLimitSpec = (DefaultLimitSpec) limitSpec;

            // If only applying an orderby without a limit, don't try to push down
            if (!defaultLimitSpec.isLimited()) {
                return false;
            }

            if (forceLimitPushDown) {
                return true;
            }

            if (!getApplyLimitPushDownFromContext()) {
                return false;
            }

            if (havingSpec != null) {
                return false;
            }

            // If the sorting order only uses columns in the grouping key, we can always push the limit down
            // to the buffer grouper without affecting result accuracy
            boolean sortHasNonGroupingFields = DefaultLimitSpec
                    .sortingOrderHasNonGroupingFields((DefaultLimitSpec) limitSpec, getDimensions());

            return !sortHasNonGroupingFields;
        }

        return false;
    }

    /**
     * When limit push down is applied, the partial results would be sorted by the ordering specified by the
     * limit/order spec (unlike non-push down case where the results always use the default natural ascending order),
     * so when merging these partial result streams, the merge needs to use the same ordering to get correct results.
     */
    private Ordering<Row> getRowOrderingForPushDown(final boolean granular, final DefaultLimitSpec limitSpec) {
        final boolean sortByDimsFirst = getContextSortByDimsFirst();

        final List<String> orderedFieldNames = new ArrayList<>();
        final Set<Integer> dimsInOrderBy = new HashSet<>();
        final List<Boolean> needsReverseList = new ArrayList<>();
        final List<ValueType> dimensionTypes = new ArrayList<>();
        final List<StringComparator> comparators = new ArrayList<>();

        for (OrderByColumnSpec orderSpec : limitSpec.getColumns()) {
            boolean needsReverse = orderSpec.getDirection() != OrderByColumnSpec.Direction.ASCENDING;
            int dimIndex = OrderByColumnSpec.getDimIndexForOrderBy(orderSpec, dimensions);
            if (dimIndex >= 0) {
                DimensionSpec dim = dimensions.get(dimIndex);
                orderedFieldNames.add(dim.getOutputName());
                dimsInOrderBy.add(dimIndex);
                needsReverseList.add(needsReverse);
                final ValueType type = dimensions.get(dimIndex).getOutputType();
                dimensionTypes.add(type);
                comparators.add(orderSpec.getDimensionComparator());
            }
        }

        for (int i = 0; i < dimensions.size(); i++) {
            if (!dimsInOrderBy.contains(i)) {
                orderedFieldNames.add(dimensions.get(i).getOutputName());
                needsReverseList.add(false);
                final ValueType type = dimensions.get(i).getOutputType();
                dimensionTypes.add(type);
                comparators.add(StringComparators.LEXICOGRAPHIC);
            }
        }

        final Comparator<Row> timeComparator = getTimeComparator(granular);

        if (timeComparator == null) {
            return Ordering.from(new Comparator<Row>() {
                @Override
                public int compare(Row lhs, Row rhs) {
                    return compareDimsForLimitPushDown(orderedFieldNames, needsReverseList, dimensionTypes,
                            comparators, lhs, rhs);
                }
            });
        } else if (sortByDimsFirst) {
            return Ordering.from(new Comparator<Row>() {
                @Override
                public int compare(Row lhs, Row rhs) {
                    final int cmp = compareDimsForLimitPushDown(orderedFieldNames, needsReverseList, dimensionTypes,
                            comparators, lhs, rhs);
                    if (cmp != 0) {
                        return cmp;
                    }

                    return timeComparator.compare(lhs, rhs);
                }
            });
        } else {
            return Ordering.from(new Comparator<Row>() {
                @Override
                public int compare(Row lhs, Row rhs) {
                    final int timeCompare = timeComparator.compare(lhs, rhs);

                    if (timeCompare != 0) {
                        return timeCompare;
                    }

                    return compareDimsForLimitPushDown(orderedFieldNames, needsReverseList, dimensionTypes,
                            comparators, lhs, rhs);
                }
            });
        }
    }

    public Ordering<Row> getRowOrdering(final boolean granular) {
        if (applyLimitPushDown) {
            if (!DefaultLimitSpec.sortingOrderHasNonGroupingFields((DefaultLimitSpec) limitSpec, dimensions)) {
                return getRowOrderingForPushDown(granular, (DefaultLimitSpec) limitSpec);
            }
        }

        final boolean sortByDimsFirst = getContextSortByDimsFirst();
        final Comparator<Row> timeComparator = getTimeComparator(granular);

        if (timeComparator == null) {
            return Ordering.from((lhs, rhs) -> compareDims(dimensions, lhs, rhs));
        } else if (sortByDimsFirst) {
            return Ordering.from((lhs, rhs) -> {
                final int cmp = compareDims(dimensions, lhs, rhs);
                if (cmp != 0) {
                    return cmp;
                }

                return timeComparator.compare(lhs, rhs);
            });
        } else {
            return Ordering.from((lhs, rhs) -> {
                final int timeCompare = timeComparator.compare(lhs, rhs);

                if (timeCompare != 0) {
                    return timeCompare;
                }

                return compareDims(dimensions, lhs, rhs);
            });
        }
    }

    @Nullable
    private Comparator<Row> getTimeComparator(boolean granular) {
        if (Granularities.ALL.equals(getGranularity())) {
            return null;
        } else if (granular) {
            return (lhs, rhs) -> Longs.compare(getGranularity().bucketStart(lhs.getTimestamp()).getMillis(),
                    getGranularity().bucketStart(rhs.getTimestamp()).getMillis());
        } else {
            return NON_GRANULAR_TIME_COMP;
        }
    }

    private static int compareDims(List<DimensionSpec> dimensions, Row lhs, Row rhs) {
        for (DimensionSpec dimension : dimensions) {
            //noinspection unchecked
            final int dimCompare = DimensionHandlerUtils.compareObjectsAsType(lhs.getRaw(dimension.getOutputName()),
                    rhs.getRaw(dimension.getOutputName()), dimension.getOutputType());
            if (dimCompare != 0) {
                return dimCompare;
            }
        }

        return 0;
    }

    private static int compareDimsForLimitPushDown(final List<String> fields, final List<Boolean> needsReverseList,
            final List<ValueType> dimensionTypes, final List<StringComparator> comparators, Row lhs, Row rhs) {
        for (int i = 0; i < fields.size(); i++) {
            final String fieldName = fields.get(i);
            final StringComparator comparator = comparators.get(i);
            final ValueType dimensionType = dimensionTypes.get(i);

            final int dimCompare;
            final Object lhsObj = lhs.getRaw(fieldName);
            final Object rhsObj = rhs.getRaw(fieldName);

            if (ValueType.isNumeric(dimensionType)) {
                if (comparator.equals(StringComparators.NUMERIC)) {
                    dimCompare = DimensionHandlerUtils.compareObjectsAsType(lhsObj, rhsObj, dimensionType);
                } else {
                    dimCompare = comparator.compare(String.valueOf(lhsObj), String.valueOf(rhsObj));
                }
            } else {
                dimCompare = comparator.compare((String) lhsObj, (String) rhsObj);
            }

            if (dimCompare != 0) {
                return needsReverseList.get(i) ? -dimCompare : dimCompare;
            }
        }
        return 0;
    }

    /**
     * Apply the havingSpec and limitSpec. Because havingSpecs are not thread safe, and because they are applied during
     * accumulation of the returned sequence, callers must take care to avoid accumulating two different Sequences
     * returned by this method in two different threads.
     *
     * @param results sequence of rows to apply havingSpec and limitSpec to
     *
     * @return sequence of rows after applying havingSpec and limitSpec
     */
    public Sequence<Row> postProcess(Sequence<Row> results) {
        return postProcessingFn.apply(results);
    }

    @Override
    public GroupByQuery withOverriddenContext(Map<String, Object> contextOverride) {
        return new Builder(this).overrideContext(contextOverride).build();
    }

    @Override
    public GroupByQuery withQuerySegmentSpec(QuerySegmentSpec spec) {
        return new Builder(this).setQuerySegmentSpec(spec).build();
    }

    public GroupByQuery withDimFilter(final DimFilter dimFilter) {
        return new Builder(this).setDimFilter(dimFilter).build();
    }

    @Override
    public Query<Row> withDataSource(DataSource dataSource) {
        return new Builder(this).setDataSource(dataSource).build();
    }

    public GroupByQuery withDimensionSpecs(final List<DimensionSpec> dimensionSpecs) {
        return new Builder(this).setDimensions(dimensionSpecs).build();
    }

    public GroupByQuery withLimitSpec(LimitSpec limitSpec) {
        return new Builder(this).setLimitSpec(limitSpec).build();
    }

    public GroupByQuery withAggregatorSpecs(final List<AggregatorFactory> aggregatorSpecs) {
        return new Builder(this).setAggregatorSpecs(aggregatorSpecs).build();
    }

    public GroupByQuery withSubtotalsSpec(final List<List<String>> subtotalsSpec) {
        return new Builder(this).setSubtotalsSpec(subtotalsSpec).build();
    }

    public GroupByQuery withPostAggregatorSpecs(final List<PostAggregator> postAggregatorSpecs) {
        return new Builder(this).setPostAggregatorSpecs(postAggregatorSpecs).build();
    }

    private static void verifyOutputNames(List<DimensionSpec> dimensions, List<AggregatorFactory> aggregators,
            List<PostAggregator> postAggregators) {
        final Set<String> outputNames = Sets.newHashSet();
        for (DimensionSpec dimension : dimensions) {
            if (!outputNames.add(dimension.getOutputName())) {
                throw new IAE("Duplicate output name[%s]", dimension.getOutputName());
            }
        }

        for (AggregatorFactory aggregator : aggregators) {
            if (!outputNames.add(aggregator.getName())) {
                throw new IAE("Duplicate output name[%s]", aggregator.getName());
            }
        }

        for (PostAggregator postAggregator : postAggregators) {
            if (!outputNames.add(postAggregator.getName())) {
                throw new IAE("Duplicate output name[%s]", postAggregator.getName());
            }
        }

        if (outputNames.contains(ColumnHolder.TIME_COLUMN_NAME)) {
            throw new IAE("'%s' cannot be used as an output name for dimensions, aggregators, or post-aggregators.",
                    ColumnHolder.TIME_COLUMN_NAME);
        }
    }

    private static Map<String, AggregatorFactory> getAggregatorsMap(List<AggregatorFactory> aggregatorSpecs) {
        Map<String, AggregatorFactory> map = new HashMap<>(aggregatorSpecs.size());
        aggregatorSpecs.stream().forEach(v -> map.put(v.getName(), v));
        return map;
    }

    public static class Builder {
        private DataSource dataSource;
        private QuerySegmentSpec querySegmentSpec;
        private VirtualColumns virtualColumns;
        private DimFilter dimFilter;
        private Granularity granularity;
        private List<DimensionSpec> dimensions;
        private List<AggregatorFactory> aggregatorSpecs;
        private List<PostAggregator> postAggregatorSpecs;
        private HavingSpec havingSpec;

        private Map<String, Object> context;

        private List<List<String>> subtotalsSpec = null;
        private LimitSpec limitSpec = null;
        private Function<Sequence<Row>, Sequence<Row>> postProcessingFn;
        private List<OrderByColumnSpec> orderByColumnSpecs = Lists.newArrayList();
        private int limit = Integer.MAX_VALUE;

        public Builder() {
        }

        public Builder(GroupByQuery query) {
            dataSource = query.getDataSource();
            querySegmentSpec = query.getQuerySegmentSpec();
            virtualColumns = query.getVirtualColumns();
            dimFilter = query.getDimFilter();
            granularity = query.getGranularity();
            dimensions = query.getDimensions();
            aggregatorSpecs = query.getAggregatorSpecs();
            postAggregatorSpecs = query.getPostAggregatorSpecs();
            havingSpec = query.getHavingSpec();
            limitSpec = query.getLimitSpec();
            subtotalsSpec = query.subtotalsSpec;
            postProcessingFn = query.postProcessingFn;
            context = query.getContext();
        }

        public Builder(Builder builder) {
            dataSource = builder.dataSource;
            querySegmentSpec = builder.querySegmentSpec;
            virtualColumns = builder.virtualColumns;
            dimFilter = builder.dimFilter;
            granularity = builder.granularity;
            dimensions = builder.dimensions;
            aggregatorSpecs = builder.aggregatorSpecs;
            postAggregatorSpecs = builder.postAggregatorSpecs;
            havingSpec = builder.havingSpec;
            limitSpec = builder.limitSpec;
            postProcessingFn = builder.postProcessingFn;
            limit = builder.limit;
            orderByColumnSpecs = new ArrayList<>(builder.orderByColumnSpecs);
            context = builder.context;
        }

        public Builder setDataSource(DataSource dataSource) {
            this.dataSource = dataSource;
            return this;
        }

        public Builder setDataSource(String dataSource) {
            this.dataSource = new TableDataSource(dataSource);
            return this;
        }

        public Builder setDataSource(Query query) {
            this.dataSource = new QueryDataSource(query);
            return this;
        }

        public Builder setInterval(QuerySegmentSpec interval) {
            return setQuerySegmentSpec(interval);
        }

        public Builder setInterval(List<Interval> intervals) {
            return setQuerySegmentSpec(new LegacySegmentSpec(intervals));
        }

        public Builder setInterval(Interval interval) {
            return setQuerySegmentSpec(new LegacySegmentSpec(interval));
        }

        public Builder setInterval(String interval) {
            return setQuerySegmentSpec(new LegacySegmentSpec(interval));
        }

        public Builder setVirtualColumns(VirtualColumns virtualColumns) {
            this.virtualColumns = Preconditions.checkNotNull(virtualColumns, "virtualColumns");
            return this;
        }

        public Builder setVirtualColumns(VirtualColumn... virtualColumns) {
            this.virtualColumns = VirtualColumns.create(Arrays.asList(virtualColumns));
            return this;
        }

        public Builder setLimit(int limit) {
            ensureExplicitLimitSpecNotSet();
            this.limit = limit;
            this.postProcessingFn = null;
            return this;
        }

        public Builder setSubtotalsSpec(List<List<String>> subtotalsSpec) {
            this.subtotalsSpec = subtotalsSpec;
            return this;
        }

        public Builder addOrderByColumn(String dimension) {
            return addOrderByColumn(dimension, null);
        }

        public Builder addOrderByColumn(String dimension, OrderByColumnSpec.Direction direction) {
            return addOrderByColumn(new OrderByColumnSpec(dimension, direction));
        }

        public Builder addOrderByColumn(OrderByColumnSpec columnSpec) {
            ensureExplicitLimitSpecNotSet();
            this.orderByColumnSpecs.add(columnSpec);
            this.postProcessingFn = null;
            return this;
        }

        public Builder setLimitSpec(LimitSpec limitSpec) {
            Preconditions.checkNotNull(limitSpec);
            ensureFluentLimitsNotSet();
            this.limitSpec = limitSpec;
            this.postProcessingFn = null;
            return this;
        }

        private void ensureExplicitLimitSpecNotSet() {
            if (limitSpec != null) {
                throw new ISE("Ambiguous build, limitSpec[%s] already set", limitSpec);
            }
        }

        private void ensureFluentLimitsNotSet() {
            if (!(limit == Integer.MAX_VALUE && orderByColumnSpecs.isEmpty())) {
                throw new ISE("Ambiguous build, limit[%s] or columnSpecs[%s] already set.", limit,
                        orderByColumnSpecs);
            }
        }

        public Builder setQuerySegmentSpec(QuerySegmentSpec querySegmentSpec) {
            this.querySegmentSpec = querySegmentSpec;
            return this;
        }

        public Builder setDimFilter(DimFilter dimFilter) {
            this.dimFilter = dimFilter;
            return this;
        }

        public Builder setGranularity(Granularity granularity) {
            this.granularity = granularity;
            return this;
        }

        public Builder addDimension(String column) {
            return addDimension(column, column);
        }

        public Builder addDimension(String column, String outputName) {
            return addDimension(new DefaultDimensionSpec(column, outputName));
        }

        public Builder addDimension(DimensionSpec dimension) {
            if (dimensions == null) {
                dimensions = Lists.newArrayList();
            }

            dimensions.add(dimension);
            this.postProcessingFn = null;
            return this;
        }

        public Builder setDimensions(List<DimensionSpec> dimensions) {
            this.dimensions = Lists.newArrayList(dimensions);
            this.postProcessingFn = null;
            return this;
        }

        public Builder setDimensions(DimensionSpec... dimensions) {
            this.dimensions = new ArrayList<>(Arrays.asList(dimensions));
            this.postProcessingFn = null;
            return this;
        }

        public Builder addAggregator(AggregatorFactory aggregator) {
            if (aggregatorSpecs == null) {
                aggregatorSpecs = Lists.newArrayList();
            }

            aggregatorSpecs.add(aggregator);
            this.postProcessingFn = null;
            return this;
        }

        public Builder setAggregatorSpecs(List<AggregatorFactory> aggregatorSpecs) {
            this.aggregatorSpecs = Lists.newArrayList(aggregatorSpecs);
            this.postProcessingFn = null;
            return this;
        }

        public Builder setAggregatorSpecs(AggregatorFactory... aggregatorSpecs) {
            this.aggregatorSpecs = new ArrayList<>(Arrays.asList(aggregatorSpecs));
            this.postProcessingFn = null;
            return this;
        }

        public Builder setPostAggregatorSpecs(List<PostAggregator> postAggregatorSpecs) {
            this.postAggregatorSpecs = Lists.newArrayList(postAggregatorSpecs);
            this.postProcessingFn = null;
            return this;
        }

        public Builder setContext(Map<String, Object> context) {
            this.context = context;
            return this;
        }

        public Builder overrideContext(Map<String, Object> contextOverride) {
            this.context = computeOverriddenContext(context, contextOverride);
            return this;
        }

        public Builder setHavingSpec(HavingSpec havingSpec) {
            this.havingSpec = havingSpec;
            this.postProcessingFn = null;
            return this;
        }

        public Builder copy() {
            return new Builder(this);
        }

        public GroupByQuery build() {
            final LimitSpec theLimitSpec;
            if (limitSpec == null) {
                if (orderByColumnSpecs.isEmpty() && limit == Integer.MAX_VALUE) {
                    theLimitSpec = NoopLimitSpec.instance();
                } else {
                    theLimitSpec = new DefaultLimitSpec(orderByColumnSpecs, limit);
                }
            } else {
                theLimitSpec = limitSpec;
            }

            return new GroupByQuery(dataSource, querySegmentSpec, virtualColumns, dimFilter, granularity,
                    dimensions, aggregatorSpecs, postAggregatorSpecs, havingSpec, theLimitSpec, subtotalsSpec,
                    postProcessingFn, context);
        }
    }

    @Override
    public String toString() {
        return "GroupByQuery{" + "dataSource='" + getDataSource() + '\'' + ", querySegmentSpec="
                + getQuerySegmentSpec() + ", virtualColumns=" + virtualColumns + ", limitSpec=" + limitSpec
                + ", dimFilter=" + dimFilter + ", granularity=" + getGranularity() + ", dimensions=" + dimensions
                + ", aggregatorSpecs=" + aggregatorSpecs + ", postAggregatorSpecs=" + postAggregatorSpecs
                + ", havingSpec=" + havingSpec + '}';
    }

    @Override
    public boolean equals(final Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        if (!super.equals(o)) {
            return false;
        }
        final GroupByQuery that = (GroupByQuery) o;
        return Objects.equals(virtualColumns, that.virtualColumns) && Objects.equals(limitSpec, that.limitSpec)
                && Objects.equals(havingSpec, that.havingSpec) && Objects.equals(dimFilter, that.dimFilter)
                && Objects.equals(dimensions, that.dimensions)
                && Objects.equals(aggregatorSpecs, that.aggregatorSpecs)
                && Objects.equals(postAggregatorSpecs, that.postAggregatorSpecs);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), virtualColumns, limitSpec, havingSpec, dimFilter, dimensions,
                aggregatorSpecs, postAggregatorSpecs);
    }
}