Litote / kmongo

[deprecated] KMongo - a Kotlin toolkit for Mongo
https://litote.org/kmongo/
Apache License 2.0
782 stars 75 forks source link

Typed queries on subclassed subdocument fields #276

Closed maxskorr closed 3 years ago

maxskorr commented 3 years ago

Hi!

Let's consider this set of classes:

sealed class Shape(
  val type: String
) {
  data class Circle(
    val radius: Int
  ): Shape("Circle")

  data class Square(
    val side: Int
  ): Shape("Square")
}

data class Box(
  val shape: Shape
)

I would like to be able to run this sort o queries:

boxes.updateMany(
  Box::shape / Shape::type eq "Circle",
  setValue(Box::shape / Circle::radius, 0)
)

However I am getting:

None of the following functions can be called with the arguments supplied. KProperty1<Box, T1>.div(KProperty1<T1, Int?>)   where T1 cannot be inferred; T0 = Box, T2 = Int for   operator fun <T0, T1, T2> KProperty1<T0, T1?>.div(p2: KProperty1<T1, T2?>): KProperty1<T0, T2?> defined in org.litote.kmongo KProperty1<Box, Iterable?>.div(KProperty1<Shape.Circle, Int?>)   where T0 = MenuItemModifierSet, T1 = Shape.Circle, T2 = Int for   operator fun <T0, T1, T2> KProperty1<T0, Iterable?>.div(p2: KProperty1<T1, T2?>): KProperty1<T0, T2?> defined in org.litote.kmongo KProperty1<Box, Map<out K, Shape.Circle>?>.div(KProperty1<Shape.Circle, Int?>)   where K cannot be inferred; T0 = Box, T1 = Shape.Circle, T2 = Int for   operator fun <T0, K, T1, T2> KProperty1<T0, Map<out K, T1>?>.div(p2: KProperty1<T1, T2?>): KProperty1<T0, T2?> defined in org.litote.kmongo

Is there any way to implement a typed query for the scenario above?

zigzago commented 3 years ago

Thank you for reporting. This is a bug. You can workaround it by adding temporarily your own extension:

operator fun <T0, T1, T2> KProperty1<T0, T1?>.div(p2: KProperty1<out T1, T2?>): KProperty1<T0, T2?> = KPropertyPath(this, p2)

vngantk commented 2 years ago

This change destroys the type safety for the div() operator method. Supposing there is another field in Box, say color as follows:

sealed class Shape(
  val type: String
) {
  data class Circle(
    val radius: Int
  ): Shape("Circle")

  data class Square(
    val side: Int
  ): Shape("Square")
}

data class Box(
  val shape: Shape,
  val color: String
)

It would allow the following programming mistake to happen without being detected as compilation error:

boxes.updateMany(
  Box::color / Shape::type eq "Circle",
  setValue(Box::shape / Circle::radius, 0)
)

The type safety for the p2 parameter in the div() method is broken.

As the use case mentioned by @aivel is relatively rare, I would prefer we revert back to the original version because that covers most of the use cases we normally need.

To accommodate what @aivel needs, we can add an extension method called unsafeCast() for KProperty1 to bypass the type checking:

@Suppress("UNCHECKED_CAST")
fun <T, V> KProperty1<*, V>.unsafeCast(): KProperty1<T, V> = this as KProperty1<T, V>

The original updateMany can be changed as follows:

boxes.updateMany(
  Box::shape / Shape::type eq "Circle",
  setValue(Box::shape / Circle::radius.unsafeCast(), 0)
)