diff --git a/foxtrot-common/src/main/java/com/flipkart/foxtrot/common/group/GroupRequest.java b/foxtrot-common/src/main/java/com/flipkart/foxtrot/common/group/GroupRequest.java index cd0545d23..fdd72c20a 100644 --- a/foxtrot-common/src/main/java/com/flipkart/foxtrot/common/group/GroupRequest.java +++ b/foxtrot-common/src/main/java/com/flipkart/foxtrot/common/group/GroupRequest.java @@ -20,6 +20,7 @@ import com.flipkart.foxtrot.common.Opcodes; import com.flipkart.foxtrot.common.enums.CountPrecision; import com.flipkart.foxtrot.common.query.Filter; +import com.flipkart.foxtrot.common.stats.Stat; import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.builder.ToStringBuilder; @@ -27,6 +28,7 @@ import javax.validation.constraints.NotNull; import java.util.List; +import java.util.Set; /** * User: Santanu Sinha (santanu.sinha@flipkart.com) @@ -41,8 +43,13 @@ public class GroupRequest extends ActionRequest { @NotEmpty private String table; + // Kept for backward compatibility private String uniqueCountOn; + private String aggregationField; + + private Stat aggregationType; + @NotNull @NotEmpty private List nesting; @@ -53,46 +60,29 @@ public GroupRequest() { super(Opcodes.GROUP); } - public GroupRequest(List filters, String table, String uniqueCountOn, List nesting) { + public GroupRequest(List filters, String table, String uniqueCountOn, + String aggregationField, Stat aggregationType, + List nesting, CountPrecision precision) { super(Opcodes.GROUP, filters); this.table = table; this.uniqueCountOn = uniqueCountOn; + this.aggregationField = aggregationField; + this.aggregationType = aggregationType; this.nesting = nesting; + this.precision = precision; } public T accept(ActionRequestVisitor visitor) { return visitor.visit(this); } - public String getTable() { - return table; - } - - public void setTable(String table) { - this.table = table; - } - - public String getUniqueCountOn() { - return uniqueCountOn; - } - - public void setUniqueCountOn(String uniqueCountOn) { - this.uniqueCountOn = uniqueCountOn; - } - - public List getNesting() { - return nesting; - } - - public void setNesting(List nesting) { - this.nesting = nesting; - } - @Override public String toString() { return new ToStringBuilder(this).appendSuper(super.toString()) .append("table", table) + .append("aggregationType", aggregationType) .append("uniqueCountOn", uniqueCountOn) + .append("aggregationField", aggregationField) .append("nesting", nesting) .toString(); } diff --git a/foxtrot-core/src/main/java/com/flipkart/foxtrot/core/querystore/actions/GroupAction.java b/foxtrot-core/src/main/java/com/flipkart/foxtrot/core/querystore/actions/GroupAction.java index 3eb9abf8e..c522f703d 100644 --- a/foxtrot-core/src/main/java/com/flipkart/foxtrot/core/querystore/actions/GroupAction.java +++ b/foxtrot-core/src/main/java/com/flipkart/foxtrot/core/querystore/actions/GroupAction.java @@ -31,6 +31,7 @@ import com.flipkart.foxtrot.common.query.general.NotInFilter; import com.flipkart.foxtrot.common.query.numeric.*; import com.flipkart.foxtrot.common.query.string.ContainsFilter; +import com.flipkart.foxtrot.common.stats.Stat; import com.flipkart.foxtrot.common.util.CollectionUtils; import com.flipkart.foxtrot.common.visitor.CountPrecisionThresholdVisitorAdapter; import com.flipkart.foxtrot.core.common.Action; @@ -44,18 +45,22 @@ import com.flipkart.foxtrot.core.querystore.impl.ElasticsearchUtils; import com.flipkart.foxtrot.core.table.TableMetadataManager; import com.flipkart.foxtrot.core.util.ElasticsearchQueryUtils; +import com.google.common.base.Strings; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.RequestOptions; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.search.aggregations.AbstractAggregationBuilder; +import org.elasticsearch.search.aggregations.AggregationBuilder; import org.elasticsearch.search.aggregations.Aggregations; import org.elasticsearch.search.aggregations.bucket.terms.Terms; import org.elasticsearch.search.aggregations.metrics.cardinality.Cardinality; import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.aggregations.metrics.cardinality.CardinalityAggregationBuilder; import org.joda.time.Interval; import java.io.IOException; @@ -65,6 +70,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; +import static com.flipkart.foxtrot.core.querystore.actions.Utils.statsString; import static com.flipkart.foxtrot.core.util.ElasticsearchQueryUtils.QUERY_SIZE; /** @@ -111,6 +117,15 @@ public String getRequestCacheKey() { .hashCode(); } + if (null != query.getAggregationField()) { + filterHashKey += 31 * query.getAggregationField() + .hashCode(); + } + + if(null != query.getAggregationType()){ + filterHashKey += 31 * query.getAggregationType().hashCode(); + } + for (int i = 0; i < query.getNesting() .size(); i++) { filterHashKey += 31 * query.getNesting() @@ -140,7 +155,12 @@ public void validateImpl(GroupRequest parameter) { if (parameter.getUniqueCountOn() != null && parameter.getUniqueCountOn() .isEmpty()) { - validationErrors.add("unique field cannot be empty (can be null)"); + validationErrors.add("uniqueCountOn cannot be empty (can be null)"); + } + + if (parameter.getAggregationField() != null && parameter.getAggregationField() + .isEmpty()) { + validationErrors.add("aggregation field cannot be empty (can be null)"); } validateCardinality(parameter); @@ -158,7 +178,7 @@ public ActionResponse execute(GroupRequest parameter) { try { SearchResponse response = getConnection() .getClient() - .search(query); + .search(query, RequestOptions.DEFAULT); return getResponse(response, parameter); } catch (IOException e) { @@ -713,18 +733,42 @@ private Long getValidCount(Long count) { : count; } - private AbstractAggregationBuilder buildAggregation(GroupRequest parameter) { + private AbstractAggregationBuilder buildAggregation(GroupRequest groupRequest) { return Utils.buildTermsAggregation(getParameter().getNesting() .stream() .map(x -> new ResultSort(x, ResultSort.Order.asc)) - .collect(Collectors.toList()), - !CollectionUtils.isNullOrEmpty(getParameter().getUniqueCountOn()) - ? Sets.newHashSet( - Utils.buildCardinalityAggregation(getParameter().getUniqueCountOn(), - parameter.accept(new CountPrecisionThresholdVisitorAdapter( - elasticsearchTuningConfig.getPrecisionThreshold())))) - : Sets.newHashSet(), elasticsearchTuningConfig.getAggregationSize()); + .collect(Collectors.toList()),buildSubAggregation(getParameter()), + elasticsearchTuningConfig.getAggregationSize()); + + } + + private Set buildSubAggregation(GroupRequest groupRequest) { + // Keep this for backward compatibility to support uniqueCountOn attribute coming from old requests + if(!Strings.isNullOrEmpty(groupRequest.getUniqueCountOn())){ + return Sets.newHashSet(buildCardinalityAggregation(groupRequest.getUniqueCountOn(), groupRequest)); + } + + if(Strings.isNullOrEmpty(groupRequest.getAggregationField())){ + return Sets.newHashSet(); + } + boolean isNumericField = Utils.isNumericField(getTableMetadataManager(), groupRequest.getTable(), + groupRequest.getAggregationField()); + final AbstractAggregationBuilder groupAggStats; + if (isNumericField) { + groupAggStats = Utils.buildStatsAggregation(groupRequest.getAggregationField(), + Collections.singleton(groupRequest.getAggregationType())); + } else { + groupAggStats = buildCardinalityAggregation(groupRequest.getAggregationField(), groupRequest); + } + return Sets.newHashSet(groupAggStats); + } + + private CardinalityAggregationBuilder buildCardinalityAggregation(String aggregationField, + GroupRequest groupRequest) { + return Utils.buildCardinalityAggregation(aggregationField, + groupRequest.accept(new CountPrecisionThresholdVisitorAdapter( + elasticsearchTuningConfig.getPrecisionThreshold()))); } private Map getMap(List fields, Aggregations aggregations) { @@ -736,12 +780,17 @@ private Map getMap(List fields, Aggregations aggregation Map levelCount = Maps.newHashMap(); for (Terms.Bucket bucket : terms.getBuckets()) { if (fields.size() == 1) { - if (!CollectionUtils.isNullOrEmpty(getParameter().getUniqueCountOn())) { + if (!Strings.isNullOrEmpty(getParameter().getUniqueCountOn())) { String key = Utils.sanitizeFieldForAggregation(getParameter().getUniqueCountOn()); Cardinality cardinality = bucket.getAggregations() .get(key); levelCount.put(String.valueOf(bucket.getKey()), cardinality.getValue()); } + else if (!Strings.isNullOrEmpty(getParameter().getAggregationField())) { + String metricKey = Utils.getExtendedStatsAggregationKey(getParameter().getAggregationField()); + levelCount.put(String.valueOf(bucket.getKey()), Utils.toStats( + bucket.getAggregations().get(metricKey)).get(statsString(getParameter().getAggregationType()))); + } else { levelCount.put(String.valueOf(bucket.getKey()), bucket.getDocCount()); } diff --git a/foxtrot-core/src/main/java/com/flipkart/foxtrot/core/querystore/actions/Utils.java b/foxtrot-core/src/main/java/com/flipkart/foxtrot/core/querystore/actions/Utils.java index 78f745dc9..b62b686d5 100644 --- a/foxtrot-core/src/main/java/com/flipkart/foxtrot/core/querystore/actions/Utils.java +++ b/foxtrot-core/src/main/java/com/flipkart/foxtrot/core/querystore/actions/Utils.java @@ -8,6 +8,7 @@ import com.flipkart.foxtrot.common.query.Filter; import com.flipkart.foxtrot.common.query.ResultSort; import com.flipkart.foxtrot.common.stats.Stat; +import com.flipkart.foxtrot.common.stats.Stat.StatVisitor; import com.flipkart.foxtrot.common.util.CollectionUtils; import com.flipkart.foxtrot.core.exception.FoxtrotExceptions; import com.flipkart.foxtrot.core.querystore.impl.ElasticsearchUtils; @@ -44,14 +45,14 @@ public class Utils { private static final double[] DEFAULT_PERCENTILES = {1d, 5d, 25, 50d, 75d, 95d, 99d}; private static final double DEFAULT_COMPRESSION = 100.0; private static final int PRECISION_THRESHOLD = 500; - private static final String COUNT = "count"; - private static final String AVG = "avg"; - private static final String SUM = "sum"; - private static final String MIN = "min"; - private static final String MAX = "max"; - private static final String SUM_OF_SQUARES = "sum_of_squares"; - private static final String VARIANCE = "variance"; - private static final String STD_DEVIATION = "std_deviation"; + public static final String COUNT = "count"; + public static final String AVG = "avg"; + public static final String SUM = "sum"; + public static final String MIN = "min"; + public static final String MAX = "max"; + public static final String SUM_OF_SQUARES = "sum_of_squares"; + public static final String VARIANCE = "variance"; + public static final String STD_DEVIATION = "std_deviation"; private static final EnumSet NUMERIC_FIELD_TYPES = EnumSet.of(FieldType.INTEGER, FieldType.LONG, FieldType.FLOAT, FieldType.DOUBLE); @@ -355,9 +356,54 @@ public static boolean isNumericField(TableMetadataManager tableMetadataManager, } public static boolean hasTemporalFilters(List filters) { - if(null == filters) { + if (null == filters) { return false; } return filters.stream().anyMatch(Filter::isFilterTemporal); } + + public static String statsString(Stat aggregationType) { + return aggregationType + .visit(new StatVisitor() { + @Override + public String visitCount() { + return Utils.COUNT; + } + + @Override + public String visitMin() { + return Utils.MIN; + } + + @Override + public String visitMax() { + return Utils.MAX; + } + + @Override + public String visitAvg() { + return Utils.AVG; + } + + @Override + public String visitSum() { + return Utils.SUM; + } + + @Override + public String visitSumOfSquares() { + return Utils.SUM_OF_SQUARES; + } + + @Override + public String visitVariance() { + return Utils.VARIANCE; + } + + @Override + public String visitStdDeviation() { + return Utils.STD_DEVIATION; + } + }); + } } diff --git a/foxtrot-core/src/test/java/com/flipkart/foxtrot/core/alerts/EmailBuilderTest.java b/foxtrot-core/src/test/java/com/flipkart/foxtrot/core/alerts/EmailBuilderTest.java index 47bee0453..fc9029742 100644 --- a/foxtrot-core/src/test/java/com/flipkart/foxtrot/core/alerts/EmailBuilderTest.java +++ b/foxtrot-core/src/test/java/com/flipkart/foxtrot/core/alerts/EmailBuilderTest.java @@ -39,7 +39,7 @@ public void testCardinalityEmailBuild() throws JsonProcessingException { Assert.assertEquals("Blocked query as it might have screwed up the cluster", email.getSubject()); Assert.assertEquals( "Blocked Query: {\"opcode\":\"group\",\"filters\":[],\"bypassCache\":false,\"table\":\"test-table\"," + - "\"uniqueCountOn\":null,\"nesting\":[\"os\",\"deviceId\"],\"precision\":null}\n" + + "\"uniqueCountOn\":null,\"aggregationField\":null,\"aggregationType\":null,\"nesting\":[\"os\",\"deviceId\"],\"precision\":null}\n" + "Suspect field: deviceId\n" + "Probability of screwing up the cluster: 0.75", email.getContent()); diff --git a/foxtrot-core/src/test/java/com/flipkart/foxtrot/core/querystore/actions/GroupActionTest.java b/foxtrot-core/src/test/java/com/flipkart/foxtrot/core/querystore/actions/GroupActionTest.java index 5b1fd8f3a..b095179c9 100644 --- a/foxtrot-core/src/test/java/com/flipkart/foxtrot/core/querystore/actions/GroupActionTest.java +++ b/foxtrot-core/src/test/java/com/flipkart/foxtrot/core/querystore/actions/GroupActionTest.java @@ -15,6 +15,10 @@ */ package com.flipkart.foxtrot.core.querystore.actions; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doReturn; + import com.fasterxml.jackson.core.JsonProcessingException; import com.flipkart.foxtrot.common.Document; import com.flipkart.foxtrot.common.group.GroupRequest; @@ -22,6 +26,7 @@ import com.flipkart.foxtrot.common.query.Filter; import com.flipkart.foxtrot.common.query.general.EqualsFilter; import com.flipkart.foxtrot.common.query.numeric.GreaterThanFilter; +import com.flipkart.foxtrot.common.stats.Stat; import com.flipkart.foxtrot.core.TestUtils; import com.flipkart.foxtrot.core.exception.ErrorCode; import com.flipkart.foxtrot.core.exception.FoxtrotException; @@ -29,16 +34,15 @@ import com.google.common.collect.Maps; import org.elasticsearch.action.admin.indices.refresh.RefreshRequest; import org.elasticsearch.client.RequestOptions; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; -import java.util.*; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.Mockito.doReturn; - /** * Created by rishabh.goyal on 28/04/14. */ @@ -52,14 +56,14 @@ public static void setUp() throws Exception { .indices() .refresh(new RefreshRequest("*"), RequestOptions.DEFAULT); getTableMetadataManager().getFieldMappings(TestUtils.TEST_TABLE_NAME, true, true); - ((ElasticsearchQueryStore)getQueryStore()).getCardinalityConfig() + ((ElasticsearchQueryStore) getQueryStore()).getCardinalityConfig() .setMaxCardinality(15000); getTableMetadataManager().updateEstimationData(TestUtils.TEST_TABLE_NAME, 1397658117000L); } @Ignore @Test - public void testGroupActionSingleQueryException() throws FoxtrotException, JsonProcessingException { + public void testGroupActionSingleQueryException() throws FoxtrotException { GroupRequest groupRequest = new GroupRequest(); groupRequest.setTable(TestUtils.TEST_TABLE_NAME); groupRequest.setNesting(Collections.singletonList("os")); @@ -84,7 +88,7 @@ public void testGroupActionSingleFieldNoFilter() throws FoxtrotException, JsonPr response.put("android", 7L); response.put("ios", 4L); - GroupResponse actualResult = GroupResponse.class.cast(getQueryExecutor().execute(groupRequest)); + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); assertEquals(response, actualResult.getResult()); } @@ -107,7 +111,7 @@ public void testGroupActionSingleFieldEmptyFieldNoFilter() throws FoxtrotExcepti public void testGroupActionSingleFieldSpecialCharactersNoFilter() throws FoxtrotException, JsonProcessingException { GroupRequest groupRequest = new GroupRequest(); groupRequest.setTable(TestUtils.TEST_TABLE_NAME); - groupRequest.setNesting(Arrays.asList("")); + groupRequest.setNesting(Collections.singletonList("")); try { getQueryExecutor().execute(groupRequest); @@ -119,7 +123,8 @@ public void testGroupActionSingleFieldSpecialCharactersNoFilter() throws Foxtrot } @Test - public void testGroupActionSingleFieldHavingSpecialCharactersWithFilter() throws FoxtrotException, JsonProcessingException { + public void testGroupActionSingleFieldHavingSpecialCharactersWithFilter() + throws FoxtrotException, JsonProcessingException { GroupRequest groupRequest = new GroupRequest(); groupRequest.setTable(TestUtils.TEST_TABLE_NAME); @@ -127,11 +132,11 @@ public void testGroupActionSingleFieldHavingSpecialCharactersWithFilter() throws equalsFilter.setField("device"); equalsFilter.setValue("nexus"); groupRequest.setFilters(Collections.singletonList(equalsFilter)); - groupRequest.setNesting(Arrays.asList("!@#$%^&*()")); + groupRequest.setNesting(Collections.singletonList("!@#$%^&*()")); Map response = Maps.newHashMap(); - GroupResponse actualResult = GroupResponse.class.cast(getQueryExecutor().execute(groupRequest)); + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); assertEquals(response, actualResult.getResult()); } @@ -144,13 +149,13 @@ public void testGroupActionSingleFieldWithFilter() throws FoxtrotException, Json equalsFilter.setField("device"); equalsFilter.setValue("nexus"); groupRequest.setFilters(Collections.singletonList(equalsFilter)); - groupRequest.setNesting(Arrays.asList("os")); + groupRequest.setNesting(Collections.singletonList("os")); Map response = Maps.newHashMap(); response.put("android", 5L); response.put("ios", 1L); - GroupResponse actualResult = GroupResponse.class.cast(getQueryExecutor().execute(groupRequest)); + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); assertEquals(response, actualResult.getResult()); } @@ -171,7 +176,7 @@ public void testGroupActionTwoFieldsNoFilter() throws FoxtrotException, JsonProc put("iphone", 1L); }}); - GroupResponse actualResult = GroupResponse.class.cast(getQueryExecutor().execute(groupRequest)); + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); assertEquals(response, actualResult.getResult()); } @@ -195,7 +200,7 @@ public void testGroupActionTwoFieldsWithFilter() throws FoxtrotException, JsonPr put("ipad", 1L); }}); - GroupResponse actualResult = GroupResponse.class.cast(getQueryExecutor().execute(groupRequest)); + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); assertEquals(response, actualResult.getResult()); } @@ -236,8 +241,7 @@ public void testGroupActionMultipleFieldsNoFilter() throws FoxtrotException, Jso put("iphone", iPhoneResponse); }}); - - GroupResponse actualResult = GroupResponse.class.cast(getQueryExecutor().execute(groupRequest)); + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); assertEquals(response, actualResult.getResult()); } @@ -274,7 +278,180 @@ public void testGroupActionMultipleFieldsWithFilter() throws FoxtrotException, J put("ipad", iPadResponse); }}); - GroupResponse actualResult = GroupResponse.class.cast(getQueryExecutor().execute(groupRequest)); + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); assertEquals(response, actualResult.getResult()); } + + @Test + @SuppressWarnings("unchecked") + public void testGroupActionDistinctCountAggregation() { + GroupRequest groupRequest = new GroupRequest(); + groupRequest.setTable(TestUtils.TEST_TABLE_NAME); + groupRequest.setNesting(Arrays.asList("os", "version")); + groupRequest.setUniqueCountOn("device"); + + Map response = Maps.newHashMap(); + response.put("android", new HashMap() {{ + put("1", 1L); + put("2", 2L); + put("3", 2L); + }}); + response.put("ios", new HashMap() {{ + put("1", 1L); + put("2", 2L); + }}); + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); + assertEquals(((Map) response.get("android")).get("1"), + ((Map) actualResult.getResult() + .get("android")).get("1")); + assertEquals(((Map) response.get("android")).get("2"), + ((Map) actualResult.getResult() + .get("android")).get("2")); + assertEquals(((Map) response.get("android")).get("3"), + ((Map) actualResult.getResult() + .get("android")).get("3")); + assertEquals(((Map) response.get("ios")).get("1"), + ((Map) actualResult.getResult() + .get("ios")).get("1")); + assertEquals(((Map) response.get("ios")).get("2"), + ((Map) actualResult.getResult() + .get("ios")).get("2")); + } + + @Test + @SuppressWarnings("unchecked") + public void testGroupActionMaxAggregation() { + GroupRequest groupRequest = new GroupRequest(); + groupRequest.setTable(TestUtils.TEST_TABLE_NAME); + groupRequest.setNesting(Arrays.asList("os", "version")); + groupRequest.setAggregationField("battery"); + groupRequest.setAggregationType(Stat.MAX); + + Map response = Maps.newHashMap(); + response.put("android", new HashMap() {{ + put("1", 48.0); + put("2", 99.0); + put("3",87.0); + }}); + response.put("ios", new HashMap() {{ + put("1", 24.0); + put("2",56.0); + }}); + + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); + assertEquals(((Map) response.get("android")).get("1"), ((Map) actualResult.getResult() + .get("android")).get("1")); + assertEquals(((Map) response.get("android")).get("2"), ((Map) actualResult.getResult() + .get("android")).get("2")); + assertEquals(((Map) response.get("android")).get("3"), ((Map) actualResult.getResult() + .get("android")).get("3")); + assertEquals(((Map) response.get("ios")).get("1"), ((Map) actualResult.getResult() + .get("ios")).get("1")); + assertEquals(((Map) response.get("ios")).get("2"), ((Map) actualResult.getResult() + .get("ios")).get("2")); + } + + @Test + @SuppressWarnings("unchecked") + public void testGroupActionAvgAggregation() { + GroupRequest groupRequest = new GroupRequest(); + groupRequest.setTable(TestUtils.TEST_TABLE_NAME); + groupRequest.setNesting(Arrays.asList("os", "version")); + groupRequest.setAggregationField("battery"); + groupRequest.setAggregationType(Stat.AVG); + + Map response = Maps.newHashMap(); + response.put("android", new HashMap() {{ + put("1", 36.0); + put("2", 84.33333333333333); + put("3", 80.5); + }}); + response.put("ios", new HashMap() {{ + put("1", 24.0); + put("2", 45.0); + }}); + + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); + assertEquals(((Map) response.get("android")).get("1"), + ((Map) actualResult.getResult() + .get("android")).get("1")); + assertEquals(((Map) response.get("android")).get("2"), + ((Map) actualResult.getResult() + .get("android")).get("2")); + assertEquals(((Map) response.get("android")).get("3"), + ((Map) actualResult.getResult() + .get("android")).get("3")); + assertEquals(((Map) response.get("ios")).get("1"), + ((Map) actualResult.getResult() + .get("ios")).get("1")); + assertEquals(((Map) response.get("ios")).get("2"), ((Map) actualResult.getResult() + .get("ios")).get("2")); + } + + @Test + @SuppressWarnings("unchecked") + public void testGroupActionSumAggregation() { + GroupRequest groupRequest = new GroupRequest(); + groupRequest.setTable(TestUtils.TEST_TABLE_NAME); + groupRequest.setNesting(Arrays.asList("os", "version")); + groupRequest.setAggregationField("battery"); + groupRequest.setAggregationType(Stat.SUM); + + Map response = Maps.newHashMap(); + response.put("android", new HashMap() {{ + put("1",72.0); + put("2", 253.0); + put("3",161.0); + }}); + response.put("ios", new HashMap() {{ + put("1", 24.0); + put("2", 135.0); + }}); + + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); + assertEquals(((Map) response.get("android")).get("1"), ((Map) actualResult.getResult() + .get("android")).get("1")); + assertEquals(((Map) response.get("android")).get("2"), ((Map) actualResult.getResult() + .get("android")).get("2")); + assertEquals(((Map) response.get("android")).get("3"), ((Map) actualResult.getResult() + .get("android")).get("3")); + assertEquals(((Map) response.get("ios")).get("1"), ((Map) actualResult.getResult() + .get("ios")).get("1")); + assertEquals(((Map) response.get("ios")).get("2"), ((Map) actualResult.getResult() + .get("ios")).get("2")); + } + + @Test + @SuppressWarnings("unchecked") + public void testGroupActionCountAggregation() { + GroupRequest groupRequest = new GroupRequest(); + groupRequest.setTable(TestUtils.TEST_TABLE_NAME); + groupRequest.setNesting(Arrays.asList("os", "version")); + groupRequest.setAggregationField("battery"); + groupRequest.setAggregationType(Stat.COUNT); + + Map response = Maps.newHashMap(); + response.put("android", new HashMap() {{ + put("1", 2L); + put("2",3L); + put("3", 2L); + }}); + response.put("ios", new HashMap() {{ + put("1", 1L); + put("2", 3L); + }}); + + GroupResponse actualResult = (GroupResponse) getQueryExecutor().execute(groupRequest); + assertEquals(((Map) response.get("android")).get("1"), ((Map) actualResult.getResult() + .get("android")).get("1")); + assertEquals(((Map) response.get("android")).get("2"), ((Map) actualResult.getResult() + .get("android")).get("2")); + assertEquals(((Map) response.get("android")).get("3"), ((Map) actualResult.getResult() + .get("android")).get("3")); + assertEquals(((Map) response.get("ios")).get("1"), ((Map) actualResult.getResult() + .get("ios")).get("1")); + assertEquals(((Map) response.get("ios")).get("2"), ((Map) actualResult.getResult() + .get("ios")).get("2")); + } + } diff --git a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/FqlQueryType.java b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/FqlQueryType.java index c82de948b..2913d346b 100644 --- a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/FqlQueryType.java +++ b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/FqlQueryType.java @@ -10,5 +10,9 @@ public enum FqlQueryType { DESC, SHOWTABLES, COUNT, + SUM, + MAX, + MIN, + AVG, DISTINCT } diff --git a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/QueryTranslator.java b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/QueryTranslator.java index b967a8f34..e79b44cdb 100644 --- a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/QueryTranslator.java +++ b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/QueryTranslator.java @@ -14,10 +14,13 @@ import com.flipkart.foxtrot.common.query.general.*; import com.flipkart.foxtrot.common.query.numeric.*; import com.flipkart.foxtrot.common.query.string.ContainsFilter; +import com.flipkart.foxtrot.common.stats.AnalyticsRequestFlags; +import com.flipkart.foxtrot.common.stats.Stat; import com.flipkart.foxtrot.common.stats.StatsRequest; import com.flipkart.foxtrot.common.stats.StatsTrendRequest; import com.flipkart.foxtrot.common.trend.TrendRequest; import com.flipkart.foxtrot.core.exception.FqlParsingException; +import com.flipkart.foxtrot.sql.constants.FqlFunctionType; import com.flipkart.foxtrot.sql.extendedsql.ExtendedSqlStatement; import com.flipkart.foxtrot.sql.extendedsql.desc.Describe; import com.flipkart.foxtrot.sql.extendedsql.showtables.ShowTables; @@ -26,6 +29,7 @@ import com.flipkart.foxtrot.sql.query.FqlShowTablesQuery; import com.flipkart.foxtrot.sql.util.QueryUtils; import com.google.common.collect.Lists; +import com.google.common.collect.Sets; import io.dropwizard.util.Duration; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.*; @@ -42,8 +46,10 @@ import java.io.StringReader; import java.util.List; +import java.util.Set; + +import static com.flipkart.foxtrot.sql.constants.Constants.*; -import static com.flipkart.foxtrot.sql.Constants.*; public class QueryTranslator extends SqlElementVisitor { private static final Logger logger = LoggerFactory.getLogger(QueryTranslator.class.getSimpleName()); @@ -81,14 +87,14 @@ public void visit(PlainSelect plainSelect) { .accept(this); //Populate table name List groupByItems = plainSelect.getGroupByColumnReferences(); if(null != groupByItems) { - for(Expression groupByItem : CollectionUtils.nullSafeList(groupByItems)) { - queryType = FqlQueryType.GROUP; - if(groupByItem instanceof Column) { - Column column = (Column)groupByItem; - groupBycolumnsList.add(column.getFullyQualifiedName().replaceAll(REGEX, REPLACEMENT)); + for (Expression groupByItem : CollectionUtils.nullSafeList(groupByItems)) { + queryType = FqlQueryType.GROUP; + if (groupByItem instanceof Column) { + Column column = (Column) groupByItem; + groupBycolumnsList.add(column.getFullyQualifiedName().replaceAll(REGEX, REPLACEMENT)); + } } } - } if(FqlQueryType.SELECT == queryType) { List orderByElements = plainSelect.getOrderByElements(); resultSort = generateResultSort(orderByElements); @@ -202,7 +208,10 @@ public FqlQuery translate(String sql) { case STATSTREND: request = createStatsTrendActionRequest(); break; - + case SUM: + case AVG: + case MIN: + case MAX: case STATS: request = createStatsActionRequest(); break; @@ -246,7 +255,7 @@ private ActionRequest createGroupActionRequest() { group.setTable(tableName); group.setNesting(groupBycolumnsList); group.setFilters(filters); - setUniqueCountOn(group); + setGroupAggregation(group); return group; } @@ -307,13 +316,19 @@ private ResultSort generateResultSort(List orderByElements) { return resultSortColumn; } - private void setUniqueCountOn(GroupRequest group) { + private void setGroupAggregation(GroupRequest group) { if(calledAction instanceof CountRequest) { CountRequest countRequest = (CountRequest)this.calledAction; - boolean distinct = countRequest.isDistinct(); - if(distinct) { + if(countRequest.isDistinct()) { group.setUniqueCountOn(countRequest.getField()); + } else { + group.setAggregationType(Stat.COUNT); + group.setAggregationField(countRequest.getField()); } + } else if (calledAction instanceof StatsRequest){ + StatsRequest statsRequest = (StatsRequest) this.calledAction; + group.setAggregationType(statsRequest.getStats().stream().findFirst().orElse(Stat.COUNT)); + group.setAggregationField(statsRequest.getField()); } } @@ -361,7 +376,25 @@ public void visit(SelectExpressionItem selectExpressionItem) { actionRequest = parseHistogramRequest(function.getParameters()); break; case COUNT: - actionRequest = parseCountRequest(function.getParameters(), function.isAllColumns(), function.isDistinct()); + actionRequest = parseCountRequest(function.getParameters(), + function.isAllColumns(), + function.isDistinct()); + break; + case SUM: + actionRequest = parseStatsFunction(function.getParameters() + .getExpressions(), Sets.newHashSet(Stat.SUM)); + break; + case MIN: + actionRequest = parseStatsFunction(function.getParameters() + .getExpressions(), Sets.newHashSet(Stat.MIN)); + break; + case MAX: + actionRequest = parseStatsFunction(function.getParameters() + .getExpressions(), Sets.newHashSet(Stat.MAX)); + break; + case AVG: + actionRequest = parseStatsFunction(function.getParameters() + .getExpressions(), Sets.newHashSet(Stat.AVG)); break; case DESC: case SELECT: @@ -381,21 +414,33 @@ public void visit(SelectExpressionItem selectExpressionItem) { } private FqlQueryType getType(String function) { - if(function.equalsIgnoreCase("trend")) { + if(function.equalsIgnoreCase(FqlFunctionType.TREND)){ return FqlQueryType.TREND; } - if(function.equalsIgnoreCase("statstrend")) { + if(function.equalsIgnoreCase(FqlFunctionType.STATSTREND)) { return FqlQueryType.STATSTREND; } - if(function.equalsIgnoreCase("stats")) { + if(function.equalsIgnoreCase(FqlFunctionType.STATS)) { return FqlQueryType.STATS; } - if(function.equalsIgnoreCase("histogram")) { + if(function.equalsIgnoreCase(FqlFunctionType.HISTOGRAM)) { return FqlQueryType.HISTOGRAM; } - if(function.equalsIgnoreCase("count")) { + if(function.equalsIgnoreCase(FqlFunctionType.COUNT)) { return FqlQueryType.COUNT; } + if(function.equalsIgnoreCase(FqlFunctionType.SUM)) { + return FqlQueryType.SUM; + } + if(function.equalsIgnoreCase(FqlFunctionType.AVG)) { + return FqlQueryType.AVG; + } + if(function.equalsIgnoreCase(FqlFunctionType.MIN)) { + return FqlQueryType.MIN; + } + if(function.equalsIgnoreCase(FqlFunctionType.MAX)) { + return FqlQueryType.MAX; + } return FqlQueryType.SELECT; } @@ -428,6 +473,16 @@ private StatsTrendRequest parseStatsTrendFunction(List expressions) { return statsTrendRequest; } + /* + When asked for specific stats then add those stats and skip percentiles to save on execution time + */ + private StatsRequest parseStatsFunction(List expressions, Set stats) { + StatsRequest statsRequest = parseStatsFunction(expressions); + statsRequest.setStats(stats); + statsRequest.setFlags(Sets.newHashSet(AnalyticsRequestFlags.STATS_SKIP_PERCENTILES)); + return statsRequest; + } + private StatsRequest parseStatsFunction(List expressions) { if(expressions == null || expressions.isEmpty() || expressions.size() > 1) { throw new FqlParsingException("stats function has following format: stats(fieldname)"); diff --git a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/Constants.java b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/constants/Constants.java similarity index 76% rename from foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/Constants.java rename to foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/constants/Constants.java index a27c2454d..d26ee8e32 100644 --- a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/Constants.java +++ b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/constants/Constants.java @@ -1,13 +1,14 @@ -package com.flipkart.foxtrot.sql; +package com.flipkart.foxtrot.sql.constants; + +import lombok.experimental.UtilityClass; /** * Created by rishabh.goyal on 17/11/14. */ +@UtilityClass public class Constants { - public static final String SQL_TABLE_REGEX = "[^a-zA-Z0-9\\-_]"; public static final String SQL_FIELD_REGEX = "[^a-zA-Z0-9.\\-_]"; public static final String REGEX = "^\"+|\"+$"; public static final String REPLACEMENT = ""; - private Constants() {} } diff --git a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/constants/FqlFunctionType.java b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/constants/FqlFunctionType.java new file mode 100644 index 000000000..b042eefbd --- /dev/null +++ b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/constants/FqlFunctionType.java @@ -0,0 +1,17 @@ +package com.flipkart.foxtrot.sql.constants; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class FqlFunctionType { + public static final String TREND = "trend"; + public static final String STATSTREND = "statstrend"; + public static final String STATS = "stats"; + public static final String HISTOGRAM = "histogram"; + public static final String COUNT = "count"; + public static final String SUM = "sum"; + public static final String AVG = "avg"; + public static final String MIN = "min"; + public static final String MAX = "max"; + +} diff --git a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/responseprocessors/Flattener.java b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/responseprocessors/Flattener.java index b5d18ae5b..2c0d384e4 100644 --- a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/responseprocessors/Flattener.java +++ b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/responseprocessors/Flattener.java @@ -17,6 +17,7 @@ import com.flipkart.foxtrot.common.stats.StatsTrendResponse; import com.flipkart.foxtrot.common.trend.TrendResponse; import com.flipkart.foxtrot.core.exception.FqlParsingException; +import com.flipkart.foxtrot.core.querystore.actions.Utils; import com.flipkart.foxtrot.sql.responseprocessors.model.FieldHeader; import com.flipkart.foxtrot.sql.responseprocessors.model.FlatRepresentation; import com.flipkart.foxtrot.sql.responseprocessors.model.MetaData; @@ -27,6 +28,7 @@ import java.util.stream.Collectors; import static com.flipkart.foxtrot.common.Opcodes.COUNT; +import static com.flipkart.foxtrot.core.querystore.actions.Utils.statsString; import static com.flipkart.foxtrot.sql.responseprocessors.FlatteningUtils.generateFieldMappings; import static com.flipkart.foxtrot.sql.responseprocessors.FlatteningUtils.genericParse; @@ -49,6 +51,9 @@ public void visit(GroupResponse groupResponse) { Map fieldNames = Maps.newTreeMap(); Map dataFields = generateFieldMappings(null, objectMapper.valueToTree(groupResponse.getResult()), separator); GroupRequest groupRequest = (GroupRequest)request; + + String statsHeader = getStatsHeader(groupRequest); + List> rows = Lists.newArrayList(); for(Map.Entry groupData : dataFields.entrySet()) { String[] values = groupData.getKey() @@ -64,19 +69,28 @@ public void visit(GroupResponse groupResponse) { } fieldNames.put(fieldName, lengthMax(fieldNames.get(fieldName), values[i])); } - row.put(COUNT, groupData.getValue() + row.put(statsHeader, groupData.getValue() .getData()); rows.add(row); } - fieldNames.put(COUNT, 10); + fieldNames.put(statsHeader, 10); List headers = Lists.newArrayList(); for(String fieldName : groupRequest.getNesting()) { headers.add(new FieldHeader(fieldName, fieldNames.get(fieldName))); } - headers.add(new FieldHeader(COUNT, 10)); + headers.add(new FieldHeader(statsHeader, 10)); flatRepresentation = new FlatRepresentation("group", headers, rows); } + private String getStatsHeader(GroupRequest groupRequest) { + String statsHeader = Utils.COUNT; + if(Objects.nonNull(groupRequest.getAggregationType())){ + statsHeader = statsString(groupRequest.getAggregationType()); + } + return statsHeader; + } + + @Override public void visit(HistogramResponse histogramResponse) { List> rows = Lists.newArrayList(); diff --git a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/util/QueryUtils.java b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/util/QueryUtils.java index f541fa494..c7eba47f2 100644 --- a/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/util/QueryUtils.java +++ b/foxtrot-sql/src/main/java/com/flipkart/foxtrot/sql/util/QueryUtils.java @@ -1,22 +1,24 @@ package com.flipkart.foxtrot.sql.util; -import com.flipkart.foxtrot.sql.Constants; import net.sf.jsqlparser.expression.DoubleValue; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.schema.Column; +import static com.flipkart.foxtrot.sql.constants.Constants.REGEX; +import static com.flipkart.foxtrot.sql.constants.Constants.REPLACEMENT; + public class QueryUtils { private QueryUtils() {} public static String expressionToString(Expression expression) { if(expression instanceof Column) { - return ((Column)expression).getFullyQualifiedName().replaceAll(Constants.REGEX, Constants.REPLACEMENT); + return ((Column)expression).getFullyQualifiedName().replaceAll(REGEX, REPLACEMENT); } if(expression instanceof StringValue) { - return ((StringValue)expression).getValue().replaceAll(Constants.REGEX, Constants.REPLACEMENT); + return ((StringValue)expression).getValue().replaceAll(REGEX, REPLACEMENT); } return null; } diff --git a/foxtrot-sql/src/test/java/com/flipkart/foxtrot/sql/ParseTest.java b/foxtrot-sql/src/test/java/com/flipkart/foxtrot/sql/ParseTest.java index 8a2de15a9..31bafbc5d 100644 --- a/foxtrot-sql/src/test/java/com/flipkart/foxtrot/sql/ParseTest.java +++ b/foxtrot-sql/src/test/java/com/flipkart/foxtrot/sql/ParseTest.java @@ -3,8 +3,10 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.flipkart.foxtrot.common.group.GroupRequest; import com.flipkart.foxtrot.common.query.Query; import com.flipkart.foxtrot.common.query.general.MissingFilter; +import com.flipkart.foxtrot.common.stats.Stat; import com.flipkart.foxtrot.sql.query.FqlActionQuery; import com.google.common.collect.ImmutableList; import org.junit.Assert; @@ -98,4 +100,126 @@ public void test() throws Exception { } } + + @Test + public void testGroupAggregationSumQueryParsing() { + QueryTranslator queryTranslator = new QueryTranslator(); + String sql = "select sum(eventData.amount) from europa where eventType = 'AWESOME_EVENT' group by date.hourOfDay"; + FqlQuery fqlQuery = queryTranslator.translate(sql); + Assert.assertTrue(fqlQuery instanceof FqlActionQuery); + FqlActionQuery actionQuery = (FqlActionQuery) fqlQuery; + Assert.assertTrue(actionQuery.getActionRequest() instanceof GroupRequest); + GroupRequest groupRequest = (GroupRequest) actionQuery.getActionRequest(); + + Assert.assertEquals(1, groupRequest.getNesting() + .size()); + Assert.assertTrue(groupRequest.getNesting() + .contains("date.hourOfDay")); + Assert.assertEquals("eventData.amount", groupRequest.getAggregationField()); + Assert.assertNotNull(groupRequest.getAggregationType()); + Assert.assertEquals(groupRequest.getAggregationType(),Stat.SUM); + + } + + @Test + public void testGroupAggregationCountDistinctQueryParsing() { + QueryTranslator queryTranslator = new QueryTranslator(); + String sql = "select count(distinct eventData.amount) from europa where eventType = 'AWESOME_EVENT' group by date.hourOfDay"; + FqlQuery fqlQuery = queryTranslator.translate(sql); + Assert.assertTrue(fqlQuery instanceof FqlActionQuery); + FqlActionQuery actionQuery = (FqlActionQuery) fqlQuery; + Assert.assertTrue(actionQuery.getActionRequest() instanceof GroupRequest); + GroupRequest groupRequest = (GroupRequest) actionQuery.getActionRequest(); + + Assert.assertEquals(1, groupRequest.getNesting() + .size()); + Assert.assertTrue(groupRequest.getNesting() + .contains("date.hourOfDay")); + Assert.assertEquals("eventData.amount", groupRequest.getUniqueCountOn()); + Assert.assertNull(groupRequest.getAggregationType()); + + } + + @Test + public void testGroupAggregationCountQueryParsing() { + QueryTranslator queryTranslator = new QueryTranslator(); + String sql = "select count(eventData.amount) from europa where eventType = 'AWESOME_EVENT' group by date.hourOfDay"; + FqlQuery fqlQuery = queryTranslator.translate(sql); + Assert.assertTrue(fqlQuery instanceof FqlActionQuery); + FqlActionQuery actionQuery = (FqlActionQuery) fqlQuery; + Assert.assertTrue(actionQuery.getActionRequest() instanceof GroupRequest); + GroupRequest groupRequest = (GroupRequest) actionQuery.getActionRequest(); + + Assert.assertEquals(1, groupRequest.getNesting() + .size()); + Assert.assertTrue(groupRequest.getNesting() + .contains("date.hourOfDay")); + Assert.assertEquals("eventData.amount", groupRequest.getAggregationField()); + Assert.assertNotNull(groupRequest.getAggregationType()); + Assert.assertNull(groupRequest.getUniqueCountOn()); + Assert.assertEquals(groupRequest.getAggregationType(),Stat.COUNT); + } + + @Test + public void testGroupAggregationAvgQueryParsing() { + QueryTranslator queryTranslator = new QueryTranslator(); + String sql = "select avg(eventData.amount) from europa where eventType = 'AWESOME_EVENT' group by date.hourOfDay"; + FqlQuery fqlQuery = queryTranslator.translate(sql); + Assert.assertTrue(fqlQuery instanceof FqlActionQuery); + FqlActionQuery actionQuery = (FqlActionQuery) fqlQuery; + Assert.assertTrue(actionQuery.getActionRequest() instanceof GroupRequest); + GroupRequest groupRequest = (GroupRequest) actionQuery.getActionRequest(); + + Assert.assertEquals(1, groupRequest.getNesting() + .size()); + Assert.assertTrue(groupRequest.getNesting() + .contains("date.hourOfDay")); + Assert.assertEquals("eventData.amount", groupRequest.getAggregationField()); + Assert.assertNotNull(groupRequest.getAggregationType()); + + Assert.assertNull(groupRequest.getUniqueCountOn()); + Assert.assertEquals(groupRequest.getAggregationType(),Stat.AVG); + + } + + @Test + public void testGroupAggregationMinQueryParsing() { + QueryTranslator queryTranslator = new QueryTranslator(); + String sql = "select min(eventData.amount) from europa where eventType = 'AWESOME_EVENT' group by date.hourOfDay"; + FqlQuery fqlQuery = queryTranslator.translate(sql); + Assert.assertTrue(fqlQuery instanceof FqlActionQuery); + FqlActionQuery actionQuery = (FqlActionQuery) fqlQuery; + Assert.assertTrue(actionQuery.getActionRequest() instanceof GroupRequest); + GroupRequest groupRequest = (GroupRequest) actionQuery.getActionRequest(); + + Assert.assertEquals(1, groupRequest.getNesting() + .size()); + Assert.assertTrue(groupRequest.getNesting() + .contains("date.hourOfDay")); + Assert.assertEquals("eventData.amount", groupRequest.getAggregationField()); + Assert.assertNotNull(groupRequest.getAggregationType()); + Assert.assertNull(groupRequest.getUniqueCountOn()); + Assert.assertEquals(groupRequest.getAggregationType(),Stat.MIN); + + } + + @Test + public void testGroupAggregationMaxQueryParsing() { + QueryTranslator queryTranslator = new QueryTranslator(); + String sql = "select max(eventData.amount) from europa where eventType = 'AWESOME_EVENT' group by date.hourOfDay"; + FqlQuery fqlQuery = queryTranslator.translate(sql); + Assert.assertTrue(fqlQuery instanceof FqlActionQuery); + FqlActionQuery actionQuery = (FqlActionQuery) fqlQuery; + Assert.assertTrue(actionQuery.getActionRequest() instanceof GroupRequest); + GroupRequest groupRequest = (GroupRequest) actionQuery.getActionRequest(); + + Assert.assertEquals(1, groupRequest.getNesting() + .size()); + Assert.assertTrue(groupRequest.getNesting() + .contains("date.hourOfDay")); + Assert.assertNull(groupRequest.getUniqueCountOn()); + Assert.assertEquals("eventData.amount", groupRequest.getAggregationField()); + Assert.assertEquals(groupRequest.getAggregationType(),Stat.MAX); + + } }