From c029bdb0f0d3f18c0e8f4067e5e07c2a55767ec5 Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Tue, 1 Apr 2025 17:52:33 +0530 Subject: [PATCH 01/13] feat: add support for self-joins --- .../expression/impl/JoinExpression.java | 38 +++++++ .../expression/impl/JoinType.java | 9 ++ .../impl/SubQueryFromExpression.java | 26 +++++ .../expression/impl/TableFromExpression.java | 24 ++++ .../parser/FromTypeExpressionVisitor.java | 9 ++ .../expression/impl/JoinExpressionTest.java | 107 ++++++++++++++++++ 6 files changed, 213 insertions(+) create mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java create mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinType.java create mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryFromExpression.java create mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/TableFromExpression.java create mode 100644 document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java new file mode 100644 index 000000000..179a11f21 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java @@ -0,0 +1,38 @@ +package org.hypertrace.core.documentstore.expression.impl; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; +import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; +import org.hypertrace.core.documentstore.parser.FilterTypeExpressionVisitor; +import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; +import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; +import org.hypertrace.core.documentstore.query.Query; + +/** + * Expression representing a condition for filtering + * + *

Example: + * company IN ('Traceable', 'Harness') + * can be constructed as + * RelationalExpression.of( IdentifierExpression.of("company"), RelationalOperator.IN, + * ConstantExpression.ofStrings("Traceable", "Harness")))); + * + */ +@Value +@Builder(toBuilder = true) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class JoinExpression implements FromTypeExpression { + FromTypeExpression left; + FromTypeExpression right; + JoinType joinType; // INNER, LEFT, RIGHT, etc. + FilterTypeExpression onCondition; // The ON clause + + @Override + public T accept(FromTypeExpressionVisitor visitor) { + return visitor.visit(this); + } +} + diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinType.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinType.java new file mode 100644 index 000000000..068ca5c36 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinType.java @@ -0,0 +1,9 @@ +package org.hypertrace.core.documentstore.expression.impl; + +public enum JoinType { + INNER, + LEFT, + RIGHT, + FULL, + CROSS +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryFromExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryFromExpression.java new file mode 100644 index 000000000..fd08d3637 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryFromExpression.java @@ -0,0 +1,26 @@ +package org.hypertrace.core.documentstore.expression.impl; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; +import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; +import org.hypertrace.core.documentstore.query.Query; + +/** + * Allows embedding an entire Query in the FROM clause. + */ +@Value +@Builder(toBuilder = true) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class SubQueryFromExpression implements FromTypeExpression { + Query subQuery; + String alias; // e.g. "rightTable" + + @Override + public T accept(FromTypeExpressionVisitor visitor) { + return visitor.visit(this); + } +} + diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/TableFromExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/TableFromExpression.java new file mode 100644 index 000000000..244fa501c --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/TableFromExpression.java @@ -0,0 +1,24 @@ +package org.hypertrace.core.documentstore.expression.impl; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; +import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; +import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; + +/** + * A simple wrapper for referencing a physical collection/table in the FROM clause. + */ +@Value +@Builder(toBuilder = true) +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class TableFromExpression implements FromTypeExpression { + // Currently, this supports joins on the same table/collection only. In the future, we can also add a tableName field here to support joins across tables/collections. + String alias; // e.g. "leftTable" + + @Override + public T accept(FromTypeExpressionVisitor visitor) { + return visitor.visit(this); + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java index ecb84b6ba..676ce51ca 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java @@ -1,7 +1,16 @@ package org.hypertrace.core.documentstore.parser; +import org.hypertrace.core.documentstore.expression.impl.JoinExpression; +import org.hypertrace.core.documentstore.expression.impl.SubQueryFromExpression; +import org.hypertrace.core.documentstore.expression.impl.TableFromExpression; import org.hypertrace.core.documentstore.expression.impl.UnnestExpression; public interface FromTypeExpressionVisitor { T visit(UnnestExpression unnestExpression); + + T visit(JoinExpression joinExpression); + + T visit(TableFromExpression tableFromExpression); + + T visit(SubQueryFromExpression subQueryFromExpression); } diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java new file mode 100644 index 000000000..99c4855f3 --- /dev/null +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java @@ -0,0 +1,107 @@ +package org.hypertrace.core.documentstore.expression.impl; + +import static org.junit.jupiter.api.Assertions.*; + +import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; +import org.hypertrace.core.documentstore.expression.operators.RelationalOperator; +import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; +import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; +import org.hypertrace.core.documentstore.query.FromClause; +import org.hypertrace.core.documentstore.query.Query; +import org.hypertrace.core.documentstore.query.SelectionSpec; + +class JoinExpressionTest { + + /* +This is the query we want to execute: +SELECT sa.suite_id, sa.vulnerability_count, sa.scan_run_number +FROM scan_analytics sa +JOIN ( + SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number + FROM scan_analytics + GROUP BY suite_id +) latest +ON sa.suite_id = latest.suite_id +AND sa.scan_run_number = latest.latest_scan_run_number; + */ + + void exampleUsage() { + // The right subquery: +// SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number +// FROM scan_analytics +// GROUP BY suite_id + Query subQuery = Query.builder() + // SELECT + .addSelection( + SelectionSpec.of(IdentifierExpression.of("suite_id"), "suite_id") + ) + .addSelection( + SelectionSpec.of( + AggregateExpression.of(AggregationOperator.MAX, IdentifierExpression.of("scan_run_number")), + "latest_scan_run_number" + )) + // FROM + .addFromClause( + TableFromExpression.builder() + .alias("ignored") // or skip if not needed + .build() + ) + // GROUP BY + .addAggregation( + IdentifierExpression.of("suite_id") + ) + .build(); + +// The main FROM side: "scan_analytics sa" + FromTypeExpression leftTable = TableFromExpression.builder() + .alias("sa") + .build(); + +// The subquery side: "(...subQuery...) latest" + FromTypeExpression rightSubQuery = SubQueryFromExpression.builder() + .subQuery(subQuery) + .alias("latest") + .build(); + +// The ON condition: +// sa.suite_id = latest.suite_id +// AND sa.scan_run_number = latest.latest_scan_run_number + FilterTypeExpression onCondition = LogicalExpression.and( + RelationalExpression.of( + IdentifierExpression.of("sa.suite_id"), + RelationalOperator.EQ, + IdentifierExpression.of("latest.suite_id") + ), + RelationalExpression.of( + IdentifierExpression.of("a.scan_run_number"), + RelationalOperator.EQ, + IdentifierExpression.of("latest.latest_scan_run_number") + ) + ); + + JoinExpression joinExpression = JoinExpression.builder() + .left(leftTable) + .right(rightSubQuery) + .joinType(JoinType.INNER) + .onCondition(onCondition) + .build(); + +// Now build the top-level Query: +// SELECT sa.suite_id, sa.vulnerability_count, sa.scan_run_number + Query mainQuery = Query.builder() + .addSelection( + SelectionSpec.of(IdentifierExpression.of("sa.suite_id"), "suite_id") + ) + .addSelection( + SelectionSpec.of(IdentifierExpression.of("sa.vulnerability_count"), "vulnerability_count") + ) + .addSelection( + SelectionSpec.of(IdentifierExpression.of("sa.scan_run_number"), "scan_run_number") + ) + .addFromClause(joinExpression) + .build(); + + System.out.println(mainQuery); + + } +} \ No newline at end of file From 26932df4d503d7405d55d8f7c362c4fc1b9968ec Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Wed, 2 Apr 2025 14:15:05 +0530 Subject: [PATCH 02/13] chore: address review comments --- .../expression/impl/JoinExpression.java | 89 +++++++++++++++++-- .../expression/impl/JoinType.java | 9 -- .../expression/impl/JoinExpressionTest.java | 4 - 3 files changed, 81 insertions(+), 21 deletions(-) delete mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinType.java diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java index 179a11f21..18beaef75 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java @@ -12,13 +12,87 @@ import org.hypertrace.core.documentstore.query.Query; /** - * Expression representing a condition for filtering + * Expression representing a join operation. * - *

Example: - * company IN ('Traceable', 'Harness') - * can be constructed as - * RelationalExpression.of( IdentifierExpression.of("company"), RelationalOperator.IN, - * ConstantExpression.ofStrings("Traceable", "Harness")))); + *

Example: In the below query,

+ * + * SELECT sa.suite_id, sa.vulnerability_count, sa.scan_run_number + * FROM scan_analytics sa + * JOIN ( + * SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number + * FROM scan_analytics + * GROUP BY suite_id + * ) latest + * ON sa.suite_id = latest.suite_id + * AND sa.scan_run_number = latest.latest_scan_run_number; + * + * + *

The join expression is

+ * + * scan_analytics sa + * JOIN ( + * SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number + * FROM scan_analytics + * GROUP BY suite_id + * ) latest + * ON sa.suite_id = latest.suite_id + * AND sa.scan_run_number = latest.latest_scan_run_number; + * + * + *

which can be constructed as

+ * + * Query subQuery = Query.builder() + * .addSelection( + * SelectionSpec.of(IdentifierExpression.of("suite_id"), "suite_id") + * ) + * .addSelection( + * SelectionSpec.of( + * AggregateExpression.of(AggregationOperator.MAX, IdentifierExpression.of("scan_run_number")), + * "latest_scan_run_number" + * )) + * .addFromClause( + * TableFromExpression.builder() + * .alias("ignored") + * .build() + * ) + * .addAggregation( + * IdentifierExpression.of("suite_id") + * ) + * .build(); + * + * // The main FROM side: "scan_analytics sa" + * FromTypeExpression leftTable = TableFromExpression.builder() + * .alias("sa") + * .build(); + * + * // The subquery side: "(...subQuery...) latest" + * FromTypeExpression rightSubQuery = SubQueryFromExpression.builder() + * .subQuery(subQuery) + * .alias("latest") + * .build(); + * + * // The ON condition: + * // sa.suite_id = latest.suite_id + * // AND sa.scan_run_number = latest.latest_scan_run_number + * FilterTypeExpression onCondition = LogicalExpression.and( + * RelationalExpression.of( + * IdentifierExpression.of("sa.suite_id"), + * RelationalOperator.EQ, + * IdentifierExpression.of("latest.suite_id") + * ), + * RelationalExpression.of( + * IdentifierExpression.of("a.scan_run_number"), + * RelationalOperator.EQ, + * IdentifierExpression.of("latest.latest_scan_run_number") + * ) + * ); + * + * JoinExpression joinExpression = JoinExpression.builder() + * .left(leftTable) + * .right(rightSubQuery) + * .joinType(JoinType.INNER) + * .onCondition(onCondition) + * .build(); * */ @Value @@ -27,8 +101,7 @@ public class JoinExpression implements FromTypeExpression { FromTypeExpression left; FromTypeExpression right; - JoinType joinType; // INNER, LEFT, RIGHT, etc. - FilterTypeExpression onCondition; // The ON clause + FilterTypeExpression onCondition; @Override public T accept(FromTypeExpressionVisitor visitor) { diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinType.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinType.java deleted file mode 100644 index 068ca5c36..000000000 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinType.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.hypertrace.core.documentstore.expression.impl; - -public enum JoinType { - INNER, - LEFT, - RIGHT, - FULL, - CROSS -} diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java index 99c4855f3..347cdb742 100644 --- a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java @@ -1,12 +1,9 @@ package org.hypertrace.core.documentstore.expression.impl; -import static org.junit.jupiter.api.Assertions.*; - import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; import org.hypertrace.core.documentstore.expression.operators.RelationalOperator; import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; -import org.hypertrace.core.documentstore.query.FromClause; import org.hypertrace.core.documentstore.query.Query; import org.hypertrace.core.documentstore.query.SelectionSpec; @@ -82,7 +79,6 @@ void exampleUsage() { JoinExpression joinExpression = JoinExpression.builder() .left(leftTable) .right(rightSubQuery) - .joinType(JoinType.INNER) .onCondition(onCondition) .build(); From 0378c9c520c774b69d5f05795c1c69755d1ea44a Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Wed, 2 Apr 2025 20:12:02 +0530 Subject: [PATCH 03/13] refactor: api changes for join expression --- .../impl/AliasedIdentifierExpression.java | 31 +++++ .../expression/impl/IdentifierExpression.java | 4 +- .../expression/impl/JoinExpression.java | 111 ------------------ ...ssion.java => SubQueryJoinExpression.java} | 9 +- .../expression/impl/TableFromExpression.java | 24 ---- .../parser/FromTypeExpressionVisitor.java | 12 +- .../expression/impl/JoinExpressionTest.java | 103 ---------------- .../impl/SubQueryJoinExpressionTest.java | 89 ++++++++++++++ 8 files changed, 133 insertions(+), 250 deletions(-) create mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java delete mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java rename document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/{SubQueryFromExpression.java => SubQueryJoinExpression.java} (59%) delete mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/TableFromExpression.java delete mode 100644 document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java create mode 100644 document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java new file mode 100644 index 000000000..9226ea8e7 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java @@ -0,0 +1,31 @@ +package org.hypertrace.core.documentstore.expression.impl; + +import com.google.common.base.Preconditions; +import lombok.Value; + +/** + * Expression representing an identifier/column name with an alias + * + *

Example: AliasedIdentifierExpression.of("col1", "col1_alias"); + */ +@Value +public class AliasedIdentifierExpression extends IdentifierExpression { + String alias; + + private AliasedIdentifierExpression(final String name, final String alias) { + super(name); + this.alias = alias; + } + + public static AliasedIdentifierExpression of(final String name, final String alias) { + Preconditions.checkArgument(name != null && !name.isBlank(), "name is null or blank"); + Preconditions.checkArgument(alias != null && !alias.isBlank(), "alias is null or blank"); + return new AliasedIdentifierExpression(name, alias); + } + + @Override + public String toString() { + return "`" + getName() + "` AS `" + getAlias() + "`"; + } + +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/IdentifierExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/IdentifierExpression.java index a9d228ee5..5f22df5b3 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/IdentifierExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/IdentifierExpression.java @@ -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; @@ -17,7 +18,8 @@ *

Example: IdentifierExpression.of("col1"); */ @Value -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NonFinal +@AllArgsConstructor(access = AccessLevel.PROTECTED) public class IdentifierExpression implements GroupTypeExpression, SelectTypeExpression, SortTypeExpression { diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java deleted file mode 100644 index 18beaef75..000000000 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/JoinExpression.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.hypertrace.core.documentstore.expression.impl; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Value; -import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; -import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; -import org.hypertrace.core.documentstore.parser.FilterTypeExpressionVisitor; -import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; -import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; -import org.hypertrace.core.documentstore.query.Query; - -/** - * Expression representing a join operation. - * - *

Example: In the below query,

- * - * SELECT sa.suite_id, sa.vulnerability_count, sa.scan_run_number - * FROM scan_analytics sa - * JOIN ( - * SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number - * FROM scan_analytics - * GROUP BY suite_id - * ) latest - * ON sa.suite_id = latest.suite_id - * AND sa.scan_run_number = latest.latest_scan_run_number; - * - * - *

The join expression is

- * - * scan_analytics sa - * JOIN ( - * SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number - * FROM scan_analytics - * GROUP BY suite_id - * ) latest - * ON sa.suite_id = latest.suite_id - * AND sa.scan_run_number = latest.latest_scan_run_number; - * - * - *

which can be constructed as

- * - * Query subQuery = Query.builder() - * .addSelection( - * SelectionSpec.of(IdentifierExpression.of("suite_id"), "suite_id") - * ) - * .addSelection( - * SelectionSpec.of( - * AggregateExpression.of(AggregationOperator.MAX, IdentifierExpression.of("scan_run_number")), - * "latest_scan_run_number" - * )) - * .addFromClause( - * TableFromExpression.builder() - * .alias("ignored") - * .build() - * ) - * .addAggregation( - * IdentifierExpression.of("suite_id") - * ) - * .build(); - * - * // The main FROM side: "scan_analytics sa" - * FromTypeExpression leftTable = TableFromExpression.builder() - * .alias("sa") - * .build(); - * - * // The subquery side: "(...subQuery...) latest" - * FromTypeExpression rightSubQuery = SubQueryFromExpression.builder() - * .subQuery(subQuery) - * .alias("latest") - * .build(); - * - * // The ON condition: - * // sa.suite_id = latest.suite_id - * // AND sa.scan_run_number = latest.latest_scan_run_number - * FilterTypeExpression onCondition = LogicalExpression.and( - * RelationalExpression.of( - * IdentifierExpression.of("sa.suite_id"), - * RelationalOperator.EQ, - * IdentifierExpression.of("latest.suite_id") - * ), - * RelationalExpression.of( - * IdentifierExpression.of("a.scan_run_number"), - * RelationalOperator.EQ, - * IdentifierExpression.of("latest.latest_scan_run_number") - * ) - * ); - * - * JoinExpression joinExpression = JoinExpression.builder() - * .left(leftTable) - * .right(rightSubQuery) - * .joinType(JoinType.INNER) - * .onCondition(onCondition) - * .build(); - * - */ -@Value -@Builder(toBuilder = true) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class JoinExpression implements FromTypeExpression { - FromTypeExpression left; - FromTypeExpression right; - FilterTypeExpression onCondition; - - @Override - public T accept(FromTypeExpressionVisitor visitor) { - return visitor.visit(this); - } -} - diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryFromExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java similarity index 59% rename from document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryFromExpression.java rename to document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java index fd08d3637..6517321d7 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryFromExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java @@ -4,19 +4,22 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Value; +import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; import org.hypertrace.core.documentstore.query.Query; /** - * Allows embedding an entire Query in the FROM clause. + * Expression representing a join operation where the right side expression is a subquery. + * Note that this currently supports a self-join only, so the collection to be joined with is implicit. */ @Value @Builder(toBuilder = true) @AllArgsConstructor(access = AccessLevel.PRIVATE) -public class SubQueryFromExpression implements FromTypeExpression { +public class SubQueryJoinExpression implements FromTypeExpression { Query subQuery; - String alias; // e.g. "rightTable" + String subQueryAlias; + FilterTypeExpression joinCondition; @Override public T accept(FromTypeExpressionVisitor visitor) { diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/TableFromExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/TableFromExpression.java deleted file mode 100644 index 244fa501c..000000000 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/TableFromExpression.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.hypertrace.core.documentstore.expression.impl; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Value; -import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; -import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; - -/** - * A simple wrapper for referencing a physical collection/table in the FROM clause. - */ -@Value -@Builder(toBuilder = true) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class TableFromExpression implements FromTypeExpression { - // Currently, this supports joins on the same table/collection only. In the future, we can also add a tableName field here to support joins across tables/collections. - String alias; // e.g. "leftTable" - - @Override - public T accept(FromTypeExpressionVisitor visitor) { - return visitor.visit(this); - } -} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java index 676ce51ca..4f5395588 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java @@ -1,16 +1,12 @@ package org.hypertrace.core.documentstore.parser; -import org.hypertrace.core.documentstore.expression.impl.JoinExpression; -import org.hypertrace.core.documentstore.expression.impl.SubQueryFromExpression; -import org.hypertrace.core.documentstore.expression.impl.TableFromExpression; +import org.hypertrace.core.documentstore.expression.impl.SubQueryJoinExpression; import org.hypertrace.core.documentstore.expression.impl.UnnestExpression; public interface FromTypeExpressionVisitor { T visit(UnnestExpression unnestExpression); - T visit(JoinExpression joinExpression); - - T visit(TableFromExpression tableFromExpression); - - T visit(SubQueryFromExpression subQueryFromExpression); + default T visit(SubQueryJoinExpression subQueryJoinExpression) { + throw new UnsupportedOperationException("This operation is not supported"); + } } diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java deleted file mode 100644 index 347cdb742..000000000 --- a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/JoinExpressionTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.hypertrace.core.documentstore.expression.impl; - -import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; -import org.hypertrace.core.documentstore.expression.operators.RelationalOperator; -import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; -import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; -import org.hypertrace.core.documentstore.query.Query; -import org.hypertrace.core.documentstore.query.SelectionSpec; - -class JoinExpressionTest { - - /* -This is the query we want to execute: -SELECT sa.suite_id, sa.vulnerability_count, sa.scan_run_number -FROM scan_analytics sa -JOIN ( - SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number - FROM scan_analytics - GROUP BY suite_id -) latest -ON sa.suite_id = latest.suite_id -AND sa.scan_run_number = latest.latest_scan_run_number; - */ - - void exampleUsage() { - // The right subquery: -// SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number -// FROM scan_analytics -// GROUP BY suite_id - Query subQuery = Query.builder() - // SELECT - .addSelection( - SelectionSpec.of(IdentifierExpression.of("suite_id"), "suite_id") - ) - .addSelection( - SelectionSpec.of( - AggregateExpression.of(AggregationOperator.MAX, IdentifierExpression.of("scan_run_number")), - "latest_scan_run_number" - )) - // FROM - .addFromClause( - TableFromExpression.builder() - .alias("ignored") // or skip if not needed - .build() - ) - // GROUP BY - .addAggregation( - IdentifierExpression.of("suite_id") - ) - .build(); - -// The main FROM side: "scan_analytics sa" - FromTypeExpression leftTable = TableFromExpression.builder() - .alias("sa") - .build(); - -// The subquery side: "(...subQuery...) latest" - FromTypeExpression rightSubQuery = SubQueryFromExpression.builder() - .subQuery(subQuery) - .alias("latest") - .build(); - -// The ON condition: -// sa.suite_id = latest.suite_id -// AND sa.scan_run_number = latest.latest_scan_run_number - FilterTypeExpression onCondition = LogicalExpression.and( - RelationalExpression.of( - IdentifierExpression.of("sa.suite_id"), - RelationalOperator.EQ, - IdentifierExpression.of("latest.suite_id") - ), - RelationalExpression.of( - IdentifierExpression.of("a.scan_run_number"), - RelationalOperator.EQ, - IdentifierExpression.of("latest.latest_scan_run_number") - ) - ); - - JoinExpression joinExpression = JoinExpression.builder() - .left(leftTable) - .right(rightSubQuery) - .onCondition(onCondition) - .build(); - -// Now build the top-level Query: -// SELECT sa.suite_id, sa.vulnerability_count, sa.scan_run_number - Query mainQuery = Query.builder() - .addSelection( - SelectionSpec.of(IdentifierExpression.of("sa.suite_id"), "suite_id") - ) - .addSelection( - SelectionSpec.of(IdentifierExpression.of("sa.vulnerability_count"), "vulnerability_count") - ) - .addSelection( - SelectionSpec.of(IdentifierExpression.of("sa.scan_run_number"), "scan_run_number") - ) - .addFromClause(joinExpression) - .build(); - - System.out.println(mainQuery); - - } -} \ No newline at end of file diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java new file mode 100644 index 000000000..332c0317b --- /dev/null +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java @@ -0,0 +1,89 @@ +package org.hypertrace.core.documentstore.expression.impl; + +import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; +import org.hypertrace.core.documentstore.expression.operators.RelationalOperator; +import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; +import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; +import org.hypertrace.core.documentstore.query.Query; +import org.hypertrace.core.documentstore.query.SelectionSpec; + +class SubQueryJoinExpressionTest { + + /* +This is the query we want to execute: +SELECT suite_id, vulnerability_count, scan_run_number +FROM +JOIN ( + SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number + FROM + GROUP BY suite_id +) latest +ON suite_id = latest.suite_id +AND scan_run_number = latest.latest_scan_run_number; + */ + + void exampleUsage() { + /* + The right subquery: + SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number + FROM + GROUP BY suite_id + */ + Query subQuery = Query.builder() + .addSelection( + SelectionSpec.of(IdentifierExpression.of("suite_id"), "suite_id") + ) + .addSelection( + SelectionSpec.of( + AggregateExpression.of(AggregationOperator.MAX, IdentifierExpression.of("scan_run_number")), + "latest_scan_run_number" + )) + .addAggregation( + IdentifierExpression.of("suite_id") + ) + .build(); + + /* + The FROM expression representing a join with the right subquery: + FROM + JOIN ( + SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number + FROM + GROUP BY suite_id + ) latest + ON suite_id = latest.suite_id + AND scan_run_number = latest.latest_scan_run_number; + */ + SubQueryJoinExpression subQueryJoinExpression = SubQueryJoinExpression.builder() + .subQuery(subQuery) + .subQueryAlias("latest") + .joinCondition( + LogicalExpression.and( + RelationalExpression.of( + IdentifierExpression.of("suite_id"), + RelationalOperator.EQ, + IdentifierExpression.of("latest.suite_id") + ), + RelationalExpression.of( + IdentifierExpression.of("scan_run_number"), + RelationalOperator.EQ, + IdentifierExpression.of("latest.latest_scan_run_number") + ) + )) + .build(); + + /* + Now build the top-level Query: + SELECT suite_id, vulnerability_count, scan_run_number FROM + */ + Query mainQuery = Query.builder() + .addSelection(IdentifierExpression.of("suite_id")) + .addSelection(IdentifierExpression.of("vulnerability_count")) + .addSelection(IdentifierExpression.of("scan_run_number")) + .addFromClause(subQueryJoinExpression) + .build(); + + System.out.println(mainQuery); + + } +} \ No newline at end of file From fb4d86deb9d02e4276972d56341833536d1e4c4e Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Wed, 2 Apr 2025 21:29:05 +0530 Subject: [PATCH 04/13] style: spotless formatting --- .../impl/AliasedIdentifierExpression.java | 1 - .../impl/SubQueryJoinExpression.java | 5 +- .../impl/SubQueryJoinExpressionTest.java | 97 +++++++++---------- 3 files changed, 47 insertions(+), 56 deletions(-) diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java index 9226ea8e7..d303ddc34 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java @@ -27,5 +27,4 @@ public static AliasedIdentifierExpression of(final String name, final String ali public String toString() { return "`" + getName() + "` AS `" + getAlias() + "`"; } - } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java index 6517321d7..fc9722114 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java @@ -10,8 +10,8 @@ import org.hypertrace.core.documentstore.query.Query; /** - * Expression representing a join operation where the right side expression is a subquery. - * Note that this currently supports a self-join only, so the collection to be joined with is implicit. + * Expression representing a join operation where the right side expression is a subquery. Note that + * this currently supports a self-join only, so the collection to be joined with is implicit. */ @Value @Builder(toBuilder = true) @@ -26,4 +26,3 @@ public T accept(FromTypeExpressionVisitor visitor) { return visitor.visit(this); } } - diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java index 332c0317b..da351a8ad 100644 --- a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java @@ -2,25 +2,23 @@ import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; import org.hypertrace.core.documentstore.expression.operators.RelationalOperator; -import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; -import org.hypertrace.core.documentstore.expression.type.FromTypeExpression; import org.hypertrace.core.documentstore.query.Query; import org.hypertrace.core.documentstore.query.SelectionSpec; class SubQueryJoinExpressionTest { /* -This is the query we want to execute: -SELECT suite_id, vulnerability_count, scan_run_number -FROM -JOIN ( - SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number - FROM - GROUP BY suite_id -) latest -ON suite_id = latest.suite_id -AND scan_run_number = latest.latest_scan_run_number; - */ + This is the query we want to execute: + SELECT suite_id, vulnerability_count, scan_run_number + FROM + JOIN ( + SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number + FROM + GROUP BY suite_id + ) latest + ON suite_id = latest.suite_id + AND scan_run_number = latest.latest_scan_run_number; + */ void exampleUsage() { /* @@ -29,20 +27,17 @@ SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number FROM GROUP BY suite_id */ - Query subQuery = Query.builder() - .addSelection( - SelectionSpec.of(IdentifierExpression.of("suite_id"), "suite_id") - ) - .addSelection( - SelectionSpec.of( - AggregateExpression.of(AggregationOperator.MAX, IdentifierExpression.of("scan_run_number")), - "latest_scan_run_number" - )) - .addAggregation( - IdentifierExpression.of("suite_id") - ) - .build(); - + Query subQuery = + Query.builder() + .addSelection(SelectionSpec.of(IdentifierExpression.of("suite_id"), "suite_id")) + .addSelection( + SelectionSpec.of( + AggregateExpression.of( + AggregationOperator.MAX, IdentifierExpression.of("scan_run_number")), + "latest_scan_run_number")) + .addAggregation(IdentifierExpression.of("suite_id")) + .build(); + /* The FROM expression representing a join with the right subquery: FROM @@ -54,36 +49,34 @@ SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number ON suite_id = latest.suite_id AND scan_run_number = latest.latest_scan_run_number; */ - SubQueryJoinExpression subQueryJoinExpression = SubQueryJoinExpression.builder() - .subQuery(subQuery) - .subQueryAlias("latest") - .joinCondition( - LogicalExpression.and( - RelationalExpression.of( - IdentifierExpression.of("suite_id"), - RelationalOperator.EQ, - IdentifierExpression.of("latest.suite_id") - ), - RelationalExpression.of( - IdentifierExpression.of("scan_run_number"), - RelationalOperator.EQ, - IdentifierExpression.of("latest.latest_scan_run_number") - ) - )) - .build(); + SubQueryJoinExpression subQueryJoinExpression = + SubQueryJoinExpression.builder() + .subQuery(subQuery) + .subQueryAlias("latest") + .joinCondition( + LogicalExpression.and( + RelationalExpression.of( + IdentifierExpression.of("suite_id"), + RelationalOperator.EQ, + IdentifierExpression.of("latest.suite_id")), + RelationalExpression.of( + IdentifierExpression.of("scan_run_number"), + RelationalOperator.EQ, + IdentifierExpression.of("latest.latest_scan_run_number")))) + .build(); /* Now build the top-level Query: SELECT suite_id, vulnerability_count, scan_run_number FROM */ - Query mainQuery = Query.builder() - .addSelection(IdentifierExpression.of("suite_id")) - .addSelection(IdentifierExpression.of("vulnerability_count")) - .addSelection(IdentifierExpression.of("scan_run_number")) - .addFromClause(subQueryJoinExpression) - .build(); + Query mainQuery = + Query.builder() + .addSelection(IdentifierExpression.of("suite_id")) + .addSelection(IdentifierExpression.of("vulnerability_count")) + .addSelection(IdentifierExpression.of("scan_run_number")) + .addFromClause(subQueryJoinExpression) + .build(); System.out.println(mainQuery); - } -} \ No newline at end of file +} From 72cbcff9a8ed83e930264b04563b7fde56c5d281 Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Tue, 8 Apr 2025 15:14:23 +0530 Subject: [PATCH 05/13] feat: implementation for sub query joins within the same collection --- .../impl/AliasedIdentifierExpression.java | 33 +++++-- .../mongo/query/MongoQueryExecutor.java | 33 ++++--- .../parser/MongoFromTypeExpressionParser.java | 96 ++++++++++++++++++- .../MongoIdentifierExpressionParser.java | 6 ++ .../query/parser/MongoLetClauseBuilder.java | 71 ++++++++++++++ .../MongoRelationalFilterParserFactory.java | 1 + .../parser/SelectTypeExpressionVisitor.java | 5 + .../impl/SubQueryJoinExpressionTest.java | 10 +- 8 files changed, 233 insertions(+), 22 deletions(-) create mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java index d303ddc34..0e45beb1c 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java @@ -17,14 +17,35 @@ private AliasedIdentifierExpression(final String name, final String alias) { this.alias = alias; } - public static AliasedIdentifierExpression of(final String name, final String alias) { - Preconditions.checkArgument(name != null && !name.isBlank(), "name is null or blank"); - Preconditions.checkArgument(alias != null && !alias.isBlank(), "alias is null or blank"); - return new AliasedIdentifierExpression(name, alias); - } - @Override public String toString() { return "`" + getName() + "` AS `" + getAlias() + "`"; } + + public static AliasedIdentifierExpressionBuilder builder() { + return new AliasedIdentifierExpressionBuilder(); + } + + public static class AliasedIdentifierExpressionBuilder { + private String name; + private String alias; + + public AliasedIdentifierExpressionBuilder name(final String name) { + this.name = name; + return this; + } + + public AliasedIdentifierExpressionBuilder alias(final String alias) { + this.alias = alias; + return this; + } + + public AliasedIdentifierExpression build() { + Preconditions.checkArgument( + this.name != null && !this.name.isBlank(), "name is null or blank"); + Preconditions.checkArgument( + this.alias != null && !this.alias.isBlank(), "alias is null or blank"); + return new AliasedIdentifierExpression(this.name, this.alias); + } + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoQueryExecutor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoQueryExecutor.java index 40890779f..adacfdf75 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoQueryExecutor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoQueryExecutor.java @@ -59,11 +59,11 @@ @Slf4j @AllArgsConstructor public class MongoQueryExecutor { - private static final List>> + private final List>> DEFAULT_AGGREGATE_PIPELINE_FUNCTIONS = List.of( query -> singleton(getFilterClause(query, Query::getFilter)), - MongoFromTypeExpressionParser::getFromClauses, + query -> new MongoFromTypeExpressionParser(this).getFromClauses(query), MongoGroupTypeExpressionParser::getGroupClauses, query -> singleton(getProjectClause(query)), query -> singleton(getFilterClause(query, Query::getAggregationFilter)), @@ -71,11 +71,11 @@ public class MongoQueryExecutor { query -> singleton(getSkipClause(query)), query -> singleton(getLimitClause(query))); - private static final List>> + private final List>> SORT_OPTIMISED_AGGREGATE_PIPELINE_FUNCTIONS = List.of( query -> singleton(getFilterClause(query, Query::getFilter)), - MongoFromTypeExpressionParser::getFromClauses, + query -> new MongoFromTypeExpressionParser(this).getFromClauses(query), query -> singleton(getNonProjectedSortClause(query)), query -> singleton(getSkipClause(query)), query -> singleton(getLimitClause(query)), @@ -157,14 +157,7 @@ public MongoCursor aggregate( Query query = transformAndLog(originalQuery); - List>> aggregatePipeline = - getAggregationPipeline(query); - - List pipeline = - aggregatePipeline.stream() - .flatMap(function -> function.apply(query).stream()) - .filter(not(BasicDBObject::isEmpty)) - .collect(toUnmodifiableList()); + List pipeline = convertToAggregatePipeline(query); logPipeline(pipeline, queryOptions); @@ -220,6 +213,22 @@ public long count(final Query originalQuery, final QueryOptions queryOptions) { return 0; } + public String getCollectionName() { + return collection.getNamespace().getCollectionName(); + } + + public List convertToAggregatePipeline(Query query) { + List>> aggregatePipeline = + getAggregationPipeline(query); + + List pipeline = + aggregatePipeline.stream() + .flatMap(function -> function.apply(query).stream()) + .filter(not(BasicDBObject::isEmpty)) + .collect(toUnmodifiableList()); + return pipeline; + } + private void logClauses( final Query query, final Bson projection, diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java index 96a0f5cef..329a74a11 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java @@ -5,7 +5,9 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import org.hypertrace.core.documentstore.expression.impl.SubQueryJoinExpression; import org.hypertrace.core.documentstore.expression.impl.UnnestExpression; +import org.hypertrace.core.documentstore.mongo.query.MongoQueryExecutor; import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; import org.hypertrace.core.documentstore.query.Query; @@ -14,10 +16,25 @@ public class MongoFromTypeExpressionParser implements FromTypeExpressionVisitor private static final String PATH_KEY = "path"; public static final String PRESERVE_NULL_AND_EMPTY_ARRAYS = "preserveNullAndEmptyArrays"; private static final String UNWIND_OPERATOR = "$unwind"; + private static final String LOOKUP_OPERATOR = "$lookup"; + private static final String MATCH_OPERATOR = "$match"; + private static final String PROJECT_OPERATOR = "$project"; + private static final String REPLACE_ROOT_OPERATOR = "$replaceRoot"; + private static final String JOINED_RESULT = "__joined_result"; + private static final String EXPR_OPERATOR = "$expr"; + private static final String AND_OPERATOR = "$and"; + private static final String EQ_OPERATOR = "$eq"; private static final MongoIdentifierPrefixingParser mongoIdentifierPrefixingParser = new MongoIdentifierPrefixingParser(new MongoIdentifierExpressionParser()); + private final MongoQueryExecutor mongoQueryExecutor; + private MongoLetClauseBuilder filterExpressionVisitor; + + public MongoFromTypeExpressionParser(MongoQueryExecutor mongoQueryExecutor) { + this.mongoQueryExecutor = mongoQueryExecutor; + } + @SuppressWarnings("unchecked") @Override public List visit(UnnestExpression unnestExpression) { @@ -42,9 +59,84 @@ public List visit(UnnestExpression unnestExpression) { return objects; } - public static List getFromClauses(final Query query) { + @SuppressWarnings("unchecked") + @Override + public List visit(SubQueryJoinExpression subQueryJoinExpression) { + this.filterExpressionVisitor = + new MongoLetClauseBuilder(subQueryJoinExpression.getSubQueryAlias()); + + List aggregatePipeline = + new ArrayList<>( + mongoQueryExecutor.convertToAggregatePipeline(subQueryJoinExpression.getSubQuery())); + + // Add the lookup stage to join the subquery results with the main collection + aggregatePipeline.add(createLookupStage(subQueryJoinExpression)); + + // Lookup Stage puts the joined results into an array field. We need to unwind that array field + // to get the joined results as separate documents. + aggregatePipeline.add(createUnwindStage()); + + // Replace root with the joined document + aggregatePipeline.add(createReplaceRootStage()); + + return aggregatePipeline; + } + + private BasicDBObject createLookupStage(SubQueryJoinExpression subQueryJoinExpression) { + BasicDBObject lookupStage = new BasicDBObject(); + BasicDBObject lookupSpec = new BasicDBObject(); + + lookupSpec.put("from", mongoQueryExecutor.getCollectionName()); + lookupSpec.put( + "let", subQueryJoinExpression.getJoinCondition().accept(filterExpressionVisitor)); + lookupSpec.put("pipeline", createLookupPipeline(subQueryJoinExpression)); + lookupSpec.put("as", JOINED_RESULT); + + lookupStage.put(LOOKUP_OPERATOR, lookupSpec); + return lookupStage; + } + + private List createLookupPipeline(SubQueryJoinExpression subQueryJoinExpression) { + List pipeline = new ArrayList<>(); + pipeline.add(createMatchStage(subQueryJoinExpression)); + // pipeline.add(createProjectStage()); + return pipeline; + } + + private BasicDBObject createMatchStage(SubQueryJoinExpression subQueryJoinExpression) { + BasicDBObject matchStage = new BasicDBObject(); + BasicDBObject expr = new BasicDBObject(); + expr.put( + EXPR_OPERATOR, + MongoFilterTypeExpressionParser.getFilterClause(subQueryJoinExpression.getJoinCondition())); + matchStage.put(MATCH_OPERATOR, expr); + return matchStage; + } + + private BasicDBObject createUnwindStage() { + BasicDBObject unwindStage = new BasicDBObject(); + BasicDBObject unwindSpec = new BasicDBObject(); + + unwindSpec.put(PATH_KEY, "$" + JOINED_RESULT); + unwindSpec.put(PRESERVE_NULL_AND_EMPTY_ARRAYS, true); + + unwindStage.put(UNWIND_OPERATOR, unwindSpec); + return unwindStage; + } + + private BasicDBObject createReplaceRootStage() { + BasicDBObject replaceRootStage = new BasicDBObject(); + BasicDBObject newRoot = new BasicDBObject(); + + newRoot.put("newRoot", "$" + JOINED_RESULT); + replaceRootStage.put(REPLACE_ROOT_OPERATOR, newRoot); + + return replaceRootStage; + } + + public List getFromClauses(final Query query) { MongoFromTypeExpressionParser mongoFromTypeExpressionParser = - new MongoFromTypeExpressionParser(); + new MongoFromTypeExpressionParser(mongoQueryExecutor); return query.getFromTypeExpressions().stream() .flatMap( fromTypeExpression -> diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java index 40d1323f0..fdfd18894 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java @@ -1,6 +1,7 @@ package org.hypertrace.core.documentstore.mongo.query.parser; import lombok.NoArgsConstructor; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; @NoArgsConstructor @@ -16,6 +17,11 @@ public String visit(final IdentifierExpression expression) { return parse(expression); } + @Override + public String visit(final AliasedIdentifierExpression expression) { + return "$" + parse(expression); + } + String parse(final IdentifierExpression expression) { return expression.getName(); } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java new file mode 100644 index 000000000..9c03dc2fc --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java @@ -0,0 +1,71 @@ +package org.hypertrace.core.documentstore.mongo.query.parser; + +import com.mongodb.BasicDBObject; +import java.util.Map; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.expression.impl.ArrayRelationalFilterExpression; +import org.hypertrace.core.documentstore.expression.impl.DocumentArrayFilterExpression; +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.type.FilterTypeExpression; +import org.hypertrace.core.documentstore.parser.FilterTypeExpressionVisitor; + +// Visitor for building the let clause in lookup stage from filter expressions +class MongoLetClauseBuilder implements FilterTypeExpressionVisitor { + + private final String subQueryAlias; + + MongoLetClauseBuilder(String subQueryAlias) { + this.subQueryAlias = subQueryAlias; + } + + @Override + public BasicDBObject visit(LogicalExpression expression) { + BasicDBObject letClause = new BasicDBObject(); + for (FilterTypeExpression operand : expression.getOperands()) { + letClause.putAll((Map) operand.accept(this)); + } + return letClause; + } + + @Override + public BasicDBObject visit(RelationalExpression expression) { + BasicDBObject letClause = new BasicDBObject(); + if (expression.getLhs() instanceof AliasedIdentifierExpression) { + letClause.putAll( + (Map) + createLetClause((AliasedIdentifierExpression) expression.getLhs(), subQueryAlias)); + } + if (expression.getRhs() instanceof AliasedIdentifierExpression) { + letClause.putAll( + (Map) + createLetClause((AliasedIdentifierExpression) expression.getRhs(), subQueryAlias)); + } + return letClause; + } + + @Override + public BasicDBObject visit(KeyExpression expression) { + return new BasicDBObject(); + } + + @Override + public BasicDBObject visit(ArrayRelationalFilterExpression expression) { + return new BasicDBObject(); + } + + @Override + public BasicDBObject visit(DocumentArrayFilterExpression expression) { + return new BasicDBObject(); + } + + private BasicDBObject createLetClause( + AliasedIdentifierExpression aliasedExpression, String subQueryAlias) { + BasicDBObject letClause = new BasicDBObject(); + if (aliasedExpression.getAlias().equals(subQueryAlias)) { + letClause.put(aliasedExpression.getName(), "$" + aliasedExpression.getName()); + } + return letClause; + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactory.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactory.java index 921fd8ded..727ec091f 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactory.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactory.java @@ -24,6 +24,7 @@ class MongoRelationalFilterContext { @Default FilterLocation location = OUTSIDE_EXPR; @Default MongoSelectTypeExpressionParser lhsParser = new MongoIdentifierExpressionParser(); + // FIXME: how to parse AliasedIdentifierExpression / IdentifierExpression in rhs? @Default MongoSelectTypeExpressionParser rhsParser = new MongoConstantExpressionParser(); } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java index 45890c733..a662062c8 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java @@ -1,6 +1,7 @@ package org.hypertrace.core.documentstore.parser; 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.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; @@ -16,4 +17,8 @@ public interface SelectTypeExpressionVisitor { T visit(final FunctionExpression expression); T visit(final IdentifierExpression expression); + + default T visit(final AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException(); + } } diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java index da351a8ad..babc2efb6 100644 --- a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java +++ b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java @@ -58,11 +58,17 @@ SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number RelationalExpression.of( IdentifierExpression.of("suite_id"), RelationalOperator.EQ, - IdentifierExpression.of("latest.suite_id")), + AliasedIdentifierExpression.builder() + .name("suite_id") + .alias("latest") + .build()), RelationalExpression.of( IdentifierExpression.of("scan_run_number"), RelationalOperator.EQ, - IdentifierExpression.of("latest.latest_scan_run_number")))) + AliasedIdentifierExpression.builder() + .name("latest_scan_run_number") + .alias("latest") + .build()))) .build(); /* From ca4d5f886c3e2ca20ce992522d67452822b4b48e Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Thu, 10 Apr 2025 02:07:40 +0530 Subject: [PATCH 06/13] chore: complete the impl for sub query self join --- .../impl/AliasedIdentifierExpression.java | 8 ++- .../impl/SubQueryJoinExpression.java | 5 ++ ...ongoAliasedIdentifierExpressionParser.java | 22 ++++++ .../MongoDollarPrefixingIdempotentParser.java | 10 +++ .../parser/MongoFromTypeExpressionParser.java | 67 ++++++++++++------- .../MongoIdentifierExpressionParser.java | 6 -- .../query/parser/MongoLetClauseBuilder.java | 51 +++++++++++--- .../MongoSelectTypeExpressionParser.java | 6 ++ 8 files changed, 136 insertions(+), 39 deletions(-) create mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java index 0e45beb1c..c8ea82ce5 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java @@ -2,6 +2,7 @@ import com.google.common.base.Preconditions; import lombok.Value; +import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; /** * Expression representing an identifier/column name with an alias @@ -17,9 +18,14 @@ private AliasedIdentifierExpression(final String name, final String alias) { this.alias = alias; } + @Override + public T accept(final SelectTypeExpressionVisitor visitor) { + return visitor.visit(this); + } + @Override public String toString() { - return "`" + getName() + "` AS `" + getAlias() + "`"; + return "`" + getAlias() + "." + getName() + "`"; } public static AliasedIdentifierExpressionBuilder builder() { diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java index fc9722114..ccdea82f2 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java @@ -25,4 +25,9 @@ public class SubQueryJoinExpression implements FromTypeExpression { public T accept(FromTypeExpressionVisitor visitor) { return visitor.visit(this); } + + @Override + public String toString() { + return String.format("JOIN (%s) AS %s ON %s", subQuery, subQueryAlias, joinCondition); + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java new file mode 100644 index 000000000..cab8a7841 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java @@ -0,0 +1,22 @@ +package org.hypertrace.core.documentstore.mongo.query.parser; + +import lombok.NoArgsConstructor; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; + +@NoArgsConstructor +public final class MongoAliasedIdentifierExpressionParser extends MongoSelectTypeExpressionParser { + + MongoAliasedIdentifierExpressionParser(final MongoSelectTypeExpressionParser baseParser) { + super(baseParser); + } + + @SuppressWarnings("unchecked") + @Override + public String visit(final AliasedIdentifierExpression expression) { + return "$" + parse(expression); + } + + String parse(final AliasedIdentifierExpression expression) { + return expression.getName(); + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoDollarPrefixingIdempotentParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoDollarPrefixingIdempotentParser.java index b2dbbbdbf..8bfa6330f 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoDollarPrefixingIdempotentParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoDollarPrefixingIdempotentParser.java @@ -3,6 +3,7 @@ import static org.hypertrace.core.documentstore.mongo.MongoUtils.PREFIX; import java.util.Optional; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; final class MongoDollarPrefixingIdempotentParser extends MongoSelectTypeExpressionParser { @@ -19,6 +20,15 @@ public String visit(final IdentifierExpression expression) { .orElse(null); } + @SuppressWarnings("unchecked") + @Override + public String visit(final AliasedIdentifierExpression expression) { + return Optional.ofNullable(baseParser.visit(expression)) + .map(Object::toString) + .map(identifier -> PREFIX + identifier) + .orElse(null); + } + private String idempotentPrefix(final String identifier) { return identifier.startsWith(PREFIX) ? identifier : PREFIX + identifier; } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java index 329a74a11..ba752ee18 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java @@ -7,7 +7,13 @@ import java.util.stream.Collectors; import org.hypertrace.core.documentstore.expression.impl.SubQueryJoinExpression; import org.hypertrace.core.documentstore.expression.impl.UnnestExpression; +import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; +import org.hypertrace.core.documentstore.mongo.MongoUtils; import org.hypertrace.core.documentstore.mongo.query.MongoQueryExecutor; +import org.hypertrace.core.documentstore.mongo.query.parser.filter.MongoRelationalFilterParserFactory.FilterLocation; +import org.hypertrace.core.documentstore.mongo.query.parser.filter.MongoRelationalFilterParserFactory.MongoRelationalFilterContext; +import org.hypertrace.core.documentstore.mongo.query.transformer.MongoQueryTransformer; +import org.hypertrace.core.documentstore.parser.FilterTypeExpressionVisitor; import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; import org.hypertrace.core.documentstore.query.Query; @@ -18,18 +24,14 @@ public class MongoFromTypeExpressionParser implements FromTypeExpressionVisitor private static final String UNWIND_OPERATOR = "$unwind"; private static final String LOOKUP_OPERATOR = "$lookup"; private static final String MATCH_OPERATOR = "$match"; - private static final String PROJECT_OPERATOR = "$project"; private static final String REPLACE_ROOT_OPERATOR = "$replaceRoot"; - private static final String JOINED_RESULT = "__joined_result"; private static final String EXPR_OPERATOR = "$expr"; - private static final String AND_OPERATOR = "$and"; - private static final String EQ_OPERATOR = "$eq"; private static final MongoIdentifierPrefixingParser mongoIdentifierPrefixingParser = new MongoIdentifierPrefixingParser(new MongoIdentifierExpressionParser()); private final MongoQueryExecutor mongoQueryExecutor; - private MongoLetClauseBuilder filterExpressionVisitor; + private MongoLetClauseBuilder mongoLetClauseBuilder; public MongoFromTypeExpressionParser(MongoQueryExecutor mongoQueryExecutor) { this.mongoQueryExecutor = mongoQueryExecutor; @@ -62,35 +64,38 @@ public List visit(UnnestExpression unnestExpression) { @SuppressWarnings("unchecked") @Override public List visit(SubQueryJoinExpression subQueryJoinExpression) { - this.filterExpressionVisitor = + this.mongoLetClauseBuilder = new MongoLetClauseBuilder(subQueryJoinExpression.getSubQueryAlias()); + Query transformedSubQuery = + MongoQueryTransformer.transform(subQueryJoinExpression.getSubQuery()); List aggregatePipeline = - new ArrayList<>( - mongoQueryExecutor.convertToAggregatePipeline(subQueryJoinExpression.getSubQuery())); + new ArrayList<>(mongoQueryExecutor.convertToAggregatePipeline(transformedSubQuery)); + String joinedResultFieldName = + getJoinedResultFieldName(subQueryJoinExpression.getSubQueryAlias()); // Add the lookup stage to join the subquery results with the main collection - aggregatePipeline.add(createLookupStage(subQueryJoinExpression)); + aggregatePipeline.add(createLookupStage(subQueryJoinExpression, joinedResultFieldName)); // Lookup Stage puts the joined results into an array field. We need to unwind that array field // to get the joined results as separate documents. - aggregatePipeline.add(createUnwindStage()); + aggregatePipeline.add(createUnwindStage(joinedResultFieldName)); // Replace root with the joined document - aggregatePipeline.add(createReplaceRootStage()); + aggregatePipeline.add(createReplaceRootStage(joinedResultFieldName)); return aggregatePipeline; } - private BasicDBObject createLookupStage(SubQueryJoinExpression subQueryJoinExpression) { + private BasicDBObject createLookupStage( + SubQueryJoinExpression subQueryJoinExpression, String joinedResultFieldName) { BasicDBObject lookupStage = new BasicDBObject(); BasicDBObject lookupSpec = new BasicDBObject(); lookupSpec.put("from", mongoQueryExecutor.getCollectionName()); - lookupSpec.put( - "let", subQueryJoinExpression.getJoinCondition().accept(filterExpressionVisitor)); + lookupSpec.put("let", subQueryJoinExpression.getJoinCondition().accept(mongoLetClauseBuilder)); lookupSpec.put("pipeline", createLookupPipeline(subQueryJoinExpression)); - lookupSpec.put("as", JOINED_RESULT); + lookupSpec.put("as", joinedResultFieldName); lookupStage.put(LOOKUP_OPERATOR, lookupSpec); return lookupStage; @@ -99,36 +104,52 @@ private BasicDBObject createLookupStage(SubQueryJoinExpression subQueryJoinExpre private List createLookupPipeline(SubQueryJoinExpression subQueryJoinExpression) { List pipeline = new ArrayList<>(); pipeline.add(createMatchStage(subQueryJoinExpression)); - // pipeline.add(createProjectStage()); return pipeline; } private BasicDBObject createMatchStage(SubQueryJoinExpression subQueryJoinExpression) { BasicDBObject matchStage = new BasicDBObject(); BasicDBObject expr = new BasicDBObject(); - expr.put( - EXPR_OPERATOR, - MongoFilterTypeExpressionParser.getFilterClause(subQueryJoinExpression.getJoinCondition())); + expr.put(EXPR_OPERATOR, getFilterClause(subQueryJoinExpression.getJoinCondition())); matchStage.put(MATCH_OPERATOR, expr); return matchStage; } - private BasicDBObject createUnwindStage() { + private BasicDBObject getFilterClause(FilterTypeExpression joinCondition) { + final FilterTypeExpressionVisitor parser = + new MongoFilterTypeExpressionParser( + MongoRelationalFilterContext.builder() + .location(FilterLocation.INSIDE_EXPR) + .lhsParser( + new MongoDollarPrefixingIdempotentParser(new MongoIdentifierExpressionParser())) + .rhsParser( + new MongoDollarPrefixingIdempotentParser( + new MongoAliasedIdentifierExpressionParser())) + .build()); + final Map filter = joinCondition.accept(parser); + return new BasicDBObject(filter); + } + + private String getJoinedResultFieldName(String subQueryAlias) { + return "__joined_result_with_" + subQueryAlias; + } + + private BasicDBObject createUnwindStage(String joinedResultFieldName) { BasicDBObject unwindStage = new BasicDBObject(); BasicDBObject unwindSpec = new BasicDBObject(); - unwindSpec.put(PATH_KEY, "$" + JOINED_RESULT); + unwindSpec.put(PATH_KEY, MongoUtils.PREFIX + joinedResultFieldName); unwindSpec.put(PRESERVE_NULL_AND_EMPTY_ARRAYS, true); unwindStage.put(UNWIND_OPERATOR, unwindSpec); return unwindStage; } - private BasicDBObject createReplaceRootStage() { + private BasicDBObject createReplaceRootStage(String joinedResultFieldName) { BasicDBObject replaceRootStage = new BasicDBObject(); BasicDBObject newRoot = new BasicDBObject(); - newRoot.put("newRoot", "$" + JOINED_RESULT); + newRoot.put("newRoot", MongoUtils.PREFIX + joinedResultFieldName); replaceRootStage.put(REPLACE_ROOT_OPERATOR, newRoot); return replaceRootStage; diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java index fdfd18894..40d1323f0 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java @@ -1,7 +1,6 @@ package org.hypertrace.core.documentstore.mongo.query.parser; import lombok.NoArgsConstructor; -import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; @NoArgsConstructor @@ -17,11 +16,6 @@ public String visit(final IdentifierExpression expression) { return parse(expression); } - @Override - public String visit(final AliasedIdentifierExpression expression) { - return "$" + parse(expression); - } - String parse(final IdentifierExpression expression) { return expression.getName(); } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java index 9c03dc2fc..c5349029b 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java @@ -2,14 +2,19 @@ import com.mongodb.BasicDBObject; import java.util.Map; +import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.ArrayRelationalFilterExpression; +import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; import org.hypertrace.core.documentstore.expression.impl.DocumentArrayFilterExpression; +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.type.FilterTypeExpression; import org.hypertrace.core.documentstore.parser.FilterTypeExpressionVisitor; +import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; // Visitor for building the let clause in lookup stage from filter expressions class MongoLetClauseBuilder implements FilterTypeExpressionVisitor { @@ -32,17 +37,45 @@ public BasicDBObject visit(LogicalExpression expression) { @Override public BasicDBObject visit(RelationalExpression expression) { BasicDBObject letClause = new BasicDBObject(); - if (expression.getLhs() instanceof AliasedIdentifierExpression) { - letClause.putAll( - (Map) - createLetClause((AliasedIdentifierExpression) expression.getLhs(), subQueryAlias)); + letClause.putAll( + (Map) + expression.getLhs().accept(new MongoLetClauseSelectTypeExpressionVisitor())); + letClause.putAll( + (Map) + expression.getRhs().accept(new MongoLetClauseSelectTypeExpressionVisitor())); + return letClause; + } + + private class MongoLetClauseSelectTypeExpressionVisitor implements SelectTypeExpressionVisitor { + @Override + public BasicDBObject visit(AggregateExpression expression) { + return new BasicDBObject(); } - if (expression.getRhs() instanceof AliasedIdentifierExpression) { - letClause.putAll( - (Map) - createLetClause((AliasedIdentifierExpression) expression.getRhs(), subQueryAlias)); + + @Override + public BasicDBObject visit(ConstantExpression expression) { + return new BasicDBObject(); + } + + @Override + public BasicDBObject visit(ConstantExpression.DocumentConstantExpression expression) { + return new BasicDBObject(); + } + + @Override + public BasicDBObject visit(FunctionExpression expression) { + return new BasicDBObject(); + } + + @Override + public BasicDBObject visit(IdentifierExpression expression) { + return new BasicDBObject(); + } + + @Override + public BasicDBObject visit(AliasedIdentifierExpression expression) { + return createLetClause(expression, subQueryAlias); } - return letClause; } @Override diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoSelectTypeExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoSelectTypeExpressionParser.java index 3a561fc04..2cf002623 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoSelectTypeExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoSelectTypeExpressionParser.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; 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.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; @@ -59,6 +60,11 @@ public T visit(final IdentifierExpression expression) { return baseParser.visit(expression); } + @Override + public T visit(final AliasedIdentifierExpression expression) { + return baseParser.visit(expression); + } + public static BasicDBObject getSelections(final Query query) { List selectionSpecs = query.getSelections(); MongoSelectTypeExpressionParser parser = From 986c619a03bab0fc0e04fa97f6f615296427934f Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Thu, 10 Apr 2025 02:42:35 +0530 Subject: [PATCH 07/13] test: add integration test for self join with sub-query --- .../documentstore/DocStoreQueryV1Test.java | 91 +++++++++++++++++++ .../self_join_with_sub_query_response.json | 6 ++ .../impl/SubQueryJoinExpressionTest.java | 88 ------------------ 3 files changed, 97 insertions(+), 88 deletions(-) create mode 100644 document-store/src/integrationTest/resources/query/self_join_with_sub_query_response.json delete mode 100644 document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java diff --git a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java index f2527bb0f..2a7ea0d13 100644 --- a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java +++ b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java @@ -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; @@ -3478,6 +3481,94 @@ 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 + JOIN ( + SELECT item, MAX(date) AS latest_date + FROM + 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 + 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 + JOIN ( + SELECT item, MAX(date) AS latest_date + FROM + 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") + .alias("latest") + .build()), + RelationalExpression.of( + IdentifierExpression.of("date"), + RelationalOperator.EQ, + AliasedIdentifierExpression.builder() + .name("latest_date") + .alias("latest") + .build()))) + .build(); + + /* + Now build the top-level Query: + SELECT item, quantity, date FROM 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 iterator = collection.aggregate(mainQuery); + assertDocsAndSizeEqual(dataStoreName, iterator, "self_join_with_sub_query_response.json", 4); + } + private static Collection getCollection(final String dataStoreName) { return getCollection(dataStoreName, COLLECTION_NAME); } diff --git a/document-store/src/integrationTest/resources/query/self_join_with_sub_query_response.json b/document-store/src/integrationTest/resources/query/self_join_with_sub_query_response.json new file mode 100644 index 000000000..f62402751 --- /dev/null +++ b/document-store/src/integrationTest/resources/query/self_join_with_sub_query_response.json @@ -0,0 +1,6 @@ +[ + {"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} +] \ No newline at end of file diff --git a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java b/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java deleted file mode 100644 index babc2efb6..000000000 --- a/document-store/src/test/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpressionTest.java +++ /dev/null @@ -1,88 +0,0 @@ -package org.hypertrace.core.documentstore.expression.impl; - -import org.hypertrace.core.documentstore.expression.operators.AggregationOperator; -import org.hypertrace.core.documentstore.expression.operators.RelationalOperator; -import org.hypertrace.core.documentstore.query.Query; -import org.hypertrace.core.documentstore.query.SelectionSpec; - -class SubQueryJoinExpressionTest { - - /* - This is the query we want to execute: - SELECT suite_id, vulnerability_count, scan_run_number - FROM - JOIN ( - SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number - FROM - GROUP BY suite_id - ) latest - ON suite_id = latest.suite_id - AND scan_run_number = latest.latest_scan_run_number; - */ - - void exampleUsage() { - /* - The right subquery: - SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number - FROM - GROUP BY suite_id - */ - Query subQuery = - Query.builder() - .addSelection(SelectionSpec.of(IdentifierExpression.of("suite_id"), "suite_id")) - .addSelection( - SelectionSpec.of( - AggregateExpression.of( - AggregationOperator.MAX, IdentifierExpression.of("scan_run_number")), - "latest_scan_run_number")) - .addAggregation(IdentifierExpression.of("suite_id")) - .build(); - - /* - The FROM expression representing a join with the right subquery: - FROM - JOIN ( - SELECT suite_id, MAX(scan_run_number) AS latest_scan_run_number - FROM - GROUP BY suite_id - ) latest - ON suite_id = latest.suite_id - AND scan_run_number = latest.latest_scan_run_number; - */ - SubQueryJoinExpression subQueryJoinExpression = - SubQueryJoinExpression.builder() - .subQuery(subQuery) - .subQueryAlias("latest") - .joinCondition( - LogicalExpression.and( - RelationalExpression.of( - IdentifierExpression.of("suite_id"), - RelationalOperator.EQ, - AliasedIdentifierExpression.builder() - .name("suite_id") - .alias("latest") - .build()), - RelationalExpression.of( - IdentifierExpression.of("scan_run_number"), - RelationalOperator.EQ, - AliasedIdentifierExpression.builder() - .name("latest_scan_run_number") - .alias("latest") - .build()))) - .build(); - - /* - Now build the top-level Query: - SELECT suite_id, vulnerability_count, scan_run_number FROM - */ - Query mainQuery = - Query.builder() - .addSelection(IdentifierExpression.of("suite_id")) - .addSelection(IdentifierExpression.of("vulnerability_count")) - .addSelection(IdentifierExpression.of("scan_run_number")) - .addFromClause(subQueryJoinExpression) - .build(); - - System.out.println(mainQuery); - } -} From a774807e2befd84903852c45265f0a36ae19b587 Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Thu, 10 Apr 2025 11:46:35 +0530 Subject: [PATCH 08/13] chore: cleaned up code and added docs --- .../documentstore/DocStoreQueryV1Test.java | 8 ++--- .../impl/AliasedIdentifierExpression.java | 34 +++++++++++++------ .../impl/SubQueryJoinExpression.java | 3 ++ ...ongoAliasedIdentifierExpressionParser.java | 7 ++-- .../query/parser/MongoLetClauseBuilder.java | 2 +- .../MongoRelationalFilterParserFactory.java | 1 - .../parser/FromTypeExpressionVisitor.java | 3 ++ 7 files changed, 35 insertions(+), 23 deletions(-) diff --git a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java index 2a7ea0d13..ccf3a91cb 100644 --- a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java +++ b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java @@ -3508,9 +3508,7 @@ SELECT item, MAX(date) AS latest_date */ Query subQuery = Query.builder() - .addSelection( - SelectionSpec.of( - IdentifierExpression.of("item"))) + .addSelection(SelectionSpec.of(IdentifierExpression.of("item"))) .addSelection( SelectionSpec.of( AggregateExpression.of( @@ -3541,14 +3539,14 @@ SELECT item, MAX(date) AS latest_date RelationalOperator.EQ, AliasedIdentifierExpression.builder() .name("item") - .alias("latest") + .contextAlias("latest") .build()), RelationalExpression.of( IdentifierExpression.of("date"), RelationalOperator.EQ, AliasedIdentifierExpression.builder() .name("latest_date") - .alias("latest") + .contextAlias("latest") .build()))) .build(); diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java index c8ea82ce5..78e9269ef 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/AliasedIdentifierExpression.java @@ -5,17 +5,28 @@ import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; /** - * Expression representing an identifier/column name with an alias + * Expression for referencing an identifier/column name within a context having an alias. * - *

Example: AliasedIdentifierExpression.of("col1", "col1_alias"); + *

Example: In this query: + * SELECT item, quantity, date + * FROM + * JOIN ( + * SELECT item, MAX(date) AS latest_date + * FROM + * GROUP BY item + * ) AS latest + * ON item = latest.item + * ORDER BY `item` ASC; + * the rhs of the join condition "latest.item" can be expressed as: + * AliasedIdentifierExpression.builder().name("item").alias("alias1").build() */ @Value public class AliasedIdentifierExpression extends IdentifierExpression { - String alias; + String contextAlias; - private AliasedIdentifierExpression(final String name, final String alias) { + private AliasedIdentifierExpression(final String name, final String contextAlias) { super(name); - this.alias = alias; + this.contextAlias = contextAlias; } @Override @@ -25,7 +36,7 @@ public T accept(final SelectTypeExpressionVisitor visitor) { @Override public String toString() { - return "`" + getAlias() + "." + getName() + "`"; + return "`" + getContextAlias() + "." + getName() + "`"; } public static AliasedIdentifierExpressionBuilder builder() { @@ -34,15 +45,15 @@ public static AliasedIdentifierExpressionBuilder builder() { public static class AliasedIdentifierExpressionBuilder { private String name; - private String alias; + private String contextAlias; public AliasedIdentifierExpressionBuilder name(final String name) { this.name = name; return this; } - public AliasedIdentifierExpressionBuilder alias(final String alias) { - this.alias = alias; + public AliasedIdentifierExpressionBuilder contextAlias(final String contextAlias) { + this.contextAlias = contextAlias; return this; } @@ -50,8 +61,9 @@ public AliasedIdentifierExpression build() { Preconditions.checkArgument( this.name != null && !this.name.isBlank(), "name is null or blank"); Preconditions.checkArgument( - this.alias != null && !this.alias.isBlank(), "alias is null or blank"); - return new AliasedIdentifierExpression(this.name, this.alias); + this.contextAlias != null && !this.contextAlias.isBlank(), + "contextAlias is null or blank"); + return new AliasedIdentifierExpression(this.name, this.contextAlias); } } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java index ccdea82f2..81a3d2f9d 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java @@ -12,6 +12,9 @@ /** * Expression representing a join operation where the right side expression is a subquery. Note that * this currently supports a self-join only, so the collection to be joined with is implicit. + * + *

For an example of using this expression, see the testSelfJoinWithSubQuery method in + * DocStoreQueryV1Test. */ @Value @Builder(toBuilder = true) diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java index cab8a7841..9843edd33 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java @@ -2,18 +2,15 @@ import lombok.NoArgsConstructor; import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; +import org.hypertrace.core.documentstore.mongo.MongoUtils; @NoArgsConstructor public final class MongoAliasedIdentifierExpressionParser extends MongoSelectTypeExpressionParser { - MongoAliasedIdentifierExpressionParser(final MongoSelectTypeExpressionParser baseParser) { - super(baseParser); - } - @SuppressWarnings("unchecked") @Override public String visit(final AliasedIdentifierExpression expression) { - return "$" + parse(expression); + return MongoUtils.PREFIX + parse(expression); } String parse(final AliasedIdentifierExpression expression) { diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java index c5349029b..0771de610 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java @@ -96,7 +96,7 @@ public BasicDBObject visit(DocumentArrayFilterExpression expression) { private BasicDBObject createLetClause( AliasedIdentifierExpression aliasedExpression, String subQueryAlias) { BasicDBObject letClause = new BasicDBObject(); - if (aliasedExpression.getAlias().equals(subQueryAlias)) { + if (aliasedExpression.getContextAlias().equals(subQueryAlias)) { letClause.put(aliasedExpression.getName(), "$" + aliasedExpression.getName()); } return letClause; diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactory.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactory.java index 727ec091f..921fd8ded 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactory.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/filter/MongoRelationalFilterParserFactory.java @@ -24,7 +24,6 @@ class MongoRelationalFilterContext { @Default FilterLocation location = OUTSIDE_EXPR; @Default MongoSelectTypeExpressionParser lhsParser = new MongoIdentifierExpressionParser(); - // FIXME: how to parse AliasedIdentifierExpression / IdentifierExpression in rhs? @Default MongoSelectTypeExpressionParser rhsParser = new MongoConstantExpressionParser(); } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java index 4f5395588..bb4116dd4 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java @@ -6,6 +6,9 @@ public interface FromTypeExpressionVisitor { T visit(UnnestExpression unnestExpression); + /* + * Subquery join expression is not supported by default. Override this method to support it. + */ default T visit(SubQueryJoinExpression subQueryJoinExpression) { throw new UnsupportedOperationException("This operation is not supported"); } From a72ddf6d976c7dee0169e8355106f2fb2c7151b6 Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Thu, 10 Apr 2025 17:54:37 +0530 Subject: [PATCH 09/13] chore: address review comments --- .../documentstore/DocStoreQueryV1Test.java | 3 +- .../self_join_with_sub_query_response.json | 26 +++- .../impl/SubQueryJoinExpression.java | 2 +- .../MongoAggregationPipelineConverter.java | 147 ++++++++++++++++++ .../mongo/query/MongoQueryExecutor.java | 144 ++--------------- ...ongoAliasedIdentifierExpressionParser.java | 19 --- .../parser/MongoFromTypeExpressionParser.java | 44 +++--- .../MongoIdentifierExpressionParser.java | 12 ++ .../query/parser/MongoLetClauseBuilder.java | 78 +++++----- 9 files changed, 260 insertions(+), 215 deletions(-) create mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoAggregationPipelineConverter.java delete mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java diff --git a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java index ccf3a91cb..2e2b54e66 100644 --- a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java +++ b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java @@ -3564,7 +3564,8 @@ SELECT item, MAX(date) AS latest_date .build(); Iterator iterator = collection.aggregate(mainQuery); - assertDocsAndSizeEqual(dataStoreName, iterator, "self_join_with_sub_query_response.json", 4); + assertDocsAndSizeEqual( + dataStoreName, iterator, "query/self_join_with_sub_query_response.json", 4); } private static Collection getCollection(final String dataStoreName) { diff --git a/document-store/src/integrationTest/resources/query/self_join_with_sub_query_response.json b/document-store/src/integrationTest/resources/query/self_join_with_sub_query_response.json index f62402751..f68626412 100644 --- a/document-store/src/integrationTest/resources/query/self_join_with_sub_query_response.json +++ b/document-store/src/integrationTest/resources/query/self_join_with_sub_query_response.json @@ -1,6 +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} -] \ No newline at end of file + { + "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 + } +] diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java index 81a3d2f9d..46a0de104 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/expression/impl/SubQueryJoinExpression.java @@ -31,6 +31,6 @@ public T accept(FromTypeExpressionVisitor visitor) { @Override public String toString() { - return String.format("JOIN (%s) AS %s ON %s", subQuery, subQueryAlias, joinCondition); + return String.format("JOIN (%s) AS %s ON (%s)", subQuery, subQueryAlias, joinCondition); } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoAggregationPipelineConverter.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoAggregationPipelineConverter.java new file mode 100644 index 000000000..dcad498fc --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoAggregationPipelineConverter.java @@ -0,0 +1,147 @@ +package org.hypertrace.core.documentstore.mongo.query; + +import static java.util.Collections.singleton; +import static java.util.function.Function.identity; +import static java.util.function.Predicate.not; +import static java.util.stream.Collectors.toUnmodifiableList; +import static org.hypertrace.core.documentstore.mongo.query.MongoPaginationHelper.getLimitClause; +import static org.hypertrace.core.documentstore.mongo.query.MongoPaginationHelper.getSkipClause; +import static org.hypertrace.core.documentstore.mongo.query.parser.MongoFilterTypeExpressionParser.getFilterClause; +import static org.hypertrace.core.documentstore.mongo.query.parser.MongoNonProjectedSortTypeExpressionParser.getNonProjectedSortClause; +import static org.hypertrace.core.documentstore.mongo.query.parser.MongoSelectTypeExpressionParser.getProjectClause; +import static org.hypertrace.core.documentstore.mongo.query.parser.MongoSortTypeExpressionParser.getSortClause; + +import com.mongodb.BasicDBObject; +import com.mongodb.client.MongoCollection; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hypertrace.core.documentstore.model.config.AggregatePipelineMode; +import org.hypertrace.core.documentstore.mongo.query.parser.AliasParser; +import org.hypertrace.core.documentstore.mongo.query.parser.MongoFromTypeExpressionParser; +import org.hypertrace.core.documentstore.mongo.query.parser.MongoGroupTypeExpressionParser; +import org.hypertrace.core.documentstore.parser.AggregateExpressionChecker; +import org.hypertrace.core.documentstore.parser.FunctionExpressionChecker; +import org.hypertrace.core.documentstore.query.Query; +import org.hypertrace.core.documentstore.query.SelectionSpec; +import org.hypertrace.core.documentstore.query.SortingSpec; + +@Slf4j +@AllArgsConstructor +public class MongoAggregationPipelineConverter { + private final AggregatePipelineMode aggregationPipelineMode; + private final MongoCollection collection; + + private final List>> + DEFAULT_AGGREGATE_PIPELINE_FUNCTIONS = + List.of( + query -> singleton(getFilterClause(query, Query::getFilter)), + query -> new MongoFromTypeExpressionParser(this).getFromClauses(query), + MongoGroupTypeExpressionParser::getGroupClauses, + query -> singleton(getProjectClause(query)), + query -> singleton(getFilterClause(query, Query::getAggregationFilter)), + query -> singleton(getSortClause(query)), + query -> singleton(getSkipClause(query)), + query -> singleton(getLimitClause(query))); + + private final List>> + SORT_OPTIMISED_AGGREGATE_PIPELINE_FUNCTIONS = + List.of( + query -> singleton(getFilterClause(query, Query::getFilter)), + query -> new MongoFromTypeExpressionParser(this).getFromClauses(query), + query -> singleton(getNonProjectedSortClause(query)), + query -> singleton(getSkipClause(query)), + query -> singleton(getLimitClause(query)), + query -> singleton(getProjectClause(query))); + + public String getCollectionName() { + return collection.getNamespace().getCollectionName(); + } + + public List convertToAggregatePipeline(Query query) { + List>> aggregatePipeline = + getAggregationPipeline(query); + + List pipeline = + aggregatePipeline.stream() + .flatMap(function -> function.apply(query).stream()) + .filter(not(BasicDBObject::isEmpty)) + .collect(toUnmodifiableList()); + return pipeline; + } + + private List>> getAggregationPipeline(Query query) { + List>> aggregatePipeline = + DEFAULT_AGGREGATE_PIPELINE_FUNCTIONS; + if (aggregationPipelineMode.equals(AggregatePipelineMode.SORT_OPTIMIZED_IF_POSSIBLE) + && query.getAggregations().isEmpty() + && query.getAggregationFilter().isEmpty() + && !isProjectionContainsAggregation(query) + && !isSortContainsAggregation(query)) { + log.debug("Using sort optimized aggregate pipeline functions for query: {}", query); + aggregatePipeline = SORT_OPTIMISED_AGGREGATE_PIPELINE_FUNCTIONS; + } + return aggregatePipeline; + } + + private boolean isProjectionContainsAggregation(Query query) { + return query.getSelections().stream() + .map(SelectionSpec::getExpression) + .anyMatch(spec -> spec.accept(new AggregateExpressionChecker())); + } + + private boolean isSortContainsAggregation(Query query) { + // ideally there should be only one alias per selection, + // in case of duplicates, we will accept the latest one + Map aliasToSelectionMap = + query.getSelections().stream() + .filter(spec -> this.getAlias(spec).isPresent()) + .collect( + Collectors.toMap( + entry -> this.getAlias(entry).orElseThrow(), identity(), (v1, v2) -> v2)); + return query.getSorts().stream() + .anyMatch(sort -> isSortOnAggregatedField(aliasToSelectionMap, sort)); + } + + private boolean isSortOnAggregatedField( + Map aliasToSelectionMap, SortingSpec sort) { + boolean isFunctionExpression = sort.getExpression().accept(new FunctionExpressionChecker()); + boolean isAggregateExpression = sort.getExpression().accept(new AggregateExpressionChecker()); + return isFunctionExpression + || isAggregateExpression + || isSortOnAggregatedProjection(aliasToSelectionMap, sort); + } + + private Optional getAlias(SelectionSpec selectionSpec) { + if (selectionSpec.getAlias() != null) { + return Optional.of(selectionSpec.getAlias()); + } + + return selectionSpec.getExpression().accept(new AliasParser()); + } + + private boolean isSortOnAggregatedProjection( + Map aliasToSelectionMap, SortingSpec sort) { + Optional alias = sort.getExpression().accept(new AliasParser()); + if (alias.isEmpty()) { + throw new UnsupportedOperationException( + "Cannot sort by an expression that does not have an alias in selection"); + } + + SelectionSpec selectionSpec = aliasToSelectionMap.get(alias.get()); + if (selectionSpec == null) { + return false; + } + + Boolean isFunctionExpression = + selectionSpec.getExpression().accept(new FunctionExpressionChecker()); + Boolean isAggregationExpression = + selectionSpec.getExpression().accept(new AggregateExpressionChecker()); + return isFunctionExpression || isAggregationExpression; + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoQueryExecutor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoQueryExecutor.java index adacfdf75..60b159294 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoQueryExecutor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/MongoQueryExecutor.java @@ -1,26 +1,17 @@ package org.hypertrace.core.documentstore.mongo.query; import static java.lang.Long.parseLong; -import static java.util.Collections.singleton; import static java.util.Comparator.comparing; import static java.util.concurrent.TimeUnit.MILLISECONDS; -import static java.util.function.Function.identity; import static java.util.function.Predicate.not; import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toUnmodifiableList; import static org.hypertrace.core.documentstore.model.options.QueryOptions.DEFAULT_QUERY_OPTIONS; import static org.hypertrace.core.documentstore.mongo.clause.MongoCountClauseSupplier.COUNT_ALIAS; import static org.hypertrace.core.documentstore.mongo.clause.MongoCountClauseSupplier.getCountClause; import static org.hypertrace.core.documentstore.mongo.query.MongoPaginationHelper.applyPagination; -import static org.hypertrace.core.documentstore.mongo.query.MongoPaginationHelper.getLimitClause; -import static org.hypertrace.core.documentstore.mongo.query.MongoPaginationHelper.getSkipClause; import static org.hypertrace.core.documentstore.mongo.query.parser.MongoFilterTypeExpressionParser.getFilter; -import static org.hypertrace.core.documentstore.mongo.query.parser.MongoFilterTypeExpressionParser.getFilterClause; -import static org.hypertrace.core.documentstore.mongo.query.parser.MongoNonProjectedSortTypeExpressionParser.getNonProjectedSortClause; -import static org.hypertrace.core.documentstore.mongo.query.parser.MongoSelectTypeExpressionParser.getProjectClause; import static org.hypertrace.core.documentstore.mongo.query.parser.MongoSelectTypeExpressionParser.getSelections; import static org.hypertrace.core.documentstore.mongo.query.parser.MongoSortTypeExpressionParser.getOrders; -import static org.hypertrace.core.documentstore.mongo.query.parser.MongoSortTypeExpressionParser.getSortClause; import com.mongodb.BasicDBObject; import com.mongodb.ServerAddress; @@ -29,58 +20,23 @@ import com.mongodb.client.FindIterable; import com.mongodb.client.MongoCursor; import java.time.Duration; -import java.util.Collection; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.stream.Collectors; import java.util.stream.Stream; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.bson.conversions.Bson; -import org.hypertrace.core.documentstore.model.config.AggregatePipelineMode; import org.hypertrace.core.documentstore.model.config.ConnectionConfig; import org.hypertrace.core.documentstore.model.options.QueryOptions; import org.hypertrace.core.documentstore.mongo.collection.MongoCollectionOptionsApplier; -import org.hypertrace.core.documentstore.mongo.query.parser.AliasParser; -import org.hypertrace.core.documentstore.mongo.query.parser.MongoFromTypeExpressionParser; -import org.hypertrace.core.documentstore.mongo.query.parser.MongoGroupTypeExpressionParser; import org.hypertrace.core.documentstore.mongo.query.transformer.MongoQueryTransformer; -import org.hypertrace.core.documentstore.parser.AggregateExpressionChecker; -import org.hypertrace.core.documentstore.parser.FunctionExpressionChecker; import org.hypertrace.core.documentstore.query.Pagination; import org.hypertrace.core.documentstore.query.Query; -import org.hypertrace.core.documentstore.query.SelectionSpec; -import org.hypertrace.core.documentstore.query.SortingSpec; @Slf4j @AllArgsConstructor public class MongoQueryExecutor { - private final List>> - DEFAULT_AGGREGATE_PIPELINE_FUNCTIONS = - List.of( - query -> singleton(getFilterClause(query, Query::getFilter)), - query -> new MongoFromTypeExpressionParser(this).getFromClauses(query), - MongoGroupTypeExpressionParser::getGroupClauses, - query -> singleton(getProjectClause(query)), - query -> singleton(getFilterClause(query, Query::getAggregationFilter)), - query -> singleton(getSortClause(query)), - query -> singleton(getSkipClause(query)), - query -> singleton(getLimitClause(query))); - - private final List>> - SORT_OPTIMISED_AGGREGATE_PIPELINE_FUNCTIONS = - List.of( - query -> singleton(getFilterClause(query, Query::getFilter)), - query -> new MongoFromTypeExpressionParser(this).getFromClauses(query), - query -> singleton(getNonProjectedSortClause(query)), - query -> singleton(getSkipClause(query)), - query -> singleton(getLimitClause(query)), - query -> singleton(getProjectClause(query))); - private static final Integer ZERO = Integer.valueOf(0); private static final MongoCursor EMPTY_CURSOR = new MongoCursor<>() { @@ -125,6 +81,17 @@ public ServerAddress getServerAddress() { private final MongoCollectionOptionsApplier optionsApplier = new MongoCollectionOptionsApplier(); private final com.mongodb.client.MongoCollection collection; private final ConnectionConfig connectionConfig; + private final MongoAggregationPipelineConverter pipelineConverter; + + public MongoQueryExecutor( + com.mongodb.client.MongoCollection collection, + ConnectionConfig connectionConfig) { + this.collection = collection; + this.connectionConfig = connectionConfig; + this.pipelineConverter = + new MongoAggregationPipelineConverter( + connectionConfig.aggregationPipelineMode(), collection); + } public MongoCursor find(final Query query) { BasicDBObject filterClause = getFilter(query, Query::getFilter); @@ -157,7 +124,7 @@ public MongoCursor aggregate( Query query = transformAndLog(originalQuery); - List pipeline = convertToAggregatePipeline(query); + List pipeline = pipelineConverter.convertToAggregatePipeline(query); logPipeline(pipeline, queryOptions); @@ -188,8 +155,7 @@ public long count(final Query originalQuery, final QueryOptions queryOptions) { final List pipeline = Stream.concat( - DEFAULT_AGGREGATE_PIPELINE_FUNCTIONS.stream() - .flatMap(function -> function.apply(query).stream()), + pipelineConverter.convertToAggregatePipeline(query).stream(), Stream.of(getCountClause())) .filter(not(BasicDBObject::isEmpty)) .collect(toList()); @@ -217,18 +183,6 @@ public String getCollectionName() { return collection.getNamespace().getCollectionName(); } - public List convertToAggregatePipeline(Query query) { - List>> aggregatePipeline = - getAggregationPipeline(query); - - List pipeline = - aggregatePipeline.stream() - .flatMap(function -> function.apply(query).stream()) - .filter(not(BasicDBObject::isEmpty)) - .collect(toUnmodifiableList()); - return pipeline; - } - private void logClauses( final Query query, final Bson projection, @@ -261,78 +215,6 @@ private Query transformAndLog(Query query) { return query; } - private List>> getAggregationPipeline(Query query) { - List>> aggregatePipeline = - DEFAULT_AGGREGATE_PIPELINE_FUNCTIONS; - if (connectionConfig - .aggregationPipelineMode() - .equals(AggregatePipelineMode.SORT_OPTIMIZED_IF_POSSIBLE) - && query.getAggregations().isEmpty() - && query.getAggregationFilter().isEmpty() - && !isProjectionContainsAggregation(query) - && !isSortContainsAggregation(query)) { - log.debug("Using sort optimized aggregate pipeline functions for query: {}", query); - aggregatePipeline = SORT_OPTIMISED_AGGREGATE_PIPELINE_FUNCTIONS; - } - return aggregatePipeline; - } - - private boolean isProjectionContainsAggregation(Query query) { - return query.getSelections().stream() - .map(SelectionSpec::getExpression) - .anyMatch(spec -> spec.accept(new AggregateExpressionChecker())); - } - - private boolean isSortContainsAggregation(Query query) { - // ideally there should be only one alias per selection, - // in case of duplicates, we will accept the latest one - Map aliasToSelectionMap = - query.getSelections().stream() - .filter(spec -> this.getAlias(spec).isPresent()) - .collect( - Collectors.toMap( - entry -> this.getAlias(entry).orElseThrow(), identity(), (v1, v2) -> v2)); - return query.getSorts().stream() - .anyMatch(sort -> isSortOnAggregatedField(aliasToSelectionMap, sort)); - } - - private boolean isSortOnAggregatedField( - Map aliasToSelectionMap, SortingSpec sort) { - boolean isFunctionExpression = sort.getExpression().accept(new FunctionExpressionChecker()); - boolean isAggregateExpression = sort.getExpression().accept(new AggregateExpressionChecker()); - return isFunctionExpression - || isAggregateExpression - || isSortOnAggregatedProjection(aliasToSelectionMap, sort); - } - - private Optional getAlias(SelectionSpec selectionSpec) { - if (selectionSpec.getAlias() != null) { - return Optional.of(selectionSpec.getAlias()); - } - - return selectionSpec.getExpression().accept(new AliasParser()); - } - - private boolean isSortOnAggregatedProjection( - Map aliasToSelectionMap, SortingSpec sort) { - Optional alias = sort.getExpression().accept(new AliasParser()); - if (alias.isEmpty()) { - throw new UnsupportedOperationException( - "Cannot sort by an expression that does not have an alias in selection"); - } - - SelectionSpec selectionSpec = aliasToSelectionMap.get(alias.get()); - if (selectionSpec == null) { - return false; - } - - Boolean isFunctionExpression = - selectionSpec.getExpression().accept(new FunctionExpressionChecker()); - Boolean isAggregationExpression = - selectionSpec.getExpression().accept(new AggregateExpressionChecker()); - return isFunctionExpression || isAggregationExpression; - } - private Duration queryTimeout( final ConnectionConfig connectionConfig, final QueryOptions queryOptions) { return Stream.of(connectionConfig.queryTimeout(), queryOptions.queryTimeout()) diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java deleted file mode 100644 index 9843edd33..000000000 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoAliasedIdentifierExpressionParser.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.hypertrace.core.documentstore.mongo.query.parser; - -import lombok.NoArgsConstructor; -import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; -import org.hypertrace.core.documentstore.mongo.MongoUtils; - -@NoArgsConstructor -public final class MongoAliasedIdentifierExpressionParser extends MongoSelectTypeExpressionParser { - - @SuppressWarnings("unchecked") - @Override - public String visit(final AliasedIdentifierExpression expression) { - return MongoUtils.PREFIX + parse(expression); - } - - String parse(final AliasedIdentifierExpression expression) { - return expression.getName(); - } -} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java index ba752ee18..7d7e407dd 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoFromTypeExpressionParser.java @@ -2,6 +2,7 @@ import com.mongodb.BasicDBObject; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -9,7 +10,7 @@ import org.hypertrace.core.documentstore.expression.impl.UnnestExpression; import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; import org.hypertrace.core.documentstore.mongo.MongoUtils; -import org.hypertrace.core.documentstore.mongo.query.MongoQueryExecutor; +import org.hypertrace.core.documentstore.mongo.query.MongoAggregationPipelineConverter; import org.hypertrace.core.documentstore.mongo.query.parser.filter.MongoRelationalFilterParserFactory.FilterLocation; import org.hypertrace.core.documentstore.mongo.query.parser.filter.MongoRelationalFilterParserFactory.MongoRelationalFilterContext; import org.hypertrace.core.documentstore.mongo.query.transformer.MongoQueryTransformer; @@ -26,15 +27,21 @@ public class MongoFromTypeExpressionParser implements FromTypeExpressionVisitor private static final String MATCH_OPERATOR = "$match"; private static final String REPLACE_ROOT_OPERATOR = "$replaceRoot"; private static final String EXPR_OPERATOR = "$expr"; + private static final String LOOKUP_FROM_FIELD = "from"; + private static final String LOOKUP_LET_FIELD = "let"; + private static final String LOOKUP_PIPELINE_FIELD = "pipeline"; + private static final String LOOKUP_AS_FIELD = "as"; + private static final String NEW_ROOT_FIELD = "newRoot"; + private static final String JOINED_RESULT_FIELD_NAME_PREFIX = "__joined_result_with_"; private static final MongoIdentifierPrefixingParser mongoIdentifierPrefixingParser = new MongoIdentifierPrefixingParser(new MongoIdentifierExpressionParser()); - private final MongoQueryExecutor mongoQueryExecutor; + private final MongoAggregationPipelineConverter pipelineConverter; private MongoLetClauseBuilder mongoLetClauseBuilder; - public MongoFromTypeExpressionParser(MongoQueryExecutor mongoQueryExecutor) { - this.mongoQueryExecutor = mongoQueryExecutor; + public MongoFromTypeExpressionParser(MongoAggregationPipelineConverter pipelineConverter) { + this.pipelineConverter = pipelineConverter; } @SuppressWarnings("unchecked") @@ -70,8 +77,11 @@ public List visit(SubQueryJoinExpression subQueryJoinExpression) Query transformedSubQuery = MongoQueryTransformer.transform(subQueryJoinExpression.getSubQuery()); List aggregatePipeline = - new ArrayList<>(mongoQueryExecutor.convertToAggregatePipeline(transformedSubQuery)); + new ArrayList<>(pipelineConverter.convertToAggregatePipeline(transformedSubQuery)); + // This is the field name in which the joined results will be put after the lookup stage. This + // field name can be used by subsequent stages in the aggregate pipeline to do operations on the + // joined result. String joinedResultFieldName = getJoinedResultFieldName(subQueryJoinExpression.getSubQueryAlias()); // Add the lookup stage to join the subquery results with the main collection @@ -84,7 +94,7 @@ public List visit(SubQueryJoinExpression subQueryJoinExpression) // Replace root with the joined document aggregatePipeline.add(createReplaceRootStage(joinedResultFieldName)); - return aggregatePipeline; + return Collections.unmodifiableList(aggregatePipeline); } private BasicDBObject createLookupStage( @@ -92,19 +102,18 @@ private BasicDBObject createLookupStage( BasicDBObject lookupStage = new BasicDBObject(); BasicDBObject lookupSpec = new BasicDBObject(); - lookupSpec.put("from", mongoQueryExecutor.getCollectionName()); - lookupSpec.put("let", subQueryJoinExpression.getJoinCondition().accept(mongoLetClauseBuilder)); - lookupSpec.put("pipeline", createLookupPipeline(subQueryJoinExpression)); - lookupSpec.put("as", joinedResultFieldName); + lookupSpec.put(LOOKUP_FROM_FIELD, pipelineConverter.getCollectionName()); + lookupSpec.put( + LOOKUP_LET_FIELD, subQueryJoinExpression.getJoinCondition().accept(mongoLetClauseBuilder)); + lookupSpec.put(LOOKUP_PIPELINE_FIELD, createLookupPipeline(subQueryJoinExpression)); + lookupSpec.put(LOOKUP_AS_FIELD, joinedResultFieldName); lookupStage.put(LOOKUP_OPERATOR, lookupSpec); return lookupStage; } private List createLookupPipeline(SubQueryJoinExpression subQueryJoinExpression) { - List pipeline = new ArrayList<>(); - pipeline.add(createMatchStage(subQueryJoinExpression)); - return pipeline; + return List.of(createMatchStage(subQueryJoinExpression)); } private BasicDBObject createMatchStage(SubQueryJoinExpression subQueryJoinExpression) { @@ -123,15 +132,14 @@ private BasicDBObject getFilterClause(FilterTypeExpression joinCondition) { .lhsParser( new MongoDollarPrefixingIdempotentParser(new MongoIdentifierExpressionParser())) .rhsParser( - new MongoDollarPrefixingIdempotentParser( - new MongoAliasedIdentifierExpressionParser())) + new MongoDollarPrefixingIdempotentParser(new MongoIdentifierExpressionParser())) .build()); final Map filter = joinCondition.accept(parser); return new BasicDBObject(filter); } private String getJoinedResultFieldName(String subQueryAlias) { - return "__joined_result_with_" + subQueryAlias; + return JOINED_RESULT_FIELD_NAME_PREFIX + subQueryAlias; } private BasicDBObject createUnwindStage(String joinedResultFieldName) { @@ -149,7 +157,7 @@ private BasicDBObject createReplaceRootStage(String joinedResultFieldName) { BasicDBObject replaceRootStage = new BasicDBObject(); BasicDBObject newRoot = new BasicDBObject(); - newRoot.put("newRoot", MongoUtils.PREFIX + joinedResultFieldName); + newRoot.put(NEW_ROOT_FIELD, MongoUtils.PREFIX + joinedResultFieldName); replaceRootStage.put(REPLACE_ROOT_OPERATOR, newRoot); return replaceRootStage; @@ -157,7 +165,7 @@ private BasicDBObject createReplaceRootStage(String joinedResultFieldName) { public List getFromClauses(final Query query) { MongoFromTypeExpressionParser mongoFromTypeExpressionParser = - new MongoFromTypeExpressionParser(mongoQueryExecutor); + new MongoFromTypeExpressionParser(pipelineConverter); return query.getFromTypeExpressions().stream() .flatMap( fromTypeExpression -> diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java index 40d1323f0..19e37c063 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java @@ -1,7 +1,9 @@ package org.hypertrace.core.documentstore.mongo.query.parser; import lombok.NoArgsConstructor; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; +import org.hypertrace.core.documentstore.mongo.MongoUtils; @NoArgsConstructor public final class MongoIdentifierExpressionParser extends MongoSelectTypeExpressionParser { @@ -16,7 +18,17 @@ public String visit(final IdentifierExpression expression) { return parse(expression); } + @SuppressWarnings("unchecked") + @Override + public String visit(final AliasedIdentifierExpression expression) { + return MongoUtils.PREFIX + parse(expression); + } + String parse(final IdentifierExpression expression) { return expression.getName(); } + + String parse(final AliasedIdentifierExpression expression) { + return expression.getName(); + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java index 0771de610..bc4b1ac21 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java @@ -1,6 +1,7 @@ package org.hypertrace.core.documentstore.mongo.query.parser; -import com.mongodb.BasicDBObject; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; @@ -13,6 +14,7 @@ import org.hypertrace.core.documentstore.expression.impl.LogicalExpression; import org.hypertrace.core.documentstore.expression.impl.RelationalExpression; import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; +import org.hypertrace.core.documentstore.mongo.MongoUtils; import org.hypertrace.core.documentstore.parser.FilterTypeExpressionVisitor; import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; @@ -26,79 +28,75 @@ class MongoLetClauseBuilder implements FilterTypeExpressionVisitor { } @Override - public BasicDBObject visit(LogicalExpression expression) { - BasicDBObject letClause = new BasicDBObject(); + public Map visit(LogicalExpression expression) { + Map letClause = new HashMap<>(); for (FilterTypeExpression operand : expression.getOperands()) { - letClause.putAll((Map) operand.accept(this)); + letClause.putAll(operand.accept(this)); } - return letClause; + return Collections.unmodifiableMap(letClause); } @Override - public BasicDBObject visit(RelationalExpression expression) { - BasicDBObject letClause = new BasicDBObject(); - letClause.putAll( - (Map) - expression.getLhs().accept(new MongoLetClauseSelectTypeExpressionVisitor())); - letClause.putAll( - (Map) - expression.getRhs().accept(new MongoLetClauseSelectTypeExpressionVisitor())); - return letClause; + public Map visit(RelationalExpression expression) { + Map letClause = new HashMap<>(); + letClause.putAll(expression.getLhs().accept(new MongoLetClauseSelectTypeExpressionVisitor())); + letClause.putAll(expression.getRhs().accept(new MongoLetClauseSelectTypeExpressionVisitor())); + return Collections.unmodifiableMap(letClause); } private class MongoLetClauseSelectTypeExpressionVisitor implements SelectTypeExpressionVisitor { @Override - public BasicDBObject visit(AggregateExpression expression) { - return new BasicDBObject(); + public Map visit(AggregateExpression expression) { + return Collections.emptyMap(); } @Override - public BasicDBObject visit(ConstantExpression expression) { - return new BasicDBObject(); + public Map visit(ConstantExpression expression) { + return Collections.emptyMap(); } @Override - public BasicDBObject visit(ConstantExpression.DocumentConstantExpression expression) { - return new BasicDBObject(); + public Map visit(ConstantExpression.DocumentConstantExpression expression) { + return Collections.emptyMap(); } @Override - public BasicDBObject visit(FunctionExpression expression) { - return new BasicDBObject(); + public Map visit(FunctionExpression expression) { + return Collections.emptyMap(); } @Override - public BasicDBObject visit(IdentifierExpression expression) { - return new BasicDBObject(); + public Map visit(IdentifierExpression expression) { + return Collections.emptyMap(); } @Override - public BasicDBObject visit(AliasedIdentifierExpression expression) { + public Map visit(AliasedIdentifierExpression expression) { return createLetClause(expression, subQueryAlias); } - } - @Override - public BasicDBObject visit(KeyExpression expression) { - return new BasicDBObject(); + private Map createLetClause( + AliasedIdentifierExpression aliasedExpression, String subQueryAlias) { + Map letClause = new HashMap<>(); + if (aliasedExpression.getContextAlias().equals(subQueryAlias)) { + letClause.put(aliasedExpression.getName(), MongoUtils.PREFIX + aliasedExpression.getName()); + } + return Collections.unmodifiableMap(letClause); + } } @Override - public BasicDBObject visit(ArrayRelationalFilterExpression expression) { - return new BasicDBObject(); + public Map visit(KeyExpression expression) { + return Collections.emptyMap(); } @Override - public BasicDBObject visit(DocumentArrayFilterExpression expression) { - return new BasicDBObject(); + public Map visit(ArrayRelationalFilterExpression expression) { + return Collections.emptyMap(); } - private BasicDBObject createLetClause( - AliasedIdentifierExpression aliasedExpression, String subQueryAlias) { - BasicDBObject letClause = new BasicDBObject(); - if (aliasedExpression.getContextAlias().equals(subQueryAlias)) { - letClause.put(aliasedExpression.getName(), "$" + aliasedExpression.getName()); - } - return letClause; + @Override + public Map visit(DocumentArrayFilterExpression expression) { + return Collections.emptyMap(); } } From 9ee78fb2e1a9bdd1ced6ee1c2ad2677c9158b30b Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Thu, 10 Apr 2025 23:59:54 +0530 Subject: [PATCH 10/13] chore: address review comments p2 --- .../core/documentstore/mongo/MongoUtils.java | 7 ++++ .../mongo/query/parser/AliasParser.java | 8 ++++ .../parser/MongoEmptySelectionTypeParser.java | 42 +++++++++++++++++++ .../MongoIdentifierExpressionParser.java | 2 +- .../query/parser/MongoLetClauseBuilder.java | 42 +++---------------- ...oNonProjectedSortTypeExpressionParser.java | 7 ++++ .../MongoSelectionsAddingTransformation.java | 7 ++++ ...MongoSelectionsUpdatingTransformation.java | 7 ++++ .../parser/AggregateExpressionChecker.java | 6 +++ .../parser/FromTypeExpressionVisitor.java | 7 +--- .../parser/FunctionExpressionChecker.java | 6 +++ .../parser/SelectTypeExpressionVisitor.java | 4 +- .../PostgresSelectionQueryTransformer.java | 11 +++++ .../PostgresUnnestQueryTransformer.java | 12 ++++++ .../PostgresFromTypeExpressionVisitor.java | 6 +++ .../PostgresSelectTypeExpressionVisitor.java | 6 +++ ...gresUnnestFilterTypeExpressionVisitor.java | 6 +++ 17 files changed, 139 insertions(+), 47 deletions(-) create mode 100644 document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoEmptySelectionTypeParser.java diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/MongoUtils.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/MongoUtils.java index 6efb08d98..f12868cff 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/MongoUtils.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/MongoUtils.java @@ -65,6 +65,13 @@ public static String decodeKey(final String key) { return key.replace("\\u002e", FIELD_SEPARATOR).replace("\\u0024", PREFIX).replace("\\\\", "\\"); } + public static String encodeVariableName(final String variableName) { + if (variableName == null) { + return null; + } + return variableName.replace(".", "_"); + } + public static String getLastField(final String fieldPath) { final String[] fields = fieldPath.split("\\" + FIELD_SEPARATOR); return fields[fields.length - 1]; diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/AliasParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/AliasParser.java index 80e29d6ef..550f6ac1e 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/AliasParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/AliasParser.java @@ -2,12 +2,14 @@ import java.util.Optional; 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.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; import org.hypertrace.core.documentstore.expression.impl.IdentifierExpression; import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; import org.hypertrace.core.documentstore.parser.SortTypeExpressionVisitor; +import org.hypertrace.core.documentstore.query.SelectionSpec; public class AliasParser implements SelectTypeExpressionVisitor, SortTypeExpressionVisitor { @@ -40,4 +42,10 @@ public Optional visit(ConstantExpression expression) { public Optional visit(DocumentConstantExpression expression) { return Optional.empty(); } + + @SuppressWarnings("unchecked") + @Override + public Optional visit(final AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoEmptySelectionTypeParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoEmptySelectionTypeParser.java new file mode 100644 index 000000000..b50616101 --- /dev/null +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoEmptySelectionTypeParser.java @@ -0,0 +1,42 @@ +package org.hypertrace.core.documentstore.mongo.query.parser; + +import java.util.Collections; +import java.util.Map; +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.parser.SelectTypeExpressionVisitor; + +class MongoEmptySelectionTypeParser implements SelectTypeExpressionVisitor { + @Override + public Map visit(AggregateExpression expression) { + return Collections.emptyMap(); + } + + @Override + public Map visit(ConstantExpression expression) { + return Collections.emptyMap(); + } + + @Override + public Map visit(ConstantExpression.DocumentConstantExpression expression) { + return Collections.emptyMap(); + } + + @Override + public Map visit(FunctionExpression expression) { + return Collections.emptyMap(); + } + + @Override + public Map visit(IdentifierExpression expression) { + return Collections.emptyMap(); + } + + @Override + public Map visit(AliasedIdentifierExpression expression) { + return Collections.emptyMap(); + } +} diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java index 19e37c063..80e093023 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoIdentifierExpressionParser.java @@ -21,7 +21,7 @@ public String visit(final IdentifierExpression expression) { @SuppressWarnings("unchecked") @Override public String visit(final AliasedIdentifierExpression expression) { - return MongoUtils.PREFIX + parse(expression); + return MongoUtils.PREFIX + MongoUtils.encodeVariableName(parse(expression)); } String parse(final IdentifierExpression expression) { diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java index bc4b1ac21..7480bd3fd 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java @@ -3,20 +3,15 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.ArrayRelationalFilterExpression; -import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; import org.hypertrace.core.documentstore.expression.impl.DocumentArrayFilterExpression; -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.type.FilterTypeExpression; import org.hypertrace.core.documentstore.mongo.MongoUtils; import org.hypertrace.core.documentstore.parser.FilterTypeExpressionVisitor; -import org.hypertrace.core.documentstore.parser.SelectTypeExpressionVisitor; // Visitor for building the let clause in lookup stage from filter expressions class MongoLetClauseBuilder implements FilterTypeExpressionVisitor { @@ -44,42 +39,15 @@ public Map visit(RelationalExpression expression) { return Collections.unmodifiableMap(letClause); } - private class MongoLetClauseSelectTypeExpressionVisitor implements SelectTypeExpressionVisitor { - @Override - public Map visit(AggregateExpression expression) { - return Collections.emptyMap(); - } - - @Override - public Map visit(ConstantExpression expression) { - return Collections.emptyMap(); - } - - @Override - public Map visit(ConstantExpression.DocumentConstantExpression expression) { - return Collections.emptyMap(); - } + private class MongoLetClauseSelectTypeExpressionVisitor extends MongoEmptySelectionTypeParser { @Override - public Map visit(FunctionExpression expression) { - return Collections.emptyMap(); - } - - @Override - public Map visit(IdentifierExpression expression) { - return Collections.emptyMap(); - } - - @Override - public Map visit(AliasedIdentifierExpression expression) { - return createLetClause(expression, subQueryAlias); - } - - private Map createLetClause( - AliasedIdentifierExpression aliasedExpression, String subQueryAlias) { + public Map visit(AliasedIdentifierExpression aliasedExpression) { Map letClause = new HashMap<>(); if (aliasedExpression.getContextAlias().equals(subQueryAlias)) { - letClause.put(aliasedExpression.getName(), MongoUtils.PREFIX + aliasedExpression.getName()); + letClause.put( + MongoUtils.encodeVariableName(aliasedExpression.getName()), + MongoUtils.PREFIX + aliasedExpression.getName()); } return Collections.unmodifiableMap(letClause); } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoNonProjectedSortTypeExpressionParser.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoNonProjectedSortTypeExpressionParser.java index cf5c4710c..bd9b89491 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoNonProjectedSortTypeExpressionParser.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoNonProjectedSortTypeExpressionParser.java @@ -13,6 +13,7 @@ import java.util.stream.Collectors; import org.apache.commons.collections4.CollectionUtils; 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.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; @@ -93,6 +94,12 @@ public Map visit(DocumentConstantExpression expression) { expression.getValue().toString())); } + @SuppressWarnings("unchecked") + @Override + public Map visit(final AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } + public static BasicDBObject getNonProjectedSortClause(final Query query) { BasicDBObject orders = getOrders(query); return orders.isEmpty() ? orders : new BasicDBObject(SORT_CLAUSE, orders); diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsAddingTransformation.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsAddingTransformation.java index 0a876eb25..2fb4ca0c8 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsAddingTransformation.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsAddingTransformation.java @@ -8,6 +8,7 @@ import java.util.Optional; import lombok.AllArgsConstructor; 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.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; @@ -123,4 +124,10 @@ public Optional visit(final FunctionExpression expression) { public Optional visit(final IdentifierExpression expression) { return Optional.empty(); } + + @SuppressWarnings("unchecked") + @Override + public Optional visit(final AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsUpdatingTransformation.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsUpdatingTransformation.java index b099542c0..ea38a3a1a 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsUpdatingTransformation.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/transformer/MongoSelectionsUpdatingTransformation.java @@ -15,6 +15,7 @@ import java.util.Set; import java.util.function.Function; 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.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; @@ -132,6 +133,12 @@ public SelectionSpec visit(final IdentifierExpression expression) { return SelectionSpec.of(IdentifierExpression.of(identifier), alias); } + @SuppressWarnings("unchecked") + @Override + public SelectionSpec visit(final AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } + private SelectionSpec substitute(final AggregateExpression expression) { return Optional.ofNullable(AGGREGATION_SUBSTITUTE_MAP.get(expression.getAggregator())) .map(converter -> SelectionSpec.of(converter.apply(expression), source.getAlias())) diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/AggregateExpressionChecker.java b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/AggregateExpressionChecker.java index b8c0ae2f9..8b70235fc 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/AggregateExpressionChecker.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/AggregateExpressionChecker.java @@ -1,6 +1,7 @@ package org.hypertrace.core.documentstore.parser; 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.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; @@ -34,4 +35,9 @@ public Boolean visit(FunctionExpression expression) { public Boolean visit(IdentifierExpression expression) { return false; } + + @Override + public Boolean visit(AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java index bb4116dd4..f5bb4be60 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FromTypeExpressionVisitor.java @@ -6,10 +6,5 @@ public interface FromTypeExpressionVisitor { T visit(UnnestExpression unnestExpression); - /* - * Subquery join expression is not supported by default. Override this method to support it. - */ - default T visit(SubQueryJoinExpression subQueryJoinExpression) { - throw new UnsupportedOperationException("This operation is not supported"); - } + T visit(SubQueryJoinExpression subQueryJoinExpression); } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FunctionExpressionChecker.java b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FunctionExpressionChecker.java index 15c19baac..e9779879f 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FunctionExpressionChecker.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/FunctionExpressionChecker.java @@ -1,6 +1,7 @@ package org.hypertrace.core.documentstore.parser; 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.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; @@ -34,4 +35,9 @@ public Boolean visit(FunctionExpression expression) { public Boolean visit(IdentifierExpression expression) { return false; } + + @Override + public Boolean visit(AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java index a662062c8..a6e0ed0e8 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/parser/SelectTypeExpressionVisitor.java @@ -18,7 +18,5 @@ public interface SelectTypeExpressionVisitor { T visit(final IdentifierExpression expression); - default T visit(final AliasedIdentifierExpression expression) { - throw new UnsupportedOperationException(); - } + T visit(final AliasedIdentifierExpression expression); } diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresSelectionQueryTransformer.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresSelectionQueryTransformer.java index 457e577f1..7052b90a6 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresSelectionQueryTransformer.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresSelectionQueryTransformer.java @@ -5,6 +5,7 @@ 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.AliasedIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; @@ -109,6 +110,11 @@ public Boolean visit(FunctionExpression expression) { public Boolean visit(IdentifierExpression expression) { return false; } + + @Override + public Boolean visit(AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } } private static class LocalSelectTypeIdentifierExpressionSelector @@ -137,6 +143,11 @@ public Boolean visit(FunctionExpression expression) { public Boolean visit(IdentifierExpression expression) { return true; } + + @Override + public Boolean visit(AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } } private static class LocalGroupByIdentifierExpressionSelector diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresUnnestQueryTransformer.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresUnnestQueryTransformer.java index fd4c2f64b..8145b8f75 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresUnnestQueryTransformer.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/transformer/PostgresUnnestQueryTransformer.java @@ -9,6 +9,7 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import org.hypertrace.core.documentstore.expression.impl.AggregateExpression; +import org.hypertrace.core.documentstore.expression.impl.AliasedIdentifierExpression; import org.hypertrace.core.documentstore.expression.impl.ArrayRelationalFilterExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression; import org.hypertrace.core.documentstore.expression.impl.ConstantExpression.DocumentConstantExpression; @@ -18,6 +19,7 @@ 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.LogicalOperator; import org.hypertrace.core.documentstore.expression.type.FilterTypeExpression; @@ -179,6 +181,11 @@ private static class UnnestExpressionProvider implements FromTypeExpressionVisit public UnnestExpression visit(UnnestExpression unnestExpression) { return unnestExpression; } + + @Override + public String visit(SubQueryJoinExpression subQueryJoinExpression) { + throw new UnsupportedOperationException("This operation is not supported"); + } } @SuppressWarnings("unchecked") @@ -211,6 +218,11 @@ public List visit(FunctionExpression expression) { public List visit(IdentifierExpression expression) { return List.of(expression.getName()); } + + @Override + public List visit(AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } } @SuppressWarnings("unchecked") diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresFromTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresFromTypeExpressionVisitor.java index 40dad767c..db5a09c66 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresFromTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresFromTypeExpressionVisitor.java @@ -4,6 +4,7 @@ import java.util.stream.Collectors; import lombok.Getter; import org.apache.commons.lang3.StringUtils; +import org.hypertrace.core.documentstore.expression.impl.SubQueryJoinExpression; import org.hypertrace.core.documentstore.expression.impl.UnnestExpression; import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser; @@ -62,6 +63,11 @@ public String visit(UnnestExpression unnestExpression) { return String.format(fmt, newTable, preTable, tableAlias, unwindExpr, unwindExprAlias); } + @Override + public String visit(SubQueryJoinExpression subQueryJoinExpression) { + throw new UnsupportedOperationException("This operation is not supported"); + } + public static Optional getFromClause(PostgresQueryParser postgresQueryParser) { PostgresFromTypeExpressionVisitor postgresFromTypeExpressionVisitor = diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresSelectTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresSelectTypeExpressionVisitor.java index ae1cf8637..44b7f4547 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresSelectTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresSelectTypeExpressionVisitor.java @@ -6,6 +6,7 @@ import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; 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.ConstantExpression.DocumentConstantExpression; import org.hypertrace.core.documentstore.expression.impl.FunctionExpression; @@ -64,6 +65,11 @@ public T visit(final IdentifierExpression expression) { return baseVisitor.visit(expression); } + @Override + public T visit(final AliasedIdentifierExpression expression) { + throw new UnsupportedOperationException("This operation is not supported"); + } + public abstract PostgresQueryParser getPostgresQueryParser(); @AllArgsConstructor diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresUnnestFilterTypeExpressionVisitor.java b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresUnnestFilterTypeExpressionVisitor.java index fcc6404a0..347243217 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresUnnestFilterTypeExpressionVisitor.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/postgres/query/v1/vistors/PostgresUnnestFilterTypeExpressionVisitor.java @@ -3,6 +3,7 @@ import java.util.Optional; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; +import org.hypertrace.core.documentstore.expression.impl.SubQueryJoinExpression; import org.hypertrace.core.documentstore.expression.impl.UnnestExpression; import org.hypertrace.core.documentstore.parser.FromTypeExpressionVisitor; import org.hypertrace.core.documentstore.postgres.query.v1.PostgresQueryParser; @@ -23,6 +24,11 @@ public String visit(UnnestExpression unnestExpression) { return where.orElse(""); } + @Override + public String visit(SubQueryJoinExpression subQueryJoinExpression) { + throw new UnsupportedOperationException("This operation is not supported"); + } + public static Optional getFilterClause(PostgresQueryParser postgresQueryParser) { PostgresUnnestFilterTypeExpressionVisitor postgresUnnestFilterTypeExpressionVisitor = new PostgresUnnestFilterTypeExpressionVisitor(postgresQueryParser); From 11f5d28cb6f2eb7222562f9d7245c8388cb323c4 Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Fri, 11 Apr 2025 00:13:49 +0530 Subject: [PATCH 11/13] test: add integration test to verify self join with sub-query works for nested fields as well --- .../documentstore/DocStoreQueryV1Test.java | 89 +++++++++++++++++++ .../query/items_data_with_nested_fields.json | 50 +++++++++++ ...uery_join_response_with_nested_fields.json | 23 +++++ 3 files changed, 162 insertions(+) create mode 100644 document-store/src/integrationTest/resources/query/items_data_with_nested_fields.json create mode 100644 document-store/src/integrationTest/resources/query/sub_query_join_response_with_nested_fields.json diff --git a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java index 2e2b54e66..0902d7cdc 100644 --- a/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java +++ b/document-store/src/integrationTest/java/org/hypertrace/core/documentstore/DocStoreQueryV1Test.java @@ -3568,6 +3568,95 @@ SELECT item, MAX(date) AS latest_date 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 + JOIN ( + SELECT itemDetails.item, MAX(itemDetails.date) AS latest_date + FROM + 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 + 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 + JOIN ( + SELECT itemDetails.item, MAX(itemDetails.date) AS latest_date + FROM + 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 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 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); } diff --git a/document-store/src/integrationTest/resources/query/items_data_with_nested_fields.json b/document-store/src/integrationTest/resources/query/items_data_with_nested_fields.json new file mode 100644 index 000000000..577e28e74 --- /dev/null +++ b/document-store/src/integrationTest/resources/query/items_data_with_nested_fields.json @@ -0,0 +1,50 @@ +[ + { + "_id": 1, + "itemDetails": { + "item": "suite-1", + "date": 1, + "quantity": 10 + } + }, + { + "_id": 2, + "itemDetails": { + "item": "suite-2", + "date": 1, + "quantity": 10 + } + }, + { + "_id": 3, + "itemDetails": { + "item": "suite-2", + "date": 2, + "quantity": 20 + } + }, + { + "_id": 4, + "itemDetails": { + "item": "suite-2", + "date": 3, + "quantity": 30 + } + }, + { + "_id": 5, + "itemDetails": { + "item": "suite-3", + "date": 2, + "quantity": 20 + } + }, + { + "_id": 6, + "itemDetails": { + "item": "suite-3", + "date": 1, + "quantity": 10 + } + } +] diff --git a/document-store/src/integrationTest/resources/query/sub_query_join_response_with_nested_fields.json b/document-store/src/integrationTest/resources/query/sub_query_join_response_with_nested_fields.json new file mode 100644 index 000000000..002acb729 --- /dev/null +++ b/document-store/src/integrationTest/resources/query/sub_query_join_response_with_nested_fields.json @@ -0,0 +1,23 @@ +[ + { + "itemDetails": { + "item": "suite-1", + "date": 1, + "quantity": 10 + } + }, + { + "itemDetails": { + "item": "suite-2", + "date": 3, + "quantity": 30 + } + }, + { + "itemDetails": { + "item": "suite-3", + "date": 2, + "quantity": 20 + } + } +] From ab72570cffdfd325721258081fcb5b9a4afd9873 Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Fri, 11 Apr 2025 00:28:22 +0530 Subject: [PATCH 12/13] chore: update collection data for integration test --- .../query/items_data_with_nested_fields.json | 24 +++++++++---------- ...uery_join_response_with_nested_fields.json | 12 +++++----- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/document-store/src/integrationTest/resources/query/items_data_with_nested_fields.json b/document-store/src/integrationTest/resources/query/items_data_with_nested_fields.json index 577e28e74..4fc28ad7e 100644 --- a/document-store/src/integrationTest/resources/query/items_data_with_nested_fields.json +++ b/document-store/src/integrationTest/resources/query/items_data_with_nested_fields.json @@ -2,48 +2,48 @@ { "_id": 1, "itemDetails": { - "item": "suite-1", - "date": 1, + "item": "Comb", + "date": "2012-01-01", "quantity": 10 } }, { "_id": 2, "itemDetails": { - "item": "suite-2", - "date": 1, + "item": "Shampoo", + "date": "2012-01-01", "quantity": 10 } }, { "_id": 3, "itemDetails": { - "item": "suite-2", - "date": 2, + "item": "Shampoo", + "date": "2012-02-02", "quantity": 20 } }, { "_id": 4, "itemDetails": { - "item": "suite-2", - "date": 3, + "item": "Shampoo", + "date": "2012-03-03", "quantity": 30 } }, { "_id": 5, "itemDetails": { - "item": "suite-3", - "date": 2, + "item": "Soap", + "date": "2012-02-02", "quantity": 20 } }, { "_id": 6, "itemDetails": { - "item": "suite-3", - "date": 1, + "item": "Soap", + "date": "2012-01-01", "quantity": 10 } } diff --git a/document-store/src/integrationTest/resources/query/sub_query_join_response_with_nested_fields.json b/document-store/src/integrationTest/resources/query/sub_query_join_response_with_nested_fields.json index 002acb729..a6e2774a9 100644 --- a/document-store/src/integrationTest/resources/query/sub_query_join_response_with_nested_fields.json +++ b/document-store/src/integrationTest/resources/query/sub_query_join_response_with_nested_fields.json @@ -1,22 +1,22 @@ [ { "itemDetails": { - "item": "suite-1", - "date": 1, + "item": "Comb", + "date": "2012-01-01", "quantity": 10 } }, { "itemDetails": { - "item": "suite-2", - "date": 3, + "item": "Shampoo", + "date": "2012-03-03", "quantity": 30 } }, { "itemDetails": { - "item": "suite-3", - "date": 2, + "item": "Soap", + "date": "2012-02-02", "quantity": 20 } } From b572d5fd5c22ab9fb6cdc0836badc583382d1ace Mon Sep 17 00:00:00 2001 From: GurtejSohi Date: Fri, 11 Apr 2025 12:10:28 +0530 Subject: [PATCH 13/13] refactor: move inner class in MongoLetClauseBuilder below the outer class methods --- .../query/parser/MongoLetClauseBuilder.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java index 7480bd3fd..fd84f1fa3 100644 --- a/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java +++ b/document-store/src/main/java/org/hypertrace/core/documentstore/mongo/query/parser/MongoLetClauseBuilder.java @@ -39,20 +39,6 @@ public Map visit(RelationalExpression expression) { return Collections.unmodifiableMap(letClause); } - private class MongoLetClauseSelectTypeExpressionVisitor extends MongoEmptySelectionTypeParser { - - @Override - public Map visit(AliasedIdentifierExpression aliasedExpression) { - Map letClause = new HashMap<>(); - if (aliasedExpression.getContextAlias().equals(subQueryAlias)) { - letClause.put( - MongoUtils.encodeVariableName(aliasedExpression.getName()), - MongoUtils.PREFIX + aliasedExpression.getName()); - } - return Collections.unmodifiableMap(letClause); - } - } - @Override public Map visit(KeyExpression expression) { return Collections.emptyMap(); @@ -67,4 +53,18 @@ public Map visit(ArrayRelationalFilterExpression expression) { public Map visit(DocumentArrayFilterExpression expression) { return Collections.emptyMap(); } + + private class MongoLetClauseSelectTypeExpressionVisitor extends MongoEmptySelectionTypeParser { + + @Override + public Map visit(AliasedIdentifierExpression aliasedExpression) { + Map letClause = new HashMap<>(); + if (aliasedExpression.getContextAlias().equals(subQueryAlias)) { + letClause.put( + MongoUtils.encodeVariableName(aliasedExpression.getName()), + MongoUtils.PREFIX + aliasedExpression.getName()); + } + return Collections.unmodifiableMap(letClause); + } + } }