From d693c1c8189150f960759dc37511dbcc958a58e9 Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Mon, 25 Nov 2024 10:45:36 +0100 Subject: [PATCH 1/3] feat: support for parsing Aggregates.unwind --- .../javadriver/JavaDriverRepository.java | 9 +- .../glossary/JavaDriverDialectParser.kt | 35 +++ .../aggregationparser/UnwindStageParser.kt | 262 ++++++++++++++++++ .../mongodb/jbplugin/mql/components/Named.kt | 1 + 4 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/aggregationparser/UnwindStageParser.kt 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 7b89e06c..84db2f86 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 @@ -1,10 +1,7 @@ package alt.mongodb.javadriver; import com.mongodb.client.MongoClient; -import com.mongodb.client.model.Aggregates; -import com.mongodb.client.model.Filters; -import com.mongodb.client.model.Projections; -import com.mongodb.client.model.Sorts; +import com.mongodb.client.model.*; import org.bson.Document; import java.util.ArrayList; @@ -64,6 +61,10 @@ public List queryMoviesByYear(String year) { Sorts.orderBy( Sorts.ascending("asd", "qwe") ) + ), + Aggregates.unwind( + "asd", + new UnwindOptions() ) ) ) 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 3c92d2dd..98d67dfb 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 @@ -505,6 +505,41 @@ object JavaDriverDialectParser : DialectParser { ) } + "unwind" -> { + val fieldExpression = stageCall.argumentList.expressions.getOrNull(0) + ?: return Node( + source = stageCall, + components = listOf( + Named(Name.UNWIND) + ) + ) + + val fieldName = fieldExpression.tryToResolveAsConstantString() + ?: return Node( + source = stageCall, + components = listOf( + Named(Name.UNWIND), + HasFieldReference( + HasFieldReference.Unknown + ) + ) + ) + + return Node( + source = stageCall, + components = listOf( + Named(Name.UNWIND), + HasFieldReference( + HasFieldReference.FromSchema( + source = fieldExpression, + fieldName = fieldName.trim('$'), + displayName = fieldName, + ) + ), + ) + ) + } + else -> return null } } diff --git a/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/aggregationparser/UnwindStageParser.kt b/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/aggregationparser/UnwindStageParser.kt new file mode 100644 index 00000000..3cfb5cac --- /dev/null +++ b/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/aggregationparser/UnwindStageParser.kt @@ -0,0 +1,262 @@ +package com.mongodb.jbplugin.dialects.javadriver.glossary.aggregationparser + +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.mongodb.jbplugin.dialects.javadriver.IntegrationTest +import com.mongodb.jbplugin.dialects.javadriver.ParsingTest +import com.mongodb.jbplugin.dialects.javadriver.getQueryAtMethod +import com.mongodb.jbplugin.dialects.javadriver.glossary.JavaDriverDialect +import com.mongodb.jbplugin.mql.components.HasAggregation +import com.mongodb.jbplugin.mql.components.HasFieldReference +import com.mongodb.jbplugin.mql.components.Name +import com.mongodb.jbplugin.mql.components.Named +import org.junit.jupiter.api.Assertions.assertEquals + +@IntegrationTest +class UnwindStageParser { + @ParsingTest( + fileName = "Aggregation.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.Filters; +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.unwind() + )); + } +} + """ + ) + fun `should be able to parse an empty unwind call`(psiFile: PsiFile) { + val aggregate = psiFile.getQueryAtMethod("Aggregation", "getAllBookTitles") + val parsedAggregate = JavaDriverDialect.parser.parse(aggregate) + val hasAggregation = parsedAggregate.component>() + assertEquals(1, hasAggregation?.children?.size) + + val unwindStageNode = hasAggregation?.children?.get(0)!! + assertEquals(1, unwindStageNode.components.size) + + val named = unwindStageNode.component()!! + assertEquals(Name.UNWIND, named.name) + } + + @ParsingTest( + fileName = "Aggregation.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.Filters; +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.unwind("${'$'}name") + )); + } +} + """ + ) + fun `should be able to parse an unwind call with fieldName`(psiFile: PsiFile) { + val aggregate = psiFile.getQueryAtMethod("Aggregation", "getAllBookTitles") + val parsedAggregate = JavaDriverDialect.parser.parse(aggregate) + val hasAggregation = parsedAggregate.component>() + assertEquals(1, hasAggregation?.children?.size) + + val unwindStageNode = hasAggregation?.children?.get(0)!! + assertEquals(2, unwindStageNode.components.size) + + val named = unwindStageNode.component()!! + assertEquals(Name.UNWIND, named.name) + + val fieldReference = unwindStageNode.component>()!!.reference + as HasFieldReference.FromSchema + assertEquals("name", fieldReference.fieldName) + assertEquals("${'$'}name", fieldReference.displayName) + } + + @ParsingTest( + fileName = "Aggregation.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.Filters; +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) { + String fieldName = "${'$'}name"; + return this.collection.aggregate(List.of( + Aggregates.unwind(fieldName) + )); + } +} + """ + ) + fun `should be able to parse an unwind call with fieldName where fieldName is variable`( + psiFile: PsiFile + ) { + val aggregate = psiFile.getQueryAtMethod("Aggregation", "getAllBookTitles") + val parsedAggregate = JavaDriverDialect.parser.parse(aggregate) + val hasAggregation = parsedAggregate.component>() + assertEquals(1, hasAggregation?.children?.size) + + val unwindStageNode = hasAggregation?.children?.get(0)!! + assertEquals(2, unwindStageNode.components.size) + + val named = unwindStageNode.component()!! + assertEquals(Name.UNWIND, named.name) + + val fieldReference = unwindStageNode.component>()!!.reference + as HasFieldReference.FromSchema + assertEquals("name", fieldReference.fieldName) + assertEquals("${'$'}name", fieldReference.displayName) + } + + @ParsingTest( + fileName = "Aggregation.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.Filters; +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 String getUnwindField() { + return "${'$'}name"; + } + + public AggregateIterable getAllBookTitles(ObjectId id) { + return this.collection.aggregate(List.of( + Aggregates.unwind(getUnwindField()) + )); + } +} + """ + ) + fun `should be able to parse an unwind call with fieldName where fieldName is from a method call`( + psiFile: PsiFile + ) { + val aggregate = psiFile.getQueryAtMethod("Aggregation", "getAllBookTitles") + val parsedAggregate = JavaDriverDialect.parser.parse(aggregate) + val hasAggregation = parsedAggregate.component>() + assertEquals(1, hasAggregation?.children?.size) + + val unwindStageNode = hasAggregation?.children?.get(0)!! + assertEquals(2, unwindStageNode.components.size) + + val named = unwindStageNode.component()!! + assertEquals(Name.UNWIND, named.name) + + val fieldReference = unwindStageNode.component>()!!.reference + as HasFieldReference.FromSchema + assertEquals("name", fieldReference.fieldName) + assertEquals("${'$'}name", fieldReference.displayName) + } + + @ParsingTest( + fileName = "Aggregation.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.Filters; +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.unwind("${'$'}name", new UnwindOptions()) + )); + } +} + """ + ) + fun `should be able to parse an unwind call with fieldName while ignoring the UnwindOptions`( + psiFile: PsiFile + ) { + val aggregate = psiFile.getQueryAtMethod("Aggregation", "getAllBookTitles") + val parsedAggregate = JavaDriverDialect.parser.parse(aggregate) + val hasAggregation = parsedAggregate.component>() + assertEquals(1, hasAggregation?.children?.size) + + val unwindStageNode = hasAggregation?.children?.get(0)!! + assertEquals(2, unwindStageNode.components.size) + + val named = unwindStageNode.component()!! + assertEquals(Name.UNWIND, named.name) + + val fieldReference = unwindStageNode.component>()!!.reference + as HasFieldReference.FromSchema + assertEquals("name", fieldReference.fieldName) + assertEquals("${'$'}name", fieldReference.displayName) + } +} 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 dba2cdda..53bcc703 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 @@ -56,6 +56,7 @@ enum class Name(val canonical: String) { ASCENDING("ascending"), DESCENDING("descending"), ADD_FIELDS("addFields"), + UNWIND("unwind"), UNKNOWN(""), ; From a9796147fdab5066a7e3419a8ddc252877f4f394 Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Mon, 25 Nov 2024 11:09:06 +0100 Subject: [PATCH 2/3] chore: add tests for lint checks on Aggregates.unwind --- ...avaDriverFieldCheckLinterInspectionTest.kt | 95 +++++++++++++++++++ .../linting/FieldCheckingLinterTest.kt | 52 ++++++++++ 2 files changed, 147 insertions(+) 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 1da59d4a..bacf8a59 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 @@ -9,6 +9,7 @@ import com.mongodb.jbplugin.fixtures.setupConnection import com.mongodb.jbplugin.fixtures.specifyDialect import com.mongodb.jbplugin.mql.BsonDouble import com.mongodb.jbplugin.mql.BsonObject +import com.mongodb.jbplugin.mql.BsonString import com.mongodb.jbplugin.mql.CollectionSchema import com.mongodb.jbplugin.mql.Namespace import org.mockito.Mockito.`when` @@ -501,4 +502,98 @@ public class Repository { fixture.enableInspections(FieldCheckInspectionBridge::class.java) fixture.testHighlighting() } + + @ParsingTest( + 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.Projections; +import com.mongodb.client.model.Sorts; +import org.bson.Document; +import org.bson.conversions.Bson; +import org.bson.types.ObjectId; +import java.util.List; +import static com.mongodb.client.model.Filters.*; + +public class Repository { + private final MongoClient client; + + public Repository(MongoClient client) { + this.client = client; + } + + public AggregateIterable exampleGoodUnwind() { + return client.getDatabase("myDatabase") + .getCollection("myCollection") + .aggregate(List.of( + Aggregates.unwind( + "${'$'}existingField" + ) + )); + } + + public AggregateIterable exampleUnwind1() { + return client.getDatabase("myDatabase") + .getCollection("myCollection") + .aggregate(List.of( + Aggregates.unwind( + "${'$'}nonExistingField" + ) + )); + } + + public AggregateIterable exampleUnwind2() { + String fieldName = "${'$'}nonExistingField"; + return client.getDatabase("myDatabase") + .getCollection("myCollection") + .aggregate(List.of( + Aggregates.unwind( + fieldName + ) + )); + } + + private String getField() { + return "${'$'}nonExistingField"; + } + + public AggregateIterable exampleUnwind3() { + return client.getDatabase("myDatabase") + .getCollection("myCollection") + .aggregate(List.of( + Aggregates.unwind( + getField() + ) + )); + } +} + """, + ) + fun `shows an inspection for Aggregates#unwind call when the field does not exist in the current namespace`( + fixture: CodeInsightTestFixture, + ) { + val (dataSource, readModelProvider) = fixture.setupConnection() + fixture.specifyDialect(JavaDriverDialect) + + `when`( + readModelProvider.slice(eq(dataSource), any()) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + Namespace("myDatabase", "myCollection"), + BsonObject( + mapOf( + "existingField" to BsonString + ) + ) + ) + ), + ) + + fixture.enableInspections(FieldCheckInspectionBridge::class.java) + fixture.testHighlighting() + } } diff --git a/packages/mongodb-linting-engine/src/test/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt b/packages/mongodb-linting-engine/src/test/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt index b364faad..b24a961e 100644 --- a/packages/mongodb-linting-engine/src/test/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt +++ b/packages/mongodb-linting-engine/src/test/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt @@ -613,4 +613,56 @@ class FieldCheckingLinterTest { assertEquals(0, result.warnings.size) } + + @Test + fun `should not warn about the referenced fields in an Aggregation#unwind`() { + val readModelProvider = mock>() + val collectionNamespace = Namespace("database", "collection") + + `when`(readModelProvider.slice(any(), any())).thenReturn( + GetCollectionSchema( + CollectionSchema( + collectionNamespace, + BsonObject( + mapOf( + "myInt" to BsonInt32, + ), + ), + ), + ), + ) + + val result = + FieldCheckingLinter.lintQuery( + Unit, + readModelProvider, + Node( + null, + listOf( + HasCollectionReference( + HasCollectionReference.Known(null, null, collectionNamespace) + ), + HasAggregation( + children = listOf( + Node( + null, + listOf( + Named(Name.UNWIND), + HasFieldReference( + HasFieldReference.FromSchema( + null, + "myBoolean", + "${'$'}myBoolean" + ) + ) + ) + ) + ) + ) + ), + ), + ) + + assertEquals(1, result.warnings.size) + } } From 87f30f90a0872225e17772c093e2e6fc9e47eeae Mon Sep 17 00:00:00 2001 From: Himanshu Singh Date: Mon, 25 Nov 2024 11:11:15 +0100 Subject: [PATCH 3/3] chore: add tests for AC in Aggregates.unwind --- .../JavaDriverCompletionContributorTest.kt | 63 ++++++++++++++++++ ...erMongoDbAutocompletionPopupHandlerTest.kt | 64 +++++++++++++++++++ 2 files changed, 127 insertions(+) diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverCompletionContributorTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverCompletionContributorTest.kt index 51b51f53..9e8b9f02 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverCompletionContributorTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverCompletionContributorTest.kt @@ -805,4 +805,67 @@ public class Repository { }, ) } + + @ParsingTest( + fileName = "Repository.java", + value = """ +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Projections; +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.*; +import static com.mongodb.client.model.Updates.*; + +public class Repository { + private final MongoClient client; + + public Repository(MongoClient client) { + this.client = client; + } + + public void exampleFind() { + client.getDatabase("myDatabase").getCollection("myCollection") + .aggregate(List.of( + Aggregates.unwind("") + )); + } +} + """, + ) + fun `should autocomplete fields from the current namespace in an Aggregates#unwind stage`( + fixture: CodeInsightTestFixture, + ) { + fixture.specifyDialect(JavaDriverDialect) + + val (dataSource, readModelProvider) = fixture.setupConnection() + val namespace = Namespace("myDatabase", "myCollection") + + `when`( + readModelProvider.slice(eq(dataSource), eq(GetCollectionSchema.Slice(namespace))) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "myField" to BsonString, + ), + ), + ), + ), + ) + + val elements = fixture.completeBasic() + + assertTrue( + elements.containsElements { + it.lookupString == "myField" + }, + ) + } } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverMongoDbAutocompletionPopupHandlerTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverMongoDbAutocompletionPopupHandlerTest.kt index 357c9b5a..f6a3dd0e 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverMongoDbAutocompletionPopupHandlerTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/autocomplete/JavaDriverMongoDbAutocompletionPopupHandlerTest.kt @@ -818,4 +818,68 @@ public class Repository { }, ) } + + @ParsingTest( + fileName = "Repository.java", + value = """ +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Aggregates; +import com.mongodb.client.model.Projections; +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.*; +import static com.mongodb.client.model.Updates.*; + +public class Repository { + private final MongoClient client; + + public Repository(MongoClient client) { + this.client = client; + } + + public void exampleFind() { + client.getDatabase("myDatabase").getCollection("myCollection") + .aggregate(List.of( + Aggregates.unwind() + )); + } +} + """, + ) + fun `should autocomplete fields from the current namespace in an Aggregates#unwind stage`( + fixture: CodeInsightTestFixture, + ) { + fixture.specifyDialect(JavaDriverDialect) + + val (dataSource, readModelProvider) = fixture.setupConnection() + val namespace = Namespace("myDatabase", "myCollection") + + `when`( + readModelProvider.slice(eq(dataSource), eq(GetCollectionSchema.Slice(namespace))) + ).thenReturn( + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "myField" to BsonString, + ), + ), + ), + ), + ) + + fixture.type('"') + val elements = fixture.completeBasic() + + assertTrue( + elements.containsElements { + it.lookupString == "myField" + }, + ) + } }