Skip to content

Commit

Permalink
chore: define AST tree for the query model INTELLIJ-18 (#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
kmruiz authored Jul 4, 2024
1 parent f4674b4 commit 0cadc3e
Show file tree
Hide file tree
Showing 22 changed files with 609 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import com.mongodb.ConnectionString
import com.mongodb.client.MongoClient
import com.mongodb.client.MongoClients
import com.mongodb.jbplugin.accessadapter.MongoDbDriver
import com.mongodb.jbplugin.accessadapter.Namespace
import com.mongodb.jbplugin.accessadapter.datagrip.DataGripBasedReadModelProvider
import com.mongodb.jbplugin.mql.Namespace
import org.bson.Document
import org.bson.conversions.Bson
import org.junit.jupiter.api.extension.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,21 +75,23 @@ internal abstract class NewConnectionActivatedProbeTest(
}

@RequiresMongoDbCluster(version = MongoDbVersion.V7_0)
internal class NewConnectionActivatedProbeTestForLocalEnvironment : NewConnectionActivatedProbeTest(
isAtlas = false,
isLocalAtlas = false,
isLocalhost = true,
isEnterprise = false,
isGenuine = true,
version = "7.0.11",
)
internal class NewConnectionActivatedProbeTestForLocalEnvironment :
NewConnectionActivatedProbeTest(
isAtlas = false,
isLocalAtlas = false,
isLocalhost = true,
isEnterprise = false,
isGenuine = true,
version = "7.0.12",
)

@RequiresMongoDbCluster(MongoDbTestingEnvironment.LOCAL_ATLAS)
internal class NewConnectionActivatedProbeTestForAtlasCliEnvironment : NewConnectionActivatedProbeTest(
isAtlas = false,
isLocalAtlas = true,
isLocalhost = true,
isEnterprise = true,
isGenuine = true,
version = "7.0.11",
)
internal class NewConnectionActivatedProbeTestForAtlasCliEnvironment :
NewConnectionActivatedProbeTest(
isAtlas = false,
isLocalAtlas = true,
isLocalhost = true,
isEnterprise = true,
isGenuine = true,
version = "7.0.12",
)
1 change: 1 addition & 0 deletions packages/mongodb-access-adapter/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
dependencies {
implementation(project(":packages:mongodb-mql-model"))
implementation(libs.mongodb.driver)
implementation(libs.gson)
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
implementation(libs.owasp.encoder)
implementation(libs.mongodb.driver)
implementation(project(":packages:mongodb-access-adapter"))
implementation(project(":packages:mongodb-mql-model"))

testImplementation("com.jetbrains.intellij.platform:test-framework-junit5:241.15989.155") {
exclude("ai.grazie.spell")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import com.intellij.database.run.ConsoleRunConfiguration
import com.intellij.openapi.project.Project
import com.mongodb.ConnectionString
import com.mongodb.jbplugin.accessadapter.MongoDbDriver
import com.mongodb.jbplugin.accessadapter.Namespace
import com.mongodb.jbplugin.mql.Namespace
import org.bson.conversions.Bson
import org.bson.json.JsonMode
import org.bson.json.JsonWriterSettings
Expand Down Expand Up @@ -195,12 +195,13 @@ internal class DataGripMongoDbDriver(
}

@VisibleForTesting
private fun withActiveConnectionList(fn: (MutableSet<DatabaseConnection>) -> Unit): Unit {
private fun withActiveConnectionList(fn: (MutableSet<DatabaseConnection>) -> Unit) {
runBlocking {
val connectionsManager = DatabaseConnectionManager.getInstance()
val myConnectionsField =
connectionsManager.javaClass
.getDeclaredField("myConnections").apply {
.getDeclaredField("myConnections")
.apply {
isAccessible = true
}
val myConnections = myConnectionsField.get(connectionsManager) as MutableSet<DatabaseConnection>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,13 @@
package com.mongodb.jbplugin.accessadapter

import com.mongodb.ConnectionString
import com.mongodb.jbplugin.mql.Namespace
import org.bson.conversions.Bson
import java.util.*
import kotlin.reflect.KClass
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

/**
* Represents a MongoDB Namespace (db/coll)
*
* @property database
* @property collection
*/
class Namespace private constructor(
val database: String,
val collection: String,
) {
override fun toString(): String = "$database.$collection"

override fun equals(other: Any?): Boolean = other is Namespace && hashCode() == other.hashCode()

override fun hashCode(): Int = Objects.hash(database, collection)

companion object {
operator fun invoke(
database: String,
collection: String,
): Namespace =
Namespace(
database,
collection,
)
}
}

/**
* Represents the MongoDB Driver facade that we will use internally.
* Usually, we won't use this class directly, only in tests. What we
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mongodb.jbplugin.accessadapter

import com.mongodb.jbplugin.mql.Namespace
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

Expand All @@ -25,12 +26,6 @@ class MongoDbDriverTest {
assertEquals("myco\"ll", namespace.collection)
}

@Test
fun `serialises to a valid namespace string`() {
val namespace = Namespace("mydb", "my.cool.col")
assertEquals("mydb.my.cool.col", namespace.toString())
}

@Test
fun `can parse back a serialised namespace`() {
val namespace = Namespace("mydb", "my.cool.col")
Expand Down
3 changes: 3 additions & 0 deletions packages/mongodb-mql-model/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dependencies {
implementation(libs.owasp.encoder)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Represents all supported Bson types. We are not using the ones defined in the driver as we need more information,
* like nullability and composability (for example, a value that can be either int or bool).
*/

package com.mongodb.jbplugin.mql

import java.math.BigDecimal
import java.math.BigInteger
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.*

/**
* Represents any of the valid BSON types.
*/
interface BsonType

/**
* A double (64 bit floating point)
*/
data object BsonDouble : BsonType

/**
* BSON String
*/
data object BsonString : BsonType

/**
* Represents a map of key -> type.
*
* @property schema
*/
data class BsonObject(
val schema: Map<String, BsonType>,
) : BsonType

/**
* Represents the possible types that can be included in an array.
*
* @property schema
*/
data class BsonArray(
val schema: BsonType,
) : BsonType

/**
* Boolean
*/
data object BsonBoolean : BsonType

/**
* Date
*/
data object BsonDate : BsonType

/**
* null / non existing field
*/

data object BsonNull : BsonType

/**
* 32-bit integer
*/

data object BsonInt32 : BsonType

/**
* 64-bit integer
*/
data object BsonInt64 : BsonType

/**
* Decimal128 (128 bit floating point)
*/
data object BsonDecimal128 : BsonType

/**
* This is not a BSON type per se, but need a value for an unknown
* bson type.
*/
data object BsonAny : BsonType

/**
* This is not a BSON type per se, but a schema is dynamic and for a single
* field we can have multiple types of values, so we will map this scenario
* with the AnyOf type.
*
* @property types
*/
data class BsonAnyOf(
val types: List<BsonType>,
) : BsonType {
constructor(vararg types: BsonType) : this(types.toList())
}

/**
* Returns the inferred BSON type of the current Java class, considering it's nullability.
*/
fun Class<*>.toBsonType(): BsonType {
return when (this) {
Double::class.javaPrimitiveType -> BsonDouble
Double::class.javaObjectType -> BsonAnyOf(BsonNull, BsonDouble)
Boolean::class.javaPrimitiveType -> BsonBoolean
Boolean::class.javaObjectType -> BsonAnyOf(BsonNull, BsonBoolean)
Int::class.javaPrimitiveType -> BsonInt32
Int::class.javaObjectType -> BsonAnyOf(BsonNull, BsonInt32)
CharSequence::class.java, String::class.java -> BsonAnyOf(BsonNull, BsonString)
Date::class.java, Instant::class.java, LocalDate::class.java, LocalDateTime::class.java ->
BsonAnyOf(BsonNull, BsonDate)
BigInteger::class.java -> BsonAnyOf(BsonNull, BsonInt64)
BigDecimal::class.java -> BsonAnyOf(BsonNull, BsonDecimal128)
else -> if (Collection::class.java.isAssignableFrom(this)) {
return BsonAnyOf(BsonNull, BsonArray(BsonAny)) // types are lost at runtime
} else {
val fields =
this.declaredFields.associate {
it.name to it.type.toBsonType()
}

return BsonAnyOf(BsonNull, BsonObject(fields))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.mongodb.jbplugin.mql

import org.intellij.lang.annotations.Language
import java.lang.Integer.parseInt

/**
* @property namespace
* @property schema
*/
class CollectionSchema(
val namespace: Namespace,
val schema: BsonObject,
) {
fun typeOf(
@Language("JSONPath") jsonPath: String,
): BsonType {
val splitJsonPath = jsonPath.split('.').toList()
return recursivelyGetType(splitJsonPath, schema)
}

private fun recursivelyGetType(
jsonPath: List<String>,
root: BsonType,
): BsonType {
if (jsonPath.isEmpty()) {
return root
}

val current = jsonPath.first()
val isCurrentNumber = kotlin.runCatching { parseInt(current) }.isSuccess

val listOfOptions = mutableListOf<BsonType>()

when (root) {
is BsonArray -> if (isCurrentNumber) {
val childType = recursivelyGetType(jsonPath.subList(1, jsonPath.size), root.schema)
listOfOptions.add(childType)
} else {
listOfOptions.add(BsonNull)
}
is BsonObject -> {
val objectType = root.schema[jsonPath[0]]
listOfOptions.add(
objectType?.let {
recursivelyGetType(jsonPath.subList(1, jsonPath.size), objectType)
} ?: BsonNull,
)
}
is BsonAnyOf -> listOfOptions.addAll(
root.types.map { recursivelyGetType(jsonPath, it) },
)
else -> listOfOptions.add(BsonNull)
}

return if (listOfOptions.size == 1) {
listOfOptions.first()
} else {
BsonAnyOf(listOfOptions)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.mongodb.jbplugin.mql

import java.util.*

/**
* Represents a MongoDB Namespace (db/coll)
*
* @property database
* @property collection
*/
class Namespace private constructor(
val database: String,
val collection: String,
) {
override fun toString(): String = "$database.$collection"

override fun equals(other: Any?): Boolean = other is Namespace && hashCode() == other.hashCode()

override fun hashCode(): Int = Objects.hash(database, collection)

companion object {
operator fun invoke(
database: String,
collection: String,
): Namespace =
Namespace(
database,
collection,
)
}
}
Loading

0 comments on commit 0cadc3e

Please sign in to comment.