Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(spring): add support for the unwind aggregation stage INTELLIJ-176 #120

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public Document queryMovieById(String id) {
.getCollection("movies")
.aggregate(List.of(Aggregates.match(
Filters.eq(id)
)))
), Aggregates.unwind("year")))
.first();
}

Expand Down Expand Up @@ -74,7 +74,7 @@ public List<Document> queryMoviesByYear(String year) {
)
),
Aggregates.unwind(
"asd",
"awards.wins",
new UnwindOptions()
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PsiElement> {
private val aggregationStageParsers: List<StageParser> = listOf(
MatchStageParser(::parseFilterRecursively),
ProjectStageParser()
ProjectStageParser(),
UnwindStageParser()
)

override fun isCandidateForQuery(source: PsiElement) =
Expand Down
Original file line number Diff line number Diff line change
@@ -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
) = true
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the method name is a little confusing but the idea of this method isSuitableForFieldAutoComplete is for parsers to check and confirm, based on their understanding, if the provided method call is a good target for autocompleter to add field autocomplete.

So it will most likely have the same logic as canParse but might also differ (for example it does differ in project parser). In this case, I think we can safely defer to canParse.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah cool, I'll fix it then! I think eventually we will need to add more information to this method then, because not all arguments in a method call expression are suitable for autocomplete. But this is something we can do later.


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<PsiElement> {
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<PsiElement> =
Node(stageCall, listOf(Named(Name.UNWIND)) + additionalComponents)
}
Original file line number Diff line number Diff line change
@@ -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<Book> 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<HasSourceDialect> {
assertEquals(HasSourceDialect.DialectName.SPRING_CRITERIA, name)
}

collection<HasCollectionReference.OnlyCollection<PsiElement>> {
assertEquals("book", collection)
}

stageN(0, Name.UNWIND) {
field<HasFieldReference.FromSchema<PsiElement>> {
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<Book> 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<HasSourceDialect> {
assertEquals(HasSourceDialect.DialectName.SPRING_CRITERIA, name)
}

collection<HasCollectionReference.OnlyCollection<PsiElement>> {
assertEquals("book", collection)
}

stageN(0, Name.UNWIND) {
field<HasFieldReference.FromSchema<PsiElement>> {
assertEquals("author", fieldName)
}
}
}
}
}
Loading