diff --git a/CHANGELOG.md b/CHANGELOG.md index 737f15aa8..066f66139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ MongoDB plugin for IntelliJ IDEA. ## [Unreleased] ### Added +* [INTELLIJ-187](https://jira.mongodb.org/browse/INTELLIJ-187) Use safe execution plans by default. Allow full execution + plans through a Plugin settings flag. * [INTELLIJ-180](https://jira.mongodb.org/browse/INTELLIJ-180) Telemetry when inspections are shown and resolved. It can be disabled in the Plugin settings. * [INTELLIJ-176](https://jira.mongodb.org/browse/INTELLIJ-176) Add support for parsing, inspecting and autocompleting in an unwind stage written using `Aggregation.unwind`. diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/codeActions/impl/RunQueryCodeActionBridge.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/codeActions/impl/RunQueryCodeActionBridge.kt index 78607d16a..fffc6c905 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/codeActions/impl/RunQueryCodeActionBridge.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/codeActions/impl/RunQueryCodeActionBridge.kt @@ -35,6 +35,7 @@ import com.mongodb.jbplugin.i18n.CodeActionsMessages import com.mongodb.jbplugin.i18n.Icons 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 @@ -86,7 +87,7 @@ internal object RunQueryCodeAction : MongoDbCodeAction { coroutineScope.launchChildBackground { val outputQuery = MongoshDialect.formatter.formatQuery( query, - explain = false + QueryContext.empty() ) if (dataSource?.isConnected() == true) { diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/IndexCheckInspectionBridge.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/IndexCheckInspectionBridge.kt index 6cfe68eb0..32ee5eb80 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/IndexCheckInspectionBridge.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/inspections/impl/IndexCheckInspectionBridge.kt @@ -21,9 +21,11 @@ import com.mongodb.jbplugin.linting.IndexCheckWarning import com.mongodb.jbplugin.linting.IndexCheckingLinter import com.mongodb.jbplugin.meta.service import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext import com.mongodb.jbplugin.observability.TelemetryEvent import com.mongodb.jbplugin.observability.probe.CreateIndexIntentionProbe import com.mongodb.jbplugin.observability.probe.InspectionStatusChangedProbe +import com.mongodb.jbplugin.settings.pluginSetting import kotlinx.coroutines.CoroutineScope class IndexCheckInspectionBridge(coroutineScope: CoroutineScope) : @@ -52,12 +54,28 @@ internal object IndexCheckLinterInspection : MongoDbInspection { query: Node, formatter: DialectFormatter, ) { + val isFullExplainPlanEnabled by pluginSetting { ::isFullExplainPlanEnabled } + if (dataSource == null || !dataSource.isConnected()) { return } + val queryContext = QueryContext( + emptyMap(), + if (isFullExplainPlanEnabled) { + QueryContext.ExplainPlanType.FULL + } else { + QueryContext.ExplainPlanType.SAFE + } + ) + val readModelProvider by query.source.project.service() - val result = IndexCheckingLinter.lintQuery(dataSource, readModelProvider, query) + val result = IndexCheckingLinter.lintQuery( + dataSource, + readModelProvider, + query, + queryContext + ) result.warnings.forEach { when (it) { diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/settings/PluginSettings.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/settings/PluginSettings.kt index 0e0b1a389..003e9c1e4 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/settings/PluginSettings.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/settings/PluginSettings.kt @@ -48,6 +48,7 @@ class PluginSettingsStateComponent : SimplePersistentStateComponent(private val settingProp: KMutableProperty0) { diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/settings/PluginSettingsConfigurable.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/settings/PluginSettingsConfigurable.kt index a98f35933..94849255c 100644 --- a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/settings/PluginSettingsConfigurable.kt +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/settings/PluginSettingsConfigurable.kt @@ -30,13 +30,16 @@ class PluginSettingsConfigurable : Configurable { override fun isModified(): Boolean { val savedSettings by service() return settingsComponent.isTelemetryEnabledCheckBox.isSelected != - savedSettings.state.isTelemetryEnabled + savedSettings.state.isTelemetryEnabled || + settingsComponent.enableFullExplainPlan.isSelected != + savedSettings.state.isFullExplainPlanEnabled } override fun apply() { val savedSettings by service() savedSettings.state.apply { isTelemetryEnabled = settingsComponent.isTelemetryEnabledCheckBox.isSelected + isFullExplainPlanEnabled = settingsComponent.enableFullExplainPlan.isSelected } } @@ -44,6 +47,8 @@ class PluginSettingsConfigurable : Configurable { val savedSettings by service() settingsComponent.isTelemetryEnabledCheckBox.isSelected = savedSettings.state.isTelemetryEnabled + settingsComponent.enableFullExplainPlan.isSelected = + savedSettings.state.isFullExplainPlanEnabled } override fun getDisplayName() = SettingsMessages.message("settings.display-name") @@ -56,15 +61,29 @@ private class PluginSettingsComponent { val root: JPanel val isTelemetryEnabledCheckBox = JBCheckBox(TelemetryMessages.message("settings.telemetry-collection-checkbox")) + val enableFullExplainPlan = + JBCheckBox(TelemetryMessages.message("settings.full-explain-plan-checkbox")) val privacyPolicyButton = JButton(TelemetryMessages.message("action.view-privacy-policy")) + val evaluateOperationPerformanceButton = + JButton(TelemetryMessages.message("settings.view-full-explain-plan-documentation")) init { privacyPolicyButton.addActionListener { BrowserUtil.browse(TelemetryMessages.message("settings.telemetry-privacy-policy")) } + evaluateOperationPerformanceButton.addActionListener { + BrowserUtil.browse( + TelemetryMessages.message("settings.full-explain-plan-documentation") + ) + } + root = FormBuilder.createFormBuilder() + .addComponent(enableFullExplainPlan) + .addTooltip(TelemetryMessages.message("settings.full-explain-plan-tooltip")) + .addComponent(evaluateOperationPerformanceButton) + .addSeparator() .addComponent(isTelemetryEnabledCheckBox) .addTooltip(TelemetryMessages.message("settings.telemetry-collection-tooltip")) .addComponent(privacyPolicyButton) @@ -73,5 +92,6 @@ private class PluginSettingsComponent { root.accessibleContext.accessibleName = "MongoDB Settings" isTelemetryEnabledCheckBox.accessibleContext.accessibleName = "MongoDB Enable Telemetry" + enableFullExplainPlan.accessibleContext.accessibleName = "MongoDB Enable Full Explain Plan" } } diff --git a/packages/jetbrains-plugin/src/main/resources/messages/TelemetryBundle.properties b/packages/jetbrains-plugin/src/main/resources/messages/TelemetryBundle.properties index 5dc276718..47e32bf4e 100644 --- a/packages/jetbrains-plugin/src/main/resources/messages/TelemetryBundle.properties +++ b/packages/jetbrains-plugin/src/main/resources/messages/TelemetryBundle.properties @@ -5,4 +5,8 @@ action.disable-telemetry=Disable Telemetry action.view-privacy-policy=View Privacy Policy settings.telemetry-collection-tooltip=Allow the collection of anonymous diagnostics and usage telemetry data to help improve the product. settings.telemetry-collection-checkbox=Enable telemetry -settings.telemetry-privacy-policy=https://www.mongodb.com/legal/privacy/privacy-policy \ No newline at end of file +settings.full-explain-plan-tooltip=Uses 'executionStats', running the actual query and providing runtime information. Might impact cluster performance. +settings.full-explain-plan-checkbox=Enable full explain plan +settings.view-full-explain-plan-documentation=Analyze Query Performance +settings.full-explain-plan-documentation=https://www.mongodb.com/docs/manual/tutorial/evaluate-operation-performance/ +settings.telemetry-privacy-policy=https://www.mongodb.com/legal/privacy/privacy-policy diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/MongoDbEnvironmentTestExtensions.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/MongoDbEnvironmentTestExtensions.kt index be4bf7393..b4a5c393a 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/MongoDbEnvironmentTestExtensions.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/MongoDbEnvironmentTestExtensions.kt @@ -15,6 +15,7 @@ import com.mongodb.jbplugin.accessadapter.MongoDbDriver import com.mongodb.jbplugin.accessadapter.datagrip.DataGripBasedReadModelProvider import com.mongodb.jbplugin.mql.Namespace import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext import kotlinx.coroutines.withTimeout import org.bson.Document import org.bson.conversions.Bson @@ -142,7 +143,7 @@ internal class DirectMongoDbDriver( override suspend fun connectionString(): ConnectionString = ConnectionString(uri) - override suspend fun explain(query: Node): ExplainPlan { + override suspend fun explain(query: Node, queryContext: QueryContext): ExplainPlan { throw UnsupportedOperationException() } diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MongoDbSettingsFixture.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MongoDbSettingsFixture.kt index ed27787c6..6409c3c7f 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MongoDbSettingsFixture.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/fixtures/components/MongoDbSettingsFixture.kt @@ -33,10 +33,22 @@ class MongoDbSettingsFixture( findAll().find { it.text == "Enable telemetry" } ?: throw NoSuchElementException() } + + val enableFullExplainPlan by lazy { + findAll().find { it.text == "Enable full explain plan" } + ?: throw NoSuchElementException() + } + val privacyPolicyButton by lazy { findAll().find { it.text == "View Privacy Policy" } ?: throw NoSuchElementException() } + + val analyzeQueryPerformanceButton by lazy { + findAll().find { it.text == "Analyze Query Performance" } + ?: throw NoSuchElementException() + } + val ok by lazy { remoteRobot.findAll().find { it.text == "OK" } ?: throw NoSuchElementException() diff --git a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/settings/SettingsUiTest.kt b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/settings/SettingsUiTest.kt index 9922ee396..c4631db9e 100644 --- a/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/settings/SettingsUiTest.kt +++ b/packages/jetbrains-plugin/src/test/kotlin/com/mongodb/jbplugin/settings/SettingsUiTest.kt @@ -24,6 +24,19 @@ class SettingsUiTest { assertNotEquals(telemetryBeforeTest, telemetryAfterTest) } + @Test + @RequiresProject("basic-java-project-with-mongodb") + fun `allows toggling the full explain plan`(remoteRobot: RemoteRobot) { + val telemetryBeforeTest = remoteRobot.useSetting("isFullExplainPlanEnabled") + + val settings = remoteRobot.openSettings() + settings.enableFullExplainPlan.click() + settings.ok.click() + + val telemetryAfterTest = remoteRobot.useSetting("isFullExplainPlanEnabled") + assertNotEquals(telemetryBeforeTest, telemetryAfterTest) + } + @Test @RequiresProject("basic-java-project-with-mongodb") fun `allows opening the privacy policy in a browser`(remoteRobot: RemoteRobot) { @@ -44,4 +57,28 @@ class SettingsUiTest { assertEquals("https://www.mongodb.com/legal/privacy/privacy-policy", lastBrowserUrl) } + + @Test + @RequiresProject("basic-java-project-with-mongodb") + fun `allows opening the analyze query performance page`(remoteRobot: RemoteRobot) { + remoteRobot.openBrowserSettings().run { + useFakeBrowser() + } + + val settings = remoteRobot.openSettings() + settings.analyzeQueryPerformanceButton.click() + settings.ok.click() + + val lastBrowserUrl = + remoteRobot.openBrowserSettings().run { + useSystemBrowser() + ok.click() + lastBrowserUrl() + } + + assertEquals( + "https://www.mongodb.com/docs/manual/tutorial/evaluate-operation-performance/", + lastBrowserUrl + ) + } } diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt index 4bb302b37..07d4dc8db 100644 --- a/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt +++ b/packages/mongodb-access-adapter/datagrip-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriver.kt @@ -23,6 +23,7 @@ import com.mongodb.jbplugin.dialects.OutputQuery import com.mongodb.jbplugin.dialects.mongosh.MongoshDialect import com.mongodb.jbplugin.mql.Namespace import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext import kotlinx.coroutines.* import org.bson.Document import org.bson.codecs.DecoderContext @@ -88,9 +89,11 @@ internal class DataGripMongoDbDriver( override suspend fun connectionString(): ConnectionString = ConnectionString(dataSource.url!!) - override suspend fun explain(query: Node): ExplainPlan = withContext(Dispatchers.IO) { + override suspend fun explain(query: Node, queryContext: QueryContext): ExplainPlan = withContext( + Dispatchers.IO + ) { val queryScript = ApplicationManager.getApplication().runReadAction { - MongoshDialect.formatter.formatQuery(query, explain = true) + MongoshDialect.formatter.formatQuery(query, queryContext) } if (queryScript !is OutputQuery.CanBeRun) { diff --git a/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriverTest.kt b/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriverTest.kt index ab1412a2e..16d85a784 100644 --- a/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriverTest.kt +++ b/packages/mongodb-access-adapter/datagrip-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/datagrip/adapter/DataGripMongoDbDriverTest.kt @@ -11,6 +11,7 @@ import com.mongodb.jbplugin.accessadapter.toNs import com.mongodb.jbplugin.mql.BsonString import com.mongodb.jbplugin.mql.Namespace import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext import com.mongodb.jbplugin.mql.components.HasCollectionReference import com.mongodb.jbplugin.mql.components.HasFieldReference import com.mongodb.jbplugin.mql.components.HasFilter @@ -270,7 +271,10 @@ class DataGripMongoDbDriverTest { Unit::class, ) - val explainPlanResult = driver.explain(query) + val explainPlanResult = driver.explain( + query, + QueryContext(emptyMap(), QueryContext.ExplainPlanType.SAFE) + ) assertEquals(ExplainPlan.CollectionScan, explainPlanResult) } @@ -318,7 +322,10 @@ class DataGripMongoDbDriverTest { ) ) - val explainPlanResult = driver.explain(query) + val explainPlanResult = driver.explain( + query, + QueryContext(emptyMap(), QueryContext.ExplainPlanType.SAFE) + ) assertEquals(ExplainPlan.IndexScan, explainPlanResult) } } diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt index 0783729be..1aa27feae 100644 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt +++ b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt @@ -12,8 +12,8 @@ package com.mongodb.jbplugin.accessadapter import com.mongodb.ConnectionString import com.mongodb.jbplugin.mql.Namespace import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext import org.bson.conversions.Bson -import java.util.* import kotlin.reflect.KClass import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds @@ -40,7 +40,7 @@ interface MongoDbDriver { suspend fun connectionString(): ConnectionString - suspend fun explain(query: Node): ExplainPlan + suspend fun explain(query: Node, queryContext: QueryContext): ExplainPlan suspend fun runCommand( database: String, diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt index d04d0cb9c..eecdc26e1 100644 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt +++ b/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt @@ -7,6 +7,7 @@ package com.mongodb.jbplugin.accessadapter.slice import com.mongodb.jbplugin.accessadapter.ExplainPlan import com.mongodb.jbplugin.accessadapter.MongoDbDriver import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext /** * Runs the explain plan of a query. @@ -21,12 +22,15 @@ data class ExplainQuery( * @property query */ data class Slice( - val query: Node + val query: Node, + val queryContext: QueryContext, ) : com.mongodb.jbplugin.accessadapter.Slice { override val id = "${javaClass.canonicalName}::$query" override suspend fun queryUsingDriver(from: MongoDbDriver): ExplainQuery { - val plan = runCatching { from.explain(query) }.getOrDefault(ExplainPlan.NotRun) + val plan = runCatching { + from.explain(query, queryContext) + }.getOrDefault(ExplainPlan.NotRun) return ExplainQuery(plan) } } diff --git a/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectFormatter.kt b/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectFormatter.kt index 39d24e731..64e6dba98 100644 --- a/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectFormatter.kt +++ b/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/JavaDriverDialectFormatter.kt @@ -5,7 +5,7 @@ import com.mongodb.jbplugin.dialects.OutputQuery import com.mongodb.jbplugin.mql.* object JavaDriverDialectFormatter : DialectFormatter { - override fun formatQuery(query: Node, explain: Boolean) = + override fun formatQuery(query: Node, queryContext: QueryContext) = OutputQuery.None override fun indexCommandForQuery(query: Node) = diff --git a/packages/mongodb-dialects/mongosh/src/main/kotlin/com/mongodb/jbplugin/dialects/mongosh/MongoshDialectFormatter.kt b/packages/mongodb-dialects/mongosh/src/main/kotlin/com/mongodb/jbplugin/dialects/mongosh/MongoshDialectFormatter.kt index 1a62c20e6..703af6cf8 100644 --- a/packages/mongodb-dialects/mongosh/src/main/kotlin/com/mongodb/jbplugin/dialects/mongosh/MongoshDialectFormatter.kt +++ b/packages/mongodb-dialects/mongosh/src/main/kotlin/com/mongodb/jbplugin/dialects/mongosh/MongoshDialectFormatter.kt @@ -8,7 +8,6 @@ import com.mongodb.jbplugin.mql.components.* import com.mongodb.jbplugin.mql.components.HasFieldReference.FromSchema import com.mongodb.jbplugin.mql.components.HasFieldReference.Inferred import com.mongodb.jbplugin.mql.components.HasFieldReference.Unknown -import com.mongodb.jbplugin.mql.components.HasValueReference.Computed import com.mongodb.jbplugin.mql.parser.anyError import com.mongodb.jbplugin.mql.parser.components.aggregationStages import com.mongodb.jbplugin.mql.parser.components.allFiltersRecursively @@ -26,12 +25,15 @@ import org.owasp.encoder.Encode object MongoshDialectFormatter : DialectFormatter { override fun formatQuery( query: Node, - explain: Boolean, + queryContext: QueryContext, ): OutputQuery { val isAggregate = isAggregate(query) val canEmitAggregate = canEmitAggregate(query) - val outputString = MongoshBackend(prettyPrint = explain).apply { + val outputString = MongoshBackend( + prettyPrint = + queryContext.explainPlan != QueryContext.ExplainPlanType.NONE + ).apply { if (isAggregate && !canEmitAggregate) { emitComment("Only aggregates with a single match stage can be converted.") return@apply @@ -39,9 +41,15 @@ object MongoshDialectFormatter : DialectFormatter { emitDbAccess() emitCollectionReference(query.component>()) - if (explain) { + if (queryContext.explainPlan != QueryContext.ExplainPlanType.NONE) { emitFunctionName("explain") - emitFunctionCall() + emitFunctionCall(long = false, { + // https://www.mongodb.com/docs/manual/reference/command/explain/#command-fields + when (queryContext.explainPlan) { + QueryContext.ExplainPlanType.FULL -> emitStringLiteral("executionStats") + else -> emitStringLiteral("queryPlanner") + } + }) emitPropertyAccess() if (isAggregate) { emitFunctionName("aggregate") diff --git a/packages/mongodb-dialects/mongosh/src/main/kotlin/com/mongodb/jbplugin/dialects/mongosh/backend/MongoshBackend.kt b/packages/mongodb-dialects/mongosh/src/main/kotlin/com/mongodb/jbplugin/dialects/mongosh/backend/MongoshBackend.kt index 1b106bb5c..f8e9fc38a 100644 --- a/packages/mongodb-dialects/mongosh/src/main/kotlin/com/mongodb/jbplugin/dialects/mongosh/backend/MongoshBackend.kt +++ b/packages/mongodb-dialects/mongosh/src/main/kotlin/com/mongodb/jbplugin/dialects/mongosh/backend/MongoshBackend.kt @@ -188,6 +188,19 @@ class MongoshBackend( return this } + /** + * Emits a literal string value into the script. + * + * This function is unsafe, it does not encode input values, so it's sensitive + * to code injection. Only use this for well-known, literal constant values that we have + * control of. + * + * @see emitContextValue for dynamic values provided by the user. + */ + fun emitStringLiteral(value: String): MongoshBackend { + return emitAsIs("\"$value\"", encode = false) + } + private fun emitAsIs(string: String, encode: Boolean = true): MongoshBackend { val stringToOutput = if (encode) Encode.forJavaScript(string) else string diff --git a/packages/mongodb-dialects/mongosh/src/test/kotlin/com/mongodb/jbplugin/dialects/mongosh/MongoshDialectFormatterTest.kt b/packages/mongodb-dialects/mongosh/src/test/kotlin/com/mongodb/jbplugin/dialects/mongosh/MongoshDialectFormatterTest.kt index 2ee138492..dfc6c4974 100644 --- a/packages/mongodb-dialects/mongosh/src/test/kotlin/com/mongodb/jbplugin/dialects/mongosh/MongoshDialectFormatterTest.kt +++ b/packages/mongodb-dialects/mongosh/src/test/kotlin/com/mongodb/jbplugin/dialects/mongosh/MongoshDialectFormatterTest.kt @@ -6,6 +6,7 @@ import com.mongodb.jbplugin.mql.BsonInt32 import com.mongodb.jbplugin.mql.BsonString import com.mongodb.jbplugin.mql.Namespace import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext import com.mongodb.jbplugin.mql.components.* import org.intellij.lang.annotations.Language import org.junit.jupiter.api.Assertions.assertEquals @@ -117,7 +118,7 @@ class MongoshDialectFormatterTest { } @Test - fun `can format a query with an explain plan`() { + fun `can format a query with a safe explain plan using queryPlanner`() { assertGeneratedQuery( """ var collection = "" @@ -125,11 +126,49 @@ class MongoshDialectFormatterTest { db.getSiblingDB(database) .getCollection(collection) - .explain().find( - {"myField": "myVal", } + .explain("queryPlanner").find( + {"myField": "myVal", } ) """.trimIndent(), - explain = true + explain = QueryContext.ExplainPlanType.SAFE + ) { + Node( + Unit, + listOf( + HasFilter( + listOf( + Node( + Unit, + listOf( + HasFieldReference( + HasFieldReference.FromSchema(Unit, "myField") + ), + HasValueReference( + HasValueReference.Constant(Unit, "myVal", BsonString) + ) + ) + ) + ) + ) + ) + ) + } + } + + @Test + fun `can format a query with a full explain plan using executionStats`() { + assertGeneratedQuery( + """ + var collection = "" + var database = "" + + db.getSiblingDB(database) + .getCollection(collection) + .explain("executionStats").find( + {"myField": "myVal", } + ) + """.trimIndent(), + explain = QueryContext.ExplainPlanType.FULL ) { Node( Unit, @@ -162,8 +201,65 @@ class MongoshDialectFormatterTest { var database = "" db.getSiblingDB(database).getCollection(collection).aggregate([{"${"$"}match": {"myField": "myVal"}}]) + """.trimIndent() + ) { + Node( + Unit, + listOf( + IsCommand(IsCommand.CommandType.AGGREGATE), + HasAggregation( + listOf( + Node( + Unit, + listOf( + Named(Name.MATCH), + HasFilter( + listOf( + Node( + Unit, + listOf( + HasFieldReference( + HasFieldReference.FromSchema( + Unit, + "myField" + ) + ), + HasValueReference( + HasValueReference.Constant( + Unit, + "myVal", + BsonString + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + ) + } + } + + @Test + fun `can format a safe explain command for a valid aggregate query`() { + assertGeneratedQuery( + """ + var collection = "" + var database = "" + + db.getSiblingDB(database) + .getCollection(collection) + .explain("queryPlanner").aggregate( + [ + {"${'$'}match": {"myField": "myVal"}} + ] + ) """.trimIndent(), - explain = false + explain = QueryContext.ExplainPlanType.SAFE ) { Node( Unit, @@ -207,7 +303,7 @@ class MongoshDialectFormatterTest { } @Test - fun `can format an explain command for a valid aggregate query`() { + fun `can format a full explain command for a valid aggregate query`() { assertGeneratedQuery( """ var collection = "" @@ -215,13 +311,13 @@ class MongoshDialectFormatterTest { db.getSiblingDB(database) .getCollection(collection) - .explain().aggregate( - [ - {"${'$'}match": {"myField": "myVal"}} - ] + .explain("executionStats").aggregate( + [ + {"${'$'}match": {"myField": "myVal"}} + ] ) """.trimIndent(), - explain = true + explain = QueryContext.ExplainPlanType.FULL ) { Node( Unit, @@ -474,10 +570,10 @@ class MongoshDialectFormatterTest { private fun assertGeneratedQuery( @Language("js") js: String, - explain: Boolean = false, + explain: QueryContext.ExplainPlanType = QueryContext.ExplainPlanType.NONE, script: () -> Node ) { - val generated = MongoshDialectFormatter.formatQuery(script(), explain) + val generated = MongoshDialectFormatter.formatQuery(script(), QueryContext(emptyMap(), explain)) assertEquals(js, generated.query) } diff --git a/packages/mongodb-dialects/src/main/kotlin/com/mongodb/jbplugin/dialects/Dialect.kt b/packages/mongodb-dialects/src/main/kotlin/com/mongodb/jbplugin/dialects/Dialect.kt index e4b4b567d..b21a9be9a 100644 --- a/packages/mongodb-dialects/src/main/kotlin/com/mongodb/jbplugin/dialects/Dialect.kt +++ b/packages/mongodb-dialects/src/main/kotlin/com/mongodb/jbplugin/dialects/Dialect.kt @@ -8,6 +8,7 @@ package com.mongodb.jbplugin.dialects import com.mongodb.jbplugin.mql.BsonType import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext /** * Represents the dialect itself, S is the input type of the dialect. It's an opaque type, @@ -102,7 +103,7 @@ sealed interface OutputQuery { * for a user given the Dialect. */ interface DialectFormatter { - fun formatQuery(query: Node, explain: Boolean): OutputQuery + fun formatQuery(query: Node, queryContext: QueryContext): OutputQuery fun indexCommandForQuery(query: Node): String fun formatType(type: BsonType): String } diff --git a/packages/mongodb-linting-engine/src/main/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinter.kt b/packages/mongodb-linting-engine/src/main/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinter.kt index 02a267681..3a24994a0 100644 --- a/packages/mongodb-linting-engine/src/main/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinter.kt +++ b/packages/mongodb-linting-engine/src/main/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinter.kt @@ -8,7 +8,7 @@ import com.mongodb.jbplugin.accessadapter.ExplainPlan import com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider import com.mongodb.jbplugin.accessadapter.slice.ExplainQuery import com.mongodb.jbplugin.linting.IndexCheckWarning.QueryNotCoveredByIndex -import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.* /** * Marker type for the result of the linter. @@ -49,8 +49,12 @@ object IndexCheckingLinter { dataSource: D, readModelProvider: MongoDbReadModelProvider, query: Node, + queryContext: QueryContext, ): IndexCheckResult { - val explainPlanResult = readModelProvider.slice(dataSource, ExplainQuery.Slice(query)) + val explainPlanResult = readModelProvider.slice( + dataSource, + ExplainQuery.Slice(query, queryContext) + ) return when (explainPlanResult.explainPlan) { is ExplainPlan.CollectionScan -> IndexCheckResult( diff --git a/packages/mongodb-linting-engine/src/test/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt b/packages/mongodb-linting-engine/src/test/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt index 9f88fda1c..f16e7e18c 100644 --- a/packages/mongodb-linting-engine/src/test/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt +++ b/packages/mongodb-linting-engine/src/test/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt @@ -4,6 +4,7 @@ import com.mongodb.jbplugin.accessadapter.ExplainPlan import com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider import com.mongodb.jbplugin.accessadapter.slice.ExplainQuery import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Test @@ -28,6 +29,7 @@ class IndexCheckingLinterTest { Unit, readModelProvider, query, + QueryContext.empty() ) assertEquals(1, result.warnings.size) @@ -50,6 +52,7 @@ class IndexCheckingLinterTest { Unit, readModelProvider, query, + QueryContext.empty() ) assertEquals(0, result.warnings.size) diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/Node.kt b/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/Node.kt index e4dc657f6..963c8c47b 100644 --- a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/Node.kt +++ b/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/Node.kt @@ -95,3 +95,23 @@ data class Node( fun copy(componentModifier: (component: Component) -> Component): Node = copy(source = source, components = components.map(componentModifier)) } + +/** + * A data holder for the information required to run a query. To be used alongside with + * a DialectFormatter. + */ +data class QueryContext( + val expansions: Map, + val explainPlan: ExplainPlanType, +) { + data class LocalVariable(val type: BsonType, val defaultValue: Any?) + enum class ExplainPlanType { + NONE, + SAFE, + FULL + } + + companion object { + fun empty(): QueryContext = QueryContext(emptyMap(), ExplainPlanType.NONE) + } +}