This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 798bd1059ea85d239202eda764eafb14d8836833 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Fri Mar 6 17:30:59 2026 -0600 hibernate7: add Positional Parameters support --- .../orm/hibernate/HibernateGormStaticApi.groovy | 18 +++--- .../orm/hibernate/query/HibernateHqlQuery.java | 48 ++++------------ .../orm/hibernate/query/HqlListQueryBuilder.java | 6 +- .../orm/hibernate/query/HqlQueryContext.java | 67 +++++++++++++++++++--- .../hibernate/query/HibernateHqlQuerySpec.groovy | 28 ++++++++- .../orm/hibernate/query/HqlQueryContextSpec.groovy | 51 ++++++++++++---- 6 files changed, 147 insertions(+), 71 deletions(-) diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy index 81fd409907..f4f49f10cb 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy @@ -34,11 +34,11 @@ import org.grails.datastore.mapping.query.event.PreQueryEvent import org.grails.datastore.mapping.proxy.ProxyHandler import org.grails.orm.hibernate.query.HibernateHqlQuery import org.grails.orm.hibernate.query.HibernateQuery +import org.grails.orm.hibernate.query.HqlListQueryBuilder import org.grails.orm.hibernate.query.HqlQueryContext import org.grails.orm.hibernate.query.PagedResultList import org.grails.orm.hibernate.support.HibernateRuntimeUtils -import org.hibernate.LockMode import org.hibernate.Session import org.hibernate.SessionFactory import org.hibernate.query.Query @@ -388,15 +388,14 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { @SuppressWarnings('GroovyAssignabilityCheck') private HibernateHqlQuery prepareHqlQuery(CharSequence hql, boolean isNative, boolean isUpdate, - Map namedParams, Collection positionalParams, Map args) { - def ctx = HqlQueryContext.prepare(hql, isNative, isUpdate, namedParams, persistentEntity) + Map namedParams, Collection positionalParams, Map querySettings) { + def ctx = HqlQueryContext.prepare(persistentEntity, hql, namedParams, positionalParams,querySettings, isNative, isUpdate,) return HibernateHqlQuery.createHqlQuery( (HibernateDatastore) datastore, sessionFactory, persistentEntity, - ctx, - args, - positionalParams, + ctx + , getHibernateTemplate(), conversionService ) @@ -432,11 +431,14 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> { @Override List<D> list(Map params = Collections.emptyMap()) { firePreQueryEvent() - HibernateHqlQuery hqlQuery = HibernateHqlQuery.forList( + HqlListQueryBuilder builder = new HqlListQueryBuilder(persistentEntity, params); + String hql = builder.buildListHql(); + HqlQueryContext ctx = HqlQueryContext.prepare(persistentEntity, hql, Collections.emptyMap(), Collections.emptyList(), params , false, false ); + HibernateHqlQuery hqlQuery = HibernateHqlQuery.createHqlQuery( (HibernateDatastore) datastore, sessionFactory, persistentEntity, - params, + ctx, getHibernateTemplate(), datastore.mappingContext.conversionService ) diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java index b7120a501f..dadaffdeb4 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java @@ -136,14 +136,11 @@ public class HibernateHqlQuery extends Query { * Full factory — opens a session via the {@link GrailsHibernateTemplate}, builds the query from * the prepared {@link HqlQueryContext}, then applies settings and parameters. */ - @SuppressWarnings({"unchecked", "rawtypes"}) - private static HibernateHqlQuery create( + protected static HibernateHqlQuery create( HibernateDatastore dataStore, SessionFactory sessionFactory, PersistentEntity entity, HqlQueryContext ctx, - Map args, - Collection positionalParams, GrailsHibernateTemplate template, ConversionService conversionService) { HibernateHqlQuery hqlQuery = @@ -153,47 +150,26 @@ public class HibernateHqlQuery extends Query { template.applySettings(selectQuery); } hqlQuery.populateQuerySettings( - MapUtils.isNotEmpty(args) ? new HashMap<>(args) : Collections.emptyMap(), + MapUtils.isNotEmpty(ctx.querySettings()) ? new HashMap<>(ctx.querySettings()) : Collections.emptyMap(), conversionService); if (MapUtils.isNotEmpty(ctx.namedParams())) { hqlQuery.populateQueryWithNamedArguments(ctx.namedParams()); - } else if (CollectionUtils.isNotEmpty(positionalParams)) { - hqlQuery.populateQueryWithIndexedArguments(List.copyOf(positionalParams)); + } else if (CollectionUtils.isNotEmpty(ctx.positionalParams())) { + hqlQuery.populateQueryWithIndexedArguments(List.copyOf(ctx.positionalParams())); } return hqlQuery; } - @SuppressWarnings({"unchecked", "rawtypes"}) public static HibernateHqlQuery createHqlQuery( HibernateDatastore dataStore, SessionFactory sessionFactory, PersistentEntity entity, HqlQueryContext ctx, - Map args, - Collection positionalParams, GrailsHibernateTemplate template, ConversionService conversionService) { - return create(dataStore, sessionFactory, entity, ctx, args, positionalParams, template, conversionService); + return create(dataStore, sessionFactory, entity, ctx, template, conversionService); } - /** - * Factory for {@code list(Map params)} — builds HQL with ORDER BY / JOIN FETCH from the params - * map, applies pagination settings, and wraps in a ready-to-execute {@link HibernateHqlQuery}. - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - public static HibernateHqlQuery forList( - HibernateDatastore dataStore, - SessionFactory sessionFactory, - PersistentEntity entity, - Map<?, ?> params, - GrailsHibernateTemplate template, - ConversionService conversionService) { - HqlListQueryBuilder builder = new HqlListQueryBuilder(entity, params); - String hql = builder.buildListHql(); - HqlQueryContext ctx = HqlQueryContext.prepare(hql, false, false, Collections.emptyMap(), entity); - Map<String, Object> mutableParams = params instanceof Map ? new HashMap<>((Map<String, Object>) params) : Collections.emptyMap(); - return create(dataStore, sessionFactory, entity, ctx, mutableParams, Collections.emptyList(), template, conversionService); - } /** * Builds the count HQL string used by {@link PagedResultList} when paging is requested. @@ -204,14 +180,14 @@ public class HibernateHqlQuery extends Query { // ─── Query configuration ───────────────────────────────────────────────── - public void setFlushMode(FlushMode flushMode) { + protected void setFlushMode(FlushMode flushMode) { session.setFlushMode( flushMode == FlushMode.AUTO || flushMode == FlushMode.ALWAYS ? FlushModeType.AUTO : FlushModeType.COMMIT); } - public void populateQuerySettings(Map<?, ?> args, ConversionService conversionService) { + protected void populateQuerySettings(Map<?, ?> args, ConversionService conversionService) { ifPresent(args, HibernateQueryArgument.MAX.value(), v -> delegate.setMaxResults(toInt(v, conversionService))); ifPresent(args, HibernateQueryArgument.OFFSET.value(), v -> delegate.setFirstResult(toInt(v, conversionService))); ifPresent(args, HibernateQueryArgument.CACHE.value(), v -> delegate.setCacheable(toBool(v))); @@ -244,7 +220,7 @@ public class HibernateHqlQuery extends Query { }; } - private static FlushMode convertFlushMode(Object object) { + public static FlushMode convertFlushMode(Object object) { if (object == null) return null; if (object instanceof FlushMode flushMode) return flushMode; try { @@ -254,7 +230,7 @@ public class HibernateHqlQuery extends Query { } } - public void populateQueryWithNamedArguments(Map<?, ?> namedArgs) { + protected void populateQueryWithNamedArguments(Map<?, ?> namedArgs) { if (namedArgs == null) return; namedArgs.forEach( (key, value) -> { @@ -276,7 +252,7 @@ public class HibernateHqlQuery extends Query { }); } - public void populateQueryWithIndexedArguments(List<?> params) { + protected void populateQueryWithIndexedArguments(List<?> params) { if (params == null) return; for (int i = 0; i < params.size(); i++) { Object val = params.get(i); @@ -307,9 +283,7 @@ public class HibernateHqlQuery extends Query { // ─── Private utilities ──────────────────────────────────────────────────── - private static int toInt(Object v) { - return Integer.parseInt(v.toString()); - } + private static int toInt(Object v, ConversionService cs) { if (v instanceof Integer i) return i; diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java index aaf3ce740b..3516b4d657 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlListQueryBuilder.java @@ -37,18 +37,18 @@ import org.grails.orm.hibernate.cfg.MappingCacheHolder; * string so it passes through {@link HqlQueryContext} without GString interpolation. */ @SuppressWarnings("PMD.DataflowAnomalyAnalysis") -class HqlListQueryBuilder { +public class HqlListQueryBuilder { private final PersistentEntity entity; private final Map<?, ?> params; - HqlListQueryBuilder(PersistentEntity entity, Map<?, ?> params) { + public HqlListQueryBuilder(PersistentEntity entity, Map<?, ?> params) { this.entity = entity; this.params = params; } /** Builds the SELECT HQL for the list query (no count). */ - String buildListHql() { + public String buildListHql() { String alias = "e"; StringBuilder hql = new StringBuilder("from ").append(entity.getName()).append(" ").append(alias); appendJoinFetch(hql, alias); diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java index 9588665b61..0368f7e238 100644 --- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java +++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java @@ -19,6 +19,9 @@ package org.grails.orm.hibernate.query; import groovy.lang.GString; + +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.Locale; import java.util.Map; @@ -44,6 +47,8 @@ public record HqlQueryContext( String hql, Class<?> targetClass, Map<String, Object> namedParams, + Collection positionalParams, + Map<String, Object> querySettings, boolean isUpdate, boolean isNative) { @@ -55,16 +60,30 @@ public record HqlQueryContext( */ @SuppressWarnings("unchecked") public static HqlQueryContext prepare( - CharSequence queryCharseq, - boolean isNative, - boolean isUpdate, - Map<?, ?> namedParams, - PersistentEntity entity) { - Map<String, Object> params = + PersistentEntity entity + , CharSequence queryCharseq + , Map<?, ?> namedParams + , Collection positionalParams + , Map querySettings + , boolean isNative + , boolean isUpdate) { + Map<String, Object> _namedParams = namedParams != null ? new HashMap<>((Map<String, Object>) namedParams) : new HashMap<>(); - String hql = resolveHql(queryCharseq, isNative, params); - return new HqlQueryContext( - hql, getTarget(hql, entity.getJavaClass()), params, isUpdate, isNative); + Collection positionalParamsCopy = positionalParams != null ? new ArrayList<>(positionalParams) : null; + Map<String, Object> querySettingsCopy = querySettings != null ? new HashMap<>(querySettings) : null; + + String hql; + // Prefer positional resolution only if positional parameters are explicitly provided (not null) + // and named parameters are empty. This preserves legacy GString->named parameter behavior + // while allowing opt-in to positional parameters via methods that pass them. + if (positionalParamsCopy != null && _namedParams.isEmpty()) { + hql = resolveHql(queryCharseq, isNative, positionalParamsCopy); + } else { + hql = resolveHql(queryCharseq, isNative, _namedParams); + } + + Class<?> target = getTarget(hql, entity.getJavaClass()); + return new HqlQueryContext(hql, target, _namedParams, positionalParamsCopy, querySettingsCopy, isUpdate, isNative); } // ─── HQL resolution ────────────────────────────────────────────────────── @@ -79,6 +98,16 @@ public record HqlQueryContext( return isNative ? normalized : normalizeNonAliasedSelect(normalized); } + public static @Nullable String resolveHql( + CharSequence queryCharseq, boolean isNative, Collection positionalParams) { + String raw = + queryCharseq instanceof GString gstr + ? buildPositionalParameterQueryFromGString(gstr, positionalParams, isNative) + : queryCharseq != null ? queryCharseq.toString() : ""; + String normalized = normalizeMultiLineQueryString(raw); + return isNative ? normalized : normalizeNonAliasedSelect(normalized); + } + // ─── Projection analysis ───────────────────────────────────────────────── /** @@ -255,4 +284,24 @@ public record HqlQueryContext( } return sql.toString(); } + + @SuppressWarnings("unchecked") + private static String buildPositionalParameterQueryFromGString( + GString query, Collection positionalParams, boolean isNative) { + StringBuilder sql = new StringBuilder(); + Object[] values = query.getValues(); + String[] strings = query.getStrings(); + for (int i = 0; i < strings.length; i++) { + sql.append(strings[i]); + if (i < values.length) { + if (isNative) { + sql.append('?'); + } else { + sql.append('?').append(positionalParams.size() + 1); + } + positionalParams.add(values[i]); + } + } + return sql.toString(); + } } diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy index f992d69659..ab4377f974 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy @@ -20,12 +20,12 @@ class HibernateHqlQuerySpec extends HibernateGormDatastoreSpec { private HibernateHqlQuery buildHqlQuery(String hql, Map namedParams = [:], List positionalParams = null, Map args = [:], boolean isUpdate = false) { def entity = mappingContext.getPersistentEntity(HibernateHqlQuerySpecBook.name) - def ctx = HqlQueryContext.prepare(hql, false, isUpdate, namedParams, entity) + def ctx = HqlQueryContext.prepare(entity, hql, namedParams, positionalParams, args, false, isUpdate) def session = sessionFactory.currentSession def hqlQuery = HibernateHqlQuery.buildQuery(session, datastore, sessionFactory, entity, ctx) if (args) hqlQuery.populateQuerySettings(new HashMap(args), mappingContext.conversionService) if (ctx.namedParams()) hqlQuery.populateQueryWithNamedArguments(new HashMap(ctx.namedParams())) - else if (positionalParams) hqlQuery.populateQueryWithIndexedArguments(positionalParams) + else if (ctx.positionalParams()) hqlQuery.populateQueryWithIndexedArguments(List.copyOf(ctx.positionalParams())) hqlQuery } @@ -145,15 +145,37 @@ class HibernateHqlQuerySpec extends HibernateGormDatastoreSpec { GString gq = "from HibernateHqlQuerySpecBook b where b.title = ${titleVal}" when: def entity = mappingContext.getPersistentEntity(HibernateHqlQuerySpecBook.name) - def ctx = HqlQueryContext.prepare(gq, false, false, [:], entity) + def ctx = HqlQueryContext.prepare(entity, gq, [:], null, [:], false, false) def hqlQuery = HibernateHqlQuery.buildQuery(sessionFactory.currentSession, datastore, sessionFactory, entity, ctx) if (ctx.namedParams()) hqlQuery.populateQueryWithNamedArguments(new HashMap(ctx.namedParams())) + else if (ctx.positionalParams()) hqlQuery.populateQueryWithIndexedArguments(List.copyOf(ctx.positionalParams())) def results = hqlQuery.list() then: results.size() == 1 results[0].title == "The Two Towers" } + void "createHqlQuery with GString can build positional parameters if explicitly requested"() { + given: + String titleVal = "The Two Towers" + GString gq = "from HibernateHqlQuerySpecBook b where b.title = ${titleVal}" + when: "positionalParams is provided as non-null (triggering positional branch in prepare)" + def entity = mappingContext.getPersistentEntity(HibernateHqlQuerySpecBook.name) + // We pass an empty but non-null list to trigger the positional branch + def positionalParams = [] + def ctx = HqlQueryContext.prepare(entity, gq, [:], positionalParams, [:], false, false) + // The GString should have appended the value as ?1 + def hqlQuery = HibernateHqlQuery.buildQuery(sessionFactory.currentSession, datastore, sessionFactory, entity, ctx) + + if (ctx.positionalParams()) hqlQuery.populateQueryWithIndexedArguments(List.copyOf(ctx.positionalParams())) + def results = hqlQuery.list() + + then: + ctx.hql().contains("?1") + results.size() == 1 + results[0].title == "The Two Towers" + } + void "createHqlQuery with multiline query normalizes whitespace"() { when: def results = buildHqlQuery("from HibernateHqlQuerySpecBook b\nwhere b.pages > :p", [p: 350]).list() diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryContextSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryContextSpec.groovy index b80fc6a085..5c13d2334a 100644 --- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryContextSpec.groovy +++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HqlQueryContextSpec.groovy @@ -29,7 +29,7 @@ class HqlQueryContextSpec extends Specification { void "record accessors return constructor values"() { given: def params = [name: "Alice"] - def ctx = new HqlQueryContext("from Person", String, params, false, false) + def ctx = new HqlQueryContext("from Person", String, params, null, [:], false, false) expect: ctx.hql() == "from Person" @@ -41,7 +41,7 @@ class HqlQueryContextSpec extends Specification { void "record isUpdate and isNative flags are set correctly"() { given: - def ctx = new HqlQueryContext("update Foo set x=1", Object, [:], true, true) + def ctx = new HqlQueryContext("update Foo set x=1", Object, [:], null, [:], true, true) expect: ctx.isUpdate() @@ -55,7 +55,7 @@ class HqlQueryContextSpec extends Specification { def entity = Mock(PersistentEntity) { getJavaClass() >> String } when: - def ctx = HqlQueryContext.prepare("from Foo", false, false, null, entity) + def ctx = HqlQueryContext.prepare(entity, "from Foo", null, null, [:], false, false) then: ctx.hql() == "from Foo" @@ -65,18 +65,47 @@ class HqlQueryContextSpec extends Specification { !ctx.isNative() } - void "prepare with GString expands interpolations into named parameters"() { + void "prepare with GString and positionalParams expands interpolations into positional parameters"() { given: def entity = Mock(PersistentEntity) { getJavaClass() >> String } String val = "bar" GString gq = "from Foo where name = ${val}" when: - def ctx = HqlQueryContext.prepare(gq, false, false, null, entity) + def ctx = HqlQueryContext.prepare(entity, gq, [:], [], [:], false, false) then: - ctx.hql() == "from Foo where name = :p0" - ctx.namedParams() == [p0: "bar"] + ctx.hql() == "from Foo where name = ?1" + ctx.positionalParams() == ["bar"] + ctx.namedParams().isEmpty() + } + + void "reproduce HibernateHqlQuerySpec failure"() { + given: + def entity = Mock(PersistentEntity) { getJavaClass() >> String; getName() >> "Book" } + String titleVal = "The Two Towers" + GString gq = "from HibernateHqlQuerySpecBook b where b.title = ${titleVal}" + + when: + def ctx = HqlQueryContext.prepare(entity, gq, [:], [], [:], false, false) + + then: + ctx.hql() == "from HibernateHqlQuerySpecBook b where b.title = ?1" + ctx.positionalParams() == ["The Two Towers"] + } + + void "prepare with GString and non-empty positionalParams appends to existing parameters"() { + given: + def entity = Mock(PersistentEntity) { getJavaClass() >> String } + String val = "bar" + GString gq = "from Foo where name = ${val}" + + when: + def ctx = HqlQueryContext.prepare(entity, gq, [:], ["first"], [:], false, false) + + then: + ctx.hql() == "from Foo where name = ?2" + ctx.positionalParams() == ["first", "bar"] } void "prepare merges caller-supplied namedParams with GString params"() { @@ -86,7 +115,7 @@ class HqlQueryContextSpec extends Specification { GString gq = "from Foo where name = ${val} and status = :status" when: - def ctx = HqlQueryContext.prepare(gq, false, false, [status: "active"], entity) + def ctx = HqlQueryContext.prepare(entity, gq, [status: "active"], null, [:], false, false) then: ctx.namedParams().p0 == "bar" @@ -98,7 +127,7 @@ class HqlQueryContextSpec extends Specification { def entity = Mock(PersistentEntity) { getJavaClass() >> String } when: - def ctx = HqlQueryContext.prepare("select name from foo", true, false, null, entity) + def ctx = HqlQueryContext.prepare(entity, "select name from foo", null, null, [:], true, false) then: ctx.hql() == "select name from foo" // alias injection skipped for native SQL @@ -110,7 +139,7 @@ class HqlQueryContextSpec extends Specification { def entity = Mock(PersistentEntity) { getJavaClass() >> String } when: - def ctx = HqlQueryContext.prepare("update Foo set x=1", false, true, null, entity) + def ctx = HqlQueryContext.prepare(entity, "update Foo set x=1", null, null, [:], false, true) then: ctx.isUpdate() @@ -121,7 +150,7 @@ class HqlQueryContextSpec extends Specification { def entity = Mock(PersistentEntity) { getJavaClass() >> String } when: - def ctx = HqlQueryContext.prepare("from Foo", false, false, null, entity) + def ctx = HqlQueryContext.prepare(entity, "from Foo", null, null, [:], false, false) then: noExceptionThrown()
