Skip to content
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 @@ -80,13 +80,16 @@
import java.util.stream.StreamSupport;
import org.hypertrace.core.documentstore.commons.DocStoreConstants;
import org.hypertrace.core.documentstore.expression.impl.AggregateExpression;
import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression;
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.impl.KeyExpression;
import org.hypertrace.core.documentstore.expression.impl.LogicalExpression;
import org.hypertrace.core.documentstore.expression.impl.RelationalExpression;
import org.hypertrace.core.documentstore.expression.impl.SubQueryJoinExpression;
import org.hypertrace.core.documentstore.expression.impl.UnnestExpression;
import org.hypertrace.core.documentstore.expression.operators.AggregationOperator;
import org.hypertrace.core.documentstore.expression.operators.FunctionOperator;
import org.hypertrace.core.documentstore.expression.operators.RelationalOperator;
import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression;
Expand Down Expand Up @@ -3478,6 +3481,182 @@ public void testToLowerCaseMongoFunctionOperator(String dataStoreName) throws Ex
dataStoreName, resultDocs, "query/case_insensitive_exact_match_response.json", 2);
}

@ParameterizedTest
@ArgumentsSource(MongoProvider.class)
void testSelfJoinWithSubQuery(String dataStoreName) throws IOException {
Collection collection = getCollection(dataStoreName);

/*
This is the query we want to execute:
SELECT item, quantity, date
FROM <implicit_collection>
JOIN (
SELECT item, MAX(date) AS latest_date
FROM <implicit_collection>
GROUP BY item
) latest
ON item = latest.item
AND date = latest.latest_date
ORDER BY `item` ASC;
*/

/*
The right subquery:
SELECT item, MAX(date) AS latest_date
FROM <implicit_collection>
GROUP BY item
*/
Query subQuery =
Query.builder()
.addSelection(SelectionSpec.of(IdentifierExpression.of("item")))
.addSelection(
SelectionSpec.of(
AggregateExpression.of(
AggregationOperator.MAX, IdentifierExpression.of("date")),
"latest_date"))
.addAggregation(IdentifierExpression.of("item"))
.build();

/*
The FROM expression representing a join with the right subquery:
FROM <implicit_collection>
JOIN (
SELECT item, MAX(date) AS latest_date
FROM <implicit_collection>
GROUP BY item
) latest
ON item = latest.item
AND date = latest.latest_date;
*/
SubQueryJoinExpression subQueryJoinExpression =
SubQueryJoinExpression.builder()
.subQuery(subQuery)
.subQueryAlias("latest")
.joinCondition(
LogicalExpression.and(
RelationalExpression.of(
IdentifierExpression.of("item"),
RelationalOperator.EQ,
AliasedIdentifierExpression.builder()
.name("item")
.contextAlias("latest")
.build()),
RelationalExpression.of(
IdentifierExpression.of("date"),
RelationalOperator.EQ,
AliasedIdentifierExpression.builder()
.name("latest_date")
.contextAlias("latest")
.build())))
.build();

/*
Now build the top-level Query:
SELECT item, quantity, date FROM <subQueryJoinExpression> ORDER BY `item` ASC;
*/
Query mainQuery =
Query.builder()
.addSelection(IdentifierExpression.of("item"))
.addSelection(IdentifierExpression.of("quantity"))
.addSelection(IdentifierExpression.of("date"))
.addFromClause(subQueryJoinExpression)
.addSort(IdentifierExpression.of("item"), ASC)
.build();

Iterator<Document> iterator = collection.aggregate(mainQuery);
assertDocsAndSizeEqual(
dataStoreName, iterator, "query/self_join_with_sub_query_response.json", 4);
}

@ParameterizedTest
@ArgumentsSource(MongoProvider.class)
void testSelfJoinWithSubQueryWithNestedFields(String dataStoreName) throws IOException {
createCollectionData(
"query/items_data_with_nested_fields.json", "items_data_with_nested_fields");
Collection collection = getCollection(dataStoreName, "items_data_with_nested_fields");

/*
This is the query we want to execute:
SELECT itemDetails.item, itemDetails.quantity, itemDetails.date
FROM <implicit_collection>
JOIN (
SELECT itemDetails.item, MAX(itemDetails.date) AS latest_date
FROM <implicit_collection>
GROUP BY itemDetails.item
) latest
ON itemDetails.item = latest.itemDetails.item
AND itemDetails.date = latest.latest_date
ORDER BY `itemDetails.item` ASC;
*/

/*
The right subquery:
SELECT itemDetails.item, MAX(itemDetails.date) AS latest_date
FROM <implicit_collection>
GROUP BY itemDetails.item
*/
Query subQuery =
Query.builder()
.addSelection(SelectionSpec.of(IdentifierExpression.of("itemDetails.item")))
.addSelection(
SelectionSpec.of(
AggregateExpression.of(
AggregationOperator.MAX, IdentifierExpression.of("itemDetails.date")),
"latest_date"))
.addAggregation(IdentifierExpression.of("itemDetails.item"))
.build();

/*
The FROM expression representing a join with the right subquery:
FROM <implicit_collection>
JOIN (
SELECT itemDetails.item, MAX(itemDetails.date) AS latest_date
FROM <implicit_collection>
GROUP BY itemDetails.item
) latest
ON itemDetails.item = latest.itemDetails.item
AND itemDetails.date = latest.latest_date;
*/
SubQueryJoinExpression subQueryJoinExpression =
SubQueryJoinExpression.builder()
.subQuery(subQuery)
.subQueryAlias("latest")
.joinCondition(
LogicalExpression.and(
RelationalExpression.of(
IdentifierExpression.of("itemDetails.item"),
RelationalOperator.EQ,
AliasedIdentifierExpression.builder()
.name("itemDetails.item")
.contextAlias("latest")
.build()),
RelationalExpression.of(
IdentifierExpression.of("itemDetails.date"),
RelationalOperator.EQ,
AliasedIdentifierExpression.builder()
.name("latest_date")
.contextAlias("latest")
.build())))
.build();

/*
Now build the top-level Query:
SELECT itemDetails.item, itemDetails.quantity, itemDetails.date FROM <subQueryJoinExpression> ORDER BY `itemDetails.item` ASC;
*/
Query mainQuery =
Query.builder()
.addSelection(IdentifierExpression.of("itemDetails.item"))
.addSelection(IdentifierExpression.of("itemDetails.quantity"))
.addSelection(IdentifierExpression.of("itemDetails.date"))
.addFromClause(subQueryJoinExpression)
.addSort(IdentifierExpression.of("itemDetails.item"), ASC)
.build();

Iterator<Document> iterator = collection.aggregate(mainQuery);
assertDocsAndSizeEqual(
dataStoreName, iterator, "query/sub_query_join_response_with_nested_fields.json", 3);
}

private static Collection getCollection(final String dataStoreName) {
return getCollection(dataStoreName, COLLECTION_NAME);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[
{
"_id": 1,
"itemDetails": {
"item": "Comb",
"date": "2012-01-01",
"quantity": 10
}
},
{
"_id": 2,
"itemDetails": {
"item": "Shampoo",
"date": "2012-01-01",
"quantity": 10
}
},
{
"_id": 3,
"itemDetails": {
"item": "Shampoo",
"date": "2012-02-02",
"quantity": 20
}
},
{
"_id": 4,
"itemDetails": {
"item": "Shampoo",
"date": "2012-03-03",
"quantity": 30
}
},
{
"_id": 5,
"itemDetails": {
"item": "Soap",
"date": "2012-02-02",
"quantity": 20
}
},
{
"_id": 6,
"itemDetails": {
"item": "Soap",
"date": "2012-01-01",
"quantity": 10
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"date": "2015-09-10T08:43:00Z",
"item": "Comb",
"quantity": 10
},
{
"date": "2014-03-01T09:00:00Z",
"item": "Mirror",
"quantity": 1
},
{
"date": "2014-04-04T11:21:39.736Z",
"item": "Shampoo",
"quantity": 20
},
{
"date": "2016-02-06T20:20:13Z",
"item": "Soap",
"quantity": 5
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[
{
"itemDetails": {
"item": "Comb",
"date": "2012-01-01",
"quantity": 10
}
},
{
"itemDetails": {
"item": "Shampoo",
"date": "2012-03-03",
"quantity": 30
}
},
{
"itemDetails": {
"item": "Soap",
"date": "2012-02-02",
"quantity": 20
}
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.hypertrace.core.documentstore.expression.impl;

import com.google.common.base.Preconditions;
import lombok.Value;
import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor;

/**
* Expression for referencing an identifier/column name within a context having an alias.
*
* <p>Example: In this query: <code>
* SELECT item, quantity, date
* FROM <implicit_collection>
* JOIN (
* SELECT item, MAX(date) AS latest_date
* FROM <implicit_collection>
* GROUP BY item
* ) AS latest
* ON item = latest.item
* ORDER BY `item` ASC;
* </code> the rhs of the join condition "latest.item" can be expressed as: <code>
* AliasedIdentifierExpression.builder().name("item").alias("alias1").build() </code>
*/
@Value
public class AliasedIdentifierExpression extends IdentifierExpression {
String contextAlias;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just alias?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Alias sounds like we are giving the field name as alias (like in SelectionSpec. If you look at the example above, this is not the alias for the field, but the alias for the context in which we are referring the field.

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense.


private AliasedIdentifierExpression(final String name, final String contextAlias) {
super(name);
this.contextAlias = contextAlias;
}

@Override
public <T> T accept(final SelectTypeExpressionVisitor visitor) {
return visitor.visit(this);
}

@Override
public String toString() {
return "`" + getContextAlias() + "." + getName() + "`";

Check warning on line 39 in document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java

View check run for this annotation

Codecov / codecov/patch

document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java#L39

Added line #L39 was not covered by tests
}

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

public static class AliasedIdentifierExpressionBuilder {
private String name;
private String contextAlias;

public AliasedIdentifierExpressionBuilder name(final String name) {
this.name = name;
return this;
}

public AliasedIdentifierExpressionBuilder contextAlias(final String contextAlias) {
this.contextAlias = contextAlias;
return this;
}

public AliasedIdentifierExpression build() {
Preconditions.checkArgument(
this.name != null && !this.name.isBlank(), "name is null or blank");
Preconditions.checkArgument(
this.contextAlias != null && !this.contextAlias.isBlank(),
"contextAlias is null or blank");
return new AliasedIdentifierExpression(this.name, this.contextAlias);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Value;
import lombok.experimental.NonFinal;
import org.hypertrace.core.documentstore.expression.type.GroupTypeExpression;
import org.hypertrace.core.documentstore.expression.type.SelectTypeExpression;
import org.hypertrace.core.documentstore.expression.type.SortTypeExpression;
Expand All @@ -17,7 +18,8 @@
* <p>Example: IdentifierExpression.of("col1");
*/
@Value
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NonFinal
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class IdentifierExpression
implements GroupTypeExpression, SelectTypeExpression, SortTypeExpression {

Expand Down
Loading