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):
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(...).
- 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.
Description
Navigation beyond a
Refforeign 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 awherefragment is not resolved to a column; it is compiled as a bind parameter, so the raw metamodel object reaches the JDBC driver.Actual behavior:
java.sql.SQLException: Type com.example.NavigableSiteMetamodel$7 not supported typeJdbcSQLDataException: 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 aRefisNavigablebut deliberately notMetamodel(value extraction across an unloaded reference is query-only):QueryModelImpl.resolveElementsconvertscase Metamodel when isColumn()to aColumnelement; a navigation-only node falls through to the default case and is compiled viaparam(...).TemplateStringvalue walker inTemplatePreparation.collectReferencedTablePathsalso only matchesMetamodel, so no referenced path is recorded and the joins beyond the reference are never derived. With only the first switch fixed, the query fails withAlias for table not found at Metamodel{root=PetOwnerRef, path='owner.address.city', field='name'}.The main template path already handles this:
TemplatePreparation.resolveElementsresolvesNavigablenodes throughtoColumnMetamodel, which rebuilds a navigation-only node into a resolvable runtime metamodel viaMetamodel.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.