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(query-generation): support for the $project stage INTELLIJ-193 #126

Merged
merged 5 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 4 commits
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-193](https://jira.mongodb.org/browse/INTELLIJ-193) Add support for generating aggregates with $match and $project.
* [INTELLIJ-189](https://jira.mongodb.org/browse/INTELLIJ-189) Add support for generating update queries.
* [INTELLIJ-177](https://jira.mongodb.org/browse/INTELLIJ-177) Add support for parsing, inspecting and autocompleting in a addFields stage written using `Aggregation.addFields` and chained `AddFieldsOperation`s using `addFieldWithValue`, `addFieldWithValueOf`, `addField().withValue()` and `addField().withValueOf()`. Parsing boxed Java values is not supported yet.
* [INTELLIJ-174](https://jira.mongodb.org/browse/INTELLIJ-174) Add support for parsing, inspecting and autocompleting in a sort stage written using `Aggregation.sort` and chained `SortOperation`s using `and`. All the overloads of creating a `Sort` object are supported.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,6 @@ import com.mongodb.jbplugin.meta.service
import com.mongodb.jbplugin.mql.Node
import com.mongodb.jbplugin.mql.QueryContext
import com.mongodb.jbplugin.mql.components.HasSourceDialect
import com.mongodb.jbplugin.mql.components.IsCommand
import com.mongodb.jbplugin.mql.parser.components.whenHasAnyCommand
import com.mongodb.jbplugin.mql.parser.components.whenIsCommand
import com.mongodb.jbplugin.mql.parser.first
import com.mongodb.jbplugin.mql.parser.map
import com.mongodb.jbplugin.mql.parser.parse
import com.mongodb.jbplugin.observability.TelemetryEvent
import com.mongodb.jbplugin.observability.TelemetryEvent.QueryRunEvent.Console
import com.mongodb.jbplugin.observability.probe.QueryRunProbe
Expand All @@ -69,10 +63,6 @@ internal object RunQueryCodeAction : MongoDbCodeAction {
query: Node<PsiElement>,
formatter: DialectFormatter
): LineMarkerInfo<PsiElement>? {
if (!shouldShowRunGutterIcon(query)) {
return null
}

return LineMarkerInfo(
query.sourceForMarker,
query.sourceForMarker.textRange,
Expand Down Expand Up @@ -124,14 +114,6 @@ internal object RunQueryCodeAction : MongoDbCodeAction {
)
}

private fun shouldShowRunGutterIcon(node: Node<PsiElement>): Boolean {
kmruiz marked this conversation as resolved.
Show resolved Hide resolved
return first(
whenIsCommand<PsiElement>(IsCommand.CommandType.AGGREGATE).map { false },
whenHasAnyCommand<PsiElement>().map { true }
).parse(node) // if we couldn't parse the query, don't show the gutter icon
.orElse { false }
}

private fun openDataGripConsole(
query: Node<PsiElement>,
newDataSource: LocalDataSource,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,39 +84,4 @@ public class Repository {
assertEquals(Icons.runQueryGutter, gutter.icon)
assertEquals(CodeActionsMessages.message("code.action.run.query"), gutter.tooltipText)
}

@ParsingTest(
fileName = "Repository.java",
value = """
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoCollection;
import org.bson.Document;
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 FindIterable<Document> exampleFind() {
return client.getDatabase("myDatabase")
.getCollection("myCollection")
.aggregate(List.of()).first();
}
}
""",
)
fun `does not show a gutter icon for aggregates`(
fixture: CodeInsightTestFixture,
) {
fixture.setupConnection()
fixture.specifyDialect(JavaDriverDialect)

val gutters = fixture.findAllGutters()
assertEquals(0, gutters.size)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.mongodb.client.model.Projections;
import com.mongodb.client.model.Sorts;
import org.bson.Document;
import org.bson.types.ObjectId;

import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -57,13 +58,13 @@ public List<Document> findMoviesByYear(int year) {
.into(new ArrayList<>());
}

public Document queryMovieById(String id) {
public Document queryMovieById(ObjectId id) {
return client
.getDatabase("sample_mflix")
.getCollection("movies")
.aggregate(List.of(Aggregates.match(
Filters.eq(id)
), Aggregates.unwind("year")))
), Aggregates.project(Projections.include("title", "year"))))
.first();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package com.mongodb.jbplugin.dialects.mongosh

import com.mongodb.jbplugin.dialects.DialectFormatter
import com.mongodb.jbplugin.dialects.OutputQuery
import com.mongodb.jbplugin.dialects.mongosh.aggr.canEmitAggregate
import com.mongodb.jbplugin.dialects.mongosh.aggr.emitAggregateBody
import com.mongodb.jbplugin.dialects.mongosh.aggr.isAggregate
import com.mongodb.jbplugin.dialects.mongosh.backend.MongoshBackend
Expand All @@ -23,14 +22,8 @@ object MongoshDialectFormatter : DialectFormatter {
queryContext: QueryContext,
): OutputQuery {
val isAggregate = query.isAggregate()
val canEmitAggregate = query.canEmitAggregate()

val outputString = MongoshBackend(prettyPrint = queryContext.prettyPrint).apply {
if (isAggregate && !canEmitAggregate) {
emitComment("Only aggregates with a single match stage can be converted.")
return@apply
}

emitDbAccess()
emitCollectionReference(query.component<HasCollectionReference<S>>())
if (queryContext.explainPlan != QueryContext.ExplainPlanType.NONE) {
Expand Down Expand Up @@ -62,7 +55,7 @@ object MongoshDialectFormatter : DialectFormatter {
} else {
emitFunctionCall(long = true, {
if (query.isAggregate()) {
emitAggregateBody(query)
emitAggregateBody(query, queryContext)
} else {
emitQueryFilter(query, firstCall = true)
}
Expand All @@ -76,7 +69,6 @@ object MongoshDialectFormatter : DialectFormatter {

val ref = query.component<HasCollectionReference<S>>()?.reference
return when {
isAggregate && !canEmitAggregate -> OutputQuery.Incomplete(outputString)
ref is HasCollectionReference.Known -> if (ref.namespace.isValid) {
OutputQuery.CanBeRun(outputString)
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package com.mongodb.jbplugin.dialects.mongosh.aggr

import com.mongodb.jbplugin.dialects.mongosh.backend.MongoshBackend
import com.mongodb.jbplugin.dialects.mongosh.query.emitQueryFilter
import com.mongodb.jbplugin.dialects.mongosh.query.resolveFieldReference
import com.mongodb.jbplugin.dialects.mongosh.query.resolveValueReference
import com.mongodb.jbplugin.mql.Node
import com.mongodb.jbplugin.mql.QueryContext
import com.mongodb.jbplugin.mql.components.HasAggregation
import com.mongodb.jbplugin.mql.components.HasFilter
import com.mongodb.jbplugin.mql.components.HasFieldReference
import com.mongodb.jbplugin.mql.components.HasValueReference
import com.mongodb.jbplugin.mql.components.IsCommand
import com.mongodb.jbplugin.mql.components.Name
import com.mongodb.jbplugin.mql.parser.anyError
import com.mongodb.jbplugin.mql.parser.components.aggregationStages
import com.mongodb.jbplugin.mql.parser.components.hasName
import com.mongodb.jbplugin.mql.components.Named
import com.mongodb.jbplugin.mql.parser.components.whenIsCommand
import com.mongodb.jbplugin.mql.parser.count
import com.mongodb.jbplugin.mql.parser.filter
import com.mongodb.jbplugin.mql.parser.map
import com.mongodb.jbplugin.mql.parser.matches
import com.mongodb.jbplugin.mql.parser.nth
import com.mongodb.jbplugin.mql.parser.parse

fun <S> Node<S>.isAggregate(): Boolean {
Expand All @@ -24,33 +21,43 @@ fun <S> Node<S>.isAggregate(): Boolean {
.parse(this).orElse { false }
}

fun <S> Node<S>.canEmitAggregate(): Boolean {
return aggregationStages<S>()
.matches(count<Node<S>>().filter { it >= 1 }.matches().anyError())
.nth(0)
.matches(hasName(Name.MATCH))
.map { true }
.parse(this).orElse { false }
}

fun <S> MongoshBackend.emitAggregateBody(node: Node<S>): MongoshBackend {
// here we can assume that we only have 1 single stage that is a match
val matchStage = node.component<HasAggregation<S>>()!!.children[0]
val filter = matchStage.component<HasFilter<S>>()?.children?.getOrNull(0)
val longFilter = filter?.component<HasFilter<S>>()?.children?.size?.let { it > 3 } == true
fun <S> MongoshBackend.emitAggregateBody(node: Node<S>, queryContext: QueryContext): MongoshBackend {
val allStages = node.component<HasAggregation<S>>()?.children ?: emptyList()
val stagesToEmit = when (queryContext.explainPlan) {
QueryContext.ExplainPlanType.NONE -> allStages
else -> allStages.takeWhile { it.isNotDestructive() }
}

emitArrayStart(long = true)
emitObjectStart()
emitObjectKey(registerConstant('$' + "match"))
if (filter != null) {
emitObjectStart(long = longFilter)
emitQueryFilter(filter)
emitObjectEnd(long = longFilter)
} else {
emitComment("No filter provided.")
for (stage in stagesToEmit) {
when (stage.component<Named>()?.name) {
Name.MATCH -> emitMatchStage(stage)
Name.PROJECT -> emitProjectStage(stage)
else -> {}
}
emitObjectValueEnd()
}
emitObjectEnd()
emitArrayEnd(long = true)
return this
}

internal fun <S> MongoshBackend.emitAsFieldValueDocument(nodes: List<Node<S>>): MongoshBackend {
for (node in nodes) {
val field = node.component<HasFieldReference<S>>() ?: continue
val value = node.component<HasValueReference<S>>() ?: continue

emitObjectKey(resolveFieldReference(field))
emitContextValue(resolveValueReference(value, field))
emitObjectValueEnd()
}

return this
}

private val NON_DESTRUCTIVE_STAGES = setOf(
Name.MATCH
)
kmruiz marked this conversation as resolved.
Show resolved Hide resolved

private fun <S> Node<S>.isNotDestructive(): Boolean {
return component<Named>()?.name?.let { NON_DESTRUCTIVE_STAGES.contains(it) } == true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.mongodb.jbplugin.dialects.mongosh.aggr

import com.mongodb.jbplugin.dialects.mongosh.backend.MongoshBackend
import com.mongodb.jbplugin.dialects.mongosh.query.emitQueryFilter
import com.mongodb.jbplugin.mql.Node
import com.mongodb.jbplugin.mql.components.HasFilter

internal fun <S> MongoshBackend.emitMatchStage(node: Node<S>): MongoshBackend {
val filter = node.component<HasFilter<S>>()?.children?.getOrNull(0)
val longFilter = filter?.component<HasFilter<S>>()?.children?.size?.let { it > 3 } == true

emitObjectStart()
emitObjectKey(registerConstant('$' + "match"))
if (filter != null) {
emitObjectStart(long = longFilter)
emitQueryFilter(filter)
emitObjectEnd(long = longFilter)
} else {
emitComment("No filter provided.")
}
emitObjectEnd()

return this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.mongodb.jbplugin.dialects.mongosh.aggr

import com.mongodb.jbplugin.dialects.mongosh.backend.MongoshBackend
import com.mongodb.jbplugin.mql.Node
import com.mongodb.jbplugin.mql.components.HasProjections

internal fun <S> MongoshBackend.emitProjectStage(node: Node<S>): MongoshBackend {
val projections = node.component<HasProjections<S>>()?.children ?: emptyList()
val isLongProjection = projections.size > 3

emitObjectStart(long = isLongProjection)
emitObjectKey(registerConstant('$' + "project"))
emitObjectStart(long = isLongProjection)
emitAsFieldValueDocument(projections)
emitObjectEnd(long = isLongProjection)
emitObjectEnd(long = isLongProjection)

return this
}
Loading
Loading