class GroupProjection extends Projection {

    private final List<Symbol> keys;
    private final List<Aggregation> values;
    private List<Symbol> outputs;

    private final AggregateMode mode;
    private final RowGranularity requiredGranularity;

    public GroupProjection(List<Symbol> keys,
                           List<Aggregation> values,
                           AggregateMode mode,
                           RowGranularity requiredGranularity) {
        assert keys.stream().noneMatch(s ->
            SymbolVisitors.any(Symbols.IS_COLUMN.or(x -> x instanceof SelectSymbol), s))
            : "Cannot operate on Reference, Field or SelectSymbol symbols: " + keys;
        assert values.stream().noneMatch(s ->
            SymbolVisitors.any(Symbols.IS_COLUMN.or(x -> x instanceof SelectSymbol), s))
            : "Cannot operate on Reference, Field or SelectSymbol symbols: " + values;
        this.keys = keys;
        this.values = values;
        this.mode = mode;
        this.requiredGranularity = requiredGranularity;
    }

    public GroupProjection(StreamInput in) throws IOException {
        mode = AggregateMode.readFrom(in);
        keys = Symbols.listFromStream(in);
        int size = in.readVInt();
        values = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            values.add((Aggregation) Symbols.fromStream(in));
        }
        requiredGranularity = RowGranularity.fromStream(in);
    }

    public List<Symbol> keys() {
        return keys;
    }

    public List<Aggregation> values() {
        return values;
    }

    @Override
    public ProjectionType projectionType() {
        return ProjectionType.GROUP;
    }

    @Override
    public <C, R> R accept(ProjectionVisitor<C, R> visitor, C context) {
        return visitor.visitGroupProjection(this, context);
    }

    /**
     * returns a list of outputs, with the group by keys going first,
     * and the aggregations coming last
     */
    @Override
    public List<? extends Symbol> outputs() {
        if (outputs == null) {
            outputs = new ArrayList<>(keys.size() + values.size());
            outputs.addAll(keys);
            outputs.addAll(values);
        }
        return outputs;
    }

    @Override
    public void writeTo(StreamOutput out) throws IOException {
        AggregateMode.writeTo(mode, out);
        Symbols.toStream(keys, out);
        Symbols.toStream(values, out);
        RowGranularity.toStream(requiredGranularity, out);
    }

    @Override
    public boolean equals(Object o) {<FILL_FUNCTION_BODY>}

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), keys, values, outputs, mode, requiredGranularity);
    }

    @Override
    public RowGranularity requiredGranularity() {
        return requiredGranularity;
    }

    public AggregateMode mode() {
        return mode;
    }

    @Override
    public Map<String, Object> mapRepresentation() {
        return MapBuilder.<String, Object>newMapBuilder()
            .put("type", "HashAggregation")
            .put("keys", Lists.joinOn(", ", keys, Symbol::toString))
            .put("aggregations", Lists.joinOn(", ", values, Symbol::toString))
            .map();
    }
}

        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        GroupProjection that = (GroupProjection) o;
        return Objects.equals(keys, that.keys) &&
               Objects.equals(values, that.values) &&
               Objects.equals(outputs, that.outputs) &&
               mode == that.mode &&
               requiredGranularity == that.requiredGranularity;
    
<methods>public non-sealed void <init>() ,public abstract R accept(ProjectionVisitor<C,R>, C) ,public abstract boolean equals(java.lang.Object) ,public static io.crate.execution.dsl.projection.Projection fromStream(org.elasticsearch.common.io.stream.StreamInput) throws java.io.IOException,public int hashCode() ,public Map<java.lang.String,java.lang.Object> mapRepresentation() ,public abstract List<? extends io.crate.expression.symbol.Symbol> outputs() ,public abstract io.crate.execution.dsl.projection.ProjectionType projectionType() ,public io.crate.metadata.RowGranularity requiredGranularity() ,public static void toStream(io.crate.execution.dsl.projection.Projection, org.elasticsearch.common.io.stream.StreamOutput) throws java.io.IOException,public abstract void writeTo(org.elasticsearch.common.io.stream.StreamOutput) throws java.io.IOException<variables>static final Predicate<io.crate.execution.dsl.projection.Projection> IS_NODE_PROJECTION,static final Predicate<io.crate.execution.dsl.projection.Projection> IS_SHARD_PROJECTION