Skip to content

Kotlin type-safe queries and updates are not type-safe at all #4798

@pmatysek

Description

@pmatysek

Hi,
spring-data-mongodb supports in Kotlin typed queries and recently introduced with my help typed updates too (#3028). While this API is handy because there's no need to keep field names in constants, it has one hidden drawback or bug - it's not type-safe at any manner. Even though docs (that one I wrote too) and its construction suggest it undeniably should be.

Problem explanation

I will show it by an example:

data class Book(val title: String)
val typed = Book::title isEqualTo 1234 // it should be prohibited by the compiler and it's not for now

isEqualTo infix fun is defined in a way that as I said undeniably suggests that it should be type-safe:

infix fun <T> KProperty<T>.isEqualTo(value: T) =
		Criteria(this.toDotPath()).isEqualTo(value)

Why the type of value is not checked properly by a compiler? The culprit is declaration-site variance Kotlin mechanism. And especially the fact that KProperty interface has its generic type marked with out variance annotation:

public expect interface KProperty<out V>

Possible solutions

While the problem seems to be complicated and rather lies on the Kotlin language side, there is a simple (but tricky) solution. JetBrains team has internal annotation (in kotlin.internal package) @OnlyInputTypes designed for cases like this. The solution would be as simple as just using it on a generic type:

infix fun <@OnlyInputTypes T> KProperty<T>.isEqualTo(value: T) =
		Criteria(this.toDotPath()).isEqualTo(value)

data class Book(val title: String)
val typed = Book::title isEqualTo 1234 // now isEqualTo is type-safe so this code will not compile

However, this solution has one drawback - while this annotation is in kotlin.internal package it can't be used directly outside the Kotlin project. There are two workarounds to use it, but both are tricky.
First:

  • create your own copy of @OnlyInputTypes annotation which is identical to an original one, ie. the same package, the same name
  • add -Xallow-kotlin-package compiler arg to bypass Only the Kotlin standard library is allowed to use the 'kotlin' package error

Second:

  • @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") before each usage or on the whole file: @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")

Both of them are rather workarounds with some drawbacks. Another drawback of fixing this bug is the fact that it breaks the backward compatibility of this API - some code will stop compiling (but let's say very error-prone code that uses this API in the wrong way)

I have prepared code with these solutions and can post PRs later on.
Moreover, I did some research and saw that ex. KMongo uses @OnlyInputTypes too: Litote/kmongo@413539e

What are your thoughts about that? Do you think that introducing a fix with @OnlyInputTypes would be OK?

Resources

https://youtrack.jetbrains.com/issue/KT-13198
https://discuss.kotlinlang.org/t/restrict-type-by-receiver/5492
https://kotlinlang.org/docs/generics.html
https://stackoverflow.com/questions/54779553/kotlin-generic-constraints-require-param-to-be-of-same-type-as-other <-- there is workaround with wrapping KProperty into own type, but I can't see a way to use it in a clean and seamless way

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions