diff --git a/CHANGELOG.md b/CHANGELOG.md index b2bca3b1..fa76beb5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ MongoDB plugin for IntelliJ IDEA. ## [Unreleased] ### Added +* [INTELLIJ-176](https://jira.mongodb.org/browse/INTELLIJ-176) Add support for parsing, inspecting and autocompleting in an unwind stage written using `Aggregation.unwind`. * [INTELLIJ-173](https://jira.mongodb.org/browse/INTELLIJ-173) Add support for parsing, inspecting and autocompleting in a project stage written using `Aggregation.match` and chained `ProjectionOperations` using `andInclude` and `andExclude`. * [INTELLIJ-172](https://jira.mongodb.org/browse/INTELLIJ-172) Add support for parsing, inspecting and autocompleting in an aggregation written using Spring Data MongoDB (`MongoTemplate.aggregate`, `MongoTemplate.aggregateStream`) and a match stage written using `Aggregation.match`. * [INTELLIJ-179](https://jira.mongodb.org/browse/INTELLIJ-179) Telemetry when Create Index intention is clicked. diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/SpringCriteriaCompletionContributorTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/SpringCriteriaCompletionContributorTest.kt index 302f6e64..dd4b7b88 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/SpringCriteriaCompletionContributorTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/SpringCriteriaCompletionContributorTest.kt @@ -910,4 +910,78 @@ class Repository { }, ) } + + @ParsingTest( + fileName = "Repository.java", + value = """ +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.query.Criteria; + +import java.util.List; + +import static org.springframework.data.mongodb.core.query.Criteria.where; + +@Document +record Book() {} + +class Repository { + private final MongoTemplate template; + + public Repository(MongoTemplate template) { + this.template = template; + } + + public List allReleasedBooks() { + return template.aggregate( + Aggregation.newAggregation( + Aggregation.unwind("") + ), + Book.class, + Book.class + ).getMappedResults(); + } +} + """, + ) + fun `should autocomplete fields from the current namespace in unwind`( + fixture: CodeInsightTestFixture, + ) { + val (dataSource, readModelProvider) = fixture.setupConnection() + fixture.specifyDatabase("myDatabase") + fixture.specifyDialect(SpringCriteriaDialect) + + val namespace = Namespace("myDatabase", "book") + + `when`( + readModelProvider.slice(eq(dataSource), eq(GetCollectionSchema.Slice(namespace))) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "myField" to BsonString, + "myField2" to BsonString, + ), + ), + ), + ), + ) + + val elements = fixture.completeBasic() + + assertTrue( + elements.containsElements { + it.lookupString == "myField" + }, + ) + + assertTrue( + elements.containsElements { + it.lookupString == "myField2" + }, + ) + } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/SpringCriteriaFieldCheckLinterInspectionTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/SpringCriteriaFieldCheckLinterInspectionTest.kt index 9490f8f8..528301e0 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/SpringCriteriaFieldCheckLinterInspectionTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/inspections/impl/SpringCriteriaFieldCheckLinterInspectionTest.kt @@ -213,6 +213,9 @@ class BookRepository { "released", releasedAsVariable, releasedFromMethodCall() + ), + Aggregation.unwind( + "released" ) ), Book.class, diff --git a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java index 5e8dc079..7a8c91ac 100644 --- a/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java +++ b/packages/jetbrains-plugin/src/test/resources/project-fixtures/basic-java-project-with-mongodb/src/main/java/alt/mongodb/javadriver/JavaDriverRepository.java @@ -44,7 +44,7 @@ public Document queryMovieById(String id) { .getCollection("movies") .aggregate(List.of(Aggregates.match( Filters.eq(id) - ))) + ), Aggregates.unwind("year"))) .first(); } @@ -74,7 +74,7 @@ public List queryMoviesByYear(String year) { ) ), Aggregates.unwind( - "asd", + "awards.wins", new UnwindOptions() ) ) diff --git a/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParser.kt b/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParser.kt index e5a41b99..36dd8395 100644 --- a/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParser.kt +++ b/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/SpringCriteriaDialectParser.kt @@ -13,23 +13,25 @@ import com.mongodb.jbplugin.dialects.springcriteria.QueryTargetCollectionExtract import com.mongodb.jbplugin.dialects.springcriteria.aggregationstageparsers.MatchStageParser import com.mongodb.jbplugin.dialects.springcriteria.aggregationstageparsers.ProjectStageParser import com.mongodb.jbplugin.dialects.springcriteria.aggregationstageparsers.StageParser +import com.mongodb.jbplugin.dialects.springcriteria.aggregationstageparsers.UnwindStageParser import com.mongodb.jbplugin.mql.BsonAny import com.mongodb.jbplugin.mql.BsonArray import com.mongodb.jbplugin.mql.Node import com.mongodb.jbplugin.mql.components.* import com.mongodb.jbplugin.mql.toBsonType -private const val CRITERIA_CLASS_FQN = "org.springframework.data.mongodb.core.query.Criteria" -private const val DOCUMENT_FQN = "org.springframework.data.mongodb.core.mapping.Document" -private const val MONGO_TEMPLATE_FQN = "org.springframework.data.mongodb.core.MongoTemplate" -const val AGGREGATE_FQN = "org.springframework.data.mongodb.core.aggregation.Aggregation" -const val PROJECTION_OPERATION_FQN = "org.springframework.data.mongodb.core.aggregation.ProjectionOperation" -const val FIELDS_FQN = "org.springframework.data.mongodb.core.aggregation.Fields" +internal const val CRITERIA_CLASS_FQN = "org.springframework.data.mongodb.core.query.Criteria" +internal const val DOCUMENT_FQN = "org.springframework.data.mongodb.core.mapping.Document" +internal const val MONGO_TEMPLATE_FQN = "org.springframework.data.mongodb.core.MongoTemplate" +internal const val AGGREGATE_FQN = "org.springframework.data.mongodb.core.aggregation.Aggregation" +internal const val PROJECTION_OPERATION_FQN = "org.springframework.data.mongodb.core.aggregation.ProjectionOperation" +internal const val FIELDS_FQN = "org.springframework.data.mongodb.core.aggregation.Fields" object SpringCriteriaDialectParser : DialectParser { private val aggregationStageParsers: List = listOf( MatchStageParser(::parseFilterRecursively), - ProjectStageParser() + ProjectStageParser(), + UnwindStageParser() ) override fun isCandidateForQuery(source: PsiElement) = diff --git a/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/aggregationstageparsers/UnwindStageParser.kt b/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/aggregationstageparsers/UnwindStageParser.kt new file mode 100644 index 00000000..3a6debb5 --- /dev/null +++ b/packages/mongodb-dialects/spring-criteria/src/main/kotlin/com/mongodb/jbplugin/dialects/springcriteria/aggregationstageparsers/UnwindStageParser.kt @@ -0,0 +1,40 @@ +package com.mongodb.jbplugin.dialects.springcriteria.aggregationstageparsers + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiMethodCallExpression +import com.mongodb.jbplugin.dialects.javadriver.glossary.tryToResolveAsConstantString +import com.mongodb.jbplugin.dialects.springcriteria.AGGREGATE_FQN +import com.mongodb.jbplugin.mql.Component +import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.components.HasFieldReference +import com.mongodb.jbplugin.mql.components.Name +import com.mongodb.jbplugin.mql.components.Named + +class UnwindStageParser : StageParser { + override fun isSuitableForFieldAutoComplete( + methodCall: PsiMethodCallExpression, + method: PsiMethod + ) = canParse(method) + + override fun canParse(stageCallMethod: PsiMethod): Boolean { + val owningClassFqn = stageCallMethod.containingClass?.qualifiedName ?: return false + return owningClassFqn == AGGREGATE_FQN && stageCallMethod.name == "unwind" + } + + override fun parse(stageCall: PsiMethodCallExpression): Node { + val psiField = + stageCall.argumentList.expressions.getOrNull(0) ?: return unwindNode(stageCall) + + val referencedField = psiField.tryToResolveAsConstantString() + ?: return unwindNode(stageCall) + + return unwindNode( + stageCall, + HasFieldReference(HasFieldReference.FromSchema(psiField, referencedField)) + ) + } + + private fun unwindNode(stageCall: PsiMethodCallExpression, vararg additionalComponents: Component): Node = + Node(stageCall, listOf(Named(Name.UNWIND)) + additionalComponents) +} diff --git a/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/aggregationstageparsers/UnwindStageParserTest.kt b/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/aggregationstageparsers/UnwindStageParserTest.kt new file mode 100644 index 00000000..47318f58 --- /dev/null +++ b/packages/mongodb-dialects/spring-criteria/src/test/kotlin/com/mongodb/jbplugin/dialects/springcriteria/aggregationstageparsers/UnwindStageParserTest.kt @@ -0,0 +1,123 @@ +package com.mongodb.jbplugin.dialects.springcriteria.aggregationstageparsers + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.mongodb.jbplugin.dialects.springcriteria.IntegrationTest +import com.mongodb.jbplugin.dialects.springcriteria.ParsingTest +import com.mongodb.jbplugin.dialects.springcriteria.SpringCriteriaDialectParser +import com.mongodb.jbplugin.dialects.springcriteria.assert +import com.mongodb.jbplugin.dialects.springcriteria.collection +import com.mongodb.jbplugin.dialects.springcriteria.component +import com.mongodb.jbplugin.dialects.springcriteria.field +import com.mongodb.jbplugin.dialects.springcriteria.getQueryAtMethod +import com.mongodb.jbplugin.dialects.springcriteria.stageN +import com.mongodb.jbplugin.mql.components.HasCollectionReference +import com.mongodb.jbplugin.mql.components.HasFieldReference +import com.mongodb.jbplugin.mql.components.HasSourceDialect +import com.mongodb.jbplugin.mql.components.IsCommand +import com.mongodb.jbplugin.mql.components.Name +import org.junit.jupiter.api.Assertions.assertEquals + +@IntegrationTest +class UnwindStageParserTest { + @ParsingTest( + fileName = "Book.java", + """ +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.List; + +@Document +record Book() {} + +class Repository { + private final MongoTemplate template; + + public Repository(MongoTemplate template) { + this.template = template; + } + + public AggregationResults allReleasedBooks() { + return template.aggregate( + Aggregation.newAggregation( + Aggregation.unwind("author") + ), + Book.class, + Book.class + ); + } +} + """ + ) + fun `should be able to parse an unwind stage with a literal field name`(psiFile: PsiFile) { + val query = psiFile.getQueryAtMethod("Repository", "allReleasedBooks") + SpringCriteriaDialectParser.parse(query).assert(IsCommand.CommandType.AGGREGATE) { + component { + assertEquals(HasSourceDialect.DialectName.SPRING_CRITERIA, name) + } + + collection> { + assertEquals("book", collection) + } + + stageN(0, Name.UNWIND) { + field> { + assertEquals("author", fieldName) + } + } + } + } + + @ParsingTest( + fileName = "Book.java", + """ +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.mapping.Document; + +import java.util.List; + +@Document +record Book() {} + +class Repository { + private static final String AUTHOR = "author"; + private final MongoTemplate template; + + public Repository(MongoTemplate template) { + this.template = template; + } + + public AggregationResults allReleasedBooks() { + return template.aggregate( + Aggregation.newAggregation( + Aggregation.unwind(AUTHOR) + ), + Book.class, + Book.class + ); + } +} + """ + ) + fun `should be able to parse an unwind stage with a constant field name`(psiFile: PsiFile) { + val query = psiFile.getQueryAtMethod("Repository", "allReleasedBooks") + SpringCriteriaDialectParser.parse(query).assert(IsCommand.CommandType.AGGREGATE) { + component { + assertEquals(HasSourceDialect.DialectName.SPRING_CRITERIA, name) + } + + collection> { + assertEquals("book", collection) + } + + stageN(0, Name.UNWIND) { + field> { + assertEquals("author", fieldName) + } + } + } + } +}