From 08f07cceaba26d5db657a21f7a6b5b464ba8878a Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Thu, 28 Nov 2024 16:43:53 +0100 Subject: [PATCH] feat: add support for parsing topN/bottomN INTELLIJ-153 (#105) --- .github/workflows/quality-check.yaml | 2 +- CHANGELOG.md | 2 + ...avaDriverFieldCheckLinterInspectionTest.kt | 8 +- .../glossary/JavaDriverDialectParser.kt | 13 +- .../aggregationparser/GroupStageParserTest.kt | 264 ++++++++++++++++++ .../jbplugin/mql/components/HasLimit.kt | 5 + .../mongodb/jbplugin/mql/components/Named.kt | 2 + 7 files changed, 293 insertions(+), 3 deletions(-) create mode 100644 packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasLimit.kt diff --git a/.github/workflows/quality-check.yaml b/.github/workflows/quality-check.yaml index 3df35d63..3270ecc5 100644 --- a/.github/workflows/quality-check.yaml +++ b/.github/workflows/quality-check.yaml @@ -70,7 +70,7 @@ jobs: CHANGELOG.md - uses: mheap/github-action-required-labels@5847eef68201219cf0a4643ea7be61e77837bbce # 5.4.1 - if: ${{ steps.verify-changelog-files.outputs.files_changed == 'false' }} + if: steps.verify-changed-files.outputs.files_changed == 'false' with: mode: minimum count: 1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b9edbce..c5dcdc9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ MongoDB plugin for IntelliJ IDEA. ## [Unreleased] ### Added +* [INTELLIJ-153](https://jira.mongodb.org/browse/INTELLIJ-153) Add support for parsing, linting and + autocompleting fields in Accumulators.topN and Accumulators.bottomN * [INTELLIJ-104](https://jira.mongodb.org/browse/INTELLIJ-104) Add support for Spring Criteria in/nin operator, like in `where(field).in(1, 2, 3)` * [INTELLIJ-61](https://jira.mongodb.org/browse/INTELLIJ-61) Add support for Spring Criteria diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt index 99dcf9fb..2d1a3e6d 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/JavaDriverFieldCheckLinterInspectionTest.kt @@ -590,7 +590,13 @@ public class Repository { getBadFieldName() ), avgCountAcc, - getAvgCountAcc() + getAvgCountAcc(), + Accumulators.topN( + "totalCount", + Sorts.ascending("otherField"), + getBadFieldName(), + 3 + ) ) )); } diff --git a/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectParser.kt b/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectParser.kt index a68fd263..c2912c3c 100644 --- a/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectParser.kt +++ b/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectParser.kt @@ -633,7 +633,9 @@ object JavaDriverDialectParser : DialectParser { "first" -> parseKeyValAccumulator(expression, Name.FIRST) "last" -> parseKeyValAccumulator(expression, Name.LAST) "top" -> parseLeadingAccumulatorExpression(expression, Name.TOP) + "topN" -> parseLeadingAccumulatorExpression(expression, Name.TOP_N) "bottom" -> parseLeadingAccumulatorExpression(expression, Name.BOTTOM) + "bottomN" -> parseLeadingAccumulatorExpression(expression, Name.BOTTOM_N) "max" -> parseKeyValAccumulator(expression, Name.MAX) "min" -> parseKeyValAccumulator(expression, Name.MIN) "push" -> parseKeyValAccumulator(expression, Name.PUSH) @@ -669,6 +671,15 @@ object JavaDriverDialectParser : DialectParser { val keyExpr = expression.argumentList.expressions.getOrNull(0) ?: return null val sortExprArgument = expression.argumentList.expressions.getOrNull(1) ?: return null val valueExpr = expression.argumentList.expressions.getOrNull(2) ?: return null + val hasLimit = expression.argumentList.expressions.getOrNull(3)?.let { + val (wasResolved, value) = it.tryToResolveAsConstant() + val valueAsInt = value as? Int + if (wasResolved && valueAsInt != null) { + listOf(HasLimit(valueAsInt)) + } else { + emptyList() + } + } ?: emptyList() val sortExpr = resolveBsonBuilderCall(sortExprArgument, SORTS_FQN) ?: return null @@ -689,7 +700,7 @@ object JavaDriverDialectParser : DialectParser { ), HasSorts(sort), accumulatorExpr - ) + ) + hasLimit ) } diff --git a/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/aggregationparser/GroupStageParserTest.kt b/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/aggregationparser/GroupStageParserTest.kt index 1c5a562b..fbf630d8 100644 --- a/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/aggregationparser/GroupStageParserTest.kt +++ b/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/aggregationparser/GroupStageParserTest.kt @@ -14,6 +14,7 @@ import com.mongodb.jbplugin.dialects.javadriver.glossary.JavaDriverDialect import com.mongodb.jbplugin.mql.components.HasAccumulatedFields import com.mongodb.jbplugin.mql.components.HasAggregation import com.mongodb.jbplugin.mql.components.HasFieldReference +import com.mongodb.jbplugin.mql.components.HasLimit import com.mongodb.jbplugin.mql.components.HasSorts import com.mongodb.jbplugin.mql.components.HasValueReference import com.mongodb.jbplugin.mql.components.Name @@ -646,4 +647,267 @@ public final class Aggregation { assertEquals("mySort", sortingField.fieldName) } } + + @WithFile( + fileName = "Repository.java", + value = """ +import com.mongodb.client.AggregateIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Accumulators; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Sorts;import org.bson.Document; +import org.bson.types.ObjectId; + +import java.util.List; + +import static com.mongodb.client.model.Filters.*; + +public final class Aggregation { + private final MongoCollection collection; + + public Aggregation(MongoClient client) { + this.collection = client.getDatabase("simple").getCollection("books"); + } + + public AggregateIterable getAllBookTitles(ObjectId id) { + return this.collection.aggregate(List.of( + Aggregates.group( + "${'$'}myField", + Accumulators."|"("myKey", Sorts.ascending("mySort"), "myVal", 3) + ) + )); + } +} + """, + ) + @ParameterizedTest + @CsvSource( + value = [ + "method;;expected", + "topN;;TOP_N", + "bottomN;;BOTTOM_N", + ], + delimiterString = ";;", + useHeadersInDisplayName = true + ) + fun `supports all relevant key-value accumulators with sorting criteria and n value`( + method: String, + expected: Name, + psiFile: PsiFile + ) { + WriteCommandAction.runWriteCommandAction(psiFile.project) { + val elementAtCaret = psiFile.caret() + val javaFacade = JavaPsiFacade.getInstance(psiFile.project) + val methodToTest = javaFacade.parserFacade.createReferenceFromText(method, null) + elementAtCaret.replace(methodToTest) + } + + ApplicationManager.getApplication().runReadAction { + val aggregate = psiFile.getQueryAtMethod("Aggregation", "getAllBookTitles") + val parsedAggregate = JavaDriverDialect.parser.parse(aggregate) + val hasAggregation = parsedAggregate.component>() + assertEquals(1, hasAggregation?.children?.size) + + val groupStage = hasAggregation?.children?.get(0)!! + val named = groupStage.component()!! + assertEquals(Name.GROUP, named.name) + + val accumulator = groupStage.component>()!!.children[0] + val accumulatorName = accumulator.component()!! + assertEquals(expected, accumulatorName.name) + + val accumulatorField = accumulator.component>()?.reference as HasFieldReference.Computed + assertEquals("myKey", accumulatorField.fieldName) + + val accumulatorComputed = accumulator.component>()?.reference as HasValueReference.Computed + val accumulatorComputedFieldValue = accumulatorComputed.type.expression.component>()!!.reference as HasFieldReference.FromSchema + assertEquals("myVal", accumulatorComputedFieldValue.fieldName) + + val accumulatorSorting = accumulator.component>()!!.children[0] + val sortingField = accumulatorSorting.component>()!!.reference as HasFieldReference.FromSchema + assertEquals("mySort", sortingField.fieldName) + + val accumulatorLimit = accumulator.component()!! + assertEquals(3, accumulatorLimit.limit) + } + } + + @WithFile( + fileName = "Repository.java", + value = """ +import com.mongodb.client.AggregateIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Accumulators; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Sorts;import org.bson.Document; +import org.bson.types.ObjectId; + +import java.util.List; + +import static com.mongodb.client.model.Filters.*; + +public final class Aggregation { + private final MongoCollection collection; + + public Aggregation(MongoClient client) { + this.collection = client.getDatabase("simple").getCollection("books"); + } + + public AggregateIterable getAllBookTitles(ObjectId id) { + Number nValue = 3; + return this.collection.aggregate(List.of( + Aggregates.group( + "${'$'}myField", + Accumulators."|"("myKey", Sorts.ascending("mySort"), "myVal", nValue) + ) + )); + } +} + """, + ) + @ParameterizedTest + @CsvSource( + value = [ + "method;;expected", + "topN;;TOP_N", + "bottomN;;BOTTOM_N", + ], + delimiterString = ";;", + useHeadersInDisplayName = true + ) + fun `supports all relevant key-value accumulators with sorting criteria and n value as variable`( + method: String, + expected: Name, + psiFile: PsiFile + ) { + WriteCommandAction.runWriteCommandAction(psiFile.project) { + val elementAtCaret = psiFile.caret() + val javaFacade = JavaPsiFacade.getInstance(psiFile.project) + val methodToTest = javaFacade.parserFacade.createReferenceFromText(method, null) + elementAtCaret.replace(methodToTest) + } + + ApplicationManager.getApplication().runReadAction { + val aggregate = psiFile.getQueryAtMethod("Aggregation", "getAllBookTitles") + val parsedAggregate = JavaDriverDialect.parser.parse(aggregate) + val hasAggregation = parsedAggregate.component>() + assertEquals(1, hasAggregation?.children?.size) + + val groupStage = hasAggregation?.children?.get(0)!! + val named = groupStage.component()!! + assertEquals(Name.GROUP, named.name) + + val accumulator = groupStage.component>()!!.children[0] + val accumulatorName = accumulator.component()!! + assertEquals(expected, accumulatorName.name) + + val accumulatorField = accumulator.component>()?.reference as HasFieldReference.Computed + assertEquals("myKey", accumulatorField.fieldName) + + val accumulatorComputed = accumulator.component>()?.reference as HasValueReference.Computed + val accumulatorComputedFieldValue = accumulatorComputed.type.expression.component>()!!.reference as HasFieldReference.FromSchema + assertEquals("myVal", accumulatorComputedFieldValue.fieldName) + + val accumulatorSorting = accumulator.component>()!!.children[0] + val sortingField = accumulatorSorting.component>()!!.reference as HasFieldReference.FromSchema + assertEquals("mySort", sortingField.fieldName) + + val accumulatorLimit = accumulator.component()!! + assertEquals(3, accumulatorLimit.limit) + } + } + + @WithFile( + fileName = "Repository.java", + value = """ +import com.mongodb.client.AggregateIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Accumulators; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.Sorts;import org.bson.Document; +import org.bson.types.ObjectId; + +import java.util.List; + +import static com.mongodb.client.model.Filters.*; + +public final class Aggregation { + private final MongoCollection collection; + + public Aggregation(MongoClient client) { + this.collection = client.getDatabase("simple").getCollection("books"); + } + + private Number getNValue() { + return 3; + } + + public AggregateIterable getAllBookTitles(ObjectId id) { + return this.collection.aggregate(List.of( + Aggregates.group( + "${'$'}myField", + Accumulators."|"("myKey", Sorts.ascending("mySort"), "myVal", getNValue()) + ) + )); + } +} + """, + ) + @ParameterizedTest + @CsvSource( + value = [ + "method;;expected", + "topN;;TOP_N", + "bottomN;;BOTTOM_N", + ], + delimiterString = ";;", + useHeadersInDisplayName = true + ) + fun `supports all relevant key-value accumulators with sorting criteria and n value from method call`( + method: String, + expected: Name, + psiFile: PsiFile + ) { + WriteCommandAction.runWriteCommandAction(psiFile.project) { + val elementAtCaret = psiFile.caret() + val javaFacade = JavaPsiFacade.getInstance(psiFile.project) + val methodToTest = javaFacade.parserFacade.createReferenceFromText(method, null) + elementAtCaret.replace(methodToTest) + } + + ApplicationManager.getApplication().runReadAction { + val aggregate = psiFile.getQueryAtMethod("Aggregation", "getAllBookTitles") + val parsedAggregate = JavaDriverDialect.parser.parse(aggregate) + val hasAggregation = parsedAggregate.component>() + assertEquals(1, hasAggregation?.children?.size) + + val groupStage = hasAggregation?.children?.get(0)!! + val named = groupStage.component()!! + assertEquals(Name.GROUP, named.name) + + val accumulator = groupStage.component>()!!.children[0] + val accumulatorName = accumulator.component()!! + assertEquals(expected, accumulatorName.name) + + val accumulatorField = accumulator.component>()?.reference as HasFieldReference.Computed + assertEquals("myKey", accumulatorField.fieldName) + + val accumulatorComputed = accumulator.component>()?.reference as HasValueReference.Computed + val accumulatorComputedFieldValue = accumulatorComputed.type.expression.component>()!!.reference as HasFieldReference.FromSchema + assertEquals("myVal", accumulatorComputedFieldValue.fieldName) + + val accumulatorSorting = accumulator.component>()!!.children[0] + val sortingField = accumulatorSorting.component>()!!.reference as HasFieldReference.FromSchema + assertEquals("mySort", sortingField.fieldName) + + val accumulatorLimit = accumulator.component()!! + assertEquals(3, accumulatorLimit.limit) + } + } } diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasLimit.kt b/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasLimit.kt new file mode 100644 index 00000000..f7208048 --- /dev/null +++ b/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasLimit.kt @@ -0,0 +1,5 @@ +package com.mongodb.jbplugin.mql.components + +import com.mongodb.jbplugin.mql.Component + +data class HasLimit(val limit: Int) : Component diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/Named.kt b/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/Named.kt index d75aae54..68951b62 100644 --- a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/Named.kt +++ b/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/Named.kt @@ -58,7 +58,9 @@ enum class Name(val canonical: String) { FIRST("first"), LAST("last"), TOP("top"), + TOP_N("topN"), BOTTOM("bottom"), + BOTTOM_N("bottomN"), MAX("max"), MIN("min"), PUSH("push"),