Skip to content

Commit c7ea09e

Browse files
authored
fix : group by on functional expressions in mongo (#200)
1 parent 43b12e8 commit c7ea09e

File tree

7 files changed

+165
-6
lines changed

7 files changed

+165
-6
lines changed

document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,11 @@
1414
import static org.hypertrace.core.documentstore.expression.operators.AggregationOperator.MAX;
1515
import static org.hypertrace.core.documentstore.expression.operators.AggregationOperator.MIN;
1616
import static org.hypertrace.core.documentstore.expression.operators.AggregationOperator.SUM;
17+
import static org.hypertrace.core.documentstore.expression.operators.FunctionOperator.DIVIDE;
18+
import static org.hypertrace.core.documentstore.expression.operators.FunctionOperator.FLOOR;
1719
import static org.hypertrace.core.documentstore.expression.operators.FunctionOperator.LENGTH;
1820
import static org.hypertrace.core.documentstore.expression.operators.FunctionOperator.MULTIPLY;
21+
import static org.hypertrace.core.documentstore.expression.operators.FunctionOperator.SUBTRACT;
1922
import static org.hypertrace.core.documentstore.expression.operators.LogicalOperator.AND;
2023
import static org.hypertrace.core.documentstore.expression.operators.LogicalOperator.OR;
2124
import static org.hypertrace.core.documentstore.expression.operators.RelationalOperator.CONTAINS;
@@ -87,6 +90,7 @@
8790
import org.hypertrace.core.documentstore.model.options.UpdateOptions;
8891
import org.hypertrace.core.documentstore.model.subdoc.SubDocumentUpdate;
8992
import org.hypertrace.core.documentstore.model.subdoc.SubDocumentValue;
93+
import org.hypertrace.core.documentstore.query.Aggregation;
9094
import org.hypertrace.core.documentstore.query.Filter;
9195
import org.hypertrace.core.documentstore.query.Pagination;
9296
import org.hypertrace.core.documentstore.query.Query;
@@ -3312,6 +3316,52 @@ public void testNotExistsOperatorWithFindUsingBooleanRhs(String dataStoreName) t
33123316
testCountApi(dataStoreName, query, "query/not_exists_filter_response.json");
33133317
}
33143318

3319+
@ParameterizedTest
3320+
@ArgumentsSource(MongoProvider.class)
3321+
public void testMongoFunctionExpressionGroupBy(String dataStoreName) throws Exception {
3322+
Collection collection = getCollection(dataStoreName);
3323+
3324+
FunctionExpression functionExpression =
3325+
FunctionExpression.builder()
3326+
.operator(FLOOR)
3327+
.operand(
3328+
FunctionExpression.builder()
3329+
.operator(DIVIDE)
3330+
.operand(
3331+
FunctionExpression.builder()
3332+
.operator(SUBTRACT)
3333+
.operand(IdentifierExpression.of("price"))
3334+
.operand(ConstantExpression.of(5))
3335+
.build())
3336+
.operand(ConstantExpression.of(5))
3337+
.build())
3338+
.build();
3339+
List<SelectionSpec> selectionSpecs =
3340+
List.of(
3341+
SelectionSpec.of(functionExpression, "function"),
3342+
SelectionSpec.of(
3343+
AggregateExpression.of(COUNT, IdentifierExpression.of("function")),
3344+
"functionCount"));
3345+
Selection selection = Selection.builder().selectionSpecs(selectionSpecs).build();
3346+
3347+
Query query =
3348+
Query.builder()
3349+
.setSelection(selection)
3350+
.setAggregation(
3351+
Aggregation.builder().expression(IdentifierExpression.of("function")).build())
3352+
.setSort(
3353+
Sort.builder()
3354+
.sortingSpec(SortingSpec.of(IdentifierExpression.of("function"), ASC))
3355+
.build())
3356+
.build();
3357+
3358+
Iterator<Document> resultDocs = collection.aggregate(query);
3359+
assertDocsAndSizeEqualWithoutOrder(
3360+
dataStoreName, resultDocs, "query/function_expression_group_by_response.json", 3);
3361+
3362+
testCountApi(dataStoreName, query, "query/function_expression_group_by_response.json");
3363+
}
3364+
33153365
private static Collection getCollection(final String dataStoreName) {
33163366
return getCollection(dataStoreName, COLLECTION_NAME);
33173367
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[
2+
{
3+
"function": 0.0,
4+
"functionCount": 4
5+
},
6+
{
7+
"function": 1.0,
8+
"functionCount": 2
9+
},
10+
{
11+
"function": 3.0,
12+
"functionCount": 2
13+
}
14+
]

document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoQueryExecutor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import static org.hypertrace.core.documentstore.mongo.query.MongoPaginationHelper.getSkipClause;
1313
import static org.hypertrace.core.documentstore.mongo.query.parser.MongoFilterTypeExpressionParser.getFilter;
1414
import static org.hypertrace.core.documentstore.mongo.query.parser.MongoFilterTypeExpressionParser.getFilterClause;
15-
import static org.hypertrace.core.documentstore.mongo.query.parser.MongoGroupTypeExpressionParser.getGroupClause;
1615
import static org.hypertrace.core.documentstore.mongo.query.parser.MongoNonProjectedSortTypeExpressionParser.getNonProjectedSortClause;
1716
import static org.hypertrace.core.documentstore.mongo.query.parser.MongoSelectTypeExpressionParser.getProjectClause;
1817
import static org.hypertrace.core.documentstore.mongo.query.parser.MongoSelectTypeExpressionParser.getSelections;
@@ -41,6 +40,7 @@
4140
import org.hypertrace.core.documentstore.model.config.ConnectionConfig;
4241
import org.hypertrace.core.documentstore.mongo.query.parser.AliasParser;
4342
import org.hypertrace.core.documentstore.mongo.query.parser.MongoFromTypeExpressionParser;
43+
import org.hypertrace.core.documentstore.mongo.query.parser.MongoGroupTypeExpressionParser;
4444
import org.hypertrace.core.documentstore.mongo.query.transformer.MongoQueryTransformer;
4545
import org.hypertrace.core.documentstore.parser.AggregateExpressionChecker;
4646
import org.hypertrace.core.documentstore.parser.FunctionExpressionChecker;
@@ -58,7 +58,7 @@ public class MongoQueryExecutor {
5858
List.of(
5959
query -> singleton(getFilterClause(query, Query::getFilter)),
6060
MongoFromTypeExpressionParser::getFromClauses,
61-
query -> singleton(getGroupClause(query)),
61+
MongoGroupTypeExpressionParser::getGroupClauses,
6262
query -> singleton(getProjectClause(query)),
6363
query -> singleton(getFilterClause(query, Query::getAggregationFilter)),
6464
query -> singleton(getSortClause(query)),

document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFunctionExpressionParser.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ Map<String, Object> parse(final FunctionExpression expression) {
6262

6363
SelectTypeExpressionVisitor parser =
6464
new MongoIdentifierPrefixingParser(
65-
new MongoIdentifierExpressionParser(new MongoConstantExpressionParser()));
65+
new MongoIdentifierExpressionParser(
66+
new MongoFunctionExpressionParser(new MongoConstantExpressionParser())));
6667

6768
if (numArgs == 1) {
6869
Object value = expression.getOperands().get(0).accept(parser);

document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoGroupTypeExpressionParser.java

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,20 @@
55
import static org.hypertrace.core.documentstore.mongo.MongoUtils.encodeKey;
66

77
import com.mongodb.BasicDBObject;
8+
import java.util.ArrayList;
89
import java.util.HashMap;
910
import java.util.LinkedHashMap;
1011
import java.util.List;
1112
import java.util.Map;
13+
import java.util.Optional;
14+
import java.util.stream.Collectors;
1215
import org.apache.commons.collections4.CollectionUtils;
1316
import org.apache.commons.collections4.MapUtils;
1417
import org.hypertrace.core.documentstore.expression.impl.FunctionExpression;
1518
import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression;
1619
import org.hypertrace.core.documentstore.expression.type.GroupTypeExpression;
20+
import org.hypertrace.core.documentstore.parser.FunctionExpressionChecker;
21+
import org.hypertrace.core.documentstore.parser.GroupByAliasGetter;
1722
import org.hypertrace.core.documentstore.parser.GroupTypeExpressionVisitor;
1823
import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor;
1924
import org.hypertrace.core.documentstore.query.Query;
@@ -22,6 +27,10 @@
2227
public final class MongoGroupTypeExpressionParser implements GroupTypeExpressionVisitor {
2328

2429
private static final String GROUP_CLAUSE = "$group";
30+
private static final String ADD_FIELDS_CLAUSE = "$addFields";
31+
private static final FunctionExpressionChecker FUNCTION_EXPRESSION_CHECKER =
32+
new FunctionExpressionChecker();
33+
private static final GroupByAliasGetter GROUP_BY_ALIAS_GETTER = new GroupByAliasGetter();
2534

2635
@SuppressWarnings("unchecked")
2736
@Override
@@ -41,10 +50,32 @@ public Map<String, Object> visit(final IdentifierExpression expression) {
4150
return Map.of(key, PREFIX + identifier);
4251
}
4352

44-
public static BasicDBObject getGroupClause(final Query query) {
53+
public static List<BasicDBObject> getGroupClauses(final Query query) {
4554
final List<SelectionSpec> selectionSpecs = query.getSelections();
4655
final List<GroupTypeExpression> expressions = query.getAggregations();
4756

57+
final List<BasicDBObject> basicDBObjects = new ArrayList<>();
58+
59+
final List<SelectionSpec> functionExpressionSelectionWithGroupBys =
60+
getFunctionExpressionSelectionWithGroupBys(selectionSpecs, expressions);
61+
62+
if (!functionExpressionSelectionWithGroupBys.isEmpty()) {
63+
MongoSelectTypeExpressionParser parser =
64+
new MongoIdentifierPrefixingParser(
65+
new MongoIdentifierExpressionParser(new MongoFunctionExpressionParser()));
66+
Map<String, Object> addFields =
67+
functionExpressionSelectionWithGroupBys.stream()
68+
.map(spec -> MongoGroupTypeExpressionParser.parse(parser, spec))
69+
.reduce(
70+
new LinkedHashMap<>(),
71+
(first, second) -> {
72+
first.putAll(second);
73+
return first;
74+
});
75+
76+
basicDBObjects.add(new BasicDBObject(ADD_FIELDS_CLAUSE, addFields));
77+
}
78+
4879
MongoGroupTypeExpressionParser parser = new MongoGroupTypeExpressionParser();
4980
Map<String, Object> groupExp;
5081

@@ -82,11 +113,13 @@ public static BasicDBObject getGroupClause(final Query query) {
82113
});
83114

84115
if (MapUtils.isEmpty(definition) && CollectionUtils.isEmpty(expressions)) {
85-
return new BasicDBObject();
116+
return basicDBObjects;
86117
}
87118

88119
definition.putAll(groupExp);
89-
return new BasicDBObject(GROUP_CLAUSE, definition);
120+
121+
basicDBObjects.add(new BasicDBObject(GROUP_CLAUSE, definition));
122+
return basicDBObjects;
90123
}
91124

92125
private static Map<String, Object> parse(
@@ -99,4 +132,31 @@ private Map<String, Object> parse(final GroupTypeExpression expression) {
99132
MongoGroupTypeExpressionParser parser = new MongoGroupTypeExpressionParser();
100133
return expression.accept(parser);
101134
}
135+
136+
private static List<SelectionSpec> getFunctionExpressionSelectionWithGroupBys(
137+
final List<SelectionSpec> selectionSpecs, final List<GroupTypeExpression> expressions) {
138+
List<String> groupByAliases = getGroupByAliases(expressions);
139+
140+
return selectionSpecs.stream()
141+
.filter(
142+
selectionSpec ->
143+
isFunctionExpressionSelectionWithGroupBy(selectionSpec, groupByAliases))
144+
.collect(Collectors.toUnmodifiableList());
145+
}
146+
147+
public static boolean isFunctionExpressionSelectionWithGroupBy(
148+
final SelectionSpec selectionSpec, final List<String> groupByAliases) {
149+
return selectionSpec.getAlias() != null
150+
&& groupByAliases.contains(selectionSpec.getAlias())
151+
&& (Boolean) selectionSpec.getExpression().accept(FUNCTION_EXPRESSION_CHECKER);
152+
}
153+
154+
@SuppressWarnings("unchecked")
155+
public static List<String> getGroupByAliases(final List<GroupTypeExpression> expressions) {
156+
return expressions.stream()
157+
.map(expression -> (Optional<String>) expression.accept(GROUP_BY_ALIAS_GETTER))
158+
.filter(Optional::isPresent)
159+
.map(Optional::get)
160+
.collect(Collectors.toUnmodifiableList());
161+
}
102162
}

document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoSelectTypeExpressionParser.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package org.hypertrace.core.documentstore.mongo.query.parser;
22

33
import static java.util.stream.Collectors.toMap;
4+
import static org.hypertrace.core.documentstore.mongo.MongoCollection.ID_KEY;
5+
import static org.hypertrace.core.documentstore.mongo.query.parser.MongoGroupTypeExpressionParser.getGroupByAliases;
6+
import static org.hypertrace.core.documentstore.mongo.query.parser.MongoGroupTypeExpressionParser.isFunctionExpressionSelectionWithGroupBy;
47

8+
import com.google.common.base.Joiner;
59
import com.mongodb.BasicDBObject;
610
import java.util.List;
711
import java.util.Map;
@@ -20,6 +24,8 @@ public abstract class MongoSelectTypeExpressionParser implements SelectTypeExpre
2024

2125
protected final MongoSelectTypeExpressionParser baseParser;
2226

27+
private static final Joiner DOT_JOINER = Joiner.on(".");
28+
2329
protected MongoSelectTypeExpressionParser() {
2430
this(MongoUnsupportedSelectTypeExpressionParser.INSTANCE);
2531
}
@@ -59,8 +65,17 @@ public static BasicDBObject getSelections(final Query query) {
5965
new MongoIdentifierPrefixingParser(
6066
new MongoIdentifierExpressionParser(new MongoFunctionExpressionParser()));
6167

68+
List<String> groupByAliases = getGroupByAliases(query.getAggregations());
69+
6270
Map<String, Object> projectionMap =
6371
selectionSpecs.stream()
72+
.map(
73+
spec ->
74+
isFunctionExpressionSelectionWithGroupBy(spec, groupByAliases)
75+
? SelectionSpec.of(
76+
IdentifierExpression.of(DOT_JOINER.join(ID_KEY, spec.getAlias())),
77+
spec.getAlias())
78+
: spec)
6479
.map(spec -> MongoSelectTypeExpressionParser.parse(parser, spec))
6580
.flatMap(map -> map.entrySet().stream())
6681
.collect(
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.hypertrace.core.documentstore.parser;
2+
3+
import java.util.Optional;
4+
import org.hypertrace.core.documentstore.expression.impl.FunctionExpression;
5+
import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression;
6+
7+
@SuppressWarnings("unchecked")
8+
public class GroupByAliasGetter implements GroupTypeExpressionVisitor {
9+
10+
@Override
11+
public Optional<String> visit(FunctionExpression expression) {
12+
return Optional.empty();
13+
}
14+
15+
@Override
16+
public Optional<String> visit(IdentifierExpression expression) {
17+
return Optional.of(expression.getName());
18+
}
19+
}

0 commit comments

Comments
 (0)