Skip to content

Query.getId() should be @Nullable #3674

@Incanus3

Description

@Incanus3

Hi guys. We've just encountered quite a problem when trying to override Query.getId() in kotlin. Since the whole Query class is marked with @NullMarked, all values are automatically considered @NonNull, but in some cases Query.getId() actually does return null, which is a problem, because

  • if we override it as override fun getId(): Any = query.getId() we get a NullPointerException when query.getId() returns null, but since the kotlin compiler understands jspecify annotations, it doesn't allow us to override it as fun getId(): Any? because "Return type of 'getId' is not a subtype of the return type of the overridden member 'fun getId(): Any'".

Example code

// the problematic code

class QueryWrapper<T : Any>(
    private val query: Query<T>,
    private val databaseService: DatabaseService,
) : SpiQuery<T> {
    override fun getId(): Any = query.getId() // <---

    override fun findList(): List<T> = withExceptionHandling { query.findList() }

    // ...

    private fun <T> withExceptionHandling(operation: () -> T): T = try {
        operation()
    } catch (e: Exception) {
        if (e is PersistenceException || e is SQLException) {
            databaseService.setStatus(ServiceStatus.Broken(e))
        }

        throw e
    }
}

// test entities

@Entity
@Suppress("unused")
class TestObject {
    @Id
    val id: String = UUID.randomUUID().toString()
}

@Entity
@Suppress("unused")
class TestConnector {
    @Id
    val id: String = UUID.randomUUID().toString()

    private var sourceId: Long? = null
    private var targetId: Long? = null

    @JsonIgnore
    @ManyToOne
    @JoinColumn(insertable = false, updatable = false)
    var source: TestObject? = null

    @JsonIgnore
    @ManyToOne(optional = false)
    @JoinColumn(insertable = false, updatable = false)
    var target: TestObject? = null
}

// test

class DatabaseServiceTest {
    private val dsConfig = DataSourceConfig().also {
        it.url = "jdbc:postgresql://localhost:123/xxx"
        it.driver = "org.postgresql.Driver"
        it.username = "xxx"
        it.password = "xxx"
        it.platform = Platform.POSTGRES.name.lowercase()

        it.isFailOnStart = false
    }

    private val dbConfig = DatabaseConfig().also {
        it.isDefaultServer = false
        it.databasePlatformName = dsConfig.platform

        it.setSkipDataSourceCheck(true)
        it.setDataSourceConfig(dsConfig)

        it.addClass(TestObject::class.java)
        it.addClass(TestConnector::class.java)
    }

    private val service = create(dbConfig).asService()

    @Test
    fun `getId works`() {
        val objectQuery = QTestObject(service).where().id.eq("1")
        val connectionQuery = QTestConnector(service).where()
            .sourceId.isIn(objectQuery.select(QTestObject._alias.id).query())

        // this fails with "getId(...) must not be null"
        connectionQuery.findList()
    }
}

If I am not mistaken, this would be fixed if @Nullable annotation was added to the getId() method.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions