Skip to content

Navigation beyond a Ref degrades to a bind parameter in query builder template fragments #364

Description

@zantvoort

Description

Navigation beyond a Ref foreign key (introduced with the ref graph traversal feature in 1.13.0) works in full SQL templates and in metamodel predicates, but not in query builder template fragments. A navigation-only node interpolated into a where fragment is not resolved to a column; it is compiled as a bind parameter, so the raw metamodel object reaches the JDBC driver.

// Kotlin: UserSurveyLog.user is Ref<User>; the path continues beyond the Ref.
orm.entity<UserSurveyLog>()
    .select(UserSurveyLogCount::class) { "${UserSurveyLog_.user}, COUNT(*)" }
    .where { "DATE(CONVERT_TZ(${UserSurveyLog_.completedTimestamp}, 'UTC', ${UserSurveyLog_.user.userPanel.site.timeZone})) BETWEEN $startDate AND $endDate" }
    .groupBy(UserSurveyLog_.user)
    .resultList
// Java equivalent with the core test model: PetOwnerRef.owner is Ref<Owner>.
orm.entity(PetOwnerRef.class).select()
        .where(raw("\0 = \0", PetOwnerRef_.owner.address.city.name, "Madison"))
        .getResultList();

Actual behavior:

  • MariaDB: java.sql.SQLException: Type com.example.NavigableSiteMetamodel$7 not supported type
  • H2: JdbcSQLDataException: Serialization failed, cause: "java.io.NotSerializableException: st.orm.core.model.NavigableCityMetamodel$2"

The same path works in the predicate form (where(PetOwnerRef_.owner.address.city.name, EQUALS, "Madison")) and in full select templates, so switching between the two query styles silently changes whether the query is valid.

Cause

Two switches on the template fragment path only match Metamodel, while a node beyond a Ref is Navigable but deliberately not Metamodel (value extraction across an unloaded reference is query-only):

  1. QueryModelImpl.resolveElements converts case Metamodel when isColumn() to a Column element; a navigation-only node falls through to the default case and is compiled via param(...).
  2. The nested TemplateString value walker in TemplatePreparation.collectReferencedTablePaths also only matches Metamodel, so no referenced path is recorded and the joins beyond the reference are never derived. With only the first switch fixed, the query fails with Alias for table not found at Metamodel{root=PetOwnerRef, path='owner.address.city', field='name'}.

The main template path already handles this: TemplatePreparation.resolveElements resolves Navigable nodes through toColumnMetamodel, which rebuilds a navigation-only node into a resolvable runtime metamodel via Metamodel.of(root, fieldPath).

Expected behavior

A navigation-only node interpolated into a template fragment resolves to a column and derives its beyond-reference joins, exactly like the predicate form and the main template path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcorestorm-core and foundation work

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions