Skip to content

io.ebean plugin 16.6.0 enhancer no longer initializes Kotlin query-bean lateinit fields #43

Closed

Description

@mschneider-lise

After upgrading the io.ebean Gradle plugin from 16.4.0 → 16.6.0 (ebean runtime + kotlin-querybean-generator unchanged at 16.6.0), accessing a fetched association on a generated Kotlin query bean throws at runtime:

  kotlin.UninitializedPropertyAccessException: lateinit property permissions has not been initialized
      at de.worldinsight.wision.auth.roles.query.QRoleEntity.getPermissions(QRoleEntity.kt:38)
      at ...GetAllPermissionsByRoleDbPort.getAllByRole$lambda$1(GetAllPermissionsByRoleDbPort.kt:19)

The generated query bean declares its association/property fields as lateinit var and never assigns them in the generated source — they are expected to be initialized by the build-time enhancer (ebeanEnhance task). With 16.6.0 the enhancer runs (> Task :ebeanEnhance) but no longer initializes these Kotlin lateinit fields, so the first access throws.

Generated query bean (relevant excerpt)

QRoleEntity is produced by io.ebean:kotlin-querybean-generator:16.6.0. The fields are lateinit and unassigned in the generated source — initialization is the enhancer's job:

  @io.ebean.typequery.Generated("io.ebean.querybean.kotlin-generator")
  @io.ebean.typequery.TypeQueryBean("v1")
  class QRoleEntity : io.ebean.typequery.QueryBean<RoleEntity, QRoleEntity> {
    lateinit var id: PLong<QRoleEntity>
    lateinit var name: PString<QRoleEntity>
    lateinit var users: QUserEntity.AssocMany<QRoleEntity>
    lateinit var permissions: QPermissionEntity.AssocMany<QRoleEntity>   // line 38 — throws

    constructor() : super(RoleEntity::class.java)
    ...
  }

Source entity:

  @Entity
  @Table(name = "roles")
  class RoleEntity(
      id: Long? = null,
      @Column var name: String,
      @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
      var users: MutableList<UserEntity> = mutableListOf(),
      @ManyToMany(fetch = FetchType.LAZY)
      @JoinTable(
          name = "roles_permissions",
          joinColumns = [JoinColumn(name = "role_id")],
          inverseJoinColumns = [JoinColumn(name = "permission_id")]
      )
      var permissions: MutableList<PermissionEntity> = mutableListOf()
  ) : AuditableEntity(id)

Triggering query (lambda receiver is the query bean; permissions is the unset lateinit):

  roleRepository.findByIdOrThrow(roleId, { RoleNotFoundException(roleId) }) {
      permissions.fetch()   // QRoleEntity.permissions — UninitializedPropertyAccessException
  }

Environment

  • io.ebean Gradle plugin: 16.6.0 (broken) vs 16.4.0 (works)
  • ebean runtime (ebean-bom) + kotlin-querybean-generator: 16.6.0 (unchanged across both)
  • Kotlin: 2.4.0 (also reproduced/cleared on 2.3.20 per table), via kotlin("jvm") + kotlin("kapt")
  • Gradle: 9.5.1
  • JDK: Temurin 25.0.3 (LTS); Kotlin jvmTarget = 25, Java source/target = 25
  • Build cache: reproduced with clean (not a stale-artifact issue)

Expectation

The 16.6.0 enhancer should initialize the Kotlin query-bean lateinit fields during ebeanEnhance, as 16.4.0 does — accessing QRoleEntity.permissions after enhancement should not throw.

Activity

  1. rbygrave commented on Jun 19, 2026

    @rbygrave
    Contributor

    Looks like it could be ebean-orm/ebean-agent#234


    Root Cause

    The regression from 16.4.0 → 16.5.0+ is in ebean-agent, not the plugin code itself.

    What changed: The new EbeanEnhanceTask (introduced via PR #41) captures sourceSet.output.classesDirs at Gradle configuration time (afterEvaluate). The Kotlin kapt plugin may not have registered its output directory — which contains META-INF/ebean-generated-info.mf — into classesDirs at that point. The old plugin (16.4.0) evaluated classesDirs inside a doLast hook at task execution time, when all kapt outputs were already registered.

    Effect (chain of failures):

    1. AgentManifest can't find ebean-generated-info.mf → entityPackages is empty
    2. DetectQueryBean.isQueryBean("QRoleEntity") iterates empty entityPackages → returns false
    3. AgentManifest.isDetectQueryBean("QRoleEntity") → false
    4. EnhanceContext.isQueryBean() returns false immediately (the null-fallback is unreachable)
    5. MethodAdapter does not replace GETFIELD permissions with _permissions()
    6. Original Kotlin getter body runs → null-check on uninitialised lateinit field → UninitializedPropertyAccessException

    Fix

    File: ebean-agent/src/main/java/io/ebean/enhance/querybean/DetectQueryBean.java

    Added an empty-packages fallback in isQueryBean(): when entityPackages is empty (manifest not found), trust naming convention alone — any class in a .../query/Q... or .../query/assoc/Q... package is treated as a query bean. This is consistent with FilterQueryBean.detectOnAll which already applies the same logic.

  2. rbygrave commented on Jun 19, 2026

    @rbygrave
    Contributor

    Try plugin version 16.11.1 to confirm if that was the correct fix

  3. mschneider-lise commented on Jun 19, 2026

    @mschneider-lise
    Author

    Yes, 16.11.1 has fixed this. Thank you.

  4. added this to the 16.11.1 milestone on Jun 22, 2026
  5. added theissue type on Jun 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions