Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle group_by identifier more generically #127

Merged
merged 4 commits into from
Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,30 @@ void testQueryQ1DistinctCountAggregationWithOnlyFilter(String dataStoreName) thr
}
}

@ParameterizedTest
@MethodSource("databaseContextBoth")
void testQueryQ1DistinctCountAggregationWithMatchingSelectionAndGroupBy(String dataStoreName)
throws IOException {
Datastore datastore = datastoreMap.get(dataStoreName);
Collection collection = datastore.getCollection(COLLECTION_NAME);
org.hypertrace.core.documentstore.query.Query query =
org.hypertrace.core.documentstore.query.Query.builder()
.addSelection(
AggregateExpression.of(DISTINCT_COUNT, IdentifierExpression.of("quantity")),
"qty_count")
.addSelection(IdentifierExpression.of("item"))
.addSelection(IdentifierExpression.of("price"))
.setFilter(
RelationalExpression.of(
IdentifierExpression.of("price"), LTE, ConstantExpression.of(10)))
.addAggregation(IdentifierExpression.of("item"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add group by on price as well, because it's a part of the selection?

.build();
try (CloseableIterator<Document> resultDocs = collection.aggregate(query)) {
assertDocsAndSizeEqualWithoutOrder(
dataStoreName, resultDocs, 3, "mongo/test_aggr_with_match_selection_and_groupby.json");
}
}

@ParameterizedTest
@MethodSource("databaseContextBoth")
public void testQueryV1ForSimpleWhereClause(String dataStoreName) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"item":"Soap",
"qty_count":2
},
{
"item":"Comb",
"qty_count":2
},
{
"item":"Shampoo",
"qty_count":2
}
]
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package org.hypertrace.core.documentstore.postgres.query.v1.transformer;

import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.hypertrace.core.documentstore.expression.impl.AggregateExpression;
import org.hypertrace.core.documentstore.expression.impl.ConstantExpression;
import org.hypertrace.core.documentstore.expression.impl.FunctionExpression;
import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression;
import org.hypertrace.core.documentstore.expression.type.GroupTypeExpression;
import org.hypertrace.core.documentstore.expression.type.SelectTypeExpression;
import org.hypertrace.core.documentstore.parser.GroupTypeExpressionVisitor;
import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor;
import org.hypertrace.core.documentstore.query.Query;
import org.hypertrace.core.documentstore.query.SelectionSpec;
Expand All @@ -29,42 +34,110 @@
*
* This is the similar behavior supported in our other document store implementation (e.g Mongo)
* */
public class PostgresSelectionQueryTransformer
implements QueryTransformer, SelectTypeExpressionVisitor {
public class PostgresSelectionQueryTransformer implements QueryTransformer {

@Override
public Query transform(Query query) {
// no-op if group by clause exits
if (!query.getAggregations().isEmpty()) return query;
LocalSelectTypeAggregationExpressionSelector aggregationExpressionSelector =
new LocalSelectTypeAggregationExpressionSelector();
Boolean noAggregation =
query.getSelections().stream()
.filter(
selectionSpec ->
selectionSpec.getExpression().accept(aggregationExpressionSelector))
.findAny()
.isEmpty();
if (noAggregation) return query;

// get all identifier of group by clause
LocalGroupByIdentifierExpressionSelector groupByIdentifierExpressionSelector =
new LocalGroupByIdentifierExpressionSelector();
Set<GroupTypeExpression> groupByIdentifierExpressions =
query.getAggregations().stream()
.filter(exp -> exp.accept(groupByIdentifierExpressionSelector))
.collect(Collectors.toUnmodifiableSet());

// check for all selections, remove non-aggregated selections.
// keep only matching identifier expression with group by along with rest.
LocalSelectTypeIdentifierExpressionSelector identifierExpressionSelector =
new LocalSelectTypeIdentifierExpressionSelector();
List<SelectionSpec> finalSelectionSpecs =
query.getSelections().stream()
.filter(selectionSpec -> selectionSpec.getExpression().accept(this))
.filter(
getSelectionSpecPredicate(
groupByIdentifierExpressions, identifierExpressionSelector))
.collect(Collectors.toUnmodifiableList());

return finalSelectionSpecs.size() > 0
return finalSelectionSpecs.size() != query.getSelections().size()
? new TransformedQueryBuilder(query).setSelections(finalSelectionSpecs).build()
: query;
}

@Override
public Boolean visit(AggregateExpression expression) {
return true;
private Predicate<SelectionSpec> getSelectionSpecPredicate(
Set<GroupTypeExpression> groupByIdentifierExpressions,
LocalSelectTypeIdentifierExpressionSelector identifierExpressionSelector) {
return selectionSpec -> {
SelectTypeExpression expression = selectionSpec.getExpression();
return (!(boolean) expression.accept(identifierExpressionSelector))
|| groupByIdentifierExpressions.contains(expression);
};
}

@Override
public Boolean visit(ConstantExpression expression) {
return false;
private static class LocalSelectTypeAggregationExpressionSelector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is just returning the instance type, do need a separate visitor hierarchy? Could we just do using instance of?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, this is a more readable pattern.

implements SelectTypeExpressionVisitor {
@Override
public Boolean visit(AggregateExpression expression) {
return true;
}

@Override
public Boolean visit(ConstantExpression expression) {
return false;
}

@Override
public Boolean visit(FunctionExpression expression) {
return false;
}

@Override
public Boolean visit(IdentifierExpression expression) {
return false;
}
}

@Override
public Boolean visit(FunctionExpression expression) {
return false;
private static class LocalSelectTypeIdentifierExpressionSelector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

implements SelectTypeExpressionVisitor {
@Override
public Boolean visit(AggregateExpression expression) {
return false;
}

@Override
public Boolean visit(ConstantExpression expression) {
return false;
}

@Override
public Boolean visit(FunctionExpression expression) {
return false;
}

@Override
public Boolean visit(IdentifierExpression expression) {
return true;
}
}

@Override
public Boolean visit(IdentifierExpression expression) {
return false;
private static class LocalGroupByIdentifierExpressionSelector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

implements GroupTypeExpressionVisitor {
@Override
public Boolean visit(FunctionExpression expression) {
return false;
}

@Override
public Boolean visit(IdentifierExpression expression) {
return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -917,4 +917,35 @@ void testQueryWithFunctionalLhsInRelationalFilter() {
assertEquals(1, params.getObjectParams().size());
assertEquals(50, params.getObjectParams().get(1));
}

@Test
void testQueryQ1DistinctCountAggregationWithMatchingSelectionAndGroupBy() {
org.hypertrace.core.documentstore.query.Query query =
org.hypertrace.core.documentstore.query.Query.builder()
.addSelection(
AggregateExpression.of(DISTINCT_COUNT, IdentifierExpression.of("quantity")),
"qty_count")
.addSelection(IdentifierExpression.of("item"))
.addSelection(IdentifierExpression.of("price"))
.setFilter(
RelationalExpression.of(
IdentifierExpression.of("price"), LTE, ConstantExpression.of(10)))
.addAggregation(IdentifierExpression.of("item"))
.build();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add price aggregation?


PostgresQueryParser postgresQueryParser =
new PostgresQueryParser(TEST_COLLECTION, PostgresQueryTransformer.transform(query));
String sql = postgresQueryParser.parse();

assertEquals(
"SELECT COUNT(DISTINCT document->>'quantity' ) AS \"qty_count\", "
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It confuses to see where the price selection is lost!
Then, I remembered the transformation logic. Anyway, that'll come up.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline.

+ "document->'item' AS item "
+ "FROM testCollection WHERE CAST (document->>'price' AS NUMERIC) <= ? "
+ "GROUP BY document->'item'",
sql);

Params params = postgresQueryParser.getParamsBuilder().build();
assertEquals(1, params.getObjectParams().size());
assertEquals(10, params.getObjectParams().get(1));
}
}