diff --git a/.gitignore b/.gitignore index 600eefbdf..d5700e5f4 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,4 @@ FAKE_BROWSER_OUTPUT /.idea/artifacts /.intellijPlatform +/kotlin-js-store diff --git a/build.gradle.kts b/build.gradle.kts index 1084ff1fc..b5f09da37 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ import java.net.URL group = "com.mongodb" // This should be bumped when releasing a new version using the versionBump task: // ./gradlew versionBump -Pmode={major,minor,patch} -version = "0.0.1" +version = "0.0.4" plugins { base @@ -12,6 +12,9 @@ plugins { id("jacoco-report-aggregation") id("org.jetbrains.changelog") id("org.jetbrains.qodana") + + id("org.jetbrains.compose") version("1.7.3") apply(false) + id("org.jetbrains.kotlin.plugin.compose") version("2.0.21") apply(false) } repositories { diff --git a/buildSrc/src/main/kotlin/com.mongodb.intellij.base-module.gradle.kts b/buildSrc/src/main/kotlin/com.mongodb.intellij.base-module.gradle.kts new file mode 100644 index 000000000..15246a7c9 --- /dev/null +++ b/buildSrc/src/main/kotlin/com.mongodb.intellij.base-module.gradle.kts @@ -0,0 +1,87 @@ +import org.gradle.accessors.dm.LibrariesForLibs +import org.jlleitschuh.gradle.ktlint.KtlintExtension +import org.jlleitschuh.gradle.ktlint.reporter.ReporterType +import java.io.ByteArrayOutputStream +import java.io.File + +plugins { + id("jacoco") + id("org.jlleitschuh.gradle.ktlint") +} + +version = rootProject.version +val libs = the() + +tasks { + withType { + reports { + xml.required = true + csv.required = false + html.outputLocation = layout.buildDirectory.dir("reports/jacocoHtml") + } + + executionData( + files(withType(Test::class.java)).filter { it.name.endsWith(".exec") && it.exists() } + ) + } + + register("checkSpecUpdates") { + group = "verification" + description = "Fails if a Kotlin file changed but no specification file was updated" + + doLast { + val rootDir = "${rootDir.absolutePath}" + val baseDir = "${project.projectDir.path}" + val specsDir = "$baseDir/src/docs/" + + if (!File(specsDir).exists()) { + logger.lifecycle("Skipping checkSpecUpdates: $specsDir does not exist.") + return@doLast + } else { + logger.lifecycle("Verifying specifications.") + } + + val outputStream = ByteArrayOutputStream() + exec { + workingDir = project.rootDir + standardOutput = outputStream + commandLine("git", "diff", "--name-only", "origin/main") + } + + val changedFiles = outputStream.toString().trim().lines().map { "$rootDir/$it" } + + logger.quiet("List of changed files:") + changedFiles.forEach { file -> + logger.quiet(file) + } + + val codeChanged = + changedFiles.any { it.startsWith("$baseDir/src/main/kotlin/") && it.endsWith(".kt") } + val specChanged = changedFiles.any { it.startsWith(specsDir) } + + if (codeChanged && !specChanged) { + logger.error("The specification is not up to date with the latest code changes.") + logger.error("Please update the relevant files in $specsDir or add the 'skip-spec-check' label to the PR.") + throw GradleException() + } + } + } +} + +configure { + version.set(libs.versions.ktlint.tool) + verbose.set(true) + outputToConsole.set(true) + ignoreFailures.set(false) + enableExperimentalRules.set(true) + + reporters { + reporter(ReporterType.PLAIN) + reporter(ReporterType.CHECKSTYLE) + } + + filter { + exclude("**/generated/**") + include("**/kotlin/**") + } +} diff --git a/buildSrc/src/main/kotlin/com.mongodb.intellij.isolated-module.gradle.kts b/buildSrc/src/main/kotlin/com.mongodb.intellij.isolated-module.gradle.kts index 86a0f3f7e..c7f693d75 100644 --- a/buildSrc/src/main/kotlin/com.mongodb.intellij.isolated-module.gradle.kts +++ b/buildSrc/src/main/kotlin/com.mongodb.intellij.isolated-module.gradle.kts @@ -1,13 +1,8 @@ import org.gradle.accessors.dm.LibrariesForLibs -import org.jlleitschuh.gradle.ktlint.KtlintExtension -import org.jlleitschuh.gradle.ktlint.reporter.ReporterType -import java.io.ByteArrayOutputStream plugins { - id("java") - kotlin("jvm") - id("jacoco") - id("org.jlleitschuh.gradle.ktlint") + id("com.mongodb.intellij.base-module") + kotlin("multiplatform") } repositories { @@ -17,116 +12,94 @@ repositories { val libs = the() -dependencies { - compileOnly(libs.kotlin.stdlib) - compileOnly(libs.kotlin.coroutines.core) - compileOnly(libs.kotlin.reflect) - testImplementation(libs.testing.jupiter.engine) - testImplementation(libs.testing.jupiter.params) - testImplementation(libs.testing.jupiter.vintage.engine) - testImplementation(libs.testing.mockito.core) - testImplementation(libs.testing.mockito.kotlin) - testImplementation(libs.kotlin.coroutines.test) - testImplementation(libs.testing.testContainers.core) - testImplementation(libs.testing.testContainers.jupiter) - testImplementation(libs.testing.testContainers.mongodb) -} - -tasks { - withType { - sourceCompatibility = libs.versions.java.target.get() - targetCompatibility = libs.versions.java.target.get() +kotlin { + jvm { + val main by compilations.getting } - withType { - useJUnitPlatform() - - extensions.configure(JacocoTaskExtension::class) { - isJmx = true - includes = listOf("com.mongodb.*") - isIncludeNoLocationClasses = true + js(IR) { + moduleName = project.name + version = project.version + + generateTypeScriptDefinitions() + browser { + testTask { + useKarma { + useFirefox() + } + } } - jacoco { - toolVersion = libs.versions.jacoco.get() - isScanForTestClasses = true + nodejs { + testTask { + useMocha() + } } - jvmArgs( - listOf( - "--add-opens=java.base/java.lang=ALL-UNNAMED" - ) - ) + binaries.library() } - withType { - reports { - xml.required = true - csv.required = false - html.outputLocation = layout.buildDirectory.dir("reports/jacocoHtml") + sourceSets { + val commonMain by getting { + dependencies { + compileOnly(libs.kotlin.stdlib) + compileOnly(libs.kotlin.coroutines.core) + compileOnly(libs.kotlin.reflect) + implementation(libs.kotlin.collections) + } } - executionData( - files(withType(Test::class.java)).filter { it.name.endsWith(".exec") && it.exists() } - ) - } -} - -configure { - version.set(libs.versions.ktlint.tool) - verbose.set(true) - outputToConsole.set(true) - ignoreFailures.set(false) - enableExperimentalRules.set(true) - - reporters { - reporter(ReporterType.PLAIN) - reporter(ReporterType.CHECKSTYLE) - } - - filter { - exclude("**/generated/**") - include("**/kotlin/**") - } -} + val commonTest by getting { + dependencies { + implementation(libs.testing.kotlin.test) + implementation(libs.testing.kotlin.coroutines) + } + } -tasks.register("checkSpecUpdates") { - group = "verification" - description = "Fails if a Kotlin file changed but no specification file was updated" + val jvmMain by getting - doLast { - val rootDir = "${rootDir.absolutePath}" - val baseDir = "${project.projectDir.path}" - val specsDir = "$baseDir/src/docs/" + val jvmTest by getting { + dependencies { + implementation(libs.testing.jupiter.engine) + implementation(libs.testing.jupiter.params) + implementation(libs.testing.jupiter.vintage.engine) - if (!File(specsDir).exists()) { - logger.lifecycle("Skipping checkSpecUpdates: $specsDir does not exist.") - return@doLast - } else { - logger.lifecycle("Verifying specifications.") + implementation(libs.testing.mockito.core) + implementation(libs.testing.mockito.kotlin) + implementation(libs.testing.kotlin.coroutines) + } } - val outputStream = ByteArrayOutputStream() - exec { - workingDir = project.rootDir - standardOutput = outputStream - commandLine("git", "diff", "--name-only", "origin/main") + val jsMain by getting { + dependencies { + implementation(libs.kotlin.stdlib) + implementation(libs.kotlin.coroutines.core) + } } - val changedFiles = outputStream.toString().trim().lines().map { "$rootDir/$it" } + val jsTest by getting + } +} - logger.quiet("List of changed files:") - changedFiles.forEach { file -> - logger.quiet(file) - } +tasks { + withType { + useJUnitPlatform() - val codeChanged = changedFiles.any { it.startsWith("$baseDir/src/main/kotlin/") && it.endsWith(".kt") } - val specChanged = changedFiles.any { it.startsWith(specsDir) } + extensions.configure(JacocoTaskExtension::class) { + isJmx = true + includes = listOf("com.mongodb.*") + isIncludeNoLocationClasses = true + } - if (codeChanged && !specChanged) { - logger.error("The specification is not up to date with the latest code changes.") - logger.error("Please update the relevant files in $specsDir or add the 'skip-spec-check' label to the PR.") - throw GradleException() + jacoco { + toolVersion = libs.versions.jacoco.get() + isScanForTestClasses = true } + + jvmArgs( + listOf( + "--add-opens=java.base/java.lang=ALL-UNNAMED" + ) + ) } } diff --git a/buildSrc/src/main/kotlin/com.mongodb.intellij.plugin-component.gradle.kts b/buildSrc/src/main/kotlin/com.mongodb.intellij.plugin-component.gradle.kts index e274132ed..b7c46fb11 100644 --- a/buildSrc/src/main/kotlin/com.mongodb.intellij.plugin-component.gradle.kts +++ b/buildSrc/src/main/kotlin/com.mongodb.intellij.plugin-component.gradle.kts @@ -6,11 +6,11 @@ import org.jetbrains.intellij.platform.gradle.TestFrameworkType import org.jetbrains.intellij.platform.gradle.tasks.VerifyPluginTask plugins { - id("com.mongodb.intellij.isolated-module") + id("com.mongodb.intellij.base-module") + id("java") + kotlin("jvm") id("org.gradle.test-retry") id("org.jetbrains.intellij.platform") - id("me.champeau.jmh") - id("io.morethan.jmhreport") id("org.jetbrains.changelog") } @@ -121,11 +121,12 @@ dependencies { testFramework(TestFrameworkType.Plugin.Java) } - jmh(libs.kotlin.stdlib) - jmh(libs.testing.jmh.core) - jmh(libs.testing.jmh.annotationProcessor) - jmh(libs.testing.jmh.generatorByteCode) - + testImplementation(libs.testing.jupiter.engine) + testImplementation(libs.testing.jupiter.params) + testImplementation(libs.testing.jupiter.vintage.engine) + testImplementation(libs.testing.mockito.core) + testImplementation(libs.testing.mockito.kotlin) + testImplementation(libs.testing.kotlin.coroutines) testImplementation(libs.mongodb.driver) testImplementation(libs.testing.spring.mongodb) testImplementation(libs.testing.assertj.swing) @@ -139,40 +140,36 @@ dependencies { } } -jmh { - benchmarkMode.set(listOf("thrpt")) - iterations.set(10) - timeOnIteration.set("6s") - timeUnit.set("s") - - warmup.set("1s") - warmupIterations.set(3) - warmupMode.set("INDI") - fork.set(1) - threads.set(1) - failOnError.set(false) - forceGC.set(true) - - humanOutputFile.set(rootProject.layout.buildDirectory.file("reports/jmh/human.txt")) - resultsFile.set(rootProject.layout.buildDirectory.file("reports/jmh/results.json")) - resultFormat.set("json") - profilers.set(listOf("gc")) - - zip64.set(true) -} +tasks { + register("jvmTest") { + dependsOn("test") + } + + withType { + sourceCompatibility = libs.versions.java.target.get() + targetCompatibility = libs.versions.java.target.get() + } + + withType { + useJUnitPlatform() -jmhReport { - jmhResultPath = - rootProject.layout.buildDirectory - .file("reports/jmh/results.json") - .get() - .asFile.absolutePath - - jmhReportOutput = - rootProject.layout.buildDirectory - .dir("reports/jmh/") - .get() - .asFile.absolutePath + extensions.configure(JacocoTaskExtension::class) { + isJmx = true + includes = listOf("com.mongodb.*") + isIncludeNoLocationClasses = true + } + + jacoco { + toolVersion = libs.versions.jacoco.get() + isScanForTestClasses = true + } + + jvmArgs( + listOf( + "--add-opens=java.base/java.lang=ALL-UNNAMED" + ) + ) + } } tasks { diff --git a/gradle.properties b/gradle.properties index d16af20dc..c55c63de9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,3 +12,7 @@ org.gradle.daemon=false kotlin.code.style=official # Memory Optimisation org.gradle.jvmargs=-Xmx4096M +# Disable these warnings because using api propagates libraries and we don't want that on commonMain +kotlin.suppressGradlePluginWarnings=IncorrectCompileOnlyDependencyWarning +# We are not using jscanvas but ignore the warning +org.jetbrains.compose.experimental.jscanvas.enabled=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4b6ed88a6..88e02e1c9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,8 @@ qodana = "2024.3.4" # Library dependencies kotlin-stdlib = "2.0.21" kotlinx-coroutines = "1.8.0" +kotlinx-collections = "0.3.8" + jupiter = "5.10.2" mockito = "5.11.0" mockito-kotlin = "5.3.1" @@ -31,6 +33,7 @@ spring-mongodb="4.3.2" semver-parser="2.0.0" snakeyaml="2.3" assertj-swing="3.17.1" +runtimeAndroid = "1.7.8" [libraries] ## Kotlin compileOnly libraries. They must not be bundled because they are already part of the @@ -38,7 +41,8 @@ assertj-swing="3.17.1" kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin-stdlib" } kotlin-reflect = { group = "org.jetbrains.kotlin", name = "kotlin-reflect", version.ref = "kotlin-stdlib" } kotlin-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } -kotlin-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } +kotlin-collections = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref="kotlinx-collections" } +kotlinx-coroutines-swing = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } ## Production Libraries. segment = { group = "com.segment.analytics.java", name = "analytics", version.ref = "segment" } gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } @@ -49,6 +53,8 @@ semver-parser = { group = "io.github.z4kn4fein", name = "semver", version.ref = snakeyaml = { group = "org.yaml", name = "snakeyaml", version.ref = "snakeyaml" } ###################################################### ## Testing Libraries. +testing-kotlin-test = { group="org.jetbrains.kotlin", name="kotlin-test", version.ref="kotlin-stdlib" } +testing-kotlin-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } testing-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "jupiter" } testing-jupiter-params = { group = "org.junit.jupiter", name = "junit-jupiter-params", version.ref = "jupiter" } testing-jupiter-vintage-engine = { group = "org.junit.vintage", name = "junit-vintage-engine", version.ref = "jupiter" } @@ -75,4 +81,5 @@ buildScript-plugin-jmh = { group = "me.champeau.jmh", name = "jmh-gradle-plugin" buildScript-plugin-jmhreport = { group = "gradle.plugin.io.morethan.jmhreport", name = "gradle-jmh-report", version = "0.9.6" } buildScript-plugin-ktlint = { group = "org.jlleitschuh.gradle", name = "ktlint-gradle", version.ref = "ktlint-plugin" } buildScript-plugin-qodana = { group ="org.jetbrains.qodana", name = "org.jetbrains.qodana.gradle.plugin", version.ref = "qodana" } +runtime-android = { group = "androidx.compose.runtime", name = "runtime-android", version.ref = "runtimeAndroid" } ###################################################### diff --git a/packages/jetbrains-plugin/build.gradle.kts b/packages/jetbrains-plugin/build.gradle.kts index 01784ba80..06adc4ba6 100644 --- a/packages/jetbrains-plugin/build.gradle.kts +++ b/packages/jetbrains-plugin/build.gradle.kts @@ -1,5 +1,14 @@ plugins { id("com.mongodb.intellij.plugin-component") + id("org.jetbrains.compose") version ("1.7.3") + id("org.jetbrains.kotlin.plugin.compose") version ("2.0.21") +} + +repositories { + google() + maven("https://www.jetbrains.com/intellij-repository/releases/") + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://packages.jetbrains.team/maven/p/kpm/public/") } pluginBundle { @@ -36,6 +45,16 @@ dependencies { implementation(project(":packages:mongodb-dialects:spring-@query")) implementation(project(":packages:mongodb-dialects:mongosh")) implementation(project(":packages:mongodb-mql-model")) + implementation(project(":packages:mongodb-design-system")) { + exclude(group = "org.jetbrains.compose.material") + exclude(group = "org.jetbrains.kotlinx") + exclude(group = "org.jetbrains.jewel", module = "jewel-int-ui-standalone-243") + } + + compileOnly("org.jetbrains.jewel:jewel-ide-laf-bridge-243:0.27.0") + + compileOnly(compose.desktop.common) + compileOnly(libs.kotlinx.coroutines.swing) implementation(libs.mongodb.driver) implementation(libs.segment) diff --git a/packages/jetbrains-plugin/src/jmh/kotlin/com/mongodb/jbplugin/jmh/SampleBenchmark.kt b/packages/jetbrains-plugin/src/jmh/kotlin/com/mongodb/jbplugin/jmh/SampleBenchmark.kt deleted file mode 100644 index e62c4ef01..000000000 --- a/packages/jetbrains-plugin/src/jmh/kotlin/com/mongodb/jbplugin/jmh/SampleBenchmark.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.mongodb.jbplugin.jmh - -import org.openjdk.jmh.annotations.Benchmark -import org.openjdk.jmh.infra.Blackhole - -/** - * Sample benchmark, does not do anything useful. - */ -open class SampleBenchmark { - @Benchmark - fun init(bh: Blackhole) { - bh.consume(1) - // Do nothing - } -} 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 14653736b..c1c430f8e 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 @@ -54,6 +54,7 @@ class PluginSettings : BaseState(), Serializable { var isFullExplainPlanEnabled by property(false) var sampleSize by property(DEFAULT_SAMPLE_SIZE) var softIndexesLimit by property(DEFAULT_INDEXES_AMOUNT_SOFT_LIMIT) + var useNewSidePanel by property(false) } class SettingsDelegate(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 bfe1cb107..70f440ecc 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 @@ -49,7 +49,8 @@ class PluginSettingsConfigurable : Configurable { settingsComponent.enableFullExplainPlan.isSelected != savedSettings.state.isFullExplainPlanEnabled || sampleSizeInComponent != sampleSizeSavedOnDisk || - softIndexLimitInComponent != softIndexLimitSavedOnDisk + softIndexLimitInComponent != softIndexLimitSavedOnDisk || + settingsComponent.useNewSidePanel.isSelected != savedSettings.state.useNewSidePanel } override fun apply() { @@ -61,6 +62,7 @@ class PluginSettingsConfigurable : Configurable { softIndexesLimit = settingsComponent.softIndexesLimit.text.toIntOrNull() ?: DEFAULT_INDEXES_AMOUNT_SOFT_LIMIT + useNewSidePanel = settingsComponent.useNewSidePanel.isSelected } } @@ -72,6 +74,7 @@ class PluginSettingsConfigurable : Configurable { savedSettings.state.isFullExplainPlanEnabled settingsComponent.sampleSize.text = savedSettings.state.sampleSize.toString() settingsComponent.softIndexesLimit.text = savedSettings.state.softIndexesLimit.toString() + settingsComponent.useNewSidePanel.isSelected = savedSettings.state.useNewSidePanel } override fun getDisplayName() = SettingsMessages.message("settings.display-name") @@ -89,6 +92,8 @@ internal class PluginSettingsComponent { val privacyPolicyButton = JButton(TelemetryMessages.message("action.view-privacy-policy")) val evaluateOperationPerformanceButton = JButton(TelemetryMessages.message("settings.view-full-explain-plan-documentation")) + val useNewSidePanel = + JBCheckBox("Use new side panel.") lateinit var sampleSize: JTextField lateinit var softIndexesLimit: JTextField @@ -106,6 +111,8 @@ internal class PluginSettingsComponent { root = FormBuilder.createFormBuilder() + .addComponent(useNewSidePanel) + .addSeparator() .addComponent(getSampleSizeField()) .addSeparator() .addComponent(enableFullExplainPlan) diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/SidePanel.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/SidePanel.kt new file mode 100644 index 000000000..4e558fee9 --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/SidePanel.kt @@ -0,0 +1,102 @@ +package com.mongodb.jbplugin.sidePanel + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.ComposePanel +import androidx.compose.ui.unit.dp +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow +import com.intellij.openapi.wm.ToolWindowFactory +import com.mongodb.jbplugin.accessadapter.datagrip.adapter.isConnected +import com.mongodb.jbplugin.designsystem.Card +import com.mongodb.jbplugin.designsystem.Connection +import com.mongodb.jbplugin.designsystem.ConnectionComboBox +import com.mongodb.jbplugin.designsystem.ConnectionState.CONNECTED +import com.mongodb.jbplugin.designsystem.ConnectionState.IDLE +import com.mongodb.jbplugin.editor.models.getToolbarModel +import com.mongodb.jbplugin.settings.pluginSetting +import com.mongodb.jbplugin.sidePanel.ui.components.TestConnectionList +import com.mongodb.jbplugin.sidePanel.ui.composition.LocalProject +import kotlinx.coroutines.flow.map +import org.jetbrains.jewel.bridge.theme.SwingBridgeTheme +import org.jetbrains.jewel.ui.icons.AllIconsKeys + +class SidePanel : ToolWindowFactory { + override fun shouldBeAvailable(project: Project): Boolean { + val isSidePanelActive by pluginSetting { ::useNewSidePanel } + return isSidePanelActive + } + + override fun createToolWindowContent( + project: Project, + toolWindow: ToolWindow + ) { + + val activeConnection = project.getToolbarModel().toolbarState.map { + it.selectedDataSource?.let { + Connection( + it.uniqueId, + it.name, + if (it.isConnected()) { + CONNECTED + } else { + IDLE + } + ) + } + } + + val connections = project.getToolbarModel().toolbarState.map { + it.dataSources.map { + Connection( + it.uniqueId, + it.name, + if (it.isConnected()) { + CONNECTED + } else { + IDLE + } + ) + } + } + + val composePanel = ComposePanel() + composePanel.setContent { + SwingBridgeTheme { + CompositionLocalProvider( + LocalProject provides project, + ) { + Column { + Box(modifier = Modifier.padding(8.dp)) { + TestConnectionList() + } + + Box(modifier = Modifier.padding(8.dp)) { + Card(AllIconsKeys.General.Error, "MongoDB Connection Unavailable") { + ConnectionComboBox(activeConnection, connections) { connection -> + project.getToolbarModel().selectDataSource( + project.getToolbarModel().toolbarState.value.dataSources.first { + it.uniqueId == connection.id + } + ) + } + } + } + } + } + } + } + + val manager = toolWindow.contentManager + val content = manager.factory.createContent( + composePanel, + null, + false + ) + + manager.addContent(content) + } +} diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/components/ConnectionList.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/components/ConnectionList.kt new file mode 100644 index 000000000..48554be70 --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/components/ConnectionList.kt @@ -0,0 +1,42 @@ +package com.mongodb.jbplugin.sidePanel.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.mongodb.jbplugin.sidePanel.ui.hooks.useViewModelMutator +import com.mongodb.jbplugin.sidePanel.ui.hooks.useViewModelState +import com.mongodb.jbplugin.sidePanel.viewModel.ConnectionState.Connected +import com.mongodb.jbplugin.sidePanel.viewModel.ConnectionState.Disconnected +import com.mongodb.jbplugin.sidePanel.viewModel.ConnectionStateViewModel +import org.jetbrains.jewel.ui.component.Text + +@Composable +fun TestConnectionList() { + val currentConnection by useViewModelState(ConnectionStateViewModel::connection, initial = Disconnected) + val connectionList by useViewModelState(ConnectionStateViewModel::connectionList, initial = emptyList()) + val connect by useViewModelMutator(ConnectionStateViewModel::connect) + + Column { + Row { + when (currentConnection) { + is Disconnected -> Text("Disconnected") + is Connected -> Text("Connected to ${(currentConnection as Connected).dataSource.name}") + } + } + + Row(Modifier.padding(1.dp).background(Color.Green)) {} + + for (connection in connectionList) { + Row(Modifier.clickable { connect(connection) }) { + Text(connection.name) + } + } + } +} diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/composition/LocalCoroutineContext.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/composition/LocalCoroutineContext.kt new file mode 100644 index 000000000..8443d92af --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/composition/LocalCoroutineContext.kt @@ -0,0 +1,17 @@ +package com.mongodb.jbplugin.sidePanel.ui.composition + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlin.coroutines.CoroutineContext + +@OptIn(ExperimentalCoroutinesApi::class) +private val UI_COMPUTE = Dispatchers.IO.limitedParallelism(1) + +@Composable +fun useCoroutineContext(): State { + return remember { derivedStateOf { UI_COMPUTE } } +} diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/composition/LocalProject.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/composition/LocalProject.kt new file mode 100644 index 000000000..867710558 --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/composition/LocalProject.kt @@ -0,0 +1,16 @@ +package com.mongodb.jbplugin.sidePanel.ui.composition + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember +import com.intellij.openapi.project.Project + +val LocalProject = compositionLocalOf { null } + +@Composable +fun useProject(): State { + val project = LocalProject.current ?: throw IllegalStateException("Project is not set up. This is a bug and must not happen.") + return remember { derivedStateOf { project } } +} diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/hooks/useViewModelState.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/hooks/useViewModelState.kt new file mode 100644 index 000000000..5a624f7d2 --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/ui/hooks/useViewModelState.kt @@ -0,0 +1,51 @@ +package com.mongodb.jbplugin.sidePanel.ui.hooks + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import com.mongodb.jbplugin.meta.service +import com.mongodb.jbplugin.sidePanel.ui.composition.useCoroutineContext +import com.mongodb.jbplugin.sidePanel.ui.composition.useProject +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.seconds + +private const val FPS: Int = 30 +val MAX_REFRESH_RATIO = 1.seconds / FPS + +@Composable +inline fun useViewModel(): State { + val project by useProject() + val viewModel by project.service() + return remember { derivedStateOf { viewModel } } +} + +@OptIn(FlowPreview::class) +@Composable +inline fun useViewModelState(prop: (V) -> StateFlow, initial: S): State { + val coroutineContext by useCoroutineContext() + val viewModel by useViewModel() + return prop(viewModel).debounce(MAX_REFRESH_RATIO).collectAsState(initial, coroutineContext) +} + +@Composable +inline fun useViewModelMutator(crossinline prop: suspend V.(P) -> Unit): State<(P) -> Unit> { + val coroutineContext by useCoroutineContext() + val viewModel by useViewModel() + val coroutineScope = rememberCoroutineScope { coroutineContext } + + val callback = { p: P -> + coroutineScope.launch { + prop(viewModel, p) + } + Unit + } + + return remember { derivedStateOf { callback } } +} diff --git a/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/viewModel/ConnectionStateViewModel.kt b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/viewModel/ConnectionStateViewModel.kt new file mode 100644 index 000000000..9dcbe2044 --- /dev/null +++ b/packages/jetbrains-plugin/src/main/kotlin/com/mongodb/jbplugin/sidePanel/viewModel/ConnectionStateViewModel.kt @@ -0,0 +1,63 @@ +package com.mongodb.jbplugin.sidePanel.viewModel + +import com.intellij.database.console.JdbcDriverManager +import com.intellij.database.dataSource.LocalDataSource +import com.intellij.database.model.RawDataSource +import com.intellij.database.psi.DataSourceManager +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import com.mongodb.jbplugin.editor.services.implementations.MdbDataSourceService +import com.mongodb.jbplugin.meta.service +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch + +sealed interface ConnectionState { + data object Disconnected : ConnectionState + data class Connected(val dataSource: LocalDataSource) : ConnectionState +} + +@Service(Service.Level.PROJECT) +class ConnectionStateViewModel( + project: Project, + private val coroutineScope: CoroutineScope +) : + DataSourceManager.Listener, + JdbcDriverManager.Listener { + val connection = MutableStateFlow(ConnectionState.Disconnected) + val connectionList: MutableStateFlow> = MutableStateFlow(emptyList()) + + init { + val mdbDataSourceService by project.service() + coroutineScope.launch { + connectionList.emit(mdbDataSourceService.listMongoDbDataSources()) + } + } + + suspend fun connect(dataSource: LocalDataSource) { + connection.emit(ConnectionState.Connected(dataSource)) + } + + override fun dataSourceAdded( + manager: DataSourceManager, + dataSource: T & Any + ) { + if (dataSource is LocalDataSource) { + val newList = connectionList.value + dataSource + coroutineScope.launch { + connectionList.emit(newList) + } + } + } + + override fun dataSourceRemoved( + manager: DataSourceManager, + dataSource: T & Any + ) { + coroutineScope.launch { + val newList = connectionList.value.filter { it.uniqueId != dataSource.uniqueId } + connectionList.emit(newList) + } + } +} diff --git a/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml b/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml index 4874e9b63..052e2d832 100644 --- a/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml +++ b/packages/jetbrains-plugin/src/main/resources/META-INF/plugin.xml @@ -100,6 +100,15 @@ + + + @@ -113,4 +122,4 @@ activeInHeadlessMode="true" activeInTestMode="true"/> - \ No newline at end of file + diff --git a/packages/mongodb-access-adapter/build.gradle.kts b/packages/mongodb-access-adapter/build.gradle.kts index 94e1313ca..7a5fb81aa 100644 --- a/packages/mongodb-access-adapter/build.gradle.kts +++ b/packages/mongodb-access-adapter/build.gradle.kts @@ -2,6 +2,12 @@ plugins { id("com.mongodb.intellij.isolated-module") } -dependencies { - implementation(project(":packages:mongodb-mql-model")) +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":packages:mongodb-mql-model")) + } + } + } } diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt similarity index 94% rename from packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt rename to packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt index b4a648512..d1882f6e8 100644 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt +++ b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriver.kt @@ -26,7 +26,7 @@ sealed interface QueryResult { } class ConnectionString(hostInfo: List) { - val hosts = hostInfo.map { it.replace("mongodb://", "").replace("mongodb+srv://", "") } + val hosts = hostInfo.flatMap { it.replace("mongodb://", "").replace("mongodb+srv://", "").split(",") } } /** diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbReadModelProvider.kt b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbReadModelProvider.kt similarity index 100% rename from packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbReadModelProvider.kt rename to packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbReadModelProvider.kt diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfo.kt b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfo.kt similarity index 93% rename from packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfo.kt rename to packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfo.kt index 078c1b63f..8fde5a0d2 100644 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfo.kt +++ b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfo.kt @@ -61,26 +61,16 @@ data class BuildInfo( ?.replace(Regex(""":\d+"""), "") .takeIf { isAtlas } object Slice : com.mongodb.jbplugin.accessadapter.Slice { - override val id = javaClass.canonicalName + override val id = "BuildInfo" private val atlasRegex = Regex(""".*\.mongodb(-dev|-qa|-stage)?\.net(:\d+)?$""") private val atlasStreamRegex = Regex("""^atlas-stream-.+""") - private val isLocalhostRegex = - Regex( - "^(localhost" + - "|127.([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])" + - ".([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])" + - ".([01]?[0-9][0-9]?|2[0-4][0-9]|25[0-5])" + - "|0.0.0.0" + - "|\\[(?:0*:)*?:?0*1]" + - ")(:[0-9]+)?$", - ) private val digitalOceanRegex = Regex(""".*\.mongo\.ondigitalocean\.com$""") private val cosmosDbRegex = Regex(""".*\.cosmos\.azure\.com$""") private val docDbRegex = Regex(""".*docdb(-elastic)?\.amazonaws\.com$""") override suspend fun queryUsingDriver(from: MongoDbDriver): BuildInfo { val connectionString = from.connectionString() - val isLocalHost = connectionString.hosts.all { it.matches(isLocalhostRegex) } + val isLocalHost = connectionString.hosts.all { isLocalhost(it) } val isAtlas = connectionString.hosts.all { it.matches(atlasRegex) } val isLocalAtlas = checkIsAtlasCliIfConnected(from) @@ -199,6 +189,10 @@ data class BuildInfo( } companion object { + private fun isLocalhost(str: String) = + str.startsWith("[::1]") || + str.startsWith("localhost") + private fun empty(): BuildInfoFromMongoDb = BuildInfoFromMongoDb( "", diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt similarity index 96% rename from packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt rename to packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt index 8fd2dbeb7..f76c7e063 100644 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt +++ b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQuery.kt @@ -56,7 +56,7 @@ data class ExplainQuery( val query: Node, val queryContext: QueryContext, ) : com.mongodb.jbplugin.accessadapter.Slice { - override val id = "${javaClass.canonicalName}::$query" + override val id = "ExplainQuery::$query" override suspend fun queryUsingDriver(from: MongoDbDriver): ExplainQuery { if (query.component() == null) { @@ -130,7 +130,7 @@ data class ExplainQuery( ExplainPlan.NotRun } - val currentStage = mapping.getOrDefault(stage["stage"], null) ?: ExplainPlan.NotRun + val currentStage = stage["stage"]?.let { mapping[it] } ?: ExplainPlan.NotRun return maxOf(parentStage, currentStage) } } diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.kt b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.kt similarity index 69% rename from packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.kt rename to packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.kt index 7c0355f92..38f771b0d 100644 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.kt +++ b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.kt @@ -20,7 +20,7 @@ data class GetCollectionSchema( private val namespace: Namespace, private val documentsSampleSize: Int, ) : com.mongodb.jbplugin.accessadapter.Slice { - override val id = "${javaClass.canonicalName}::$namespace" + override val id = "GetCollectionSchema::$namespace" override suspend fun queryUsingDriver(from: MongoDbDriver): GetCollectionSchema { if (namespace.database.isBlank() || namespace.collection.isBlank()) { @@ -56,7 +56,7 @@ data class GetCollectionSchema( } // we need to generate the schema from these docs - val sampleSchemas = sampleSomeDocs.map(this::recursivelyBuildSchema) + val sampleSchemas = sampleSomeDocs.map(::recursivelyBuildSchema) // now we want to merge them together val consolidatedSchema = sampleSchemas.reduceOrNull(::mergeSchemaTogether) ?: BsonObject( @@ -73,33 +73,7 @@ data class GetCollectionSchema( ), ) } - - private fun recursivelyBuildSchema(value: Any?): BsonType = - when (value) { - null -> BsonNull - is Map<*, *> -> BsonObject( - value.map { - it.key.toString() to - recursivelyBuildSchema(it.value) - }.toMap() - ) - is Collection<*> -> recursivelyBuildSchema(value.toTypedArray()) - is Array<*> -> - BsonArray( - value - .map { - it?.javaClass?.toBsonType(it) ?: BsonNull - }.toSet() - .let { - if (it.size == 1) { - it.first() - } else { - BsonAnyOf(it) - } - }, - ) - - else -> primitiveOrWrapper(value.javaClass).toBsonType() - } } } + +internal expect fun recursivelyBuildSchema(value: Any?): BsonType diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollections.kt b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollections.kt similarity index 97% rename from packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollections.kt rename to packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollections.kt index ca06be915..e0e2515bd 100644 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollections.kt +++ b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollections.kt @@ -37,7 +37,7 @@ data class ListCollections( private val database: String, ) : com.mongodb.jbplugin.accessadapter.Slice { override val id: String - get() = "${javaClass.canonicalName}::$database" + get() = "ListCollections::$database" override suspend fun queryUsingDriver(from: MongoDbDriver): ListCollections { if (database.isBlank()) { diff --git a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListDatabases.kt b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListDatabases.kt similarity index 97% rename from packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListDatabases.kt rename to packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListDatabases.kt index d97df8241..460335809 100644 --- a/packages/mongodb-access-adapter/src/main/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListDatabases.kt +++ b/packages/mongodb-access-adapter/src/commonMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListDatabases.kt @@ -20,7 +20,7 @@ data class ListDatabases( val databases: List, ) { object Slice : com.mongodb.jbplugin.accessadapter.Slice { - override val id = javaClass.canonicalName + override val id = "ListDatabases" override suspend fun queryUsingDriver(from: MongoDbDriver): ListDatabases { val query = Node( diff --git a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriverTest.kt b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriverTest.kt similarity index 73% rename from packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriverTest.kt rename to packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriverTest.kt index 7025f4605..ec3a1b8bc 100644 --- a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriverTest.kt +++ b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/MongoDbDriverTest.kt @@ -1,45 +1,45 @@ package com.mongodb.jbplugin.accessadapter import com.mongodb.jbplugin.mql.Namespace -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.assertEquals class MongoDbDriverTest { @Test - fun `cleans up the url schema for mongodb`() { + fun cleans_up_the_url_schema_for_mongodb() { val conn = ConnectionString(listOf("mongodb://localhost")) assertEquals("localhost", conn.hosts[0]) } @Test - fun `cleans up the url schema for mongodb+srv`() { + fun cleans_up_the_url_schema_for_mongodb_srv() { val conn = ConnectionString(listOf("mongodb+srv://localhost")) assertEquals("localhost", conn.hosts[0]) } @Test - fun `parses a namespace`() { + fun parses_a_namespace() { val namespace = "mydb.mycoll".toNs() assertEquals("mydb", namespace.database) assertEquals("mycoll", namespace.collection) } @Test - fun `parses a namespace where collections have dots in a name`() { + fun parses_a_namespace_where_collections_have_dots_in_a_name() { val namespace = "mydb.myco.ll".toNs() assertEquals("mydb", namespace.database) assertEquals("myco.ll", namespace.collection) } @Test - fun `removes trailing spaces`() { + fun removes_trailing_spaces() { val namespace = """ mydb.myco"ll """.toNs() assertEquals("mydb", namespace.database) assertEquals("myco\"ll", namespace.collection) } @Test - fun `can parse back a serialised namespace`() { + fun can_parse_back_a_serialised_namespace() { val namespace = Namespace("mydb", "my.cool.col") val deserialized = namespace.toString().toNs() diff --git a/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/StubMongoDbDriver.kt b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/StubMongoDbDriver.kt new file mode 100644 index 000000000..9b83f51cb --- /dev/null +++ b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/StubMongoDbDriver.kt @@ -0,0 +1,25 @@ +package com.mongodb.jbplugin.accessadapter + +import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext +import kotlin.reflect.KClass +import kotlin.time.Duration + +data class StubMongoDbDriver( + override val connected: Boolean = true, + private val connectionString: String = "mongodb://localhost:27017", + private val responses: Map, (query: Node) -> QueryResult> +) : MongoDbDriver { + override suspend fun connectionString(): ConnectionString { + return ConnectionString(listOf(connectionString)) + } + + override suspend fun runQuery( + query: Node, + result: KClass, + queryContext: QueryContext, + timeout: Duration + ): QueryResult { + return responses[result]!!.invoke(query as Node) as QueryResult + } +} diff --git a/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoTest.kt b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoTest.kt new file mode 100644 index 000000000..2bb991cd3 --- /dev/null +++ b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoTest.kt @@ -0,0 +1,174 @@ +package com.mongodb.jbplugin.accessadapter.slice + +import com.mongodb.jbplugin.accessadapter.QueryResult +import com.mongodb.jbplugin.accessadapter.StubMongoDbDriver +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class BuildInfoTest { + @Test + fun returns_a_valid_build_info() = runTest { + val driver = StubMongoDbDriver( + connected = true, + connectionString = "mongodb://localhost/", + responses = mapOf( + Long::class to { QueryResult.Run(1L) }, + BuildInfoFromMongoDb::class to { QueryResult.Run(defaultBuildInfo()) } + ) + ) + + val data = BuildInfo.Slice.queryUsingDriver(driver) + assertEquals("7.8.0", data.version) + assertEquals("1235abc", data.gitVersion) + } + + @Test + fun when_not_connected_do_not_run_queries() = runTest { + val driver = StubMongoDbDriver( + connected = false, + connectionString = "mongodb://localhost/", + responses = mapOf( + Long::class to { throw NotImplementedError() }, + BuildInfoFromMongoDb::class to { throw NotImplementedError() } + ) + ) + BuildInfo.Slice.queryUsingDriver(driver) + } + + @Test + fun parses_different_type_of_url_connections_properly() = runTest { + fun assertTestCase( + data: BuildInfo, + testCase: Array, + ) { + assertEquals(testCase[1], data.isLocalhost, "isLocalhost does not match ${testCase[0]} -> ${data.isLocalhost}") + assertEquals(testCase[2], data.isAtlas, "isAtlas does not match") + assertEquals(testCase[3], data.isAtlasStream, "isAtlasStream does not match") + assertEquals(testCase[4], data.isDigitalOcean, "isDigitalOcean does not match") + assertEquals(testCase[5], data.isGenuineMongoDb, "isGenuineMongoDb does not match") + assertEquals( + testCase[6]?.toString(), + data.nonGenuineVariant, + "mongodbVariant does not match" + ) + } + + arrayOf>( + arrayOf("mongodb://localhost", true, false, false, false, true, null), + arrayOf("mongodb://localhost,another-server", false, false, false, false, true, null), + arrayOf( + "mongodb+srv://example-atlas-cluster.e06cc.mongodb.net", + false, + true, + false, + false, + true, + null + ), + arrayOf( + "mongodb://example-atlas-cluster.e06cc.mongodb.net,another-server", + false, + false, + false, + false, + true, + null + ), + arrayOf( + "mongodb+srv://atlas-stream-example-atlas-stream.e06cc.mongodb.net", + false, + true, + true, + false, + true, + null + ), + arrayOf("mongodb://[::1]", true, false, false, false, true, null), + arrayOf( + "mongodb://my-cluster.mongo.ondigitalocean.com", + false, + false, + false, + true, + true, + null + ), + arrayOf( + "mongodb://my-cluster.cosmos.azure.com", + false, + false, + false, + false, + false, + "cosmosdb" + ), + arrayOf( + "mongodb://my-cluster.docdb.amazonaws.com", + false, + false, + false, + false, + false, + "documentdb" + ), + arrayOf( + "mongodb://my-cluster.docdb-elastic.amazonaws.com", + false, + false, + false, + false, + false, + "documentdb" + ) + ).forEach { testCase -> + val driver = StubMongoDbDriver( + connected = false, + connectionString = testCase[0].toString(), + responses = mapOf( + Long::class to { throw NotImplementedError() }, + BuildInfoFromMongoDb::class to { throw NotImplementedError() } + ) + ) + + val data = BuildInfo.Slice.queryUsingDriver(driver) + assertTestCase(data, testCase) + } + } + + @Test + fun provides_the_correct_connection_host_for_atlas() = runTest { + arrayOf( + arrayOf("mongodb+srv://example-atlas-cluster.e06cc.mongodb.net", "example-atlas-cluster.e06cc.mongodb.net"), + arrayOf("mongodb://example-atlas-cluster-00.e06cc.mongodb.net:27107", "example-atlas-cluster-00.e06cc.mongodb.net"), + arrayOf("mongodb://localhost,another-server", null), + arrayOf("mongodb+srv://ex-atlas-stream.e06cc.mongodb.net", "ex-atlas-stream.e06cc.mongodb.net"), + arrayOf("mongodb://[::1]", null), + arrayOf("mongodb://my-cluster.mongo.ondigitalocean.com", null), + arrayOf("mongodb://my-cluster.cosmos.azure.com", null), + arrayOf("mongodb://my-cluster.docdb.amazonaws.com", null), + arrayOf("mongodb://my-cluster.docdb-elastic.amazonaws.com", null), + ).forEach { testCase -> + val driver = StubMongoDbDriver( + connected = true, + connectionString = testCase[0]!!, + responses = mapOf( + Long::class to { QueryResult.Run(1L) }, + BuildInfoFromMongoDb::class to { QueryResult.Run(defaultBuildInfo()) } + ) + ) + + val data = BuildInfo.Slice.queryUsingDriver(driver) + assertEquals(testCase[1], data.atlasHost, "atlasHost does not match") + } + } + + private fun defaultBuildInfo() = + BuildInfoFromMongoDb( + "7.8.0", + "1235abc", + emptyList(), + emptyMap(), + false, + ) +} diff --git a/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQueryTest.kt b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQueryTest.kt new file mode 100644 index 000000000..ef9f30d1b --- /dev/null +++ b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQueryTest.kt @@ -0,0 +1,240 @@ +package com.mongodb.jbplugin.accessadapter.slice + +import com.mongodb.jbplugin.accessadapter.QueryResult +import com.mongodb.jbplugin.accessadapter.StubMongoDbDriver +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.HasExplain +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class ExplainQueryTest { + @Test + fun it_is_able_to_run_an_explain_plan_given_a_query_and_returns_a_collscan_if_no_index_available() = runTest { + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + mapOf( + "queryPlanner" to mapOf( + "winningPlan" to mapOf( + "stage" to "COLLSCAN" + ) + ) + ) + ) + } + ) + ) + + val namespace = Namespace("myDb", "myCollection") + + val query = Node( + Unit, + listOf( + HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), + HasExplain(HasExplain.ExplainPlanType.SAFE), + ) + ) + + val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) + .queryUsingDriver(driver) + + assertEquals(ExplainQuery(ExplainPlan.CollectionScan), explainPlanResult) + } + + @Test + fun it_is_able_to_run_an_explain_plan_given_a_query_and_returns_an_ixscan() = runTest { + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + mapOf( + "queryPlanner" to mapOf( + "winningPlan" to mapOf( + "stage" to "FETCH", + "inputStage" to mapOf( + "stage" to "IXSCAN" + ) + ) + ) + ) + ) + } + ) + ) + + val namespace = Namespace("myDb", "myCollection") + + val query = Node( + Unit, + listOf( + HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), + HasExplain(HasExplain.ExplainPlanType.SAFE), + ) + ) + + val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) + .queryUsingDriver(driver) + + assertEquals(ExplainQuery(ExplainPlan.IndexScan), explainPlanResult) + } + + @Test + fun it_is_able_to_run_an_explain_plan_given_a_query_and_returns_an_ineffective_index_usage_if_filtering_in_memory() = runTest { + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + mapOf( + "queryPlanner" to mapOf( + "winningPlan" to mapOf( + "stage" to "FILTER", + "inputStage" to mapOf( + "stage" to "IXSCAN" + ) + ) + ) + ) + ) + } + ) + ) + + val namespace = Namespace("myDb", "myCollection") + + val query = Node( + Unit, + listOf( + HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), + HasExplain(HasExplain.ExplainPlanType.SAFE), + ) + ) + + val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) + .queryUsingDriver(driver) + + assertEquals(ExplainQuery(ExplainPlan.IneffectiveIndexUsage), explainPlanResult) + } + + @Test + fun it_is_able_to_run_an_explain_plan_and_checks_for_effective_index_usage_with_the_executionStats_warning_when_the_returned_vs_fetched_ratio_is_greater_than_50() = runTest { + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + mapOf( + "executionStats" to mapOf( + "nReturned" to 1, + "totalDocsExamined" to 50 + ), + "queryPlanner" to mapOf( + "winningPlan" to mapOf( + "stage" to "FETCH", + "inputStage" to mapOf( + "stage" to "IXSCAN" + ) + ) + ) + ) + ) + } + ) + ) + + val namespace = Namespace("myDb", "myCollection") + + val query = Node( + Unit, + listOf( + HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), + HasExplain(HasExplain.ExplainPlanType.SAFE), + ) + ) + + val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) + .queryUsingDriver(driver) + + assertEquals(ExplainQuery(ExplainPlan.IneffectiveIndexUsage), explainPlanResult) + } + + @Test + fun it_is_able_to_run_an_explain_plan_and_checks_for_effective_index_usage_with_the_executionStats() = runTest { + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + mapOf( + "executionStats" to mapOf( + "nReturned" to 1, + "totalDocsExamined" to 15 + ), + "queryPlanner" to mapOf( + "winningPlan" to mapOf( + "stage" to "FETCH", + "inputStage" to mapOf( + "stage" to "IXSCAN" + ) + ) + ) + ) + ) + } + ) + ) + + val namespace = Namespace("myDb", "myCollection") + + val query = Node( + Unit, + listOf( + HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), + HasExplain(HasExplain.ExplainPlanType.SAFE), + ) + ) + + val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) + .queryUsingDriver(driver) + + assertEquals(ExplainQuery(ExplainPlan.IndexScan), explainPlanResult) + } + + @Test + fun it_is_able_to_run_an_explain_plan_given_a_query_and_returns_an_ineffective_index_usage_for_queries_sorting_in_memory() = runTest { + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + mapOf( + "queryPlanner" to mapOf( + "winningPlan" to mapOf( + "stage" to "SORT", + "inputStage" to mapOf( + "stage" to "IXSCAN" + ) + ) + ) + ) + ) + } + ) + ) + + val namespace = Namespace("myDb", "myCollection") + val query = Node( + Unit, + listOf( + HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), + HasExplain(HasExplain.ExplainPlanType.SAFE), + ) + ) + + val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) + .queryUsingDriver(driver) + + assertEquals(ExplainQuery(ExplainPlan.IneffectiveIndexUsage), explainPlanResult) + } +} diff --git a/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchemaTest.kt b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchemaTest.kt new file mode 100644 index 000000000..29bb4f950 --- /dev/null +++ b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchemaTest.kt @@ -0,0 +1,216 @@ +package com.mongodb.jbplugin.accessadapter.slice + +import com.mongodb.jbplugin.accessadapter.QueryResult +import com.mongodb.jbplugin.accessadapter.StubMongoDbDriver +import com.mongodb.jbplugin.mql.BsonAnyOf +import com.mongodb.jbplugin.mql.BsonArray +import com.mongodb.jbplugin.mql.BsonDouble +import com.mongodb.jbplugin.mql.BsonInt32 +import com.mongodb.jbplugin.mql.BsonNull +import com.mongodb.jbplugin.mql.BsonObject +import com.mongodb.jbplugin.mql.BsonString +import com.mongodb.jbplugin.mql.Namespace +import com.mongodb.jbplugin.mql.OccurrencePercentage +import com.mongodb.jbplugin.mql.Value +import com.mongodb.jbplugin.mql.components.HasLimit +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class GetCollectionSchemaTest { + @Test + fun returns_an_empty_schema_if_the_database_is_not_provided() = runTest { + val namespace = Namespace("", "myColl") + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { throw AssertionError("This must not be called") } + ) + ) + + val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) + + assertEquals(namespace, result.schema.namespace) + assertEquals( + BsonObject( + emptyMap(), + ), + result.schema.schema, + ) + } + + @Test + fun returns_an_empty_schema_if_the_collection_is_not_provided() = runTest { + val namespace = Namespace("myDb", "") + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { throw AssertionError("This must not be called") } + ) + ) + val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) + + assertEquals(namespace, result.schema.namespace) + assertEquals( + BsonObject( + emptyMap(), + ), + result.schema.schema, + ) + } + + @Test + fun should_build_a_schema_based_on_the_result_of_the_query() = runTest { + val namespace = Namespace("myDb", "myColl") + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + listOf( + mapOf("string" to "myString"), + mapOf("integer" to 52, "string" to "anotherString"), + ) + ) + } + ) + ) + + val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) + + assertEquals(namespace, result.schema.namespace) + assertEquals( + BsonObject( + mapOf( + "string" to BsonAnyOf(BsonNull, BsonString), + "integer" to BsonInt32, + ), + ), + result.schema.schema, + ) + } + + @Test + fun should_be_aware_of_different_shapes_of_sub_documents() = runTest { + val namespace = Namespace("myDb", "myColl") + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + listOf( + mapOf("book" to mapOf("author" to "Someone")), + mapOf("book" to mapOf("author" to "Someone Else", "isbn" to "XXXXXXXX")) + ) + ) + } + ) + ) + + val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) + + assertEquals(namespace, result.schema.namespace) + assertEquals( + BsonObject( + mapOf( + "book" to + BsonObject( + mapOf( + "author" to BsonAnyOf(BsonString, BsonNull), + "isbn" to BsonAnyOf(BsonString, BsonNull), + ), + ), + ), + ), + result.schema.schema, + ) + } + + @Test + fun should_be_aware_of_arrays_with_different_types_of_elements() = runTest { + val namespace = Namespace("myDb", "myColl") + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + listOf( + mapOf("array" to arrayOf(1, 2, 3, "abc")), + mapOf("array" to arrayOf(1.2f, "jkl")), + ) + ) + } + ) + ) + + val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) + + assertEquals(namespace, result.schema.namespace) + assertEquals( + BsonObject( + mapOf( + "array" to BsonArray( + BsonAnyOf(BsonNull, BsonString, BsonDouble, BsonInt32) + ), + ), + ), + result.schema.schema, + ) + } + + @Test + fun should_respect_the_provided_limit_for_fetching_sample_documents() = runTest { + val namespace = Namespace("myDb", "myColl") + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + assertEquals(1, it.component()?.limit) + QueryResult.Run( + listOf( + mapOf("string" to "myString"), + mapOf("integer" to 52, "string" to "anotherString"), + ) + ) + } + ) + ) + + GetCollectionSchema.Slice(namespace, 1).queryUsingDriver(driver) + } + + @Test + fun should_hold_data_distribution_based_on_the_samples_collected() = runTest { + val namespace = Namespace("myDb", "myColl") + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + listOf( + mapOf("string" to "myString"), + mapOf("integer" to 52, "string" to "anotherString"), + ) + ) + } + ) + ) + + val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) + + assertEquals(namespace, result.schema.namespace) + assertEquals( + BsonObject( + mapOf( + "string" to BsonAnyOf(BsonNull, BsonString), + "integer" to BsonInt32, + ), + ), + result.schema.schema, + ) + assertEquals>( + mapOf( + "myString" to 50.0, + "anotherString" to 50.0, + ), + result.schema.dataDistribution.getDistributionForPath("string")!! + ) + assertEquals( + 50.0, + result.schema.dataDistribution.getDistributionForPath("integer")?.get(52) + ) + } +} diff --git a/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollectionsTest.kt b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollectionsTest.kt new file mode 100644 index 000000000..6fe2db93b --- /dev/null +++ b/packages/mongodb-access-adapter/src/commonTest/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollectionsTest.kt @@ -0,0 +1,50 @@ +package com.mongodb.jbplugin.accessadapter.slice + +import com.mongodb.jbplugin.accessadapter.QueryResult +import com.mongodb.jbplugin.accessadapter.StubMongoDbDriver +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ListCollectionsTest { + @Test + fun returns_no_collections_if_database_is_not_provided() = runTest { + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { throw AssertionError("This must not be called") } + ) + ) + val result = ListCollections.Slice("").queryUsingDriver(driver) + + assertTrue(result.collections.isEmpty()) + } + + @Test + fun returns_collections_if_the_database_is_provided() = runTest { + val driver = StubMongoDbDriver( + responses = mapOf( + Map::class to { + QueryResult.Run( + mapOf( + "cursor" to mapOf( + "firstBatch" to listOf( + mapOf("name" to "myCollection", "type" to "collection") + ) + ) + ) + ) + } + ) + ) + + val result = ListCollections.Slice("myDb").queryUsingDriver(driver) + + assertEquals( + listOf( + ListCollections.Collection("myCollection", "collection") + ), + result.collections + ) + } +} diff --git a/packages/mongodb-access-adapter/src/jsMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.js.kt b/packages/mongodb-access-adapter/src/jsMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.js.kt new file mode 100644 index 000000000..defbc99ea --- /dev/null +++ b/packages/mongodb-access-adapter/src/jsMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.js.kt @@ -0,0 +1,47 @@ +package com.mongodb.jbplugin.accessadapter.slice + +import com.mongodb.jbplugin.mql.BsonAny +import com.mongodb.jbplugin.mql.BsonArray +import com.mongodb.jbplugin.mql.BsonBoolean +import com.mongodb.jbplugin.mql.BsonDate +import com.mongodb.jbplugin.mql.BsonDecimal128 +import com.mongodb.jbplugin.mql.BsonDouble +import com.mongodb.jbplugin.mql.BsonInt32 +import com.mongodb.jbplugin.mql.BsonInt64 +import com.mongodb.jbplugin.mql.BsonObject +import com.mongodb.jbplugin.mql.BsonObjectId +import com.mongodb.jbplugin.mql.BsonString +import com.mongodb.jbplugin.mql.BsonType +import kotlin.js.Date + +private fun jsTypeOf(v: dynamic): String = js("typeof v") as String + +actual fun recursivelyBuildSchema(value: Any?): BsonType = when { + jsTypeOf(value) == "string" -> BsonString + jsTypeOf(value) == "boolean" -> BsonBoolean + jsTypeOf(value) == "number" -> BsonDouble + jsTypeOf(value) == "bigint" -> BsonInt64 + value is Date -> BsonDate + js("Array.isArray(value)") as Boolean -> BsonArray(BsonAny) + value != null && jsTypeOf(value) == "object" -> run { + val keys = js("Object.keys(value)") as Array + if (keys.size == 1 && keys[0].startsWith("$")) { + when (keys[0]) { + "\$numberLong" -> BsonInt64 + "\$numberDecimal" -> BsonDecimal128 + "\$numberDouble" -> BsonDouble + "\$numberInt" -> BsonInt32 + "\$oid" -> BsonObjectId + "\$date" -> BsonDate + else -> BsonAny + } + } else { + val map = keys.associateWith { key -> + recursivelyBuildSchema((value.asDynamic())[key]) + } + + BsonObject(map) + } + } + else -> BsonAny +} diff --git a/packages/mongodb-access-adapter/src/jvmMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.jvm.kt b/packages/mongodb-access-adapter/src/jvmMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.jvm.kt new file mode 100644 index 000000000..273fae10c --- /dev/null +++ b/packages/mongodb-access-adapter/src/jvmMain/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchema.jvm.kt @@ -0,0 +1,41 @@ +package com.mongodb.jbplugin.accessadapter.slice + +import com.mongodb.jbplugin.mql.BsonAnyOf +import com.mongodb.jbplugin.mql.BsonArray +import com.mongodb.jbplugin.mql.BsonNull +import com.mongodb.jbplugin.mql.BsonObject +import com.mongodb.jbplugin.mql.BsonType +import com.mongodb.jbplugin.mql.primitiveOrWrapper +import com.mongodb.jbplugin.mql.toBsonType + +actual fun recursivelyBuildSchema(value: Any?): BsonType { + return when (value) { + null -> BsonNull + is Map<*, *> -> BsonObject( + value.map { + it.key.toString() to + recursivelyBuildSchema(it.value) + }.toMap() + ) + + is Collection<*> -> recursivelyBuildSchema(value.toTypedArray()) + is Array<*> -> + BsonArray( + value + .map { toBsonType(it?.javaClass, it) } + .toSet() + .let { + if (it.size == 1) { + it.first() + } else { + BsonAnyOf(it) + } + }, + ) + + else -> { + val realJavaClass = primitiveOrWrapper(value.javaClass) as Class + toBsonType(realJavaClass, value) + } + } +} diff --git a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoTest.kt b/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoTest.kt deleted file mode 100644 index 758dd63be..000000000 --- a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/BuildInfoTest.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.mongodb.jbplugin.accessadapter.slice - -import com.mongodb.jbplugin.accessadapter.ConnectionString -import com.mongodb.jbplugin.accessadapter.MongoDbDriver -import com.mongodb.jbplugin.accessadapter.QueryResult -import com.mongodb.jbplugin.mql.QueryContext -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.CsvSource -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.mockito.kotlin.any -import org.mockito.kotlin.doThrow -import org.mockito.kotlin.eq -import kotlin.time.Duration.Companion.seconds - -class BuildInfoTest { - @Test - fun `returns a valid build info`(): Unit = - runBlocking { - val driver = mock() - `when`(driver.connected).thenReturn(true) - `when`( - driver.connectionString() - ).thenReturn(ConnectionString(listOf("mongodb://localhost/"))) - `when`( - driver.runQuery(any(), eq(Long::class), any(), eq(1.seconds)) - ).thenReturn(QueryResult.Run(1L)) - `when`(driver.runQuery(any(), any())).thenReturn( - QueryResult.Run(defaultBuildInfo()), - ) - - val data = BuildInfo.Slice.queryUsingDriver(driver) - assertEquals("7.8.0", data.version) - assertEquals("1235abc", data.gitVersion) - } - - @Test - fun `when not connected do not run queries`(): Unit = - runBlocking { - val driver = mock() - `when`(driver.connected).thenReturn(false) - `when`( - driver.connectionString() - ).thenReturn(ConnectionString(listOf("mongodb://localhost/"))) - `when`( - driver.runQuery(any(), eq(Long::class), any(), eq(1.seconds)) - ).doThrow(NotImplementedError()) - `when`(driver.runQuery(any(), any())).doThrow( - NotImplementedError(), - ) - - BuildInfo.Slice.queryUsingDriver(driver) - } - - @ParameterizedTest - @CsvSource( - value = [ - "URL;;isLocalhost;;isAtlas;;isAtlasStream;;isDigitalOcean;;isGenuineMongoDb;;mongodbVariant", - "mongodb://localhost;;true;;false;;false;;false;;true;;", - "mongodb://localhost,another-server;;false;;false;;false;;false;;true;;", - "mongodb+srv://example-atlas-cluster.e06cc.mongodb.net;;false;;true;;false;;false;;true;;", - "mongodb://example-atlas-cluster.e06cc.mongodb.net,another-server;;false;;false;;false;;false;;true;;", - "mongodb+srv://atlas-stream-example-atlas-stream.e06cc.mongodb.net;;false;;true;;true;;false;;true;;", - "mongodb://[::1];;true;;false;;false;;false;;true;;", - "mongodb://my-cluster.mongo.ondigitalocean.com;;false;;false;;false;;true;;true;;", - "mongodb://my-cluster.cosmos.azure.com;;false;;false;;false;;false;;false;;cosmosdb", - "mongodb://my-cluster.docdb.amazonaws.com;;false;;false;;false;;false;;false;;documentdb", - "mongodb://my-cluster.docdb-elastic.amazonaws.com;;false;;false;;false;;false;;false;;documentdb", - ], - delimiterString = ";;", - useHeadersInDisplayName = true, - ) - fun `parses different type of url connections properly`( - url: String, - isLocalhost: Boolean, - isAtlas: Boolean, - isAtlasStream: Boolean, - isDigitalOcean: Boolean, - isGenuineMongoDb: Boolean, - mongodbVariant: String?, - ): Unit = - runBlocking { - val driver = mock() - `when`(driver.connected).thenReturn(true) - `when`(driver.connectionString()).thenReturn(ConnectionString(listOf(url))) - `when`( - driver.runQuery( - any(), - eq(Long::class), - eq(QueryContext.empty()), - eq(1.seconds) - ) - ).thenReturn(QueryResult.Run(1L)) - `when`(driver.runQuery(any(), any())).thenReturn( - QueryResult.Run(defaultBuildInfo()), - ) - - val data = BuildInfo.Slice.queryUsingDriver(driver) - assertEquals(isLocalhost, data.isLocalhost, "isLocalhost does not match") - assertEquals(isAtlas, data.isAtlas, "isAtlas does not match") - assertEquals(isAtlasStream, data.isAtlasStream, "isAtlasStream does not match") - assertEquals(isDigitalOcean, data.isDigitalOcean, "isDigitalOcean does not match") - assertEquals(isGenuineMongoDb, data.isGenuineMongoDb, "isGenuineMongoDb does not match") - assertEquals(mongodbVariant, data.nonGenuineVariant, "mongodbVariant does not match") - } - - @ParameterizedTest - @CsvSource( - value = [ - "URL;;atlasHost", - "mongodb+srv://example-atlas-cluster.e06cc.mongodb.net;;example-atlas-cluster.e06cc.mongodb.net", - "mongodb://example-atlas-cluster-00.e06cc.mongodb.net:27107;;example-atlas-cluster-00.e06cc.mongodb.net", - "mongodb://localhost,another-server;;", - "mongodb+srv://ex-atlas-stream.e06cc.mongodb.net;;ex-atlas-stream.e06cc.mongodb.net", - "mongodb://[::1];;", - "mongodb://my-cluster.mongo.ondigitalocean.com;;", - "mongodb://my-cluster.cosmos.azure.com;;", - "mongodb://my-cluster.docdb.amazonaws.com;;", - "mongodb://my-cluster.docdb-elastic.amazonaws.com;;", - ], - delimiterString = ";;", - useHeadersInDisplayName = true, - ) - fun `provides the correct connection host for atlas`( - url: String, - atlasHost: String?, - ): Unit = - runBlocking { - val driver = mock() - `when`(driver.connected).thenReturn(true) - `when`(driver.connectionString()).thenReturn(ConnectionString(listOf(url))) - `when`( - driver.runQuery(any(), eq(Long::class), any(), eq(1.seconds)) - ).thenReturn(QueryResult.Run(1L)) - `when`(driver.runQuery(any(), any())).thenReturn( - QueryResult.Run(defaultBuildInfo()), - ) - - val data = BuildInfo.Slice.queryUsingDriver(driver) - assertEquals(atlasHost, data.atlasHost, "atlasHost does not match") - } - - private fun defaultBuildInfo() = - BuildInfoFromMongoDb( - "7.8.0", - "1235abc", - emptyList(), - emptyMap(), - false, - ) -} diff --git a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQueryTest.kt b/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQueryTest.kt deleted file mode 100644 index 8c3272f96..000000000 --- a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/ExplainQueryTest.kt +++ /dev/null @@ -1,226 +0,0 @@ -package com.mongodb.jbplugin.accessadapter.slice - -import com.mongodb.jbplugin.accessadapter.MongoDbDriver -import com.mongodb.jbplugin.accessadapter.QueryResult -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.HasExplain -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.mockito.Mockito.mock -import org.mockito.kotlin.any -import org.mockito.kotlin.whenever - -class ExplainQueryTest { - @Test - fun `it is able to run an explain plan given a query and returns a collscan if no index available`() = runTest { - val driver = mock() - val namespace = Namespace("myDb", "myCollection") - - whenever(driver.runQuery, Unit>(any(), any(), any())).thenReturn( - QueryResult.Run( - mapOf( - "queryPlanner" to mapOf( - "winningPlan" to mapOf( - "stage" to "COLLSCAN" - ) - ) - ) - ) - ) - - val query = Node( - Unit, - listOf( - HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), - HasExplain(HasExplain.ExplainPlanType.SAFE), - ) - ) - - val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) - .queryUsingDriver(driver) - - assertEquals(ExplainQuery(ExplainPlan.CollectionScan), explainPlanResult) - } - - @Test - fun `it is able to run an explain plan given a query and returns an ixscan`() = runTest { - val driver = mock() - val namespace = Namespace("myDb", "myCollection") - - whenever(driver.runQuery, Unit>(any(), any(), any())).thenReturn( - QueryResult.Run( - mapOf( - "queryPlanner" to mapOf( - "winningPlan" to mapOf( - "stage" to "FETCH", - "inputStage" to mapOf( - "stage" to "IXSCAN" - ) - ) - ) - ) - ) - ) - - val query = Node( - Unit, - listOf( - HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), - HasExplain(HasExplain.ExplainPlanType.SAFE), - ) - ) - - val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) - .queryUsingDriver(driver) - - assertEquals(ExplainQuery(ExplainPlan.IndexScan), explainPlanResult) - } - - @Test - fun `it is able to run an explain plan given a query and returns an ineffective index usage if filtering in memory`() = runTest { - val driver = mock() - val namespace = Namespace("myDb", "myCollection") - - whenever(driver.runQuery, Unit>(any(), any(), any())).thenReturn( - QueryResult.Run( - mapOf( - "queryPlanner" to mapOf( - "winningPlan" to mapOf( - "stage" to "FILTER", - "inputStage" to mapOf( - "stage" to "IXSCAN" - ) - ) - ) - ) - ) - ) - - val query = Node( - Unit, - listOf( - HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), - HasExplain(HasExplain.ExplainPlanType.SAFE), - ) - ) - - val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) - .queryUsingDriver(driver) - - assertEquals(ExplainQuery(ExplainPlan.IneffectiveIndexUsage), explainPlanResult) - } - - @Test - fun `it is able to run an explain plan and checks for effective index usage with the executionStats warning when the returned vs fetched ratio is greater than 50`() = runTest { - val driver = mock() - val namespace = Namespace("myDb", "myCollection") - - whenever(driver.runQuery, Unit>(any(), any(), any())).thenReturn( - QueryResult.Run( - mapOf( - "executionStats" to mapOf( - "nReturned" to 1, - "totalDocsExamined" to 50 - ), - "queryPlanner" to mapOf( - "winningPlan" to mapOf( - "stage" to "FETCH", - "inputStage" to mapOf( - "stage" to "IXSCAN" - ) - ) - ) - ) - ) - ) - - val query = Node( - Unit, - listOf( - HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), - HasExplain(HasExplain.ExplainPlanType.SAFE), - ) - ) - - val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) - .queryUsingDriver(driver) - - assertEquals(ExplainQuery(ExplainPlan.IneffectiveIndexUsage), explainPlanResult) - } - - @Test - fun `it is able to run an explain plan and checks for effective index usage with the executionStats`() = runTest { - val driver = mock() - val namespace = Namespace("myDb", "myCollection") - - whenever(driver.runQuery, Unit>(any(), any(), any())).thenReturn( - QueryResult.Run( - mapOf( - "executionStats" to mapOf( - "nReturned" to 1, - "totalDocsExamined" to 15 - ), - "queryPlanner" to mapOf( - "winningPlan" to mapOf( - "stage" to "FETCH", - "inputStage" to mapOf( - "stage" to "IXSCAN" - ) - ) - ) - ) - ) - ) - - val query = Node( - Unit, - listOf( - HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), - HasExplain(HasExplain.ExplainPlanType.SAFE), - ) - ) - - val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) - .queryUsingDriver(driver) - - assertEquals(ExplainQuery(ExplainPlan.IndexScan), explainPlanResult) - } - - @Test - fun `it is able to run an explain plan given a query and returns an ineffective index usage for queries sorting in memory`() = runTest { - val driver = mock() - val namespace = Namespace("myDb", "myCollection") - - whenever(driver.runQuery, Unit>(any(), any(), any())).thenReturn( - QueryResult.Run( - mapOf( - "queryPlanner" to mapOf( - "winningPlan" to mapOf( - "stage" to "SORT", - "inputStage" to mapOf( - "stage" to "IXSCAN" - ) - ) - ) - ) - ) - ) - - val query = Node( - Unit, - listOf( - HasCollectionReference(HasCollectionReference.Known(Unit, Unit, namespace)), - HasExplain(HasExplain.ExplainPlanType.SAFE), - ) - ) - - val explainPlanResult = ExplainQuery.Slice(query, QueryContext(emptyMap(), false, true)) - .queryUsingDriver(driver) - - assertEquals(ExplainQuery(ExplainPlan.IneffectiveIndexUsage), explainPlanResult) - } -} diff --git a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchemaTest.kt b/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchemaTest.kt deleted file mode 100644 index d6fb282b0..000000000 --- a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/GetCollectionSchemaTest.kt +++ /dev/null @@ -1,231 +0,0 @@ -package com.mongodb.jbplugin.accessadapter.slice - -import com.mongodb.jbplugin.accessadapter.MongoDbDriver -import com.mongodb.jbplugin.accessadapter.QueryResult -import com.mongodb.jbplugin.mql.BsonAnyOf -import com.mongodb.jbplugin.mql.BsonArray -import com.mongodb.jbplugin.mql.BsonDouble -import com.mongodb.jbplugin.mql.BsonInt32 -import com.mongodb.jbplugin.mql.BsonNull -import com.mongodb.jbplugin.mql.BsonObject -import com.mongodb.jbplugin.mql.BsonString -import com.mongodb.jbplugin.mql.Namespace -import com.mongodb.jbplugin.mql.components.HasLimit -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.mockito.kotlin.any -import org.mockito.kotlin.argThat -import org.mockito.kotlin.eq -import org.mockito.kotlin.never -import org.mockito.kotlin.times -import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever -import kotlin.time.Duration.Companion.seconds - -class GetCollectionSchemaTest { - @Test - fun `returns an empty schema if the database is not provided`() { - runBlocking { - val namespace = Namespace("", "myColl") - val driver = mock() - val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) - - assertEquals(namespace, result.schema.namespace) - assertEquals( - BsonObject( - emptyMap(), - ), - result.schema.schema, - ) - - verify(driver, never()).runQuery(any(), any(), any(), eq(1.seconds)) - } - } - - @Test - fun `returns an empty schema if the collection is not provided`() { - runBlocking { - val namespace = Namespace("myDb", "") - val driver = mock() - val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) - - assertEquals(namespace, result.schema.namespace) - assertEquals( - BsonObject( - emptyMap(), - ), - result.schema.schema, - ) - - verify(driver, never()).runQuery(any(), any(), any(), eq(1.seconds)) - } - } - - @Test - fun `should build a schema based on the result of the query`() { - runBlocking { - val namespace = Namespace("myDb", "myColl") - val driver = mock() - - whenever(driver.runQuery>, Any>(any(), any())) - .thenReturn( - QueryResult.Run( - listOf( - mapOf("string" to "myString"), - mapOf("integer" to 52, "string" to "anotherString"), - ) - ) - ) - - val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) - - assertEquals(namespace, result.schema.namespace) - assertEquals( - BsonObject( - mapOf( - "string" to BsonAnyOf(BsonNull, BsonString), - "integer" to BsonInt32, - ), - ), - result.schema.schema, - ) - } - } - - @Test - fun `should be aware of different shapes of sub documents`() { - runBlocking { - val namespace = Namespace("myDb", "myColl") - val driver = mock() - - `when`(driver.runQuery>, Any>(any(), any())).thenReturn( - QueryResult.Run( - listOf( - mapOf("book" to mapOf("author" to "Someone")), - mapOf("book" to mapOf("author" to "Someone Else", "isbn" to "XXXXXXXX")) - ) - ) - ) - - val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) - - assertEquals(namespace, result.schema.namespace) - assertEquals( - BsonObject( - mapOf( - "book" to - BsonObject( - mapOf( - "author" to BsonAnyOf(BsonString, BsonNull), - "isbn" to BsonAnyOf(BsonString, BsonNull), - ), - ), - ), - ), - result.schema.schema, - ) - } - } - - @Test - fun `should be aware of arrays with different types of elements`() { - runBlocking { - val namespace = Namespace("myDb", "myColl") - val driver = mock() - - `when`(driver.runQuery>, Any>(any(), any())).thenReturn( - QueryResult.Run( - listOf( - mapOf("array" to arrayOf(1, 2, 3, "abc")), - mapOf("array" to arrayOf(1.2f, "jkl")), - ) - ) - ) - - val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) - - assertEquals(namespace, result.schema.namespace) - assertEquals( - BsonObject( - mapOf( - "array" to BsonArray( - BsonAnyOf(BsonNull, BsonString, BsonDouble, BsonInt32) - ), - ), - ), - result.schema.schema, - ) - } - } - - @Test - fun `should respect the provided limit for fetching sample documents`() { - runBlocking { - val namespace = Namespace("myDb", "myColl") - val driver = mock() - - `when`(driver.runQuery>, Any>(any(), any(), any())).thenReturn( - QueryResult.Run( - listOf( - mapOf("string" to "myString"), - mapOf("integer" to 52, "string" to "anotherString"), - ) - ) - ) - - GetCollectionSchema.Slice(namespace, 1).queryUsingDriver(driver) - - verify(driver, times(1)).runQuery>, Any>( - argThat { - component()?.limit == 1 - }, - any() - ) - } - } - - @Test - fun `should hold data distribution based on the samples collected`() { - runBlocking { - val namespace = Namespace("myDb", "myColl") - val driver = mock() - - whenever(driver.runQuery>, Any>(any(), any())) - .thenReturn( - QueryResult.Run( - listOf( - mapOf("string" to "myString"), - mapOf("integer" to 52, "string" to "anotherString"), - ) - ) - ) - - val result = GetCollectionSchema.Slice(namespace, 50).queryUsingDriver(driver) - - assertEquals(namespace, result.schema.namespace) - assertEquals( - BsonObject( - mapOf( - "string" to BsonAnyOf(BsonNull, BsonString), - "integer" to BsonInt32, - ), - ), - result.schema.schema, - ) - assertEquals( - mapOf( - "myString" to 50.0, - "anotherString" to 50.0, - ), - result.schema.dataDistribution.getDistributionForPath("string") - ) - assertEquals( - 50.0, - result.schema.dataDistribution.getDistributionForPath("integer")?.get(52) - ) - } - } -} diff --git a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollectionsTest.kt b/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollectionsTest.kt deleted file mode 100644 index 4678b0c68..000000000 --- a/packages/mongodb-access-adapter/src/test/kotlin/com/mongodb/jbplugin/accessadapter/slice/ListCollectionsTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.mongodb.jbplugin.accessadapter.slice - -import com.mongodb.jbplugin.accessadapter.MongoDbDriver -import com.mongodb.jbplugin.accessadapter.QueryResult -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.mockito.kotlin.any -import org.mockito.kotlin.never -import org.mockito.kotlin.verify - -class ListCollectionsTest { - @Test - fun `returns no collections if database is not provided`() { - runBlocking { - val driver = mock() - val result = ListCollections.Slice("").queryUsingDriver(driver) - - assertTrue(result.collections.isEmpty()) - verify(driver, never()).runQuery(any(), any()) - } - } - - @Test - fun `returns collections if the database is provided`() { - runBlocking { - val driver = mock() - - `when`(driver.runQuery, Unit>(any(), any())).thenReturn( - QueryResult.Run( - mapOf( - "cursor" to mapOf( - "firstBatch" to listOf( - mapOf("name" to "myCollection", "type" to "collection") - ) - ) - ) - ) - ) - - val result = ListCollections.Slice("myDb").queryUsingDriver(driver) - - assertEquals( - listOf( - ListCollections.Collection("myCollection", "collection") - ), - result.collections - ) - } - } -} diff --git a/packages/mongodb-design-system/build.gradle.kts b/packages/mongodb-design-system/build.gradle.kts new file mode 100644 index 000000000..e47384d17 --- /dev/null +++ b/packages/mongodb-design-system/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("com.mongodb.intellij.isolated-module") + id("org.jetbrains.compose") version ("1.7.3") + id("org.jetbrains.kotlin.plugin.compose") version ("2.0.21") +} + +repositories { + google() + maven("https://www.jetbrains.com/intellij-repository/releases/") + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://packages.jetbrains.team/maven/p/kpm/public/") +} + +kotlin { + sourceSets { + jvmMain { + dependencies { + compileOnly(compose.runtime) + compileOnly(compose.foundation) + + implementation("org.jetbrains.jewel:jewel-ide-laf-bridge-243:0.27.0") { + exclude(group = "org.jetbrains.compose.material") + exclude(group = "org.jetbrains.kotlinx") + } + + implementation("org.jetbrains.jewel:jewel-int-ui-standalone-243:0.27.0") { + exclude(group = "org.jetbrains.compose.material") + exclude(group = "org.jetbrains.kotlinx") + } + + compileOnly("androidx.lifecycle:lifecycle-viewmodel:2.8.5") + compileOnly("androidx.lifecycle:lifecycle-runtime:2.8.5") + + // Do not bring in Material (we use Jewel) and Coroutines (the IDE has its own) + compileOnly(compose.desktop.currentOs) { + exclude(group = "org.jetbrains.compose.material") + exclude(group = "org.jetbrains.kotlinx") + } + + api(libs.kotlinx.coroutines.swing) + // api(compose.desktop.currentOs) { + // exclude(group = "org.jetbrains.compose.material") + // exclude(group = "org.jetbrains.kotlinx") + // } + // + // compileOnly(compose.ui) + // compileOnly(compose.components.uiToolingPreview) + // compileOnly(compose.components.resources) + } + } + } +} diff --git a/packages/mongodb-design-system/src/jvmMain/kotlin/com/mongodb/jbplugin/designsystem/Card.kt b/packages/mongodb-design-system/src/jvmMain/kotlin/com/mongodb/jbplugin/designsystem/Card.kt new file mode 100644 index 000000000..cfb16c53e --- /dev/null +++ b/packages/mongodb-design-system/src/jvmMain/kotlin/com/mongodb/jbplugin/designsystem/Card.kt @@ -0,0 +1,39 @@ +package com.mongodb.jbplugin.designsystem + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.component.Icon +import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.icon.IconKey + +@Composable +fun Card(icon: IconKey, title: String, body: @Composable () -> Unit) { + Box( + modifier = Modifier.padding(12.dp).background(Color.DarkGray).fillMaxWidth() + .border(BorderStroke(1.dp, Color.DarkGray), RoundedCornerShape(12.dp)) + ) { + Column(Modifier.padding(12.dp)) { + Row { + Icon(icon, title) + Text(style = JewelTheme.defaultTextStyle.merge(fontWeight = FontWeight.Bold), text = title) + } + + Box(Modifier.padding(top = 38.dp)) { + body() + } + } + } +} diff --git a/packages/mongodb-design-system/src/jvmMain/kotlin/com/mongodb/jbplugin/designsystem/ConnectionComboBox.kt b/packages/mongodb-design-system/src/jvmMain/kotlin/com/mongodb/jbplugin/designsystem/ConnectionComboBox.kt new file mode 100644 index 000000000..9c85d0ea8 --- /dev/null +++ b/packages/mongodb-design-system/src/jvmMain/kotlin/com/mongodb/jbplugin/designsystem/ConnectionComboBox.kt @@ -0,0 +1,65 @@ +package com.mongodb.jbplugin.designsystem + +import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.mongodb.jbplugin.designsystem.ConnectionState.CONNECTED +import kotlinx.coroutines.flow.Flow +import org.jetbrains.jewel.ui.component.ComboBox +import org.jetbrains.jewel.ui.component.Icon +import org.jetbrains.jewel.ui.component.Text +import org.jetbrains.jewel.ui.icons.AllIconsKeys + +enum class ConnectionState { + IDLE, + CONNECTING, + CONNECTED, + ERROR +} + +data class Connection(val id: String, val name: String, val state: ConnectionState) + +@Composable +@Preview +fun ConnectionComboBox( + activeConnection: Flow, + allConnections: Flow>, + onConnectionSelected: (Connection) -> Unit +) { + val connections by allConnections.collectAsState(emptyList()) + val selected by activeConnection.collectAsState(null as Connection?) + + Row { + Column { + ComboBox(selected?.name ?: "Choose a connection") { + Column { + for (connection in connections) { + ComboBoxConnectionElement(connection, onConnectionSelected) + } + } + } + } + } +} + +@Composable +private fun ComboBoxConnectionElement(connection: Connection, onClick: (Connection) -> Unit) { + Box(modifier = Modifier.clickable { onClick(connection) }) { + Row { + Icon( + when (connection.state) { + CONNECTED -> AllIconsKeys.General.GreenCheckmark + else -> AllIconsKeys.General.Error + }, + "MongoDB Connection ${connection.name}" + ) + Text(connection.name) + } + } +} diff --git a/packages/mongodb-dialects/build.gradle.kts b/packages/mongodb-dialects/build.gradle.kts index 31d9a2084..844ecb5ec 100644 --- a/packages/mongodb-dialects/build.gradle.kts +++ b/packages/mongodb-dialects/build.gradle.kts @@ -2,7 +2,19 @@ plugins { id("com.mongodb.intellij.isolated-module") } -dependencies { - implementation(project(":packages:mongodb-mql-engines")) - implementation(project(":packages:mongodb-mql-model")) +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":packages:mongodb-mql-engines")) + implementation(project(":packages:mongodb-mql-model")) + } + } + + jvmMain { + dependencies { + implementation(libs.bson.kotlin) + } + } + } } 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 ab27b496c..4716d11f0 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 @@ -289,7 +289,7 @@ object JavaDriverDialectParser : DialectParser { } else { // case 2 HasValueReference.Runtime( secondArg, - secondArg.type?.toBsonType() ?: BsonArray(BsonAny) + toBsonType(secondArg.type) ?: BsonArray(BsonAny) ) } } else if (filter.argumentList.expressionCount > 2) { @@ -794,7 +794,7 @@ object JavaDriverDialectParser : DialectParser { HasValueReference.Constant( source = valueExpression, value = it, - type = it.javaClass.toBsonType(), + type = toBsonType(it.javaClass), ) } ?: HasValueReference.Unknown @@ -826,13 +826,13 @@ object JavaDriverDialectParser : DialectParser { val (wasResolvedAtCompileTime, resolvedValue) = expression.tryToResolveAsConstant() val resolvedType = if (resolvedValue is PsiEnumConstant) { - resolvedValue.type.toBsonType() + toBsonType(resolvedValue.type) } else if (resolvedValue is PsiType) { - resolvedValue.toBsonType() + toBsonType(resolvedValue) } else if (wasResolvedAtCompileTime) { - resolvedValue?.javaClass.toBsonType() + toBsonType(resolvedValue?.javaClass) } else { - expression.type?.toBsonType() + toBsonType(expression.type) } val valueReference = @@ -968,17 +968,17 @@ fun PsiExpressionList.inferFromSingleArrayArgument(start: Int = 0): HasValueRefe val arrayArg = expressions[start] val (constant, value) = arrayArg.tryToResolveAsConstant() - return if (constant) { + return if (constant && value != null) { HasValueReference.Constant( arrayArg, listOf(value), - BsonArray(value?.javaClass.toBsonType(value)) + BsonArray(toBsonType(value.javaClass, value)) ) } else { HasValueReference.Runtime( arrayArg, BsonArray( - arrayArg.type?.toBsonType() ?: BsonAny + toBsonType(arrayArg.type) ?: BsonAny ) ) } @@ -1020,11 +1020,13 @@ fun PsiType.guessIterableContentType(project: Project): BsonType { } val typeStr = text.substring(start + 1, end) - return PsiType.getTypeByName( - typeStr, - project, - GlobalSearchScope.everythingScope(project) - ).toBsonType() + return toBsonType( + PsiType.getTypeByName( + typeStr, + project, + GlobalSearchScope.everythingScope(project) + ) + ) ?: BsonArray(BsonAny) } fun PsiExpressionList.inferValueReferenceFromVarArg(start: Int = 0): HasValueReference.ValueReference { @@ -1036,7 +1038,7 @@ fun PsiExpressionList.inferValueReferenceFromVarArg(start: Int = 0): HasValueRef return HasValueReference.Runtime(parent, BsonArray(BsonAny)) } else if (allConstants.all { it.first }) { val eachType = allConstants.mapNotNull { - it.second?.javaClass?.toBsonType(it.second) + it.second?.javaClass?.let { klass -> toBsonType(klass, it.second) } }.map { flattenAnyOfReferences(it) }.toSet() @@ -1050,7 +1052,7 @@ fun PsiExpressionList.inferValueReferenceFromVarArg(start: Int = 0): HasValueRef ) } else { val eachType = allConstants.mapNotNull { - it.second?.javaClass?.toBsonType(it.second) + it.second?.javaClass?.let { klass -> toBsonType(klass, it.second) } }.toSet() val schema = flattenAnyOfReferences(BsonAnyOf(eachType)) return HasValueReference.Constant( @@ -1199,11 +1201,11 @@ fun PsiElement.parseFieldExpressionAsValueReference(): HasValueReference HasValueReference.Constant( element, value, - value?.javaClass.toBsonType(value) + value.toBsonType() ) !constant && element is PsiExpression -> HasValueReference.Runtime( element, - element.type?.toBsonType() ?: BsonAny + toBsonType(element.type) ?: BsonAny ) else -> HasValueReference.Unknown as HasValueReference.ValueReference } diff --git a/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/PsiMdbTreeUtil.kt b/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/PsiMdbTreeUtil.kt index 0ca498451..4a8588e69 100644 --- a/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/PsiMdbTreeUtil.kt +++ b/packages/mongodb-dialects/java-driver/src/main/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/PsiMdbTreeUtil.kt @@ -394,11 +394,15 @@ fun PsiElement.tryToResolveAsConstantString(): String? = * Maps a PsiType to its BSON counterpart. * PsiClassReferenceType */ -fun PsiType.toBsonType(): BsonType { - val javaClass = if (this is PsiClassReferenceType) { - resolve() ?: return BsonAny - } else if (this is PsiImmediateClassType) { - resolve() ?: return BsonAny +fun toBsonType(type: PsiType?): BsonType? { + if (type == null) { + return null + } + + val javaClass = if (type is PsiClassReferenceType) { + type.resolve() ?: return BsonAny + } else if (type is PsiImmediateClassType) { + type.resolve() ?: return BsonAny } else { null } @@ -408,53 +412,53 @@ fun PsiType.toBsonType(): BsonType { return BsonEnum(enumConstants.map { it.name }.toSet(), javaClass.name) } - return this.canonicalText.toBsonType() + return toBsonType(type.canonicalText) } /** * Maps a Java FQN to a BsonType. */ -fun String.toBsonType(): BsonType { - if (this == ("org.bson.types.ObjectId")) { +fun toBsonType(typeName: String): BsonType { + if (typeName == ("org.bson.types.ObjectId")) { return BsonAnyOf(BsonObjectId, BsonNull) - } else if (this == ("boolean") || this == ("java.lang.Boolean")) { + } else if (typeName == ("boolean") || typeName == ("java.lang.Boolean")) { return BsonBoolean - } else if (this == ("short") || this == ("java.lang.Short")) { + } else if (typeName == ("short") || typeName == ("java.lang.Short")) { return BsonInt32 - } else if (this == ("int") || this == ("java.lang.Integer")) { + } else if (typeName == ("int") || typeName == ("java.lang.Integer")) { return BsonInt32 - } else if (this == ("long") || this == ("java.lang.Long")) { + } else if (typeName == ("long") || typeName == ("java.lang.Long")) { return BsonInt64 - } else if (this == ("float") || this == ("java.lang.Float")) { + } else if (typeName == ("float") || typeName == ("java.lang.Float")) { return BsonDouble - } else if (this == ("double") || this == ("java.lang.Double")) { + } else if (typeName == ("double") || typeName == ("java.lang.Double")) { return BsonDouble - } else if (this == ("java.lang.CharSequence") || - this == ("java.lang.String") || - this == "String" + } else if (typeName == ("java.lang.CharSequence") || + typeName == ("java.lang.String") || + typeName == "String" ) { return BsonAnyOf(BsonString, BsonNull) - } else if (this == ("java.util.Date") || - this == ("java.time.Instant") || - this == ("java.time.LocalDate") || - this == ("java.time.LocalDateTime") + } else if (typeName == ("java.util.Date") || + typeName == ("java.time.Instant") || + typeName == ("java.time.LocalDate") || + typeName == ("java.time.LocalDateTime") ) { return BsonAnyOf(BsonDate, BsonNull) - } else if (this == ("java.math.BigInteger")) { + } else if (typeName == ("java.math.BigInteger")) { return BsonAnyOf(BsonInt64, BsonNull) - } else if (this == ("java.math.BigDecimal")) { + } else if (typeName == ("java.math.BigDecimal")) { return BsonAnyOf(BsonDecimal128, BsonNull) - } else if (this.endsWith("[]")) { - val baseType = this.substring(0, this.length - 2) - return BsonArray(baseType.toBsonType()) - } else if (this.contains("List") || this.contains("Set")) { - if (!this.contains("<")) { // not passing the generic types, so assume an array of BsonAny + } else if (typeName.endsWith("[]")) { + val baseType = typeName.substring(0, typeName.length - 2) + return BsonArray(toBsonType(baseType)) + } else if (typeName.contains("List") || typeName.contains("Set")) { + if (!typeName.contains("<")) { // not passing the generic types, so assume an array of BsonAny return BsonArray(BsonAny) } - val baseType = this.substringAfter("<").substringBeforeLast(">") - return BsonArray(baseType.toBsonType()) - } else if (this == ("java.util.UUID")) { + val baseType = typeName.substringAfter("<").substringBeforeLast(">") + return BsonArray(toBsonType(baseType)) + } else if (typeName == ("java.util.UUID")) { return BsonUUID } diff --git a/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/PsiMdbTreeUtilTest.kt b/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/PsiMdbTreeUtilTest.kt index 666cc2a65..43164e8bc 100644 --- a/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/PsiMdbTreeUtilTest.kt +++ b/packages/mongodb-dialects/java-driver/src/test/kotlin/com/mongodb/jbplugin/dialects/javadriver/glossary/PsiMdbTreeUtilTest.kt @@ -30,7 +30,7 @@ class PsiMdbTreeUtilTest { ) { ApplicationManager.getApplication().invokeAndWait { val psiType = typeProvider(project) - assertEquals(expected, psiType.toBsonType()) + assertEquals(expected, toBsonType(psiType)) } } @@ -40,7 +40,7 @@ class PsiMdbTreeUtilTest { javaQualifiedName: String, expected: BsonType, ) { - assertEquals(expected, javaQualifiedName.toBsonType()) + assertEquals(expected, toBsonType(javaQualifiedName)) } @ParsingTest( diff --git a/packages/mongodb-dialects/mongosh/build.gradle.kts b/packages/mongodb-dialects/mongosh/build.gradle.kts index b1c975e89..461591dcf 100644 --- a/packages/mongodb-dialects/mongosh/build.gradle.kts +++ b/packages/mongodb-dialects/mongosh/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.mongodb.intellij.isolated-module") + id("com.mongodb.intellij.plugin-component") } dependencies { 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 c7d41f773..a8a697e6f 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 @@ -1,5 +1,7 @@ package com.mongodb.jbplugin.dialects.mongosh.backend +import com.intellij.openapi.application.ApplicationManager +import com.intellij.psi.PsiEnumConstant import com.mongodb.jbplugin.mql.* import com.mongodb.jbplugin.mql.QueryContext.AsIs import org.bson.types.ObjectId @@ -267,6 +269,9 @@ private fun serializePrimitive(value: Any?): String = when (value) { "\"${it.key}\": ${serializePrimitive(it.value)}" } is AsIs -> value.value + is PsiEnumConstant -> ApplicationManager.getApplication().runReadAction { + '"' + Encode.forJavaScript(value.name) + '"' + } null -> "null" else -> "{}" } diff --git a/packages/mongodb-dialects/spring-@query/src/main/kotlin/com/mongodb/jbplugin/dialects/springquery/SpringAtQueryDialectParser.kt b/packages/mongodb-dialects/spring-@query/src/main/kotlin/com/mongodb/jbplugin/dialects/springquery/SpringAtQueryDialectParser.kt index 0a08962b2..64af364fb 100644 --- a/packages/mongodb-dialects/spring-@query/src/main/kotlin/com/mongodb/jbplugin/dialects/springquery/SpringAtQueryDialectParser.kt +++ b/packages/mongodb-dialects/spring-@query/src/main/kotlin/com/mongodb/jbplugin/dialects/springquery/SpringAtQueryDialectParser.kt @@ -160,7 +160,7 @@ object SpringAtQueryDialectParser : DialectParser { HasValueReference.Unknown as HasValueReference.ValueReference ) - val argType = methodArg.type.toBsonType() + val argType = toBsonType(methodArg.type) ?: BsonAny return HasValueReference( HasValueReference.Runtime(methodArg, argType) ) 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 a0ea90780..49f1c7d93 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 @@ -543,7 +543,7 @@ object SpringCriteriaDialectParser : DialectParser { } else { // case 2 HasValueReference.Runtime( secondArg, - secondArg.type?.toBsonType() ?: BsonArray(BsonAny) + toBsonType(secondArg.type) ?: BsonArray(BsonAny) ) } } else if (argumentList.expressionCount > (start + 1)) { @@ -563,13 +563,13 @@ object SpringCriteriaDialectParser : DialectParser { else -> HasValueReference( HasValueReference.Runtime( valuePsi, - valuePsi.type?.toBsonType() ?: BsonAny + toBsonType(valuePsi.type) ?: BsonAny ) ) } else -> HasValueReference( - HasValueReference.Constant(valuePsi, value, value.javaClass.toBsonType(value)) + HasValueReference.Constant(valuePsi, value, toBsonType(value.javaClass, value)) ) } return valueReference as HasValueReference diff --git a/packages/mongodb-dialects/src/main/kotlin/com/mongodb/jbplugin/dialects/Dialect.kt b/packages/mongodb-dialects/src/commonMain/kotlin/com/mongodb/jbplugin/dialects/Dialect.kt similarity index 100% rename from packages/mongodb-dialects/src/main/kotlin/com/mongodb/jbplugin/dialects/Dialect.kt rename to packages/mongodb-dialects/src/commonMain/kotlin/com/mongodb/jbplugin/dialects/Dialect.kt diff --git a/packages/mongodb-mql-engines/build.gradle.kts b/packages/mongodb-mql-engines/build.gradle.kts index be04c23be..263408402 100644 --- a/packages/mongodb-mql-engines/build.gradle.kts +++ b/packages/mongodb-mql-engines/build.gradle.kts @@ -2,7 +2,34 @@ plugins { id("com.mongodb.intellij.isolated-module") } -dependencies { - implementation(project(":packages:mongodb-mql-model")) - implementation(project(":packages:mongodb-access-adapter")) +kotlin { + sourceSets { + commonMain { + dependencies { + implementation(project(":packages:mongodb-mql-model")) + implementation(project(":packages:mongodb-access-adapter")) + } + } + } + + js { + compilations["main"].packageJson { + customField( + "repository", + mapOf( + "type" to "git", + "url" to "git+https://github.com/mongodb/intellij.git" + ) + ) + customField("homepage", "https://github.com/mongodb/intellij") + customField("license", "Apache-2.0") + } + } +} + +task("publishNpm") { + dependsOn("jsNodeProductionLibraryDistribution") + workingDir(layout.buildDirectory.dir("dist/js/productionLibrary")) + + commandLine("npm", "publish") } diff --git a/packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/autocomplete/Autocompletion.kt b/packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/autocomplete/Autocompletion.kt similarity index 100% rename from packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/autocomplete/Autocompletion.kt rename to packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/autocomplete/Autocompletion.kt diff --git a/packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidation.kt b/packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidation.kt similarity index 100% rename from packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidation.kt rename to packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidation.kt diff --git a/packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzer.kt b/packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzer.kt similarity index 100% rename from packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzer.kt rename to packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzer.kt diff --git a/packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinter.kt b/packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinter.kt similarity index 100% rename from packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinter.kt rename to packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinter.kt diff --git a/packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinter.kt b/packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinter.kt similarity index 100% rename from packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinter.kt rename to packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinter.kt diff --git a/packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinter.kt b/packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinter.kt similarity index 100% rename from packages/mongodb-mql-engines/src/main/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinter.kt rename to packages/mongodb-mql-engines/src/commonMain/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinter.kt diff --git a/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/StubReadModelProvider.kt b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/StubReadModelProvider.kt new file mode 100644 index 000000000..895ec0c9a --- /dev/null +++ b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/StubReadModelProvider.kt @@ -0,0 +1,16 @@ +package com.mongodb.jbplugin + +import com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider +import com.mongodb.jbplugin.accessadapter.Slice + +class StubReadModelProvider( + private val responses: Map, () -> Any?> = emptyMap(), + private val default: () -> Any? = { null }, +) : MongoDbReadModelProvider { + override fun slice( + dataSource: D, + slice: Slice + ): T { + return (responses[slice] ?: default).invoke() as T + } +} diff --git a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/autocomplete/AutocompletionTest.kt b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/autocomplete/AutocompletionTest.kt similarity index 60% rename from packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/autocomplete/AutocompletionTest.kt rename to packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/autocomplete/AutocompletionTest.kt index 199772811..dd78c5dd7 100644 --- a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/autocomplete/AutocompletionTest.kt +++ b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/autocomplete/AutocompletionTest.kt @@ -1,24 +1,25 @@ package com.mongodb.jbplugin.autocomplete -import com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider +import com.mongodb.jbplugin.StubReadModelProvider import com.mongodb.jbplugin.accessadapter.slice.GetCollectionSchema import com.mongodb.jbplugin.accessadapter.slice.ListCollections import com.mongodb.jbplugin.accessadapter.slice.ListDatabases import com.mongodb.jbplugin.autocomplete.Autocompletion.autocompleteCollections import com.mongodb.jbplugin.autocomplete.Autocompletion.autocompleteDatabases import com.mongodb.jbplugin.autocomplete.Autocompletion.autocompleteFields -import com.mongodb.jbplugin.mql.* -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.mockito.Mockito.`when` -import org.mockito.kotlin.mock +import com.mongodb.jbplugin.mql.BsonObject +import com.mongodb.jbplugin.mql.BsonObjectId +import com.mongodb.jbplugin.mql.BsonString +import com.mongodb.jbplugin.mql.CollectionSchema +import com.mongodb.jbplugin.mql.Namespace +import kotlin.test.Test +import kotlin.test.assertEquals class AutocompletionTest { @Test - fun `returns the list of all available databases`() { - val readModelProvider = mock>() - `when`(readModelProvider.slice(null, ListDatabases.Slice)).thenReturn( - ListDatabases(listOf(ListDatabases.Database("myDb"))), + fun returns_the_list_of_all_available_databases() { + val readModelProvider = StubReadModelProvider( + mapOf(ListDatabases.Slice to { ListDatabases(listOf(ListDatabases.Database("myDb"))) }) ) val result = @@ -40,13 +41,11 @@ class AutocompletionTest { } @Test - fun `notifies when the provided database does not exist`() { - val readModelProvider = mock>() + fun notifies_when_the_provided_database_does_not_exist() { val slice = ListCollections.Slice("myDb") - - // the server returns an error if the database provided to - // runCommand does not exist - `when`(readModelProvider.slice(null, slice)).thenThrow(RuntimeException("")) + val readModelProvider = StubReadModelProvider( + mapOf(slice to { throw RuntimeException("") }) + ) val result = autocompleteCollections(null, readModelProvider, "myDb") @@ -57,14 +56,16 @@ class AutocompletionTest { } @Test - fun `returns the list of collections for the given database`() { - val readModelProvider = mock>() + fun returns_the_list_of_collections_for_the_given_database() { val slice = ListCollections.Slice("myDb") - - `when`(readModelProvider.slice(null, slice)).thenReturn( - ListCollections( - listOf(ListCollections.Collection("myColl", "collection")), - ), + val readModelProvider = StubReadModelProvider( + mapOf( + slice to { + ListCollections( + listOf(ListCollections.Collection("myColl", "collection")), + ) + } + ) ) val result = @@ -87,23 +88,25 @@ class AutocompletionTest { } @Test - fun `returns the list of fields for sample documents`() { - val readModelProvider = mock>() + fun returns_the_list_of_fields_for_sample_documents() { val namespace = Namespace("myDb", "myColl") val slice = GetCollectionSchema.Slice(namespace, 50) - - `when`(readModelProvider.slice(null, slice)).thenReturn( - GetCollectionSchema( - CollectionSchema( - namespace, - BsonObject( - mapOf( - "_id" to BsonObjectId, - "text" to BsonString, + val readModelProvider = StubReadModelProvider( + mapOf( + slice to { + GetCollectionSchema( + CollectionSchema( + namespace, + BsonObject( + mapOf( + "_id" to BsonObjectId, + "text" to BsonString, + ), + ), ), - ), - ), - ), + ) + } + ) ) val result = diff --git a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidationTest.kt b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidationTest.kt similarity index 79% rename from packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidationTest.kt rename to packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidationTest.kt index 69048fe57..f13756a90 100644 --- a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidationTest.kt +++ b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/indexing/CollectionIndexConsolidationTest.kt @@ -10,33 +10,33 @@ import com.mongodb.jbplugin.utils.ModelDsl.indexOf import com.mongodb.jbplugin.utils.ModelDsl.predicate import com.mongodb.jbplugin.utils.ModelDsl.query import com.mongodb.jbplugin.utils.ModelDsl.schema -import org.junit.jupiter.api.Assertions.assertFalse -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue class CollectionIndexConsolidationTest { @Test - fun `isPrefixOf should return true if they are equal`() { + fun isPrefixOf_should_return_true_if_they_are_equal() { assertTrue(indexOf("f1" to 1).isPrefixOf(indexOf("f1" to 1))) } @Test - fun `isPrefixOf should return true if the left side is a prefix to the right side`() { + fun isPrefixOf_should_return_true_if_the_left_side_is_a_prefix_to_the_right_side() { assertTrue(indexOf("f1" to 1).isPrefixOf(indexOf("f1" to 1, "f2" to 1))) } @Test - fun `isPrefixOf should return false if the left side is bigger than the right side`() { + fun isPrefixOf_should_return_false_if_the_left_side_is_bigger_than_the_right_side() { assertFalse(indexOf("f1" to 1, "f2" to 1).isPrefixOf(indexOf("f1" to 1))) } @Test - fun `isPrefixOf should return false if there is a shared prefix but additional fields`() { + fun isPrefixOf_should_return_false_if_there_is_a_shared_prefix_but_additional_fields() { assertFalse(indexOf("f1" to 1, "f3" to 1).isPrefixOf(indexOf("f1" to 1, "f2" to 1))) } @Test - fun `should return itself if no other index candidates`() { + fun should_return_itself_if_no_other_index_candidates() { val index = CollectionIndexConsolidation.apply( baseIndex = indexOf("f1" to 1), indexes = emptyList(), @@ -47,7 +47,7 @@ class CollectionIndexConsolidationTest { } @Test - fun `should return the index with highest cardinality if there are multiple matches`() { + fun should_return_the_index_with_highest_cardinality_if_there_are_multiple_matches() { val index = CollectionIndexConsolidation.apply( baseIndex = indexOf("f1" to 1), indexes = listOf(indexOf("f1" to 1, "f2" to 1)), @@ -58,7 +58,7 @@ class CollectionIndexConsolidationTest { } @Test - fun `should return the index with highest cardinality if there are multiple matches from a bigger index`() { + fun should_return_the_index_with_highest_cardinality_if_there_are_multiple_matches_from_a_bigger_index() { val index = CollectionIndexConsolidation.apply( baseIndex = indexOf("f1" to 1, "f2" to 1), indexes = listOf(indexOf("f1" to 1)), @@ -69,7 +69,7 @@ class CollectionIndexConsolidationTest { } @Test - fun `should inherit all covered queries`() { + fun should_inherit_all_covered_queries() { val index = CollectionIndexConsolidation.apply( baseIndex = indexOf("f1" to 1, "f2" to 1) { query { diff --git a/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzerTest.kt b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzerTest.kt new file mode 100644 index 000000000..bb391ef05 --- /dev/null +++ b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzerTest.kt @@ -0,0 +1,653 @@ +package com.mongodb.jbplugin.indexing + +import com.mongodb.jbplugin.accessadapter.toNs +import com.mongodb.jbplugin.mql.BsonBoolean +import com.mongodb.jbplugin.mql.BsonInt32 +import com.mongodb.jbplugin.mql.BsonObject +import com.mongodb.jbplugin.mql.BsonString +import com.mongodb.jbplugin.mql.CollectionSchema +import com.mongodb.jbplugin.mql.DataDistribution +import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.SiblingQueriesFinder +import com.mongodb.jbplugin.mql.components.Name +import com.mongodb.jbplugin.utils.ModelAssertions.assertIndexCollectionIs +import com.mongodb.jbplugin.utils.ModelAssertions.assertMongoDbIndexIs +import com.mongodb.jbplugin.utils.ModelDsl.aggregate +import com.mongodb.jbplugin.utils.ModelDsl.ascending +import com.mongodb.jbplugin.utils.ModelDsl.constant +import com.mongodb.jbplugin.utils.ModelDsl.filterBy +import com.mongodb.jbplugin.utils.ModelDsl.findMany +import com.mongodb.jbplugin.utils.ModelDsl.include +import com.mongodb.jbplugin.utils.ModelDsl.match +import com.mongodb.jbplugin.utils.ModelDsl.predicate +import com.mongodb.jbplugin.utils.ModelDsl.project +import com.mongodb.jbplugin.utils.ModelDsl.schema +import com.mongodb.jbplugin.utils.ModelDsl.sortBy +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals + +class IndexAnalyzerTest { + class EmptySiblingQueriesFinder : SiblingQueriesFinder { + override fun allSiblingsOf(query: Node): Array> { + return emptyArray() + } + } + + class PredefinedSiblingQueriesFinder(private val other: Array>) : SiblingQueriesFinder { + override fun allSiblingsOf(query: Node): Array> { + return other + } + } + + @Test + fun queries_without_a_collection_reference_component_are_not_supported() = runTest { + val query = Node(Unit, emptyList()) + val result = IndexAnalyzer.analyze(query, EmptySiblingQueriesFinder(), emptyOptions()) + + assertEquals(IndexAnalyzer.SuggestedIndex.NoIndex.cast(), result) + } + + @Test + fun returns_the_suggested_list_of_fields_for_a_mongodb_query() = runTest { + val query = findMany("myDb.myColl".toNs()) { + filterBy { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs(arrayOf("myField" to 1), result) + } + + @Test + fun places_low_cardinality_types_earlier_into_the_index_for_prefix_compression() = runTest { + val query = findMany("myDb.myColl".toNs()) { + filterBy { + predicate(Name.EQ) { + schema("highCardinality") + constant(52) + } + predicate(Name.EQ) { + schema("lowCardinality") + constant(true) + } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs(arrayOf("lowCardinality" to 1, "highCardinality" to 1), result) + } + + @Test + fun puts_equality_fields_before_sorting_fields_and_them_before_range_fields() = runTest { + val query = findMany("myDb.myColl".toNs()) { + filterBy { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + predicate(Name.GT) { + schema("myRangeField") + constant(true) + } + } + sortBy { + ascending { schema("mySortField") } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs( + arrayOf( + "myField" to 1, + "mySortField" to 1, + "myRangeField" to 1 + ), + result + ) + } + + @Test + fun removes_repeated_field_references() = runTest { + val query = findMany("myDb.myColl".toNs()) { + filterBy { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + predicate(Name.EQ) { + schema("mySecondField") + constant(true) + } + predicate(Name.EQ) { + schema("myField") + constant(55) + } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs( + arrayOf( + "mySecondField" to 1, + "myField" to 1, + ), + result + ) + } + + @Test + fun promotes_repeated_field_references_into_the_most_important_stage() = runTest { + val query = findMany("myDb.myColl".toNs()) { + filterBy { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + predicate(Name.GT) { + schema("mySecondField") + constant(12) + } + } + + sortBy { + ascending { schema("mySecondField") } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs( + arrayOf( + "myField" to 1, + "mySecondField" to 1, + ), + result + ) + } + + @Test + fun considers_aggregation_pipelines_match_stages() = runTest { + val query = aggregate("myDb.myColl".toNs()) { + match { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + predicate(Name.GT) { + schema("mySecondField") + constant(12) + } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs( + arrayOf( + "myField" to 1, + "mySecondField" to 1, + ), + result + ) + } + + @Test + fun does_not_consider_aggregation_pipelines_match_stages_in_the_second_position() = runTest { + val query = aggregate("myDb.myColl".toNs()) { + match { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + } + + match { + predicate(Name.EQ) { + schema("myIgnoredField") + constant(52) + } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs( + arrayOf( + "myField" to 1, + ), + result + ) + } + + @Test + fun does_not_consider_aggregation_pipelines_stages_that_are_not_match() = runTest { + val query = aggregate("myDb.myColl".toNs()) { + project { + include { + schema("projectedField") + } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs(emptyArray(), result) + } + + @Test + fun finds_the_index_with_more_fields_that_matches_the_current_query_from_sibling_queries() = runTest { + val predefinedSiblingQueries = PredefinedSiblingQueriesFinder( + arrayOf( + findMany("myDb.myColl".toNs()) { + filterBy { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + predicate(Name.GT) { + schema("mySecondField") + constant(12) + } + } + + sortBy { + ascending { schema("mySortField") } + } + } + ) + ) + + val query = findMany("myDb.myColl".toNs()) { + filterBy { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + } + } + + val result = IndexAnalyzer.analyze( + query, + predefinedSiblingQueries, + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs( + arrayOf( + "myField" to 1, + "mySortField" to 1, + "mySecondField" to 1, + ), + result + ) + } + + @Test + fun finds_the_index_with_more_fields_that_matches_the_current_query_from_sibling_queries_even_if_they_are_aggregates() = runTest { + val predefinedSiblingQueries = PredefinedSiblingQueriesFinder( + arrayOf( + aggregate("myDb.myColl".toNs()) { + match { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + predicate(Name.GT) { + schema("mySecondField") + constant(12) + } + } + } + ) + ) + + val query = findMany("myDb.myColl".toNs()) { + filterBy { + predicate(Name.EQ) { + schema("myField") + constant(52) + } + } + } + + val result = IndexAnalyzer.analyze( + query, + predefinedSiblingQueries, + emptyOptions() + ) + + assertIndexCollectionIs("myDb.myColl".toNs(), result) + assertMongoDbIndexIs( + arrayOf( + "myField" to 1, + "mySecondField" to 1 + ), + result + ) + } + + private val ns = "myDb.myColl".toNs() + + @Test + fun places_fields_with_low_selectivity_earlier_in_the_index_definition_for_prefix_compression() = runTest { + val schema = CollectionSchema( + namespace = ns, + schema = BsonObject( + mapOf( + "highSelectivityHighCardinality" to BsonInt32, + "highSelectivityLowCardinality" to BsonBoolean, + "lowSelectivityHighCardinality" to BsonString, + "lowSelectivityLowCardinality" to BsonBoolean, + ) + ), + dataDistribution = DataDistribution.generate( + listOf( + mapOf( + "highSelectivityHighCardinality" to 2, + "highSelectivityLowCardinality" to true, + "lowSelectivityHighCardinality" to "US", + "lowSelectivityLowCardinality" to true + ), + mapOf( + "highSelectivityHighCardinality" to 3, + "highSelectivityLowCardinality" to false, + "lowSelectivityHighCardinality" to "US", + "lowSelectivityLowCardinality" to true + ), + mapOf( + "highSelectivityHighCardinality" to 4, + "highSelectivityLowCardinality" to false, + "lowSelectivityHighCardinality" to "US", + "lowSelectivityLowCardinality" to true + ), + mapOf( + "highSelectivityHighCardinality" to 5, + "highSelectivityLowCardinality" to false, + "lowSelectivityHighCardinality" to "US", + "lowSelectivityLowCardinality" to true + ), + ) + ) + ) + + val query = findMany(ns, schema) { + filterBy { + predicate(Name.EQ) { + schema("highSelectivityHighCardinality") + constant(2) + } + predicate(Name.EQ) { + schema("highSelectivityLowCardinality") + constant(true) + } + predicate(Name.EQ) { + schema("lowSelectivityHighCardinality") + constant("US") + } + predicate(Name.EQ) { + schema("lowSelectivityLowCardinality") + constant(true) + } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertMongoDbIndexIs( + arrayOf( + "lowSelectivityLowCardinality" to 1, + "lowSelectivityHighCardinality" to 1, + "highSelectivityLowCardinality" to 1, + "highSelectivityHighCardinality" to 1, + ), + result + ) + } + + @Test + fun when_all_fields_have_same_selectivity_orders_by_cardinality() = runTest { + val schema = CollectionSchema( + namespace = ns, + schema = BsonObject( + mapOf( + "highCardinality" to BsonInt32, + "lowCardinality" to BsonBoolean, + ) + ), + dataDistribution = DataDistribution.generate( + listOf( + mapOf("highCardinality" to 1, "lowCardinality" to true), + mapOf("highCardinality" to 2, "lowCardinality" to false) + ) + ) + ) + + val query = findMany(ns, schema) { + filterBy { + predicate(Name.EQ) { + schema("highCardinality") + constant(1) + } + predicate(Name.EQ) { + schema("lowCardinality") + constant(true) + } + } + } + + val result = IndexAnalyzer.analyze(query, EmptySiblingQueriesFinder(), emptyOptions()) + assertMongoDbIndexIs(arrayOf("lowCardinality" to 1, "highCardinality" to 1), result) + } + + @Test + fun when_selectivity_is_not_known_for_a_value_it_places_fields_with_low_cardinality_first_in_the_index_definition() = runTest { + val schema = CollectionSchema( + namespace = ns, + schema = BsonObject( + mapOf( + "highSelectivityHighCardinality" to BsonInt32, + "highSelectivityLowCardinality" to BsonBoolean, + "unknownSelectivityHighCardinality" to BsonString, + "lowSelectivityLowCardinality" to BsonBoolean, + ) + ), + dataDistribution = DataDistribution.generate( + listOf( + mapOf( + "highSelectivityHighCardinality" to 2, + "highSelectivityLowCardinality" to true, + "lowSelectivityLowCardinality" to true + ), + mapOf( + "highSelectivityHighCardinality" to 3, + "highSelectivityLowCardinality" to false, + "lowSelectivityLowCardinality" to true + ), + mapOf( + "highSelectivityHighCardinality" to 4, + "highSelectivityLowCardinality" to false, + "lowSelectivityLowCardinality" to true + ), + mapOf( + "highSelectivityHighCardinality" to 5, + "highSelectivityLowCardinality" to false, + "lowSelectivityLowCardinality" to true + ), + ) + ) + ) + + val query = findMany(ns, schema) { + filterBy { + predicate(Name.EQ) { + schema("highSelectivityHighCardinality") + constant(2) + } + predicate(Name.EQ) { + schema("highSelectivityLowCardinality") + constant(true) + } + predicate(Name.EQ) { + schema("unknownSelectivityHighCardinality") + constant("US") + } + predicate(Name.EQ) { + schema("lowSelectivityLowCardinality") + constant(true) + } + } + } + + val result = IndexAnalyzer.analyze( + query, + EmptySiblingQueriesFinder(), + emptyOptions() + ) + + assertMongoDbIndexIs( + arrayOf( + "lowSelectivityLowCardinality" to 1, + "highSelectivityLowCardinality" to 1, + "highSelectivityHighCardinality" to 1, + "unknownSelectivityHighCardinality" to 1, + ), + result + ) + } + + @Test + fun maintains_ESR_order_even_when_lower_selectivity_fields_exist_in_different_roles() = runTest { + val schema = CollectionSchema( + namespace = ns, + schema = BsonObject( + mapOf( + "highSelectivityEquality" to BsonString, + "lowSelectivityEquality" to BsonBoolean, + "highSelectivitySort" to BsonString, + "lowSelectivitySort" to BsonBoolean, + "highSelectivityRange" to BsonString, + "lowSelectivityRange" to BsonBoolean + ) + ), + dataDistribution = DataDistribution.generate( + listOf( + mapOf( + "highSelectivityEquality" to "rare", + "lowSelectivityEquality" to true, + "highSelectivitySort" to "rare", + "lowSelectivitySort" to true, + "highSelectivityRange" to "rare", + "lowSelectivityRange" to true, + ), + mapOf( + "highSelectivityEquality" to "common", + "lowSelectivityEquality" to true, + "highSelectivitySort" to "common", + "lowSelectivitySort" to true, + "highSelectivityRange" to "common", + "lowSelectivityRange" to true + ) + ) + ) + ) + + val query = findMany(ns, schema) { + filterBy { + predicate(Name.EQ) { + schema("highSelectivityEquality") + constant("rare") + } + predicate(Name.EQ) { + schema("lowSelectivityEquality") + constant(true) + } + predicate(Name.GT) { + schema("highSelectivityRange") + constant("rare") + } + predicate(Name.GT) { + schema("lowSelectivityRange") + constant(true) + } + } + sortBy { + ascending { + schema("highSelectivitySort") + } + ascending { + schema("lowSelectivitySort") + } + } + } + + val result = IndexAnalyzer.analyze(query, EmptySiblingQueriesFinder(), emptyOptions()) + + assertMongoDbIndexIs( + arrayOf( + "lowSelectivityEquality" to 1, + "highSelectivityEquality" to 1, + "lowSelectivitySort" to 1, + "highSelectivitySort" to 1, + "lowSelectivityRange" to 1, + "highSelectivityRange" to 1 + ), + result + ) + } + + private fun emptyOptions() = CollectionIndexConsolidationOptions(10) +} diff --git a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt similarity index 86% rename from packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt rename to packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt index d4465fa82..74bf0b734 100644 --- a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt +++ b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/linting/FieldCheckingLinterTest.kt @@ -1,6 +1,6 @@ package com.mongodb.jbplugin.linting -import com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider +import com.mongodb.jbplugin.StubReadModelProvider import com.mongodb.jbplugin.accessadapter.slice.GetCollectionSchema import com.mongodb.jbplugin.mql.* import com.mongodb.jbplugin.mql.components.HasAggregation @@ -13,20 +13,14 @@ import com.mongodb.jbplugin.mql.components.HasValueReference import com.mongodb.jbplugin.mql.components.Name import com.mongodb.jbplugin.mql.components.Named import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertInstanceOf -import org.junit.jupiter.api.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.mockito.kotlin.any +import kotlin.test.Test +import kotlin.test.assertEquals class FieldCheckingLinterTest { @Test - fun `warns about a referenced field not in the specified collection`() = runTest { - val readModelProvider = mock>() + fun warns_about_a_referenced_field_not_in_the_specified_collection() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -37,8 +31,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( @@ -76,17 +70,14 @@ class FieldCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(FieldCheckWarning.FieldDoesNotExist::class.java, result.warnings[0]) val warning = result.warnings[0] as FieldCheckWarning.FieldDoesNotExist assertEquals("myBoolean", warning.field) } @Test - fun `warns about a referenced field in a nested query not in the specified collection`() = runTest { - val readModelProvider = mock>() + fun warns_about_a_referenced_field_in_a_nested_query_not_in_the_specified_collection() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -96,8 +87,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( @@ -140,17 +131,14 @@ class FieldCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(FieldCheckWarning.FieldDoesNotExist::class.java, result.warnings[0]) val warning = result.warnings[0] as FieldCheckWarning.FieldDoesNotExist assertEquals("myString", warning.field) } @Test - fun `warns about a referenced field not in the specified collection (alongside a value reference)`() = runTest { - val readModelProvider = mock>() + fun warns_about_a_referenced_field_not_in_the_specified_collection_alongside_a_value_reference() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -161,8 +149,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( @@ -203,17 +191,14 @@ class FieldCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(FieldCheckWarning.FieldDoesNotExist::class.java, result.warnings[0]) val warning = result.warnings[0] as FieldCheckWarning.FieldDoesNotExist assertEquals("myBoolean", warning.field) } @Test - fun `warns about a value not matching the type of underlying field`() = runTest { - val readModelProvider = mock>() + fun warns_about_a_value_not_matching_the_type_of_underlying_field() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -224,8 +209,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( @@ -258,17 +243,14 @@ class FieldCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(FieldCheckWarning.FieldValueTypeMismatch::class.java, result.warnings[0]) val warning = result.warnings[0] as FieldCheckWarning.FieldValueTypeMismatch assertEquals("myInt", warning.field) } @Test - fun `warns about the referenced fields in an Aggregation#match not in the specified collection`() = runTest { - val readModelProvider = mock>() + fun warns_about_the_referenced_fields_in_an_Aggregation_match_not_in_the_specified_collection() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -279,8 +261,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( @@ -334,17 +316,14 @@ class FieldCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(FieldCheckWarning.FieldDoesNotExist::class.java, result.warnings[0]) val warning = result.warnings[0] as FieldCheckWarning.FieldDoesNotExist assertEquals("myBoolean", warning.field) } @Test - fun `warns about a value not matching the type of underlying field in Aggregation#match`() = runTest { - val readModelProvider = mock>() + fun warns_about_a_value_not_matching_the_type_of_underlying_field_in_Aggregation_match() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -355,8 +334,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( @@ -406,17 +385,14 @@ class FieldCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(FieldCheckWarning.FieldValueTypeMismatch::class.java, result.warnings[0]) val warning = result.warnings[0] as FieldCheckWarning.FieldValueTypeMismatch assertEquals("myInt", warning.field) } @Test - fun `warns about the referenced fields in an Aggregation#project not in the specified collection`() = runTest { - val readModelProvider = mock>() + fun warns_about_the_referenced_fields_in_an_Aggregation_project_not_in_the_specified_collection() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -426,8 +402,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( @@ -478,17 +454,14 @@ class FieldCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(FieldCheckWarning.FieldDoesNotExist::class.java, result.warnings[0]) val warning = result.warnings[0] as FieldCheckWarning.FieldDoesNotExist assertEquals("myBoolean", warning.field) } @Test - fun `warns about the referenced fields in an Aggregation#sort not in the specified collection`() = runTest { - val readModelProvider = mock>() + fun warns_about_the_referenced_fields_in_an_Aggregation_sort_not_in_the_specified_collection() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -498,8 +471,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( @@ -550,17 +523,14 @@ class FieldCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(FieldCheckWarning.FieldDoesNotExist::class.java, result.warnings[0]) val warning = result.warnings[0] as FieldCheckWarning.FieldDoesNotExist assertEquals("myBoolean", warning.field) } @Test - fun `should not warn about the referenced fields in an Aggregation#addFields`() = runTest { - val readModelProvider = mock>() + fun should_not_warn_about_the_referenced_fields_in_an_Aggregation_addFields() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -570,8 +540,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( @@ -625,11 +595,9 @@ class FieldCheckingLinterTest { } @Test - fun `should not warn about the referenced fields in an Aggregation#unwind`() = runTest { - val readModelProvider = mock>() + fun should_not_warn_about_the_referenced_fields_in_an_Aggregation_unwind() = runTest { val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( + val readModelProvider = StubReadModelProvider(default = { GetCollectionSchema( CollectionSchema( collectionNamespace, @@ -639,8 +607,8 @@ class FieldCheckingLinterTest { ), ), ), - ), - ) + ) + }) val result = FieldCheckingLinter.lintQuery( diff --git a/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt new file mode 100644 index 000000000..0aceea2e8 --- /dev/null +++ b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt @@ -0,0 +1,63 @@ +package com.mongodb.jbplugin.linting + +import com.mongodb.jbplugin.StubReadModelProvider +import com.mongodb.jbplugin.accessadapter.slice.ExplainPlan +import com.mongodb.jbplugin.accessadapter.slice.ExplainQuery +import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.QueryContext +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class IndexCheckingLinterTest { + @Test + fun warns_query_plans_using_a_collscan() { + val readModelProvider = StubReadModelProvider(default = { ExplainQuery(ExplainPlan.CollectionScan) }) + val query = Node(Unit, emptyList()) + + val result = + IndexCheckingLinter.lintQuery( + Unit, + readModelProvider, + query, + QueryContext.empty() + ) + + assertEquals(1, result.warnings.size) + assertTrue(result.warnings[0] is IndexCheckWarning.QueryNotCoveredByIndex) + } + + @Test + fun warns_query_plans_using_an_ineffective_index() { + val readModelProvider = StubReadModelProvider(default = { ExplainQuery(ExplainPlan.IneffectiveIndexUsage) }) + val query = Node(Unit, emptyList()) + + val result = + IndexCheckingLinter.lintQuery( + Unit, + readModelProvider, + query, + QueryContext.empty() + ) + + assertEquals(1, result.warnings.size) + assertTrue(result.warnings[0] is IndexCheckWarning.QueryNotUsingEffectiveIndex) + } + + @Test + fun does_not_warn_on_index_scans() { + val readModelProvider = StubReadModelProvider(default = { ExplainQuery(ExplainPlan.IndexScan) }) + + val query = Node(Unit, emptyList()) + + val result = + IndexCheckingLinter.lintQuery( + Unit, + readModelProvider, + query, + QueryContext.empty() + ) + + assertEquals(0, result.warnings.size) + } +} diff --git a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinterTest.kt b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinterTest.kt similarity index 60% rename from packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinterTest.kt rename to packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinterTest.kt index e80fdbf90..58274721c 100644 --- a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinterTest.kt +++ b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/linting/NamespaceCheckingLinterTest.kt @@ -1,29 +1,22 @@ package com.mongodb.jbplugin.linting -import com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider +import com.mongodb.jbplugin.StubReadModelProvider import com.mongodb.jbplugin.accessadapter.slice.ListCollections import com.mongodb.jbplugin.accessadapter.slice.ListDatabases import com.mongodb.jbplugin.mql.Namespace import com.mongodb.jbplugin.mql.Node import com.mongodb.jbplugin.mql.components.HasCollectionReference import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertInstanceOf -import org.junit.jupiter.api.Test -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.mockito.kotlin.any +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class NamespaceCheckingLinterTest { @Test - fun `warns about a referenced database not existing`() = runTest { - val readModelProvider = mock>() + fun warns_about_a_referenced_database_not_existing() = runTest { + val readModelProvider = StubReadModelProvider(default = { ListDatabases(emptyList()) }) val collectionNamespace = Namespace("database", "collection") - `when`(readModelProvider.slice(any(), any())).thenReturn( - ListDatabases(emptyList()) - ) - val result = NamespaceCheckingLinter.lintQuery( Unit, @@ -39,23 +32,20 @@ class NamespaceCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(NamespaceCheckWarning.DatabaseDoesNotExist::class.java, result.warnings[0]) val warning = result.warnings[0] as NamespaceCheckWarning.DatabaseDoesNotExist assertEquals("database", warning.database) } @Test - fun `warns about a referenced collection not existing`() = runTest { - val readModelProvider = mock>() - val collectionNamespace = Namespace("database", "collection") - - `when`(readModelProvider.slice(any(), any())).thenReturn( - ListDatabases(listOf(ListDatabases.Database("database"))) + fun warns_about_a_referenced_collection_not_existing() = runTest { + val readModelProvider = StubReadModelProvider( + responses = mapOf( + ListDatabases.Slice to { ListDatabases(listOf(ListDatabases.Database("database"))) } + ), + default = { ListCollections(emptyList()) } ) - `when`(readModelProvider.slice(any(), any())).thenReturn( - ListCollections(emptyList()) - ) + val collectionNamespace = Namespace("database", "collection") val result = NamespaceCheckingLinter.lintQuery( @@ -72,18 +62,16 @@ class NamespaceCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf( - NamespaceCheckWarning.CollectionDoesNotExist::class.java, - result.warnings[0] - ) val warning = result.warnings[0] as NamespaceCheckWarning.CollectionDoesNotExist assertEquals("database", warning.database) assertEquals("collection", warning.collection) } @Test - fun `warns about an unknown namespace if only collection is provided`() = runTest { - val readModelProvider = mock>() + fun warns_about_an_unknown_namespace_if_only_collection_is_provided() = runTest { + val readModelProvider = StubReadModelProvider( + default = { ListDatabases(emptyList()) } + ) val result = NamespaceCheckingLinter.lintQuery( @@ -100,12 +88,14 @@ class NamespaceCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(NamespaceCheckWarning.NoNamespaceInferred::class.java, result.warnings[0]) + assertTrue(result.warnings[0] is NamespaceCheckWarning.NoNamespaceInferred) } @Test - fun `warns about an unknown namespace if unknown`() = runTest { - val readModelProvider = mock>() + fun warns_about_an_unknown_namespace_if_unknown() = runTest { + val readModelProvider = StubReadModelProvider( + default = { ListDatabases(emptyList()) } + ) val result = NamespaceCheckingLinter.lintQuery( @@ -120,12 +110,14 @@ class NamespaceCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(NamespaceCheckWarning.NoNamespaceInferred::class.java, result.warnings[0]) + assertTrue(result.warnings[0] is NamespaceCheckWarning.NoNamespaceInferred) } @Test - fun `warns about an unknown namespace if not provided`() = runTest { - val readModelProvider = mock>() + fun warns_about_an_unknown_namespace_if_not_provided() = runTest { + val readModelProvider = StubReadModelProvider( + default = { ListDatabases(emptyList()) } + ) val result = NamespaceCheckingLinter.lintQuery( @@ -138,6 +130,6 @@ class NamespaceCheckingLinterTest { ) assertEquals(1, result.warnings.size) - assertInstanceOf(NamespaceCheckWarning.NoNamespaceInferred::class.java, result.warnings[0]) + assertTrue(result.warnings[0] is NamespaceCheckWarning.NoNamespaceInferred) } } diff --git a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/utils/ModelAssertions.kt b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/utils/ModelAssertions.kt similarity index 88% rename from packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/utils/ModelAssertions.kt rename to packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/utils/ModelAssertions.kt index 014fe0eef..353242925 100644 --- a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/utils/ModelAssertions.kt +++ b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/utils/ModelAssertions.kt @@ -3,14 +3,14 @@ package com.mongodb.jbplugin.utils import com.mongodb.jbplugin.indexing.IndexAnalyzer import com.mongodb.jbplugin.mql.Namespace import com.mongodb.jbplugin.mql.components.HasCollectionReference -import org.junit.jupiter.api.Assertions.assertEquals +import kotlin.test.assertEquals object ModelAssertions { fun assertCollectionIs(expected: Namespace, actual: HasCollectionReference) { val ref = actual.reference if (ref !is HasCollectionReference.Known) { throw AssertionError( - "Collection reference is not equals to $expected because it's not Known, but ${ref.javaClass.name}" + "Collection reference is not equals to $expected because it's not Known, but $ref" ) } @@ -20,7 +20,7 @@ object ModelAssertions { fun assertIndexCollectionIs(expected: Namespace, actual: IndexAnalyzer.SuggestedIndex) { if (actual !is IndexAnalyzer.SuggestedIndex.MongoDbIndex) { throw AssertionError( - "Collection is not equals to $expected because index is not a MongoDbIndex, but ${actual.javaClass.name}" + "Collection is not equals to $expected because index is not a MongoDbIndex, but $actual" ) } @@ -30,7 +30,7 @@ object ModelAssertions { fun assertNumberOfCoveredQueriesForIndex(expected: Int, actual: IndexAnalyzer.SuggestedIndex) { if (actual !is IndexAnalyzer.SuggestedIndex.MongoDbIndex) { throw AssertionError( - "Could find number of covered queries because index is not a MongoDbIndex, but ${actual.javaClass.name}" + "Could find number of covered queries because index is not a MongoDbIndex, but $actual" ) } @@ -45,7 +45,7 @@ object ModelAssertions { ) { if (actual !is IndexAnalyzer.SuggestedIndex.MongoDbIndex) { throw AssertionError( - "Index is not equals to $expected because it's not a MongoDbIndex, but ${actual.javaClass.name}" + "Index is not equals to $expected because it's not a MongoDbIndex, but $actual" ) } diff --git a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/utils/ModelDsl.kt b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/utils/ModelDsl.kt similarity index 94% rename from packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/utils/ModelDsl.kt rename to packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/utils/ModelDsl.kt index 7d06e5848..03ec2fa8c 100644 --- a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/utils/ModelDsl.kt +++ b/packages/mongodb-mql-engines/src/commonTest/kotlin/com/mongodb/jbplugin/utils/ModelDsl.kt @@ -18,10 +18,9 @@ import com.mongodb.jbplugin.mql.components.HasValueReference import com.mongodb.jbplugin.mql.components.Name import com.mongodb.jbplugin.mql.components.Named import com.mongodb.jbplugin.mql.toBsonType -import kotlin.jvm.javaClass -class ComponentHolder(internal val components: MutableList) -class NodeHolder(internal val nodes: MutableList>) +class ComponentHolder(val components: MutableList) +class NodeHolder(val nodes: MutableList>) object ModelDsl { fun findMany( @@ -115,10 +114,10 @@ object ModelDsl { ) } - fun ComponentHolder.constant(value: T) { + inline fun ComponentHolder.constant(value: T) { components.add( HasValueReference( - HasValueReference.Constant(Unit, value, value.javaClass.toBsonType(value)) + HasValueReference.Constant(Unit, value, value.toBsonType()) ) ) } diff --git a/packages/mongodb-mql-engines/src/jsMain/kotlin/DataDistributionAdapter.kt b/packages/mongodb-mql-engines/src/jsMain/kotlin/DataDistributionAdapter.kt new file mode 100644 index 000000000..bf405865a --- /dev/null +++ b/packages/mongodb-mql-engines/src/jsMain/kotlin/DataDistributionAdapter.kt @@ -0,0 +1,12 @@ +import com.mongodb.jbplugin.mql.DataDistribution +import kotlin.js.Json + +private fun dynamicToMap(obj: dynamic): Map { + val keys = js("Object.keys(obj)") as Array + return keys.associateWith { key -> obj[key] } +} + +fun calculateDataDistribution(sample: Array): DataDistribution { + val map = sample.map { dynamicToMap(it) } + return DataDistribution.generate(map) +} diff --git a/packages/mongodb-mql-engines/src/jsMain/kotlin/EJsonParser.kt b/packages/mongodb-mql-engines/src/jsMain/kotlin/EJsonParser.kt new file mode 100644 index 000000000..3b0084654 --- /dev/null +++ b/packages/mongodb-mql-engines/src/jsMain/kotlin/EJsonParser.kt @@ -0,0 +1,51 @@ +import com.mongodb.jbplugin.mql.BsonAny +import com.mongodb.jbplugin.mql.BsonArray +import com.mongodb.jbplugin.mql.BsonBoolean +import com.mongodb.jbplugin.mql.BsonDate +import com.mongodb.jbplugin.mql.BsonDecimal128 +import com.mongodb.jbplugin.mql.BsonDouble +import com.mongodb.jbplugin.mql.BsonInt32 +import com.mongodb.jbplugin.mql.BsonInt64 +import com.mongodb.jbplugin.mql.BsonObjectId +import com.mongodb.jbplugin.mql.BsonString +import com.mongodb.jbplugin.mql.components.HasValueReference +import kotlin.js.Date + +private fun jsTypeOf(v: dynamic): String = js("typeof v") as String + +fun isEjsonKey(v: String): Boolean = setOf( + "${'$'}numberLong", + "${'$'}numberDecimal", + "${'$'}numberDouble", + "${'$'}numberInt", + "${'$'}oid", + "${'$'}date" +).contains(v) + +fun parseEjson(value: dynamic): HasValueReference = HasValueReference( + when { + jsTypeOf(value) == "string" -> HasValueReference.Runtime(value, BsonString) + jsTypeOf(value) == "boolean" -> HasValueReference.Runtime(value, BsonBoolean) + jsTypeOf(value) == "number" -> HasValueReference.Runtime(value, BsonDouble) + jsTypeOf(value) == "bigint" -> HasValueReference.Runtime(value, BsonInt64) + value is Date -> HasValueReference.Runtime(value, BsonDate) + js("Array.isArray(value)") as Boolean -> HasValueReference.Runtime(value, BsonArray(BsonAny)) + value != null && jsTypeOf(value) == "object" -> run { + val keys = js("Object.keys(value)") as Array + if (keys.size == 1 && keys[0].startsWith("$")) { + when (keys[0]) { + "\$numberLong" -> HasValueReference.Runtime(value, BsonInt64) + "\$numberDecimal" -> HasValueReference.Runtime(value, BsonDecimal128) + "\$numberDouble" -> HasValueReference.Runtime(value, BsonDouble) + "\$numberInt" -> HasValueReference.Runtime(value, BsonInt32) + "\$oid" -> HasValueReference.Runtime(value, BsonObjectId) + "\$date" -> HasValueReference.Runtime(value, BsonDate) + else -> HasValueReference.Runtime(value, BsonAny) + } + } else { + HasValueReference.Runtime(value, BsonAny) + } + } + else -> HasValueReference.Runtime(value, BsonAny) + } +) diff --git a/packages/mongodb-mql-engines/src/jsMain/kotlin/Facade.kt b/packages/mongodb-mql-engines/src/jsMain/kotlin/Facade.kt new file mode 100644 index 000000000..8e847e941 --- /dev/null +++ b/packages/mongodb-mql-engines/src/jsMain/kotlin/Facade.kt @@ -0,0 +1,107 @@ +@file:OptIn(ExperimentalJsExport::class, DelicateCoroutinesApi::class) + +import com.mongodb.jbplugin.accessadapter.slice.recursivelyBuildSchema +import com.mongodb.jbplugin.indexing.CollectionIndexConsolidationOptions +import com.mongodb.jbplugin.indexing.IndexAnalyzer +import com.mongodb.jbplugin.mql.BsonObject +import com.mongodb.jbplugin.mql.CollectionSchema +import com.mongodb.jbplugin.mql.Namespace +import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.SiblingQueriesFinder +import com.mongodb.jbplugin.mql.components.HasCollectionReference +import com.mongodb.jbplugin.mql.flattenAnyOfReferences +import com.mongodb.jbplugin.mql.mergeSchemaTogether +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.promise +import kotlin.js.Json +import kotlin.js.Promise + +@JsExport +external interface InputNamespace { + val database: String + val collection: String +} + +@JsExport +interface Opaque + +@JsExport +interface Query + +@JsExport +fun analyzeNamespace(ns: InputNamespace, sample: Array?): Opaque { + val namespace = Namespace(ns.database, ns.collection) + if (sample != null) { + val everySchema = sample.map(::recursivelyBuildSchema) + val consolidatedSchema = everySchema.reduceOrNull(::mergeSchemaTogether) ?: BsonObject( + emptyMap(), + ) + val schema = flattenAnyOfReferences(consolidatedSchema) as BsonObject + val distribution = calculateDataDistribution(sample) + return CollectionSchema( + namespace, + schema, + distribution + ).asDynamic() + } else { + return CollectionSchema( + namespace, + BsonObject(emptyMap()), + ).asDynamic() + } +} + +@JsExport +fun parseQuery(filter: Json, opaqueSchema: Opaque): Opaque { + val query = parseFilter(filter) + val schema = opaqueSchema.unsafeCast() + + return query.with(HasCollectionReference(HasCollectionReference.Known(filter, filter, schema.namespace, schema))) + .asDynamic() +} + +@JsExport +interface SuggestedIndex { + val index: Json + val coveredQueries: Array +} + +@JsExport +fun suggestIndex(queries: Array>): Promise = GlobalScope.promise { + val mainQuery: dynamic = queries[0] + val siblings: Array = queries.drop(1).toTypedArray() + + val suggestedIndex = IndexAnalyzer.analyze( + mainQuery, + object : SiblingQueriesFinder { + override fun allSiblingsOf(query: Node): Array> { + return siblings + } + }, + CollectionIndexConsolidationOptions(indexesSoftLimit = 10) + ) + + val suggestedIndexJson = js("{}") + when (suggestedIndex) { + is IndexAnalyzer.SuggestedIndex.MongoDbIndex -> { + suggestedIndex.fields.forEach { + suggestedIndexJson[it.fieldName] = 1 + } + } + else -> {} + } + + fun toNode(query: Opaque): Node { + val dyn: dynamic = query + return dyn + } + + val coveredQueries = queries.map(::toNode).map { it.source } + val result = js("{}") + + result["index"] = suggestedIndexJson + result["coveredQueries"] = coveredQueries.toTypedArray() + + result +} diff --git a/packages/mongodb-mql-engines/src/jsMain/kotlin/QueryParser.kt b/packages/mongodb-mql-engines/src/jsMain/kotlin/QueryParser.kt new file mode 100644 index 000000000..bb8b0295f --- /dev/null +++ b/packages/mongodb-mql-engines/src/jsMain/kotlin/QueryParser.kt @@ -0,0 +1,98 @@ +import com.mongodb.jbplugin.mql.Node +import com.mongodb.jbplugin.mql.components.HasFieldReference +import com.mongodb.jbplugin.mql.components.HasFilter +import com.mongodb.jbplugin.mql.components.Name +import com.mongodb.jbplugin.mql.components.Named + +fun parseFilter(query: dynamic): Node = when { + jsTypeOf(query) == "object" -> run { + val keys = js("Object.keys(query)") as Array + val isOperatorObject = keys.all { it.startsWith("$") } + if (isOperatorObject) { + val children = keys.map { op -> + val value = query[op] + when (op) { + "\$and", "\$or", "\$nor" -> { + val arr = value as Array + val parsedChildren = arr.map { parseFilter(it) } + + val flattened = if (op == "\$and") { + parsedChildren.flatMap { + val isAndNode = it.component()?.name?.canonical == "and" + val filter = it.component>() + if (isAndNode && filter != null) filter.children else listOf(it) + } + } else { + parsedChildren + } + + Node( + value, + listOf( + Named(Name.from(op.removePrefix("$"))), + HasFilter(flattened as List>) + ) + ) + } + "\$not" -> { + Node( + value, + listOf( + Named(Name.from("not")), + HasFilter(listOf(parseFilter(value))) + ) + ) + } + else -> Node( + value, + listOf( + Named(Name.from(op.removePrefix("$"))), + parseEjson(value) + ) + ) + } + } + Node(query, listOf(Named(Name.AND), HasFilter(children))) + } else { + // Template or nested-operator fields + val children = keys.map { k -> + val value = query[k] + val valueIsObject = jsTypeOf(value) == "object" + val valueKeys = if (valueIsObject) js("Object.keys(value)") as Array else emptyArray() + val nestedOperatorKeys = valueKeys.filter { it.startsWith("$") } + + if (nestedOperatorKeys.isNotEmpty() && nestedOperatorKeys.any { !isEjsonKey(it) }) { + val subconditions = nestedOperatorKeys.map { op -> + Node( + value, + listOf( + Named(Name.from(op.removePrefix("$"))), + HasFieldReference(HasFieldReference.FromSchema(query, k)), + parseEjson(value[op]) + ) + ) + } + Node( + query, + listOf( + Named(Name.AND), + HasFilter(subconditions) + ) + ) + } else { + Node( + value, + listOf( + Named(Name.EQ), + HasFieldReference(HasFieldReference.FromSchema(query, k)), + parseEjson(value) + ) + ) + } + } + Node(query, listOf(Named(Name.AND), HasFilter(children))) + } + } + else -> + Node(query, listOf(Named(Name.EQ), parseEjson(query))) +} diff --git a/packages/mongodb-mql-engines/src/jsMain/resources/README.md b/packages/mongodb-mql-engines/src/jsMain/resources/README.md new file mode 100644 index 000000000..302243884 --- /dev/null +++ b/packages/mongodb-mql-engines/src/jsMain/resources/README.md @@ -0,0 +1,42 @@ +# mongodb-mql-engines + +Exposes, on demand, engines used by the MongoDB IntelliJ Plugin for +index suggestions. + +## Example Usage: + +```js +const mql = require('mongodb-mql-engines'); + +const sampleDocuments = [ { a: 1, b: 2 }, { a: 5, b: true } ] + +const ns = mql.analyzeNamespace({ database: "mydb", collection: "mycoll" }, sampleDocuments) +const query = mql.parseQuery({ a: 1, b: 2 }, ns) +const index = await mql.suggestIndex([ query ]) + +console.log(index); +``` + +Expected output: + +```js +{ index: { a: 1, b: 1 }, coveredQueries: [ { a: 1, b: 2 } ] } +``` + +## API + +### analyzeNamespace(namespace: { database: String, collection: String }, sampleDocuments?: Array): Namespace + +Creates a new namespace reference with the data distribution calculated from the sample documents. If no sample +documents are provided, it won't use this information for suggesting indexes or running any other engine. + +The more documents are provided as a sample, the better the index suggestion will be. + +### mql.parseQuery(query: Json, namespace: Namespace): Query + +Parses a JSON query, defined as plain JSON or relaxed EJSON and attaches it to the provided namespace. + +### mql.suggestIndex(queries: Array): SuggestedIndex + +Analyses all the provided queries and returns the best index to cover all of them. In case only +a single query is analysed, provide an array with that single query. diff --git a/packages/mongodb-mql-engines/src/jsTest/kotlin/EJsonParserTest.kt b/packages/mongodb-mql-engines/src/jsTest/kotlin/EJsonParserTest.kt new file mode 100644 index 000000000..28e58f782 --- /dev/null +++ b/packages/mongodb-mql-engines/src/jsTest/kotlin/EJsonParserTest.kt @@ -0,0 +1,119 @@ +import com.mongodb.jbplugin.mql.* +import com.mongodb.jbplugin.mql.components.HasValueReference +import kotlin.js.Date +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class EJsonParserTest { + @Test fun parseStringReturnsBsonString() { + val input: dynamic = "hello" + val result = parseEjson(input) + assertTrue(result.reference is HasValueReference.Runtime<*>) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(input, ref.source) + assertEquals(BsonString, ref.type) + } + + @Test fun parseBooleanReturnsBsonBoolean() { + val inputTrue: dynamic = true + val inputFalse: dynamic = false + val r1 = parseEjson(inputTrue) + val r2 = parseEjson(inputFalse) + listOf(r1, r2).forEach { + assertTrue(it.reference is HasValueReference.Runtime<*>) + val ref = it.reference as HasValueReference.Runtime<*> + assertEquals(BsonBoolean, ref.type) + } + assertEquals(inputTrue, (r1.reference as HasValueReference.Runtime<*>).source) + assertEquals(inputFalse, (r2.reference as HasValueReference.Runtime<*>).source) + } + + @Test fun parseNumberReturnsBsonDouble() { + val input: dynamic = 3.14 + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonDouble, ref.type) + assertEquals(input, ref.source) + } + + @Test fun parseBigintReturnsBsonInt64() { + val input: dynamic = js("BigInt(9007199254740991)") + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonInt64, ref.type) + assertEquals(input, ref.source) + } + + @Test fun parseJsDateReturnsBsonDate() { + val input: dynamic = Date(1620000000000) + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonDate, ref.type) + assertEquals(input, ref.source) + } + + @Test fun parseArrayReturnsBsonArrayOfBsonAny() { + val input: dynamic = js("[1, 'two', true]") + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonArray(BsonAny), ref.type) + assertEquals(input, ref.source) + } + + @Test fun parseEjsonNumberLongReturnsBsonInt64() { + val input: dynamic = js("{ '${'$'}numberLong': '1234567890' }") + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonInt64, ref.type) + assertEquals(input, ref.source) + } + + @Test fun parseEjsonNumberDecimalReturnsBsonDecimal128() { + val input: dynamic = js("{ '${'$'}numberDecimal': '1234.5678' }") + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonDecimal128, ref.type) + assertEquals(input, ref.source) + } + + @Test fun parseEjsonNumberDoubleReturnsBsonDouble() { + val input: dynamic = js("{ '${'$'}numberDouble': '9.81' }") + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonDouble, ref.type) + assertEquals(input, ref.source) + } + + @Test fun parseEjsonNumberIntReturnsBsonInt32() { + val input: dynamic = js("{ '${'$'}numberInt': '42' }") + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonInt32, ref.type) + assertEquals(input, ref.source) + } + + @Test fun parseEjsonOidReturnsBsonObjectId() { + val input: dynamic = js("{ '${'$'}oid': '507f1f77bcf86cd799439011' }") + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonObjectId, ref.type) + assertEquals(input, ref.source) + } + + @Test fun parseNestedEjsonDateReturnsBsonDate() { + val input: dynamic = js("{ '${'$'}date': { '${'$'}numberLong': 1620000000000 } }") + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonDate, ref.type) + assertEquals(input, ref.source) + } + + @Test fun parsePlainObjectReturnsBsonAny() { + val input: dynamic = js("{ foo: 'bar', num: 99 }") + val result = parseEjson(input) + val ref = result.reference as HasValueReference.Runtime<*> + assertEquals(BsonAny, ref.type) + assertEquals(input, ref.source) + } +} diff --git a/packages/mongodb-mql-engines/src/jsTest/kotlin/QueryParserTest.kt b/packages/mongodb-mql-engines/src/jsTest/kotlin/QueryParserTest.kt new file mode 100644 index 000000000..3332ededa --- /dev/null +++ b/packages/mongodb-mql-engines/src/jsTest/kotlin/QueryParserTest.kt @@ -0,0 +1,143 @@ +import com.mongodb.jbplugin.mql.components.* +import kotlin.test.* + +class ParseQueryTest { + @Test + fun parseTemplateQuery_a1_b2() { + val query: dynamic = js("{ a: 1, b: 2 }") + val root = parseFilter(query) + + val filter = root.component>() + assertNotNull(filter) + val children = filter.children + assertEquals(2, children.size) + + val childA = children[0] + assertEquals("eq", childA.component()?.name?.canonical) + assertEquals("a", (childA.component>()?.reference as HasFieldReference.FromSchema).fieldName) + + val childB = children[1] + assertEquals("eq", childB.component()?.name?.canonical) + assertEquals("b", (childB.component>()?.reference as HasFieldReference.FromSchema).fieldName) + } + + @Test + fun parseAndQuery() { + val query: dynamic = js("{ '${'$'}and': [{ a: 1 }, { b: 2 }] }") + val root = parseFilter(query) + + val andNode = root.component>()?.children?.get(0) + assertNotNull(andNode) + assertEquals("and", andNode.component()?.name?.canonical) + val subFilter = andNode.component>() + assertNotNull(subFilter) + assertEquals(2, subFilter.children.size) + } + + @Test + fun parseOrQuery() { + val query: dynamic = js("{ '${'$'}or': [{ a: 1 }, { b: 2 }] }") + val root = parseFilter(query) + + val orNode = root.component>()?.children?.get(0) + assertNotNull(orNode) + assertEquals("or", orNode.component()?.name?.canonical) + val subFilter = orNode.component>() + assertNotNull(subFilter) + assertEquals(2, subFilter.children.size) + } + + @Test + fun parseNotQuery() { + val query: dynamic = js("{ '${'$'}not': { a: 1 } }") + val root = parseFilter(query) + + val notNode = root.component>()?.children?.get(0) + assertNotNull(notNode) + assertEquals("not", notNode.component()?.name?.canonical) + val subFilter = notNode.component>() + assertNotNull(subFilter) + assertEquals(1, subFilter.children.size) + } + + @Test + fun parseNestedAndOrQuery() { + val query: dynamic = js( + """ + { + "${'$'}and": [ + { "a": { "${'$'}gt": 5, "${'$'}lt": 10 } }, + { "${'$'}or": [ + { "b": 2 }, + { "c": 3 } + ]} + ] + } + """ + ) + val root = parseFilter(query) + val andNode = root.component>()?.children?.get(0) + assertNotNull(andNode) + assertEquals("and", andNode.component()?.name?.canonical) + val subFilter = andNode.component>() + assertNotNull(subFilter) + assertEquals(2, subFilter.children.size) + + val andSubConditions = subFilter.children[0] + assertEquals("and", andSubConditions.component()?.name?.canonical) + val gtAndLtFilter = andSubConditions.component>() + assertNotNull(gtAndLtFilter) + assertEquals(2, gtAndLtFilter.children.size) + + val gtNode = gtAndLtFilter.children[0] + assertEquals("gt", gtNode.component()?.name?.canonical) + assertEquals("a", (gtNode.component>()?.reference as HasFieldReference.FromSchema).fieldName) + + val ltNode = gtAndLtFilter.children[1] + assertEquals("lt", ltNode.component()?.name?.canonical) + assertEquals("a", (ltNode.component>()?.reference as HasFieldReference.FromSchema).fieldName) + + val orNode = subFilter.children[1] + assertEquals("or", orNode.component()?.name?.canonical) + val orFilter = orNode.component>() + assertNotNull(orFilter) + assertEquals(2, orFilter.children.size) + } + + @Test + fun parseEjsonValues() { + val query: dynamic = js( + """ + { + "a": { "${'$'}numberLong": "1234567890123456789" }, + "b": { "${'$'}oid": "507f1f77bcf86cd799439011" }, + "c": { "${'$'}date": { "${'$'}numberLong": "1609459200000" } }, + "d": { "${'$'}numberDecimal": "123.456" }, + "e": { "${'$'}numberInt": "42" }, + "f": { "${'$'}numberDouble": "3.14" } + } + """ + ) + + val root = parseFilter(query) + val filter = root.component>() + assertNotNull(filter) + val children = filter.children + assertEquals(6, children.size) + + val expectedFields = listOf("a", "b", "c", "d", "e", "f") + val expectedTypes = listOf("BsonInt64", "BsonObjectId", "BsonDate", "BsonDecimal128", "BsonInt32", "BsonDouble") + + println(root) + + for ((index, fieldName) in expectedFields.withIndex()) { + val node = children[index] + assertEquals("eq", node.component()?.name?.canonical) + val fieldRef = node.component>()?.reference as HasFieldReference.FromSchema + assertEquals(fieldName, fieldRef.fieldName) + val valueRef = node.component>()?.reference + assertNotNull(valueRef) + assertTrue(valueRef.toString().contains(expectedTypes[index])) + } + } +} diff --git a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzerTest.kt b/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzerTest.kt deleted file mode 100644 index 0bda14b04..000000000 --- a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/indexing/IndexAnalyzerTest.kt +++ /dev/null @@ -1,657 +0,0 @@ -package com.mongodb.jbplugin.indexing - -import com.mongodb.jbplugin.accessadapter.toNs -import com.mongodb.jbplugin.mql.BsonBoolean -import com.mongodb.jbplugin.mql.BsonInt32 -import com.mongodb.jbplugin.mql.BsonObject -import com.mongodb.jbplugin.mql.BsonString -import com.mongodb.jbplugin.mql.CollectionSchema -import com.mongodb.jbplugin.mql.DataDistribution -import com.mongodb.jbplugin.mql.Node -import com.mongodb.jbplugin.mql.SiblingQueriesFinder -import com.mongodb.jbplugin.mql.components.Name -import com.mongodb.jbplugin.utils.ModelAssertions.assertIndexCollectionIs -import com.mongodb.jbplugin.utils.ModelAssertions.assertMongoDbIndexIs -import com.mongodb.jbplugin.utils.ModelDsl.aggregate -import com.mongodb.jbplugin.utils.ModelDsl.ascending -import com.mongodb.jbplugin.utils.ModelDsl.constant -import com.mongodb.jbplugin.utils.ModelDsl.filterBy -import com.mongodb.jbplugin.utils.ModelDsl.findMany -import com.mongodb.jbplugin.utils.ModelDsl.include -import com.mongodb.jbplugin.utils.ModelDsl.match -import com.mongodb.jbplugin.utils.ModelDsl.predicate -import com.mongodb.jbplugin.utils.ModelDsl.project -import com.mongodb.jbplugin.utils.ModelDsl.schema -import com.mongodb.jbplugin.utils.ModelDsl.sortBy -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test - -class IndexAnalyzerTest { - class EmptySiblingQueriesFinder : SiblingQueriesFinder { - override fun allSiblingsOf(query: Node): Array> { - return emptyArray() - } - } - - class PredefinedSiblingQueriesFinder(private val other: Array>) : SiblingQueriesFinder { - override fun allSiblingsOf(query: Node): Array> { - return other - } - } - - @Test - fun `queries without a collection reference component are not supported`() = runTest { - val query = Node(Unit, emptyList()) - val result = IndexAnalyzer.analyze(query, EmptySiblingQueriesFinder(), emptyOptions()) - - assertEquals(IndexAnalyzer.SuggestedIndex.NoIndex, result) - } - - @Test - fun `returns the suggested list of fields for a mongodb query`() = runTest { - val query = findMany("myDb.myColl".toNs()) { - filterBy { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs(arrayOf("myField" to 1), result) - } - - @Test - fun `places low cardinality types earlier into the index for prefix compression`() = runTest { - val query = findMany("myDb.myColl".toNs()) { - filterBy { - predicate(Name.EQ) { - schema("highCardinality") - constant(52) - } - predicate(Name.EQ) { - schema("lowCardinality") - constant(true) - } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs(arrayOf("lowCardinality" to 1, "highCardinality" to 1), result) - } - - @Test - fun `puts equality fields before sorting fields and them before range fields`() = runTest { - val query = findMany("myDb.myColl".toNs()) { - filterBy { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - predicate(Name.GT) { - schema("myRangeField") - constant(true) - } - } - sortBy { - ascending { schema("mySortField") } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs( - arrayOf( - "myField" to 1, - "mySortField" to 1, - "myRangeField" to 1 - ), - result - ) - } - - @Test - fun `removes repeated field references`() = runTest { - val query = findMany("myDb.myColl".toNs()) { - filterBy { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - predicate(Name.EQ) { - schema("mySecondField") - constant(true) - } - predicate(Name.EQ) { - schema("myField") - constant(55) - } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs( - arrayOf( - "mySecondField" to 1, - "myField" to 1, - ), - result - ) - } - - @Test - fun `promotes repeated field references into the most important stage`() = runTest { - val query = findMany("myDb.myColl".toNs()) { - filterBy { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - predicate(Name.GT) { - schema("mySecondField") - constant(12) - } - } - - sortBy { - ascending { schema("mySecondField") } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs( - arrayOf( - "myField" to 1, - "mySecondField" to 1, - ), - result - ) - } - - @Test - fun `considers aggregation pipelines match stages`() = runTest { - val query = aggregate("myDb.myColl".toNs()) { - match { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - predicate(Name.GT) { - schema("mySecondField") - constant(12) - } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs( - arrayOf( - "myField" to 1, - "mySecondField" to 1, - ), - result - ) - } - - @Test - fun `does not consider aggregation pipelines match stages in the second position`() = runTest { - val query = aggregate("myDb.myColl".toNs()) { - match { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - } - - match { - predicate(Name.EQ) { - schema("myIgnoredField") - constant(52) - } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs( - arrayOf( - "myField" to 1, - ), - result - ) - } - - @Test - fun `does not consider aggregation pipelines stages that are not match`() = runTest { - val query = aggregate("myDb.myColl".toNs()) { - project { - include { - schema("projectedField") - } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs(emptyArray(), result) - } - - @Test - fun `finds the index with more fields that matches the current query from sibling queries`() = runTest { - val predefinedSiblingQueries = PredefinedSiblingQueriesFinder( - arrayOf( - findMany("myDb.myColl".toNs()) { - filterBy { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - predicate(Name.GT) { - schema("mySecondField") - constant(12) - } - } - - sortBy { - ascending { schema("mySortField") } - } - } - ) - ) - - val query = findMany("myDb.myColl".toNs()) { - filterBy { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - } - } - - val result = IndexAnalyzer.analyze( - query, - predefinedSiblingQueries, - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs( - arrayOf( - "myField" to 1, - "mySortField" to 1, - "mySecondField" to 1, - ), - result - ) - } - - @Test - fun `finds the index with more fields that matches the current query from sibling queries even if they are aggregates`() = runTest { - val predefinedSiblingQueries = PredefinedSiblingQueriesFinder( - arrayOf( - aggregate("myDb.myColl".toNs()) { - match { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - predicate(Name.GT) { - schema("mySecondField") - constant(12) - } - } - } - ) - ) - - val query = findMany("myDb.myColl".toNs()) { - filterBy { - predicate(Name.EQ) { - schema("myField") - constant(52) - } - } - } - - val result = IndexAnalyzer.analyze( - query, - predefinedSiblingQueries, - emptyOptions() - ) - - assertIndexCollectionIs("myDb.myColl".toNs(), result) - assertMongoDbIndexIs( - arrayOf( - "myField" to 1, - "mySecondField" to 1 - ), - result - ) - } - - @Nested - inner class WhenDataDistributionIsAvailable { - private val ns = "myDb.myColl".toNs() - - @Test - fun `places fields with low selectivity earlier in the index definition for prefix compression`() = runTest { - val schema = CollectionSchema( - namespace = ns, - schema = BsonObject( - mapOf( - "highSelectivityHighCardinality" to BsonInt32, - "highSelectivityLowCardinality" to BsonBoolean, - "lowSelectivityHighCardinality" to BsonString, - "lowSelectivityLowCardinality" to BsonBoolean, - ) - ), - dataDistribution = DataDistribution.generate( - listOf( - mapOf( - "highSelectivityHighCardinality" to 2, - "highSelectivityLowCardinality" to true, - "lowSelectivityHighCardinality" to "US", - "lowSelectivityLowCardinality" to true - ), - mapOf( - "highSelectivityHighCardinality" to 3, - "highSelectivityLowCardinality" to false, - "lowSelectivityHighCardinality" to "US", - "lowSelectivityLowCardinality" to true - ), - mapOf( - "highSelectivityHighCardinality" to 4, - "highSelectivityLowCardinality" to false, - "lowSelectivityHighCardinality" to "US", - "lowSelectivityLowCardinality" to true - ), - mapOf( - "highSelectivityHighCardinality" to 5, - "highSelectivityLowCardinality" to false, - "lowSelectivityHighCardinality" to "US", - "lowSelectivityLowCardinality" to true - ), - ) - ) - ) - - val query = findMany(ns, schema) { - filterBy { - predicate(Name.EQ) { - schema("highSelectivityHighCardinality") - constant(2) - } - predicate(Name.EQ) { - schema("highSelectivityLowCardinality") - constant(true) - } - predicate(Name.EQ) { - schema("lowSelectivityHighCardinality") - constant("US") - } - predicate(Name.EQ) { - schema("lowSelectivityLowCardinality") - constant(true) - } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertMongoDbIndexIs( - arrayOf( - "lowSelectivityLowCardinality" to 1, - "lowSelectivityHighCardinality" to 1, - "highSelectivityLowCardinality" to 1, - "highSelectivityHighCardinality" to 1, - ), - result - ) - } - - @Test - fun `when all fields have same selectivity orders by cardinality`() = runTest { - val schema = CollectionSchema( - namespace = ns, - schema = BsonObject( - mapOf( - "highCardinality" to BsonInt32, - "lowCardinality" to BsonBoolean, - ) - ), - dataDistribution = DataDistribution.generate( - listOf( - mapOf("highCardinality" to 1, "lowCardinality" to true), - mapOf("highCardinality" to 2, "lowCardinality" to false) - ) - ) - ) - - val query = findMany(ns, schema) { - filterBy { - predicate(Name.EQ) { - schema("highCardinality") - constant(1) - } - predicate(Name.EQ) { - schema("lowCardinality") - constant(true) - } - } - } - - val result = IndexAnalyzer.analyze(query, EmptySiblingQueriesFinder(), emptyOptions()) - assertMongoDbIndexIs(arrayOf("lowCardinality" to 1, "highCardinality" to 1), result) - } - - @Test - fun `when selectivity is not known for a value, it places fields with low cardinality first in the index definition`() = runTest { - val schema = CollectionSchema( - namespace = ns, - schema = BsonObject( - mapOf( - "highSelectivityHighCardinality" to BsonInt32, - "highSelectivityLowCardinality" to BsonBoolean, - "unknownSelectivityHighCardinality" to BsonString, - "lowSelectivityLowCardinality" to BsonBoolean, - ) - ), - dataDistribution = DataDistribution.generate( - listOf( - mapOf( - "highSelectivityHighCardinality" to 2, - "highSelectivityLowCardinality" to true, - "lowSelectivityLowCardinality" to true - ), - mapOf( - "highSelectivityHighCardinality" to 3, - "highSelectivityLowCardinality" to false, - "lowSelectivityLowCardinality" to true - ), - mapOf( - "highSelectivityHighCardinality" to 4, - "highSelectivityLowCardinality" to false, - "lowSelectivityLowCardinality" to true - ), - mapOf( - "highSelectivityHighCardinality" to 5, - "highSelectivityLowCardinality" to false, - "lowSelectivityLowCardinality" to true - ), - ) - ) - ) - - val query = findMany(ns, schema) { - filterBy { - predicate(Name.EQ) { - schema("highSelectivityHighCardinality") - constant(2) - } - predicate(Name.EQ) { - schema("highSelectivityLowCardinality") - constant(true) - } - predicate(Name.EQ) { - schema("unknownSelectivityHighCardinality") - constant("US") - } - predicate(Name.EQ) { - schema("lowSelectivityLowCardinality") - constant(true) - } - } - } - - val result = IndexAnalyzer.analyze( - query, - EmptySiblingQueriesFinder(), - emptyOptions() - ) - - assertMongoDbIndexIs( - arrayOf( - "lowSelectivityLowCardinality" to 1, - "highSelectivityLowCardinality" to 1, - "highSelectivityHighCardinality" to 1, - "unknownSelectivityHighCardinality" to 1, - ), - result - ) - } - - @Test - fun `maintains ESR order even when lower selectivity fields exist in different roles`() = runTest { - val schema = CollectionSchema( - namespace = ns, - schema = BsonObject( - mapOf( - "highSelectivityEquality" to BsonString, - "lowSelectivityEquality" to BsonBoolean, - "highSelectivitySort" to BsonString, - "lowSelectivitySort" to BsonBoolean, - "highSelectivityRange" to BsonString, - "lowSelectivityRange" to BsonBoolean - ) - ), - dataDistribution = DataDistribution.generate( - listOf( - mapOf( - "highSelectivityEquality" to "rare", - "lowSelectivityEquality" to true, - "highSelectivitySort" to "rare", - "lowSelectivitySort" to true, - "highSelectivityRange" to "rare", - "lowSelectivityRange" to true, - ), - mapOf( - "highSelectivityEquality" to "common", - "lowSelectivityEquality" to true, - "highSelectivitySort" to "common", - "lowSelectivitySort" to true, - "highSelectivityRange" to "common", - "lowSelectivityRange" to true - ) - ) - ) - ) - - val query = findMany(ns, schema) { - filterBy { - predicate(Name.EQ) { - schema("highSelectivityEquality") - constant("rare") - } - predicate(Name.EQ) { - schema("lowSelectivityEquality") - constant(true) - } - predicate(Name.GT) { - schema("highSelectivityRange") - constant("rare") - } - predicate(Name.GT) { - schema("lowSelectivityRange") - constant(true) - } - } - sortBy { - ascending { - schema("highSelectivitySort") - } - ascending { - schema("lowSelectivitySort") - } - } - } - - val result = IndexAnalyzer.analyze(query, EmptySiblingQueriesFinder(), emptyOptions()) - - assertMongoDbIndexIs( - arrayOf( - "lowSelectivityEquality" to 1, - "highSelectivityEquality" to 1, - "lowSelectivitySort" to 1, - "highSelectivitySort" to 1, - "lowSelectivityRange" to 1, - "highSelectivityRange" to 1 - ), - result - ) - } - } - - private fun emptyOptions() = CollectionIndexConsolidationOptions(10) -} diff --git a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt b/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt deleted file mode 100644 index b6d408a2e..000000000 --- a/packages/mongodb-mql-engines/src/test/kotlin/com/mongodb/jbplugin/linting/IndexCheckingLinterTest.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.mongodb.jbplugin.linting - -import com.mongodb.jbplugin.accessadapter.MongoDbReadModelProvider -import com.mongodb.jbplugin.accessadapter.slice.ExplainPlan -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 -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.mockito.kotlin.any - -class IndexCheckingLinterTest { - @Test - fun `warns query plans using a collscan`() { - val readModelProvider = mock>() - val query = Node(Unit, emptyList()) - - `when`(readModelProvider.slice(any(), any>())).thenReturn( - ExplainQuery( - ExplainPlan.CollectionScan - ) - ) - - val result = - IndexCheckingLinter.lintQuery( - Unit, - readModelProvider, - query, - QueryContext.empty() - ) - - assertEquals(1, result.warnings.size) - assertInstanceOf(IndexCheckWarning.QueryNotCoveredByIndex::class.java, result.warnings[0]) - } - - @Test - fun `warns query plans using an ineffective index`() { - val readModelProvider = mock>() - val query = Node(Unit, emptyList()) - - `when`(readModelProvider.slice(any(), any>())).thenReturn( - ExplainQuery( - ExplainPlan.IneffectiveIndexUsage - ) - ) - - val result = - IndexCheckingLinter.lintQuery( - Unit, - readModelProvider, - query, - QueryContext.empty() - ) - - assertEquals(1, result.warnings.size) - assertInstanceOf( - IndexCheckWarning.QueryNotUsingEffectiveIndex::class.java, - result.warnings[0] - ) - } - - @Test - fun `does not warn on index scans`() { - val readModelProvider = mock>() - val query = Node(Unit, emptyList()) - - `when`(readModelProvider.slice(any(), any>())).thenReturn( - ExplainQuery( - ExplainPlan.IndexScan - ) - ) - - val result = - IndexCheckingLinter.lintQuery( - Unit, - readModelProvider, - query, - QueryContext.empty() - ) - - assertEquals(0, result.warnings.size) - } -} diff --git a/packages/mongodb-mql-model/build.gradle.kts b/packages/mongodb-mql-model/build.gradle.kts index 39c4f7657..cb000c9db 100644 --- a/packages/mongodb-mql-model/build.gradle.kts +++ b/packages/mongodb-mql-model/build.gradle.kts @@ -2,8 +2,19 @@ plugins { id("com.mongodb.intellij.isolated-module") } -dependencies { - implementation(libs.bson.kotlin) - implementation(libs.owasp.encoder) - implementation(libs.semver.parser) +kotlin { + sourceSets { + commonMain { + dependencies { + // implementation(libs.owasp.encoder) + implementation(libs.semver.parser) + } + } + + jvmMain { + dependencies { + implementation(libs.bson.kotlin) + } + } + } } diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/BsonType.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/BsonType.kt similarity index 70% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/BsonType.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/BsonType.kt index 3d96e90b3..56501f862 100644 --- a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/BsonType.kt +++ b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/BsonType.kt @@ -5,14 +5,7 @@ package com.mongodb.jbplugin.mql -import org.bson.types.Decimal128 -import org.bson.types.ObjectId -import java.math.BigDecimal -import java.math.BigInteger -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.util.* +import kotlinx.collections.immutable.persistentMapOf /** * Represents any of the valid BSON types. @@ -217,61 +210,6 @@ data class BsonEnum(val members: Set, val name: String? = null) : BsonTy data class ComputedBsonType(val baseType: BsonType, val expression: Node) : BsonType by baseType // for now it will behave as baseType -/** - * Returns the inferred BSON type of the current Java class, considering it's nullability. - * - * @param value - */ -fun Class?.toBsonType(value: T? = null): BsonType { - return when (this) { - null -> BsonNull - Float::class.javaPrimitiveType -> BsonDouble - Float::class.javaObjectType -> BsonAnyOf(BsonNull, BsonDouble) - Double::class.javaPrimitiveType -> BsonDouble - Double::class.javaObjectType -> BsonAnyOf(BsonNull, BsonDouble) - Boolean::class.javaPrimitiveType -> BsonBoolean - Boolean::class.javaObjectType -> BsonAnyOf(BsonNull, BsonBoolean) - Short::class.javaPrimitiveType -> BsonInt32 - Short::class.javaObjectType -> BsonAnyOf(BsonNull, BsonInt32) - Int::class.javaPrimitiveType -> BsonInt32 - Int::class.javaObjectType -> BsonAnyOf(BsonNull, BsonInt32) - Long::class.javaPrimitiveType -> BsonInt64 - Long::class.javaObjectType -> BsonAnyOf(BsonNull, BsonInt64) - CharSequence::class.java, String::class.java -> BsonAnyOf(BsonNull, BsonString) - Date::class.java, Instant::class.java, LocalDate::class.java, LocalDateTime::class.java -> - BsonAnyOf(BsonNull, BsonDate) - UUID::class.java -> BsonAnyOf(BsonNull, BsonUUID) - ObjectId::class.java -> BsonAnyOf(BsonNull, BsonObjectId) - BigInteger::class.java -> BsonAnyOf(BsonNull, BsonInt64) - BigDecimal::class.java -> BsonAnyOf(BsonNull, BsonDecimal128) - Decimal128::class.java -> BsonAnyOf(BsonNull, BsonDecimal128) - else -> - if (isEnum) { - val variants = this.enumConstants.map { it.toString() }.toSet() - BsonEnum(variants) - } else if (Collection::class.java.isAssignableFrom(this) || - Array::class.java.isAssignableFrom(this) - ) { - return BsonAnyOf(BsonNull, BsonArray(BsonAny)) // types are lost at runtime - } else if (Map::class.java.isAssignableFrom(this)) { - value?.let { - val fields = - Map::class.java.cast(value).entries.associate { - it.key.toString() to it.value?.javaClass.toBsonType(it.value) - } - return BsonAnyOf(BsonNull, BsonObject(fields)) - } ?: return BsonAnyOf(BsonNull, BsonAny) - } else { - val fields = - this.declaredFields.associate { - it.name to it.type.toBsonType() - } - - return BsonAnyOf(BsonNull, BsonObject(fields)) - } - } -} - fun mergeSchemaTogether( first: BsonType, second: BsonType, @@ -280,14 +218,13 @@ fun mergeSchemaTogether( val mergedMap = first.schema.entries .union(second.schema.entries) - .fold(mutableMapOf()) { acc, entry -> - acc.compute(entry.key) { _, current -> - current?.let { - mergeSchemaTogether(current, entry.value) - } ?: entry.value + .fold(persistentMapOf()) { acc, entry -> + val currentValue = acc[entry.key] + if (currentValue != null) { + acc.put(entry.key, mergeSchemaTogether(currentValue, entry.value)) + } else { + acc.put(entry.key, entry.value) } - - acc } return BsonObject(mergedMap) @@ -346,11 +283,6 @@ fun flattenAnyOfReferences(schema: BsonType): BsonType = else -> schema } -fun primitiveOrWrapper(example: Class<*>): Class<*> { - val type = runCatching { example.getField("TYPE").get(null) as? Class<*> }.getOrNull() - return type ?: example -} - fun BsonType.toNonNullableType(): BsonType { return when (this) { is BsonAnyOf -> types.first { it != BsonNull }.toNonNullableType() @@ -358,3 +290,5 @@ fun BsonType.toNonNullableType(): BsonType { else -> this } } + +expect inline fun T.toBsonType(): BsonType diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/CollectionSchema.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/CollectionSchema.kt similarity index 93% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/CollectionSchema.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/CollectionSchema.kt index 85839088e..60169801e 100644 --- a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/CollectionSchema.kt +++ b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/CollectionSchema.kt @@ -1,8 +1,5 @@ package com.mongodb.jbplugin.mql -import org.intellij.lang.annotations.Language -import java.lang.Integer.parseInt - /** * @property namespace * @property schema @@ -13,7 +10,7 @@ data class CollectionSchema( val dataDistribution: DataDistribution = DataDistribution.generate(emptyList()), ) { fun typeOf( - @Language("JSONPath") jsonPath: String, + jsonPath: String, ): BsonType { val splitJsonPath = jsonPath.split('.').toList() return recursivelyGetType(splitJsonPath, schema) @@ -59,7 +56,7 @@ data class CollectionSchema( } val current = jsonPath.first() - val isCurrentNumber = kotlin.runCatching { parseInt(current) }.isSuccess + val isCurrentNumber = kotlin.runCatching { current.toInt() }.isSuccess val listOfOptions = mutableListOf() diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/DataDistribution.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/DataDistribution.kt similarity index 93% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/DataDistribution.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/DataDistribution.kt index 4d3fe4186..eee016790 100644 --- a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/DataDistribution.kt +++ b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/DataDistribution.kt @@ -27,7 +27,7 @@ data class DataDistribution(private val distribution: Map? { - return distribution.getOrDefault(fieldPath, null) + return distribution.getOrElse(fieldPath) { null } } companion object { @@ -55,7 +55,7 @@ data class DataDistribution(private val distribution: Map -> { fieldDistribution[JsonObject] = - fieldDistribution.getOrDefault(JsonObject, 0) + 1 + (fieldDistribution.getOrElse(JsonObject) { 0 }) + 1 val mappedValue = value.map { it.key.toString() to it.value }.toMap() populateDistributionForList( listOf(mappedValue), @@ -66,20 +66,20 @@ data class DataDistribution(private val distribution: Map -> { fieldDistribution[JsonArray] = - fieldDistribution.getOrDefault(JsonArray, 0) + 1 + (fieldDistribution.getOrElse(JsonArray) { 0 }) + 1 val listOfMaps = value.filterIsInstance>() populateDistributionForList(listOfMaps, distribution, fieldPath) } is Collection<*> -> { fieldDistribution[JsonArray] = - fieldDistribution.getOrDefault(JsonArray, 0) + 1 + (fieldDistribution.getOrElse(JsonArray) { 0 }) + 1 val listOfMaps = value.filterIsInstance>() populateDistributionForList(listOfMaps, distribution, fieldPath) } else -> { - fieldDistribution[value] = fieldDistribution.getOrDefault(value, 0) + 1 + fieldDistribution[value] = fieldDistribution.getOrElse(value) { 0 } + 1 } } } @@ -95,7 +95,7 @@ data class DataDistribution(private val distribution: Map( ) { inline fun component(): C? = components.firstOrNull { it is C } as C? - fun component(withClass: Class): C? = components.firstOrNull { - withClass.isInstance(it) - } as C? - inline fun components(): List = components.filterIsInstance() fun with(component: Component): Node { diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/SiblingQueriesFinder.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/SiblingQueriesFinder.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/SiblingQueriesFinder.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/SiblingQueriesFinder.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/adt/Either.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/adt/Either.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/adt/Either.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/adt/Either.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/adt/EitherInclusive.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/adt/EitherInclusive.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/adt/EitherInclusive.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/adt/EitherInclusive.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasAccumulatedFields.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasAccumulatedFields.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasAccumulatedFields.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasAccumulatedFields.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasAddedFields.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasAddedFields.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasAddedFields.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasAddedFields.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasAggregation.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasAggregation.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasAggregation.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasAggregation.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReference.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReference.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReference.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReference.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasExplain.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasExplain.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasExplain.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasExplain.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasFieldReference.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasFieldReference.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasFieldReference.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasFieldReference.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasFilter.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasFilter.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasFilter.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasFilter.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasLimit.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasLimit.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasLimit.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasLimit.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasProjections.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasProjections.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasProjections.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasProjections.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasRunCommand.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasRunCommand.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasRunCommand.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasRunCommand.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasSorts.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasSorts.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasSorts.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasSorts.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasSourceDialect.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasSourceDialect.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasSourceDialect.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasSourceDialect.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasTargetCluster.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasTargetCluster.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasTargetCluster.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasTargetCluster.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasUpdates.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasUpdates.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasUpdates.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasUpdates.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasValueReference.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasValueReference.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/HasValueReference.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/HasValueReference.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/IsCommand.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/IsCommand.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/IsCommand.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/IsCommand.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/Named.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/Named.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/components/Named.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/components/Named.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/Parser.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/Parser.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/Parser.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/Parser.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasAggregation.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasAggregation.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasAggregation.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasAggregation.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasCollectionReference.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasCollectionReference.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasCollectionReference.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasCollectionReference.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasFieldReference.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasFieldReference.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasFieldReference.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasFieldReference.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasFilter.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasFilter.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasFilter.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasFilter.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasSourceDialect.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasSourceDialect.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasSourceDialect.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasSourceDialect.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasUpdates.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasUpdates.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasUpdates.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasUpdates.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasValueReference.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasValueReference.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/HasValueReference.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/HasValueReference.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/IsCommand.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/IsCommand.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/IsCommand.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/IsCommand.kt diff --git a/packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/Named.kt b/packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/Named.kt similarity index 100% rename from packages/mongodb-mql-model/src/main/kotlin/com/mongodb/jbplugin/mql/parser/components/Named.kt rename to packages/mongodb-mql-model/src/commonMain/kotlin/com/mongodb/jbplugin/mql/parser/components/Named.kt diff --git a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/BsonTypeTest.kt b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/BsonTypeTest.kt similarity index 81% rename from packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/BsonTypeTest.kt rename to packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/BsonTypeTest.kt index cae1a6f4c..a5482b3bd 100644 --- a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/BsonTypeTest.kt +++ b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/BsonTypeTest.kt @@ -1,79 +1,68 @@ package com.mongodb.jbplugin.mql -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource -import java.math.BigDecimal -import java.math.BigInteger -import java.time.Instant -import java.time.LocalDate -import java.time.LocalDateTime -import java.util.* - -@Suppress("TOO_LONG_FUNCTION") +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + class BsonTypeTest { - @ParameterizedTest - @MethodSource("java to bson") - fun `should map correctly all java types`( - javaClass: Class<*>, - expected: BsonType, - ) { - assertEquals(expected, javaClass.toBsonType()) - } + @Test + fun should_calculate_correctly_whether_a_type_matches_the_other_provided_type() { + assignable_to_a_non_higher_precision_number_non_collection_non_arbitrary_BsonType().forEach { + assertShouldMatchCorrectlyTypes(it[0] as BsonType, it[1] as BsonType, it[2] as Boolean) + } - @ParameterizedTest - @MethodSource( - "assignable to a non higher precision number, non collection, non arbitrary BsonType", - "assignable to BsonInt64", - "assignable to BsonDecimal128", - "assignable to BsonNull", - "assignable to BsonAny", - "assignable to BsonAnyOf", - "BsonArray match assertions", - "BsonObject match assertions", - ) - fun `should calculate correctly whether a type matches the other provided type`( - // calling these value and field type just for clarity around usage - valueType: BsonType, - fieldType: BsonType, - expectedToMatch: Boolean, - ) { - val assertionFailureMessage = if (expectedToMatch) { - "$valueType was expected to be assignable to $fieldType but it was not!" - } else { - "$valueType was not expected to be assignable to $fieldType but it was!" + assignable_to_BsonInt64().forEach { + assertShouldMatchCorrectlyTypes(it[0] as BsonType, it[1] as BsonType, it[2] as Boolean) + } + + assignable_to_BsonDecimal128().forEach { + assertShouldMatchCorrectlyTypes(it[0] as BsonType, it[1] as BsonType, it[2] as Boolean) + } + + assignable_to_BsonNull().forEach { + assertShouldMatchCorrectlyTypes(it[0] as BsonType, it[1] as BsonType, it[2] as Boolean) + } + + assignable_to_BsonAny().forEach { + assertShouldMatchCorrectlyTypes(it[0] as BsonType, it[1] as BsonType, it[2] as Boolean) + } + + assignable_to_BsonAnyOf().forEach { + assertShouldMatchCorrectlyTypes(it[0] as BsonType, it[1] as BsonType, it[2] as Boolean) + } + + bsonArray_match_assertions().forEach { + assertShouldMatchCorrectlyTypes(it[0] as BsonType, it[1] as BsonType, it[2] as Boolean) + } + + bsonObject_match_assertions().forEach { + assertShouldMatchCorrectlyTypes(it[0] as BsonType, it[1] as BsonType, it[2] as Boolean) } - val result = valueType.isAssignableTo(fieldType) - assertEquals( - result, - expectedToMatch, - assertionFailureMessage - ) } @Test - fun `bson enum should be assignable to a string`() { + fun bson_enum_should_be_assignable_to_a_string() { val bsonEnum = BsonEnum(setOf("A", "B")) assertTrue(bsonEnum.isAssignableTo(BsonString)) } @Test - fun `bson enum should be assignable to another bson enum if they are equals`() { + fun bson_enum_should_be_assignable_to_another_bson_enum_if_they_are_equals() { val bsonEnum = BsonEnum(setOf("A", "B")) val otherBsonEnum = BsonEnum(setOf("A", "B")) assertTrue(bsonEnum.isAssignableTo(otherBsonEnum)) } @Test - fun `bson enum should be assignable to another bson enum if it is an strict subset`() { + fun bson_enum_should_be_assignable_to_another_bson_enum_if_it_is_an_strict_subset() { val bsonEnum = BsonEnum(setOf("A")) val otherBsonEnum = BsonEnum(setOf("A", "B")) assertTrue(bsonEnum.isAssignableTo(otherBsonEnum)) } @Test - fun `bson enum should not be assignable to another bson enum if it is not a subset`() { + fun bson_enum_should_not_be_assignable_to_another_bson_enum_if_it_is_not_a_subset() { val bsonEnum = BsonEnum(setOf("A", "C")) val otherBsonEnum = BsonEnum(setOf("A", "B")) assertFalse(bsonEnum.isAssignableTo(otherBsonEnum)) @@ -91,9 +80,23 @@ class BsonTypeTest { BsonDecimal128, ) - private fun List.listExcluding(type: BsonType): List = subtract( - setOf(type) - ).toList() + private fun assertShouldMatchCorrectlyTypes( + valueType: BsonType, + fieldType: BsonType, + expectedToMatch: Boolean + ) { + val assertionFailureMessage = if (expectedToMatch) { + "$valueType was expected to be assignable to $fieldType but it was not!" + } else { + "$valueType was not expected to be assignable to $fieldType but it was!" + } + val result = valueType.isAssignableTo(fieldType) + assertEquals( + result, + expectedToMatch, + assertionFailureMessage + ) + } private fun List.listExcluding(types: Set): List = subtract( types @@ -107,39 +110,7 @@ class BsonTypeTest { types ).random() - @JvmStatic - fun `java to bson`(): Array = - arrayOf( - arrayOf(Double::class.javaObjectType, BsonAnyOf(BsonNull, BsonDouble)), - arrayOf(Double::class.javaPrimitiveType, BsonDouble), - arrayOf(CharSequence::class.java, BsonAnyOf(BsonNull, BsonString)), - arrayOf(String::class.java, BsonAnyOf(BsonNull, BsonString)), - arrayOf(Boolean::class.javaObjectType, BsonAnyOf(BsonNull, BsonBoolean)), - arrayOf(Boolean::class.javaPrimitiveType, BsonBoolean), - arrayOf(Date::class.java, BsonAnyOf(BsonNull, BsonDate)), - arrayOf(Instant::class.java, BsonAnyOf(BsonNull, BsonDate)), - arrayOf(LocalDate::class.java, BsonAnyOf(BsonNull, BsonDate)), - arrayOf(LocalDateTime::class.java, BsonAnyOf(BsonNull, BsonDate)), - arrayOf(Int::class.javaObjectType, BsonAnyOf(BsonNull, BsonInt32)), - arrayOf(Int::class.javaPrimitiveType, BsonInt32), - arrayOf(BigInteger::class.java, BsonAnyOf(BsonNull, BsonInt64)), - arrayOf(BigDecimal::class.java, BsonAnyOf(BsonNull, BsonDecimal128)), - arrayOf(ArrayList::class.java, BsonAnyOf(BsonNull, BsonArray(BsonAny))), - arrayOf( - ExampleClass::class.java, - BsonAnyOf( - BsonNull, - BsonObject( - mapOf( - "field" to BsonAnyOf(BsonNull, BsonString), - ), - ), - ), - ), - ) - - @JvmStatic - fun `assignable to a non higher precision number, non collection, non arbitrary BsonType`(): Array> { + fun assignable_to_a_non_higher_precision_number_non_collection_non_arbitrary_BsonType(): Array> { // Special cases for assignability for numerical types down below val types = simpleTypes.listExcluding(setOf(BsonInt64, BsonDecimal128)) return types.flatMap { simpleType -> @@ -173,8 +144,7 @@ class BsonTypeTest { }.toTypedArray() } - @JvmStatic - fun `assignable to BsonInt64`(): Array> = arrayOf( + fun assignable_to_BsonInt64(): Array> = arrayOf( arrayOf(BsonInt64, BsonInt64, true), // Lower precision number can also be assigned arrayOf(BsonInt32, BsonInt64, true), @@ -208,8 +178,7 @@ class BsonTypeTest { arrayOf(BsonObject(mapOf("simpleTypeField" to BsonInt64)), BsonInt64, false), ) - @JvmStatic - fun `assignable to BsonDecimal128`(): Array> = arrayOf( + fun assignable_to_BsonDecimal128(): Array> = arrayOf( arrayOf(BsonDecimal128, BsonDecimal128, true), // Lower precision number can also be assigned arrayOf(BsonDouble, BsonDecimal128, true), @@ -247,8 +216,7 @@ class BsonTypeTest { arrayOf(BsonObject(mapOf("simpleTypeField" to BsonDecimal128)), BsonDecimal128, false), ) - @JvmStatic - fun `assignable to BsonNull`(): Array> = arrayOf( + fun assignable_to_BsonNull(): Array> = arrayOf( arrayOf(BsonNull, BsonNull, true), arrayOf(BsonAny, BsonNull, true), arrayOf(BsonAnyOf(BsonNull), BsonNull, true), @@ -261,8 +229,7 @@ class BsonTypeTest { arrayOf(BsonObject(mapOf("simpleTypeField" to simpleTypes.random())), BsonNull, false), ) - @JvmStatic - fun `assignable to BsonAny`(): Array> = arrayOf( + fun assignable_to_BsonAny(): Array> = arrayOf( arrayOf(BsonAny, BsonAny, true), arrayOf(BsonNull, BsonAny, true), arrayOf(simpleTypes.random(), BsonAny, true), @@ -273,8 +240,7 @@ class BsonTypeTest { arrayOf(BsonObject(mapOf("simpleTypeField" to simpleTypes.random())), BsonAny, true), ) - @JvmStatic - fun `assignable to BsonAnyOf`(): Array> { + fun assignable_to_BsonAnyOf(): Array> { val types = simpleTypes.listExcluding(setOf(BsonInt64, BsonDecimal128)) val positiveAssertions = types.flatMap { simpleType -> listOf( @@ -375,8 +341,7 @@ class BsonTypeTest { return (positiveAssertions + negativeAssertions).toTypedArray() } - @JvmStatic - fun `BsonArray match assertions`(): Array> = + fun bsonArray_match_assertions(): Array> = arrayOf( // Similar type arrays can be assigned to each other arrayOf(BsonArray(BsonString), BsonArray(BsonString), true), @@ -423,8 +388,7 @@ class BsonTypeTest { ), ) - @JvmStatic - fun `BsonObject match assertions`(): Array> = + fun bsonObject_match_assertions(): Array> = arrayOf( // Matches when structurally similar arrayOf( @@ -570,9 +534,5 @@ class BsonTypeTest { false ) ) - - data class ExampleClass( - val field: String, - ) } } diff --git a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/CollectionSchemaTest.kt b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/CollectionSchemaTest.kt similarity index 85% rename from packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/CollectionSchemaTest.kt rename to packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/CollectionSchemaTest.kt index 8a843549e..bc767e98e 100644 --- a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/CollectionSchemaTest.kt +++ b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/CollectionSchemaTest.kt @@ -1,11 +1,11 @@ package com.mongodb.jbplugin.mql -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.assertEquals class CollectionSchemaTest { @Test - fun `should return the type of a field in the root object`() { + fun should_return_the_type_of_a_field_in_the_root_object() { val schema = CollectionSchema( Namespace("a", "b"), @@ -20,7 +20,7 @@ class CollectionSchemaTest { } @Test - fun `should be able to merge when multiple options inside an object`() { + fun should_be_able_to_merge_when_multiple_options_inside_an_object() { val schema = CollectionSchema( Namespace("a", "b"), @@ -45,7 +45,7 @@ class CollectionSchemaTest { } @Test - fun `should be able to iterate over an array with objects`() { + fun should_be_able_to_iterate_over_an_array_with_objects() { val schema = CollectionSchema( Namespace("a", "b"), diff --git a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/DataDistributionTest.kt b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/DataDistributionTest.kt similarity index 80% rename from packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/DataDistributionTest.kt rename to packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/DataDistributionTest.kt index c7394980d..7dc3e1c52 100644 --- a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/DataDistributionTest.kt +++ b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/DataDistributionTest.kt @@ -1,11 +1,11 @@ package com.mongodb.jbplugin.mql -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.assertEquals class DataDistributionTest { @Test - fun `returns a distribution for simple key value pairs`() { + fun returns_a_distribution_for_simple_key_value_pairs() { val distribution = DataDistribution.generate( listOf( mapOf( @@ -20,17 +20,17 @@ class DataDistributionTest { ) ) - assertEquals( + assertEquals>( mapOf( "MongoDB" to 66.66666666666667, "BongoDB" to 33.333333333333336, ), - distribution.getDistributionForPath("name") + distribution.getDistributionForPath("name")!! ) } @Test - fun `returns a distribution for simple key value pairs with mixed primitive value types`() { + fun returns_a_distribution_for_simple_key_value_pairs_with_mixed_primitive_value_types() { val distribution = DataDistribution.generate( listOf( mapOf( @@ -79,7 +79,7 @@ class DataDistributionTest { } @Test - fun `returns a distribution for key value pairs where value is a List of maps`() { + fun returns_a_distribution_for_key_value_pairs_where_value_is_a_List_of_maps() { val distribution = DataDistribution.generate( listOf( mapOf( @@ -108,33 +108,33 @@ class DataDistributionTest { ) ) - assertEquals( + assertEquals>( mapOf( JsonArray to 100.0 ), - distribution.getDistributionForPath("name") + distribution.getDistributionForPath("name")!! ) - assertEquals( + assertEquals>( mapOf( "Mongo" to 33.333333333333336, "Bongo" to 33.333333333333336, JsonUndefined to 33.333333333333336, ), - distribution.getDistributionForPath("name.firstName") + distribution.getDistributionForPath("name.firstName")!! ) - assertEquals( + assertEquals>( mapOf( "DB" to 66.66666666666667, JsonUndefined to 33.333333333333336, ), - distribution.getDistributionForPath("name.lastName") + distribution.getDistributionForPath("name.lastName")!! ) } @Test - fun `returns a distribution for key value pairs where value is a Map`() { + fun returns_a_distribution_for_key_value_pairs_where_value_is_a_Map() { val distribution = DataDistribution.generate( listOf( mapOf( @@ -153,34 +153,34 @@ class DataDistributionTest { ) ) - assertEquals( + assertEquals>( mapOf( JsonObject to 66.66666666666667, JsonUndefined to 33.333333333333336 ), - distribution.getDistributionForPath("name") + distribution.getDistributionForPath("name")!! ) - assertEquals( + assertEquals>( mapOf( "Mongo" to 33.333333333333336, "Bongo" to 33.333333333333336, JsonUndefined to 33.333333333333336 ), - distribution.getDistributionForPath("name.firstName") + distribution.getDistributionForPath("name.firstName")!! ) - assertEquals( + assertEquals>( mapOf( "DB" to 66.66666666666667, JsonUndefined to 33.333333333333336 ), - distribution.getDistributionForPath("name.lastName") + distribution.getDistributionForPath("name.lastName")!! ) } @Test - fun `returns a distribution for key value pairs where value is mix of primitive and map values`() { + fun returns_a_distribution_for_key_value_pairs_where_value_is_mix_of_primitive_and_map_values() { val distribution = DataDistribution.generate( listOf( mapOf( @@ -204,35 +204,35 @@ class DataDistributionTest { ) ) - assertEquals( + assertEquals>( mapOf( "MongoDB" to 50.0, JsonObject to 50.0, ), - distribution.getDistributionForPath("name") + distribution.getDistributionForPath("name")!! ) - assertEquals( + assertEquals>( mapOf( "BongoDB" to 25.0, "Bingo" to 25.0, JsonUndefined to 50.0 ), - distribution.getDistributionForPath("name.firstName") + distribution.getDistributionForPath("name.firstName")!! ) - assertEquals( + assertEquals>( mapOf( "MongoDB" to 25.0, "Normo" to 25.0, JsonUndefined to 50.0 ), - distribution.getDistributionForPath("name.lastName") + distribution.getDistributionForPath("name.lastName")!! ) } @Test - fun `returns a distribution for key value pairs where value is mix of primitive and list values`() { + fun returns_a_distribution_for_key_value_pairs_where_value_is_mix_of_primitive_and_list_values() { val distribution = DataDistribution.generate( listOf( mapOf( @@ -254,12 +254,12 @@ class DataDistributionTest { ) ) - assertEquals( + assertEquals>( mapOf( "MongoDB" to 50.0, JsonArray to 50.0, ), - distribution.getDistributionForPath("name") + distribution.getDistributionForPath("name")!! ) } } diff --git a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/NamespaceTest.kt b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/NamespaceTest.kt similarity index 60% rename from packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/NamespaceTest.kt rename to packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/NamespaceTest.kt index 42d4e2071..95cd9a8ea 100644 --- a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/NamespaceTest.kt +++ b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/NamespaceTest.kt @@ -1,29 +1,31 @@ package com.mongodb.jbplugin.mql -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class NamespaceTest { @Test - fun `serialises to a valid namespace string`() { + fun serialises_to_a_valid_namespace_string() { val namespace = Namespace("mydb", "my.cool.col") assertEquals("mydb.my.cool.col", namespace.toString()) } @Test - fun `is not valid if both database and collections are provided`() { + fun is_not_valid_if_both_database_and_collections_are_provided() { val namespace = Namespace("mydb", "mycoll") assertTrue(namespace.isValid) } @Test - fun `is not valid if the database is empty`() { + fun is_not_valid_if_the_database_is_empty() { val namespace = Namespace("", "my.cool.col") assertFalse(namespace.isValid) } @Test - fun `is not valid if the collection is empty`() { + fun is_not_valid_if_the_collection_is_empty() { val namespace = Namespace("mydb", "") assertFalse(namespace.isValid) } diff --git a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/NodeTest.kt b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/NodeTest.kt similarity index 57% rename from packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/NodeTest.kt rename to packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/NodeTest.kt index 07ff5a8ef..04f2d9c7c 100644 --- a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/NodeTest.kt +++ b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/NodeTest.kt @@ -2,16 +2,16 @@ package com.mongodb.jbplugin.mql import com.mongodb.jbplugin.mql.components.* import com.mongodb.jbplugin.mql.components.HasCollectionReference.Known -import com.mongodb.jbplugin.mql.components.HasCollectionReference.OnlyCollection import io.github.z4kn4fein.semver.Version -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull +import kotlin.test.assertTrue class NodeTest { @Test - fun `is able to get a component if exists`() { + fun is_able_to_get_a_component_if_exists() { val node = Node(null, listOf(Named(Name.EQ))) val named = node.component() @@ -19,7 +19,7 @@ class NodeTest { } @Test - fun `returns null if a component does not exist`() { + fun returns_null_if_a_component_does_not_exist() { val node = Node(null, listOf()) val named = node.component>() @@ -27,7 +27,7 @@ class NodeTest { } @Test - fun `is able to get all components of the same type`() { + fun is_able_to_get_all_components_of_the_same_type() { val node = Node( null, @@ -54,7 +54,7 @@ class NodeTest { } @Test - fun `returns true if a component of that type exists`() { + fun returns_true_if_a_component_of_that_type_exists() { val node = Node( null, @@ -66,7 +66,7 @@ class NodeTest { } @Test - fun `returns false if a component of that type does not exist`() { + fun returns_false_if_a_component_of_that_type_does_not_exist() { val node = Node( null, @@ -78,7 +78,7 @@ class NodeTest { } @Test - fun `it copies the Node by correctly mapping the underlying components`() { + fun it_copies_the_Node_by_correctly_mapping_the_underlying_components() { val node = Node( null, listOf( @@ -109,7 +109,7 @@ class NodeTest { } @Test - fun `it creates a copy of the query with overwritten database in the components`() { + fun it_creates_a_copy_of_the_query_with_overwritten_database_in_the_components() { val node = Node( null, listOf( @@ -123,42 +123,23 @@ class NodeTest { assertTrue( node.component>()?.let { it.reference is HasCollectionReference.OnlyCollection - } - ?: false + } == true ) assertTrue( nodeReference ?.let { - it.reference is HasCollectionReference.Known - } - ?: false + it.reference is Known + } == true ) assertEquals( "foo", - (nodeReference?.reference as HasCollectionReference.Known).namespace.database, + (nodeReference.reference as Known).namespace.database, ) } - @MethodSource("validComponents") - @ParameterizedTest - fun `does support the following component`( - component: Component, - componentClass: Class, - ) { - val node = - Node( - null, - listOf( - component, - ), - ) - - assertNotNull(node.component(componentClass)) - } - @Test - fun `adds target cluster if does not exist`() { + fun adds_target_cluster_if_does_not_exist() { val targetCluster = HasTargetCluster(Version.parse("7.0.0")) val query = Node(Unit, emptyList()).withTargetCluster(targetCluster) @@ -166,7 +147,7 @@ class NodeTest { } @Test - fun `removes old target cluster and adds a new one`() { + fun removes_old_target_cluster_and_adds_a_new_one() { val oldCluster = HasTargetCluster(Version.parse("5.0.0")) val targetCluster = HasTargetCluster(Version.parse("7.0.0")) val query = Node(Unit, listOf(oldCluster)).withTargetCluster(targetCluster) @@ -175,12 +156,12 @@ class NodeTest { } @Test - fun `on calling queryWithInjectedCollectionSchema it returns a Node with injected CollectionSchema if CollectionReference is Known`() { + fun on_calling_queryWithInjectedCollectionSchema_it_returns_a_Node_with_injected_CollectionSchema_if_CollectionReference_is_Known() { val node = Node( null, listOf( HasCollectionReference( - HasCollectionReference.Known( + Known( 1, 2, Namespace("db", "coll"), @@ -198,51 +179,4 @@ class NodeTest { val nodeReference = modifiedNode.component>() assertEquals(schema, (nodeReference?.reference as? Known<*>)?.schema) } - - companion object { - @JvmStatic - fun validComponents(): Array> = - arrayOf( - arrayOf(HasFilter(emptyList()), HasFilter::class.java), - arrayOf( - HasCollectionReference(HasCollectionReference.Unknown), - HasCollectionReference::class.java - ), - arrayOf( - HasCollectionReference( - HasCollectionReference.Known(1, 2, Namespace("db", "coll")) - ), - HasCollectionReference::class.java, - ), - arrayOf( - HasCollectionReference(HasCollectionReference.OnlyCollection(1, "coll")), - HasCollectionReference::class.java, - ), - arrayOf( - HasFieldReference(HasFieldReference.Unknown), - HasFieldReference::class.java - ), - arrayOf( - HasFieldReference(HasFieldReference.FromSchema(null, "abc")), - HasFieldReference::class.java - ), - arrayOf( - HasValueReference(HasValueReference.Unknown), - HasValueReference::class.java - ), - arrayOf( - HasValueReference(HasValueReference.Constant(null, 123, BsonInt32)), - HasValueReference::class.java - ), - arrayOf( - HasValueReference(HasValueReference.Runtime(null, BsonInt32)), - HasValueReference::class.java - ), - arrayOf( - HasValueReference(HasValueReference.Unknown), - HasValueReference::class.java - ), - arrayOf(Named(Name.EQ), Named::class.java), - ) - } } diff --git a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReferenceTest.kt b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReferenceTest.kt similarity index 86% rename from packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReferenceTest.kt rename to packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReferenceTest.kt index 5d40f44d3..b3e47e341 100644 --- a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReferenceTest.kt +++ b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/components/HasCollectionReferenceTest.kt @@ -3,13 +3,13 @@ package com.mongodb.jbplugin.mql.components import com.mongodb.jbplugin.mql.BsonObject import com.mongodb.jbplugin.mql.CollectionSchema import com.mongodb.jbplugin.mql.Namespace -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue class HasCollectionReferenceTest { @Test - fun `when the underlying reference is Known, it creates a copy with the database modified`() { + fun when_the_underlying_reference_is_Known_it_creates_a_copy_with_the_database_modified() { val dbRef = 1 val collRef = 2 @@ -38,7 +38,7 @@ class HasCollectionReferenceTest { } @Test - fun `when the underlying reference is Known, it creates a copy with the collection schema injected`() { + fun when_the_underlying_reference_is_Known_it_creates_a_copy_with_the_collection_schema_injected() { val dbRef = 1 val collRef = 2 @@ -71,7 +71,7 @@ class HasCollectionReferenceTest { } @Test - fun `when the underlying reference is OnlyCollection, it converts it to Known`() { + fun when_the_underlying_reference_is_OnlyCollection_it_converts_it_to_Known() { val collRef = 1 val collectionReference = HasCollectionReference( HasCollectionReference.OnlyCollection( @@ -96,7 +96,7 @@ class HasCollectionReferenceTest { } @Test - fun `when the underlying reference is Unknown, it does nothing`() { + fun when_the_underlying_reference_is_Unknown_it_does_nothing() { val collectionReference = HasCollectionReference( HasCollectionReference.Unknown ) diff --git a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/components/NamedTest.kt b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/components/NamedTest.kt similarity index 67% rename from packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/components/NamedTest.kt rename to packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/components/NamedTest.kt index eecb87f94..e1a6c6d31 100644 --- a/packages/mongodb-mql-model/src/test/kotlin/com/mongodb/jbplugin/mql/components/NamedTest.kt +++ b/packages/mongodb-mql-model/src/commonTest/kotlin/com/mongodb/jbplugin/mql/components/NamedTest.kt @@ -2,313 +2,313 @@ package com.mongodb.jbplugin.mql.components import com.mongodb.jbplugin.mql.BsonAny import com.mongodb.jbplugin.mql.BsonType -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test +import kotlin.test.Test +import kotlin.test.assertEquals class NamedTest { @Test - fun `all operation should be irrelevant`() { + fun all_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.ALL, BsonAny) } @Test - fun `and operation should be irrelevant`() { + fun and_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.AND, BsonAny) } @Test - fun `bitsAllClear operation should be irrelevant`() { + fun bitsAllClear_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.BITS_ALL_CLEAR, BsonAny) } @Test - fun `bitsAllSet operation should be irrelevant`() { + fun bitsAllSet_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.BITS_ALL_SET, BsonAny) } @Test - fun `bitsAnyClear operation should be irrelevant`() { + fun bitsAnyClear_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.BITS_ANY_CLEAR, BsonAny) } @Test - fun `bitsAnySet operation should be irrelevant`() { + fun bitsAnySet_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.BITS_ANY_SET, BsonAny) } @Test - fun `combine operation should be irrelevant`() { + fun combine_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.COMBINE, BsonAny) } @Test - fun `elementMatch operation should be irrelevant`() { + fun elementMatch_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.ALL, BsonAny) } @Test - fun `eq operation should be equality`() { + fun eq_operation_should_be_equality() { assertThatOperationHasRoleForType(QueryRole.EQUALITY, Name.EQ, BsonAny) } @Test - fun `exists operation should be irrelevant`() { + fun exists_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.EXISTS, BsonAny) } @Test - fun `geoIntersects operation should be range`() { + fun geoIntersects_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.GEO_INTERSECTS, BsonAny) } @Test - fun `geoWithin operation should be range`() { + fun geoWithin_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.GEO_WITHIN, BsonAny) } @Test - fun `geoWithinBox operation should be range`() { + fun geoWithinBox_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.GEO_WITHIN_BOX, BsonAny) } @Test - fun `geoWithinCenter operation should be range`() { + fun geoWithinCenter_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.GEO_WITHIN_CENTER, BsonAny) } @Test - fun `geoWithinCenterSphere operation should be range`() { + fun geoWithinCenterSphere_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.GEO_WITHIN_CENTER_SPHERE, BsonAny) } @Test - fun `geoWithinPolygon operation should be range`() { + fun geoWithinPolygon_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.GEO_WITHIN_POLYGON, BsonAny) } @Test - fun `gt operation should be range`() { + fun gt_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.GT, BsonAny) } @Test - fun `gte operation should be range`() { + fun gte_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.GTE, BsonAny) } @Test - fun `in operation should be range`() { + fun in_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.IN, BsonAny) } @Test - fun `inc operation should be irrelevant`() { + fun inc_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.INC, BsonAny) } @Test - fun `lt operation should be range`() { + fun lt_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.LT, BsonAny) } @Test - fun `lte operation should be range`() { + fun lte_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.LTE, BsonAny) } @Test - fun `ne operation should be range`() { + fun ne_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.NE, BsonAny) } @Test - fun `near operation should be range`() { + fun near_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.NEAR, BsonAny) } @Test - fun `nearSphere operation should be range`() { + fun nearSphere_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.NEAR_SPHERE, BsonAny) } @Test - fun `nin operation should be range`() { + fun nin_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.NIN, BsonAny) } @Test - fun `nor operation should be union`() { + fun nor_operation_should_be_union() { assertThatOperationHasRoleForType(QueryRole.UNION, Name.NOR, BsonAny) } @Test - fun `not operation should be irrelevant`() { + fun not_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.NOT, BsonAny) } @Test - fun `or operation should be union`() { + fun or_operation_should_be_union() { assertThatOperationHasRoleForType(QueryRole.UNION, Name.OR, BsonAny) } @Test - fun `regex operation should be range`() { + fun regex_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.REGEX, BsonAny) } @Test - fun `set operation should be irrelevant`() { + fun set_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.SET, BsonAny) } @Test - fun `setOnInsert operation should be irrelevant`() { + fun setOnInsert_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.SET_ON_INSERT, BsonAny) } @Test - fun `size operation should be irrelevant`() { + fun size_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.SIZE, BsonAny) } @Test - fun `text operation should be range`() { + fun text_operation_should_be_range() { assertThatOperationHasRoleForType(QueryRole.RANGE, Name.TEXT, BsonAny) } @Test - fun `type operation should be irrelevant`() { + fun type_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.TYPE, BsonAny) } @Test - fun `unset operation should be irrelevant`() { + fun unset_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.UNSET, BsonAny) } @Test - fun `match operation should be irrelevant`() { + fun match_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.MATCH, BsonAny) } @Test - fun `project operation should be irrelevant`() { + fun project_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.PROJECT, BsonAny) } @Test - fun `include operation should be irrelevant`() { + fun include_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.INCLUDE, BsonAny) } @Test - fun `exclude operation should be irrelevant`() { + fun exclude_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.EXCLUDE, BsonAny) } @Test - fun `group operation should be irrelevant`() { + fun group_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.GROUP, BsonAny) } @Test - fun `sum operation should be irrelevant`() { + fun sum_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.SUM, BsonAny) } @Test - fun `avg operation should be irrelevant`() { + fun avg_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.AVG, BsonAny) } @Test - fun `first operation should be irrelevant`() { + fun first_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.FIRST, BsonAny) } @Test - fun `last operation should be irrelevant`() { + fun last_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.LAST, BsonAny) } @Test - fun `top operation should be irrelevant`() { + fun top_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.TOP, BsonAny) } @Test - fun `topN operation should be irrelevant`() { + fun topN_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.TOP_N, BsonAny) } @Test - fun `bottom operation should be irrelevant`() { + fun bottom_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.BOTTOM, BsonAny) } @Test - fun `bottomN operation should be irrelevant`() { + fun bottomN_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.BOTTOM_N, BsonAny) } @Test - fun `max operation should be sort`() { + fun max_operation_should_be_sort() { assertThatOperationHasRoleForType(QueryRole.SORT, Name.MAX, BsonAny) } @Test - fun `min operation should be sort`() { + fun min_operation_should_be_sort() { assertThatOperationHasRoleForType(QueryRole.SORT, Name.MIN, BsonAny) } @Test - fun `push operation should be irrelevant`() { + fun push_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.PUSH, BsonAny) } @Test - fun `pull operation should be irrelevant`() { + fun pull_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.PULL, BsonAny) } @Test - fun `pullAll operation should be irrelevant`() { + fun pullAll_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.PULL_ALL, BsonAny) } @Test - fun `pop operation should be irrelevant`() { + fun pop_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.POP, BsonAny) } @Test - fun `addToSet operation should be irrelevant`() { + fun addToSet_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.ADD_TO_SET, BsonAny) } @Test - fun `sort operation should be sort`() { + fun sort_operation_should_be_sort() { assertThatOperationHasRoleForType(QueryRole.SORT, Name.SORT, BsonAny) } @Test - fun `ascending operation should be sort`() { + fun ascending_operation_should_be_sort() { assertThatOperationHasRoleForType(QueryRole.SORT, Name.ASCENDING, BsonAny) } @Test - fun `descending operation should be sort`() { + fun descending_operation_should_be_sort() { assertThatOperationHasRoleForType(QueryRole.SORT, Name.DESCENDING, BsonAny) } @Test - fun `addFields operation should be irrelevant`() { + fun addFields_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.ADD_FIELDS, BsonAny) } @Test - fun `unwind operation should be irrelevant`() { + fun unwind_operation_should_be_irrelevant() { assertThatOperationHasRoleForType(QueryRole.IRRELEVANT, Name.UNWIND, BsonAny) } diff --git a/packages/mongodb-mql-model/src/jsMain/kotlin/com/mongodb/jbplugin/mql/BsonType.js.kt b/packages/mongodb-mql-model/src/jsMain/kotlin/com/mongodb/jbplugin/mql/BsonType.js.kt new file mode 100644 index 000000000..4582c40d2 --- /dev/null +++ b/packages/mongodb-mql-model/src/jsMain/kotlin/com/mongodb/jbplugin/mql/BsonType.js.kt @@ -0,0 +1,42 @@ +package com.mongodb.jbplugin.mql + +import kotlin.reflect.KClass + +actual inline fun T.toBsonType(): BsonType { + return this?.runtimeBsonType(T::class) ?: BsonNull +} + +fun Any.runtimeBsonType(klass: KClass<*>): BsonType { + return when (klass) { + Int::class -> BsonInt32 + Long::class -> BsonInt64 + Float::class -> BsonDouble + Double::class -> BsonDouble + String::class -> BsonAnyOf(BsonNull, BsonString) + Array::class -> BsonArray(schema = BsonAny) + Boolean::class -> BsonBoolean + Map::class -> BsonObject( + (this as Map<*, *>).map { (k, v) -> k.toString() to v.toBsonType() } as Map + ) + else -> when (jsTypeOf(this)) { + "boolean" -> BsonBoolean + "string" -> BsonAnyOf(BsonNull, BsonString) + "number" -> if (isInteger(this)) { + BsonInt32 + } else { + BsonDouble + } + else -> BsonAny + } + } +} + +// We are inlining JavaScript here, so it's actually used. +private fun jsTypeOf(@Suppress("UNUSED_PARAMETER") value: Any): String { + return js("typeof value") +} + +// We are inlining JavaScript here, so it's actually used. +private fun isInteger(@Suppress("UNUSED_PARAMETER") value: Any): Boolean { + return js("Number.isInteger(value)") +} diff --git a/packages/mongodb-mql-model/src/jvmMain/kotlin/com/mongodb/jbplugin/mql/BsonType.jvm.kt b/packages/mongodb-mql-model/src/jvmMain/kotlin/com/mongodb/jbplugin/mql/BsonType.jvm.kt new file mode 100644 index 000000000..f5aa345d0 --- /dev/null +++ b/packages/mongodb-mql-model/src/jvmMain/kotlin/com/mongodb/jbplugin/mql/BsonType.jvm.kt @@ -0,0 +1,84 @@ +package com.mongodb.jbplugin.mql + +import org.bson.types.Decimal128 +import org.bson.types.ObjectId +import java.math.BigDecimal +import java.math.BigInteger +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.Date +import java.util.UUID +import kotlin.collections.get +import kotlin.jvm.javaClass + +/** + * Returns the inferred BSON type of the current Java class, considering it's nullability. + * + * @param value + */ +fun toBsonType(klass: Class?, value: T? = null): BsonType { + return when (klass) { + null -> BsonNull + Float::class.javaPrimitiveType -> BsonDouble + Float::class.javaObjectType -> BsonAnyOf(BsonNull, BsonDouble) + Double::class.javaPrimitiveType -> BsonDouble + Double::class.javaObjectType -> BsonAnyOf(BsonNull, BsonDouble) + Boolean::class.javaPrimitiveType -> BsonBoolean + Boolean::class.javaObjectType -> BsonAnyOf(BsonNull, BsonBoolean) + Short::class.javaPrimitiveType -> BsonInt32 + Short::class.javaObjectType -> BsonAnyOf(BsonNull, BsonInt32) + Int::class.javaPrimitiveType -> BsonInt32 + Int::class.javaObjectType -> BsonAnyOf(BsonNull, BsonInt32) + Long::class.javaPrimitiveType -> BsonInt64 + Long::class.javaObjectType -> BsonAnyOf(BsonNull, BsonInt64) + CharSequence::class.java, String::class.java -> BsonAnyOf(BsonNull, BsonString) + Date::class.java, Instant::class.java, LocalDate::class.java, LocalDateTime::class.java -> + BsonAnyOf(BsonNull, BsonDate) + UUID::class.java -> BsonAnyOf(BsonNull, BsonUUID) + ObjectId::class.java -> BsonAnyOf(BsonNull, BsonObjectId) + BigInteger::class.java -> BsonAnyOf(BsonNull, BsonInt64) + BigDecimal::class.java -> BsonAnyOf(BsonNull, BsonDecimal128) + Decimal128::class.java -> BsonAnyOf(BsonNull, BsonDecimal128) + else -> + if (klass.isEnum) { + val variants = klass.enumConstants.map { it.toString() }.toSet() + BsonEnum(variants) + } else if (Collection::class.java.isAssignableFrom(klass) || + Array::class.java.isAssignableFrom(klass) + ) { + return BsonAnyOf(BsonNull, BsonArray(BsonAny)) // types are lost at runtime + } else if (Map::class.java.isAssignableFrom(klass)) { + if (value == null) { + return BsonAnyOf(BsonNull, BsonAny) + } else { + val fields = Map::class.java.cast(value).entries.associate { + it.key.toString() to toBsonType(it.value?.javaClass, it.value) + } + return BsonAnyOf(BsonNull, BsonObject(fields)) + } + } else if (Class::class.java.isAssignableFrom(klass)) { + return BsonAnyOf(BsonNull, BsonObject(emptyMap())) + } else { + val fields = + klass.declaredFields.associate { + it.name to toBsonType(it.type) + } + + return BsonAnyOf(BsonNull, BsonObject(fields)) + } + } +} + +fun primitiveOrWrapper(example: Class<*>): Class<*> { + val type = runCatching { example.getField("TYPE").get(null) as? Class<*> }.getOrNull() + return type ?: example +} + +actual inline fun T.toBsonType(): BsonType { + if (this == null) { + return BsonNull + } + + return toBsonType(this.javaClass, this) +} diff --git a/packages/mongodb-mql-model/src/jvmTest/kotlin/com/mongodb/jbplugin/mql/BsonTypeTestJvm.kt b/packages/mongodb-mql-model/src/jvmTest/kotlin/com/mongodb/jbplugin/mql/BsonTypeTestJvm.kt new file mode 100644 index 000000000..e9416ae7b --- /dev/null +++ b/packages/mongodb-mql-model/src/jvmTest/kotlin/com/mongodb/jbplugin/mql/BsonTypeTestJvm.kt @@ -0,0 +1,60 @@ +package com.mongodb.jbplugin.mql + +import com.mongodb.jbplugin.mql.toBsonType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource +import java.math.BigDecimal +import java.math.BigInteger +import java.time.Instant +import java.time.LocalDate +import java.time.LocalDateTime +import java.util.Date + +class BsonTypeTestJvm { + @ParameterizedTest + @MethodSource("java to bson") + fun `should map correctly all java types`( + javaClass: Class<*>, + expected: BsonType, + ) { + assertEquals(expected, toBsonType(javaClass)) + } + + companion object { + @JvmStatic + fun `java to bson`(): Array = + arrayOf( + arrayOf(Double::class.javaObjectType, BsonAnyOf(BsonNull, BsonDouble)), + arrayOf(Double::class.javaPrimitiveType, BsonDouble), + arrayOf(CharSequence::class.java, BsonAnyOf(BsonNull, BsonString)), + arrayOf(String::class.java, BsonAnyOf(BsonNull, BsonString)), + arrayOf(Boolean::class.javaObjectType, BsonAnyOf(BsonNull, BsonBoolean)), + arrayOf(Boolean::class.javaPrimitiveType, BsonBoolean), + arrayOf(Date::class.java, BsonAnyOf(BsonNull, BsonDate)), + arrayOf(Instant::class.java, BsonAnyOf(BsonNull, BsonDate)), + arrayOf(LocalDate::class.java, BsonAnyOf(BsonNull, BsonDate)), + arrayOf(LocalDateTime::class.java, BsonAnyOf(BsonNull, BsonDate)), + arrayOf(Int::class.javaObjectType, BsonAnyOf(BsonNull, BsonInt32)), + arrayOf(Int::class.javaPrimitiveType, BsonInt32), + arrayOf(BigInteger::class.java, BsonAnyOf(BsonNull, BsonInt64)), + arrayOf(BigDecimal::class.java, BsonAnyOf(BsonNull, BsonDecimal128)), + arrayOf(ArrayList::class.java, BsonAnyOf(BsonNull, BsonArray(BsonAny))), + arrayOf( + ExampleClass::class.java, + BsonAnyOf( + BsonNull, + BsonObject( + mapOf( + "field" to BsonAnyOf(BsonNull, BsonString), + ), + ), + ), + ), + ) + + data class ExampleClass( + val field: String, + ) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index e5f7984cf..65c1b78bc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,6 +1,7 @@ rootProject.name = "mongodb-jetbrains-plugin" include( + "packages:mongodb-design-system", "packages:mongodb-mql-model", "packages:mongodb-dialects", "packages:mongodb-dialects:java-driver",