You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by bo...@apache.org on 2026/02/25 03:03:04 UTC
(grails-core) 11/18: test and cleanup HibernateHqlQuery
This is an automated email from the ASF dual-hosted git repository.
borinquenkid pushed a commit to branch 8.0.x-hibernate7
in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 35d187c36d4a12a0a52758e95fad069ac962242a
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Tue Feb 24 09:56:27 2026 -0600
test and cleanup HibernateHqlQuery
---
.../orm/hibernate/HibernateGormStaticApi.groovy | 37 +-
.../orm/hibernate/query/HibernateHqlQuery.java | 558 ++++-----------------
.../orm/hibernate/query/HqlQueryContext.java | 242 +++++++++
.../hibernate/query/HibernateHqlQuerySpec.groovy | 226 +++++++++
4 files changed, 593 insertions(+), 470 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 ad81338df5..7cc6a103c2 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
@@ -35,6 +35,7 @@ import org.grails.datastore.mapping.query.event.PreQueryEvent
import org.grails.datastore.mapping.proxy.ProxyHandler
import org.grails.orm.hibernate.query.GrailsHibernateQueryUtils
import org.grails.orm.hibernate.query.HibernateHqlQuery
+import org.grails.orm.hibernate.query.HqlQueryContext
import org.grails.orm.hibernate.query.HibernateQuery
import org.grails.orm.hibernate.query.PagedResultList
import org.grails.orm.hibernate.support.HibernateRuntimeUtils
@@ -329,17 +330,15 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
Map args
, boolean isNative) {
boolean isUpdate = false
+ def ctx = HqlQueryContext.prepare(hql, isNative, isUpdate, namedParams, persistentEntity)
def hqlQuery = HibernateHqlQuery.createHqlQuery(
- (HibernateDatastore) datastore,
+ (HibernateDatastore) datastore,
sessionFactory,
persistentEntity,
- hql,
- isNative,
- isUpdate,
+ ctx,
args,
- namedParams,
- positionalParams
- ,getHibernateTemplate()
+ positionalParams,
+ getHibernateTemplate()
)
firePreQueryEvent()
def ds = (List<D>) hqlQuery.list()
@@ -354,17 +353,15 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
Map args
, boolean isNative) {
boolean isUpdate = false
+ def ctx = HqlQueryContext.prepare(hql, isNative, isUpdate, namedParams, persistentEntity)
def hqlQuery = HibernateHqlQuery.createHqlQuery(
- (HibernateDatastore) datastore,
+ (HibernateDatastore) datastore,
sessionFactory,
persistentEntity,
- hql,
- isNative,
- isUpdate,
+ ctx,
args,
- namedParams,
- positionalParams
- ,getHibernateTemplate()
+ positionalParams,
+ getHibernateTemplate()
)
firePreQueryEvent()
def sm = hqlQuery.singleResult()
@@ -378,17 +375,15 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
Map args) {
boolean isNative = false
boolean isUpdate = true
+ def ctx = HqlQueryContext.prepare(hql, isNative, isUpdate, namedParams, persistentEntity)
def hqlQuery = HibernateHqlQuery.createHqlQuery(
- (HibernateDatastore) datastore,
+ (HibernateDatastore) datastore,
sessionFactory,
persistentEntity,
- hql,
- isNative,
- isUpdate,
+ ctx,
args,
- namedParams,
- positionalParams
- ,getHibernateTemplate()
+ positionalParams,
+ getHibernateTemplate()
)
firePreQueryEvent()
def execute = hqlQuery.executeUpdate()
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 79761e1164..eb4c7a5746 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
@@ -18,17 +18,12 @@
*/
package org.grails.orm.hibernate.query;
-import groovy.lang.GString;
import jakarta.persistence.FlushModeType;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.IntStream;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.groovy.parser.antlr4.util.StringUtils;
@@ -43,410 +38,99 @@ import org.grails.orm.hibernate.GrailsHibernateTemplate;
import org.grails.orm.hibernate.HibernateDatastore;
import org.grails.orm.hibernate.HibernateSession;
import org.grails.orm.hibernate.exceptions.GrailsQueryException;
+
import org.hibernate.FlushMode;
import org.hibernate.SessionFactory;
import org.springframework.context.ApplicationEventPublisher;
/**
- * A query implementation for HQL queries
+ * A query implementation for HQL queries.
*
* @author Graeme Rocher
* @since 6.0
*/
public class HibernateHqlQuery extends Query {
- private org.hibernate.query.Query query;
+
+ private org.hibernate.query.Query<?> query;
public HibernateHqlQuery(
- Session session, PersistentEntity entity, org.hibernate.query.Query query) {
+ Session session, PersistentEntity entity, org.hibernate.query.Query<?> query) {
super(session, entity);
this.query = query;
}
@Override
protected void flushBeforeQuery() {
- // do nothing, hibernate handles this
+ // Hibernate handles flushing internally
}
@Override
+ @SuppressWarnings("rawtypes")
protected List executeQuery(PersistentEntity entity, Junction criteria) {
Datastore datastore = getSession().getDatastore();
- ApplicationEventPublisher applicationEventPublisher = datastore.getApplicationEventPublisher();
- PreQueryEvent preQueryEvent = new PreQueryEvent(datastore, this);
- applicationEventPublisher.publishEvent(preQueryEvent);
-
- if (uniqueResult) {
- query.setMaxResults(1);
- List results = query.list();
- applicationEventPublisher.publishEvent(new PostQueryEvent(datastore, this, results));
- return results;
- } else {
-
- List results = query.list();
- applicationEventPublisher.publishEvent(new PostQueryEvent(datastore, this, results));
- return results;
- }
- }
-
- private static String normalizeMultiLineQueryString(String query) {
- if (query == null || query.indexOf('\n') == -1) {
- return query;
- }
- return query.trim().replace("\n", " ");
- }
-
- private static String buildNamedParameterQueryFromGString(
- GString query, Map<String, Object> params) {
- StringBuilder sqlString = new StringBuilder();
- Object[] values = query.getValues();
- String[] strings = query.getStrings();
-
- for (int i = 0; i < strings.length; i++) {
- sqlString.append(strings[i]);
- if (i < values.length) {
- String parameterName = "p" + i;
- sqlString.append(':').append(parameterName);
- params.put(parameterName, values[i]);
- }
- }
- return sqlString.toString();
+ ApplicationEventPublisher publisher = datastore.getApplicationEventPublisher();
+ publisher.publishEvent(new PreQueryEvent(datastore, this));
+ if (uniqueResult) query.setMaxResults(1);
+ List results = query.list();
+ publisher.publishEvent(new PostQueryEvent(datastore, this, results));
+ return results;
}
- public static HibernateHqlQuery createHqlQuery(
- HibernateDatastore dataStore,
- SessionFactory sessionFactory,
- PersistentEntity persistentEntity,
- CharSequence queryCharseq,
- boolean isNative,
- boolean isUpdate,
- Map _args,
- Map _namedParams,
- Collection _positionalParams,
- GrailsHibernateTemplate grailsHibernateTemplate) {
-
- Map namedParams = _namedParams != null ? new HashMap(_namedParams) : new HashMap();
- String queryString =
- queryCharseq instanceof GString
- ? buildNamedParameterQueryFromGString((GString) queryCharseq, namedParams)
- : queryCharseq != null ? queryCharseq.toString() : "";
- List positionalParams =
- CollectionUtils.isNotEmpty(_positionalParams)
- ? new ArrayList(_positionalParams)
- : new ArrayList();
- String sqlString = normalizeMultiLineQueryString(queryString);
- Map args = MapUtils.isNotEmpty(_args) ? new HashMap(_args) : Collections.emptyMap();
-
- HibernateHqlQuery hibernateHqlQuery =
- grailsHibernateTemplate.execute(
- session -> {
- // Normalize only for HQL (not for native SQL)
- String hqlToUse = isNative ? sqlString : normalizeNonAliasedSelect(sqlString);
- var clazz = getTarget(hqlToUse, persistentEntity.getJavaClass());
- org.hibernate.query.Query q = null;
- if (StringUtils.isEmpty(hqlToUse)) {
- q = session.createQuery("from " + clazz.getName(), clazz);
- } else if (isUpdate) {
- q = session.createQuery(hqlToUse);
- } else {
- q =
- isNative
- ? session.createNativeQuery(hqlToUse, clazz)
- : session.createQuery(hqlToUse, clazz);
- }
- var hibernateSession = new HibernateSession(dataStore, sessionFactory);
- HibernateHqlQuery hibernateHqlQuery1 =
- new HibernateHqlQuery(hibernateSession, persistentEntity, q);
- hibernateHqlQuery1.setFlushMode(session.getHibernateFlushMode());
- return hibernateHqlQuery1;
- });
- grailsHibernateTemplate.applySettings(hibernateHqlQuery.getQuery());
- // apply query settings (max, offset, cache, etc.)
- hibernateHqlQuery.populateQuerySettings(args);
- // apply parameters
- if (MapUtils.isNotEmpty(namedParams)) {
- Map namedCopy = new HashMap(namedParams);
- hibernateHqlQuery.populateQueryWithNamedArguments(namedCopy);
- } else if (CollectionUtils.isNotEmpty(positionalParams)) {
- List positionalList =
- (positionalParams instanceof List)
- ? (List) positionalParams
- : new ArrayList(positionalParams);
- hibernateHqlQuery.populateQueryWithIndexedArguments(positionalList);
- }
- return hibernateHqlQuery;
- }
+ // ─── Static factory API ──────────────────────────────────────────────────
/**
- * Determine the number of top-level projections in the HQL query. Returns 0 if there is no
- * explicit SELECT clause (implicit entity projection), 1 if there is a single top-level
- * projection expression (including constructs like DISTINCT x or NEW map(...)), and 2 if there
- * are two or more top-level projection expressions (e.g. "select a, b from ...").
- *
- * <p>Notes: - Commas within parentheses or string literals are ignored. - Constructor expressions
- * like "new map(a as n, b as m)" count as a single projection. - Aggregate and function calls
- * with commas in their argument lists are handled by parentheses tracking.
+ * Session-bound step — creates the {@link org.hibernate.query.Query} from an open
+ * {@link org.hibernate.Session} and wraps it in a {@link HibernateHqlQuery}.
*/
- static int countHqlProjections(CharSequence hql) {
- if (hql == null) return 0;
- String s = hql.toString().trim();
- if (s.isEmpty()) return 0;
- // Find select and from in a case-insensitive way
- String lower = s.toLowerCase();
- int selectIdx = lower.indexOf("select ");
- if (selectIdx < 0) {
- // no explicit select -> implicit single entity projection following "from"
- return 0;
- }
- // Ensure this select occurs before the corresponding from
- int fromIdx = lower.indexOf(" from ", selectIdx);
- if (fromIdx < 0) {
- // malformed or incomplete query; treat as one projection if select exists
- fromIdx = s.length();
- }
- int selectStart = selectIdx + "select".length();
- // Extract the select clause between 'select' and 'from'
- String sel = s.substring(selectStart, fromIdx).trim();
- if (sel.isEmpty()) return 0;
- // Strip leading DISTINCT/ALL keywords
- String selLower = sel.toLowerCase();
- if (selLower.startsWith("distinct ")) {
- sel = sel.substring("distinct ".length()).trim();
- selLower = sel.toLowerCase();
- } else if (selLower.startsWith("all ")) {
- sel = sel.substring("all ".length()).trim();
- selLower = sel.toLowerCase();
- }
- // Now count top-level commas ignoring those within parentheses and string literals
- int depth = 0;
- boolean inSingleQuote = false;
- boolean inDoubleQuote = false;
- int topLevelCommas = 0;
- char singleQuote = '\'';
- char doubleQuote = '"';
- char leftParen = '('; // Left parenthesis
- char rightParen = ')'; // Right parenthesis
- char comma = ','; // Comma
-
- for (int i = 0; i < sel.length(); i++) {
- char c = sel.charAt(i);
- // handle quotes (simple handling: toggle on quote not escaped by another same quote)
- if (!inDoubleQuote && c == singleQuote) {
- // handle doubled single quotes inside strings
- if (inSingleQuote) {
- if (i + 1 < sel.length() && sel.charAt(i + 1) == singleQuote) {
- i++; // skip escaped quote
- continue;
- } else {
- inSingleQuote = false;
- continue;
- }
- } else {
- inSingleQuote = true;
- continue;
- }
- }
- if (!inSingleQuote && c == doubleQuote) {
- inDoubleQuote = !inDoubleQuote;
- continue;
- }
- if (inSingleQuote || inDoubleQuote) continue;
- if (c == leftParen) {
- depth++;
- continue;
- }
- if (c == rightParen && depth > 0) {
- depth--;
- continue;
- }
- if (c == comma && depth == 0) {
- topLevelCommas++;
- }
- }
- if (topLevelCommas == 0) return 1;
- return 2;
- }
-
- static Class getTarget(CharSequence hql, Class clazz) {
- // Normalize non-aliased queries to an aliased form, then reuse the logic
- String normalized = normalizeNonAliasedSelect(hql == null ? null : hql.toString());
-
- int projections = countHqlProjections(normalized);
- switch (projections) {
- case 0:
- return clazz; // No explicit SELECT - implicit entity projection
- case 1:
- // Single projection - property vs entity
- if (isPropertyProjection(normalized)) {
- return Object.class; // Scalar result
- } else {
- return clazz; // Entity result
- }
- default:
- return Object[].class; // Multiple projections
+ @SuppressWarnings("unchecked")
+ public static HibernateHqlQuery buildQuery(
+ org.hibernate.Session session,
+ HibernateDatastore dataStore,
+ SessionFactory sessionFactory,
+ PersistentEntity entity,
+ HqlQueryContext ctx) {
+ org.hibernate.query.Query<?> q;
+ if (StringUtils.isEmpty(ctx.hql())) {
+ q = session.createQuery("from " + ctx.targetClass().getName(), ctx.targetClass());
+ } else if (ctx.isUpdate()) {
+ q = session.createQuery(ctx.hql());
+ } else {
+ q = ctx.isNative()
+ ? session.createNativeQuery(ctx.hql(), ctx.targetClass())
+ : session.createQuery(ctx.hql(), ctx.targetClass());
}
+ HibernateHqlQuery result = new HibernateHqlQuery(
+ new HibernateSession(dataStore, sessionFactory), entity, q);
+ result.setFlushMode(session.getHibernateFlushMode());
+ return result;
}
/**
- * If the HQL query has no alias in the FROM clause, inject a synthetic alias ("e") and qualify
- * the SELECT projection accordingly. Only SELECT is adjusted; the rest of the query is left
- * intact (WHERE/JOIN conditions are valid as-is in HQL).
- *
- * <p>Examples: "select nameType from NameType where lower(nameType)=:nameType" -> "select
- * e.nameType from NameType e where lower(nameType)=:nameType" "select NameType from NameType" ->
- * "select e from NameType e" "select name from Person" -> "select e.name from Person e"
+ * Full factory — opens a session via the {@link GrailsHibernateTemplate}, builds the query
+ * from the prepared {@link HqlQueryContext}, then applies settings and parameters.
*/
- private static String normalizeNonAliasedSelect(String hql) {
- if (hql == null) return null;
- String s = hql.trim();
- if (s.isEmpty()) return s;
-
- String lower = s.toLowerCase();
- int selectIdx = lower.indexOf("select ");
- if (selectIdx < 0) {
- // No explicit select -> nothing to normalize for target detection
- return s;
- }
- int fromIdx = lower.indexOf(" from ", selectIdx);
- if (fromIdx < 0) {
- // Malformed or incomplete; leave as-is
- return s;
- }
-
- // Extract SELECT clause original text
- int selectStart = selectIdx + "select ".length();
- String selectClauseOrig = s.substring(selectStart, fromIdx).trim();
- String selectClauseLower = lower.substring(selectStart, fromIdx).trim();
-
- // Extract FROM head to detect alias: "from Entity [as] alias ..."
- int afterFrom = fromIdx + " from ".length();
- int endOfFromHead = afterFrom;
- // read entity name token
- while (endOfFromHead < s.length()) {
- char ch = s.charAt(endOfFromHead);
- if (Character.isWhitespace(ch)) break;
- endOfFromHead++;
- }
- String entityName = s.substring(afterFrom, endOfFromHead).trim();
- if (entityName.isEmpty()) return s;
-
- // Skip spaces
- int cur = endOfFromHead;
- while (cur < s.length() && Character.isWhitespace(s.charAt(cur))) cur++;
-
- // Optional "as"
- boolean hasAlias = false;
- String alias = null;
- int aliasStart = cur;
- if (cur + 2 < s.length()) {
- String nextWord = s.substring(cur, Math.min(cur + 2, s.length())).toLowerCase();
- }
- if (cur + 2 < s.length()
- && s.substring(cur, Math.min(cur + 2, s.length())).equalsIgnoreCase("as")) {
- cur += 2;
- while (cur < s.length() && Character.isWhitespace(s.charAt(cur))) cur++;
- }
-
- // Try to read alias token unless we hit a clause keyword
- int aliasEnd = cur;
- while (aliasEnd < s.length()) {
- char ch = s.charAt(aliasEnd);
- if (Character.isWhitespace(ch)) break;
- aliasEnd++;
- }
- String maybeAlias = (cur < aliasEnd) ? s.substring(cur, aliasEnd) : "";
-
- // Keywords that indicate no alias present
- String maybeAliasLower = maybeAlias.toLowerCase();
- boolean isClauseKeyword =
- maybeAliasLower.isEmpty()
- || maybeAliasLower.equals("where")
- || maybeAliasLower.equals("join")
- || maybeAliasLower.equals("left")
- || maybeAliasLower.equals("right")
- || maybeAliasLower.equals("inner")
- || maybeAliasLower.equals("outer")
- || maybeAliasLower.equals("group")
- || maybeAliasLower.equals("order")
- || maybeAliasLower.equals("having");
-
- if (!isClauseKeyword) {
- hasAlias = true;
- alias = maybeAlias;
- }
-
- if (hasAlias) {
- // Already aliased; no normalization needed
- return s;
- }
-
- // Inject synthetic alias
- String syntheticAlias = "e";
-
- // Adjust SELECT clause:
- // Preserve DISTINCT/ALL prefix
- String prefix = "";
- String projOrig = selectClauseOrig;
- String projLower = selectClauseLower;
- if (projLower.startsWith("distinct ")) {
- prefix =
- selectClauseOrig.substring(
- 0, selectClauseOrig.length() - projOrig.substring("distinct ".length()).length());
- projOrig = selectClauseOrig.substring("distinct ".length()).trim();
- projLower = projLower.substring("distinct ".length()).trim();
- prefix = "distinct ";
- } else if (projLower.startsWith("all ")) {
- prefix = "all ";
- projOrig = selectClauseOrig.substring("all ".length()).trim();
- projLower = projLower.substring("all ".length()).trim();
- }
-
- String adjustedProjection = projOrig;
- // If projection equals entity name -> "select e"
- if (projLower.equals(entityName.toLowerCase())) {
- adjustedProjection = syntheticAlias;
- }
- // If projection has no dot, treat as property -> qualify with alias
- else if (!projLower.contains("(")
- && !projLower.contains(".")
- && !projLower.startsWith("new ")) {
- adjustedProjection = syntheticAlias + "." + projOrig;
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public static HibernateHqlQuery createHqlQuery(
+ HibernateDatastore dataStore,
+ SessionFactory sessionFactory,
+ PersistentEntity entity,
+ HqlQueryContext ctx,
+ Map args,
+ Collection positionalParams,
+ GrailsHibernateTemplate template) {
+ HibernateHqlQuery hqlQuery = template.execute(
+ session -> buildQuery(session, dataStore, sessionFactory, entity, ctx));
+ template.applySettings(hqlQuery.getQuery());
+ hqlQuery.populateQuerySettings(MapUtils.isNotEmpty(args) ? new HashMap<>(args) : Collections.emptyMap());
+ if (MapUtils.isNotEmpty(ctx.namedParams())) {
+ hqlQuery.populateQueryWithNamedArguments(ctx.namedParams());
+ } else if (CollectionUtils.isNotEmpty(positionalParams)) {
+ hqlQuery.populateQueryWithIndexedArguments(List.copyOf(positionalParams));
}
- // else leave as-is (functions, constructor expr, already-qualified, etc.)
-
- // Build normalized SELECT ... FROM ... (inject alias right after entity name)
- StringBuilder out = new StringBuilder();
- out.append("select ").append(prefix).append(adjustedProjection);
- // Original FROM and the rest
- String tail = s.substring(fromIdx); // starts with " from "
- // Insert alias after entity name in tail
- StringBuilder tailOut = new StringBuilder();
- tailOut.append(" from ").append(entityName).append(" ").append(syntheticAlias);
- // Append the remainder after entity name in the FROM head
- tailOut.append(s.substring(endOfFromHead));
- out.append(tailOut);
- return out.toString();
+ return hqlQuery;
}
- private static boolean isPropertyProjection(CharSequence hql) {
- String s = hql.toString().toLowerCase().trim();
- int selectIdx = s.indexOf("select ");
- if (selectIdx < 0) return false;
-
- int fromIdx = s.indexOf(" from ", selectIdx);
- if (fromIdx < 0) fromIdx = s.length();
-
- String selectClause = s.substring(selectIdx + "select ".length(), fromIdx).trim();
-
- // Remove DISTINCT/ALL if present
- if (selectClause.startsWith("distinct ")) {
- selectClause = selectClause.substring("distinct ".length()).trim();
- } else if (selectClause.startsWith("all ")) {
- selectClause = selectClause.substring("all ".length()).trim();
- }
-
- // Only return true for clear property projections (containing dots)
- // This is the safest approach - only treat selections with dots as scalar projections
- return selectClause.contains(".");
- }
+ // ─── Query configuration ─────────────────────────────────────────────────
public void setFlushMode(FlushMode flushMode) {
session.setFlushMode(
@@ -455,88 +139,64 @@ public class HibernateHqlQuery extends Query {
: FlushModeType.COMMIT);
}
- public void populateQuerySettings(Map args) {
- Optional.ofNullable(args.remove(DynamicFinder.ARGUMENT_MAX))
- .map(Object::toString)
- .map(Integer::parseInt)
- .ifPresent(query::setMaxResults);
- Optional.ofNullable(args.remove(DynamicFinder.ARGUMENT_OFFSET))
- .map(Object::toString)
- .map(Integer::parseInt)
- .ifPresent(query::setFirstResult);
- Optional.ofNullable(args.remove(DynamicFinder.ARGUMENT_CACHE))
- .map(Object::toString)
- .map(Boolean::parseBoolean)
- .ifPresent(query::setCacheable);
- Optional.ofNullable(args.remove(DynamicFinder.ARGUMENT_FETCH_SIZE))
- .map(Object::toString)
- .map(Integer::parseInt)
- .ifPresent(query::setFetchSize);
- Optional.ofNullable(args.remove(DynamicFinder.ARGUMENT_TIMEOUT))
- .map(Object::toString)
- .map(Integer::parseInt)
- .ifPresent(query::setTimeout);
- Optional.ofNullable(args.remove(DynamicFinder.ARGUMENT_READ_ONLY))
- .map(Object::toString)
- .map(Boolean::parseBoolean)
- .ifPresent(query::setReadOnly);
- Optional.ofNullable(args.remove(DynamicFinder.ARGUMENT_FLUSH_MODE))
- .filter(FlushMode.class::isInstance)
- .map(FlushMode.class::cast)
- .ifPresent(query::setHibernateFlushMode);
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void populateQuerySettings(Map<?, ?> args) {
+ ifPresent(args, DynamicFinder.ARGUMENT_MAX, v -> query.setMaxResults(toInt(v)));
+ ifPresent(args, DynamicFinder.ARGUMENT_OFFSET, v -> query.setFirstResult(toInt(v)));
+ ifPresent(args, DynamicFinder.ARGUMENT_CACHE, v -> query.setCacheable(toBool(v)));
+ ifPresent(args, DynamicFinder.ARGUMENT_FETCH_SIZE, v -> query.setFetchSize(toInt(v)));
+ ifPresent(args, DynamicFinder.ARGUMENT_TIMEOUT, v -> query.setTimeout(toInt(v)));
+ ifPresent(args, DynamicFinder.ARGUMENT_READ_ONLY, v -> query.setReadOnly(toBool(v)));
+ if (args.containsKey(DynamicFinder.ARGUMENT_FLUSH_MODE)) {
+ Object v = args.get(DynamicFinder.ARGUMENT_FLUSH_MODE);
+ if (v instanceof FlushMode fm) query.setHibernateFlushMode(fm);
+ }
}
- public void populateQueryWithNamedArguments(Map queryNamedArgs) {
- Optional.ofNullable(queryNamedArgs)
- .ifPresent(
- map -> {
- map.forEach(
- (key, value) -> {
- if (key instanceof CharSequence) {
- String stringKey = key.toString();
- if (value == null) {
- query.setParameter(stringKey, null);
- } else if (value instanceof CharSequence) {
- query.setParameter(stringKey, value.toString());
- } else if (List.class.isAssignableFrom(value.getClass())) {
- query.setParameterList(stringKey, (List) value);
- } else if (Set.class.isAssignableFrom(value.getClass())) {
- query.setParameterList(stringKey, (Set) value);
- } else if (value.getClass().isArray()) {
- query.setParameterList(stringKey, (Object[]) value);
- } else {
- query.setParameter(stringKey, value);
- }
- } else {
- throw new GrailsQueryException(
- "Named parameter's name must be String: $queryNamedArgs");
- }
- });
- });
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void populateQueryWithNamedArguments(Map<?, ?> namedArgs) {
+ if (namedArgs == null) return;
+ namedArgs.forEach((key, value) -> {
+ if (!(key instanceof CharSequence)) {
+ throw new GrailsQueryException("Named parameter's name must be a String: " + namedArgs);
+ }
+ String name = key.toString();
+ if (value == null) {
+ query.setParameter(name, null);
+ } else if (value instanceof Collection<?> col) {
+ query.setParameterList(name, col);
+ } else if (value.getClass().isArray()) {
+ query.setParameterList(name, (Object[]) value);
+ } else if (value instanceof CharSequence cs) {
+ query.setParameter(name, cs.toString(), String.class);
+ } else {
+ query.setParameter(name, value);
+ }
+ });
}
- public void populateQueryWithIndexedArguments(List params) {
- Optional.ofNullable(params)
- .ifPresent(
- collection -> {
- IntStream.range(1, collection.size() + 1)
- .forEach(
- index -> {
- var val = collection.get(index - 1);
- if (val instanceof CharSequence) {
- query.setParameter(index, val.toString());
- } else {
- query.setParameter(index, val);
- }
- });
- });
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ public void populateQueryWithIndexedArguments(List<?> params) {
+ if (params == null) return;
+ for (int i = 0; i < params.size(); i++) {
+ Object val = params.get(i);
+ if (val instanceof CharSequence cs) query.setParameter(i + 1, cs.toString(), String.class);
+ else if (val != null) query.setParameter(i + 1, val);
+ else query.setParameter(i + 1, null);
+ }
}
- public org.hibernate.query.Query getQuery() {
- return query;
- }
+ public org.hibernate.query.Query<?> getQuery() { return query; }
+
+ public int executeUpdate() { return query.executeUpdate(); }
+
+ // ─── Private utilities ────────────────────────────────────────────────────
+
+ private static int toInt(Object v) { return Integer.parseInt(v.toString()); }
+ private static boolean toBool(Object v) { return Boolean.parseBoolean(v.toString()); }
- public int executeUpdate() {
- return query.executeUpdate();
+ private static void ifPresent(Map<?, ?> map, String key, java.util.function.Consumer<Object> action) {
+ Object v = map.get(key);
+ if (v != null) action.accept(v);
}
}
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
new file mode 100644
index 0000000000..bb3861abbc
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlQueryContext.java
@@ -0,0 +1,242 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.grails.orm.hibernate.query;
+
+import groovy.lang.GString;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.grails.datastore.mapping.model.PersistentEntity;
+
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * Immutable value object that holds all resolved HQL query state which can be computed
+ * without a Hibernate {@code Session}: the final HQL string, the result target class,
+ * any named parameters (including those expanded from a {@link GString}), and flags
+ * for whether the query is an update or native SQL.
+ *
+ * <p>Use {@link #prepare} to build an instance from raw inputs.
+ */
+public record HqlQueryContext(
+ String hql,
+ Class<?> targetClass,
+ Map<String, Object> namedParams,
+ boolean isUpdate,
+ boolean isNative) {
+
+ // ─── Factory ─────────────────────────────────────────────────────────────
+
+ /**
+ * Resolves the final HQL string, the result target class, and expands any
+ * {@link GString} into named parameters. No {@code Session} is required.
+ */
+ @SuppressWarnings("unchecked")
+ public static HqlQueryContext prepare(
+ CharSequence queryCharseq,
+ boolean isNative,
+ boolean isUpdate,
+ Map<?, ?> namedParams,
+ PersistentEntity entity) {
+ Map<String, Object> params = 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);
+ }
+
+ // ─── HQL resolution ──────────────────────────────────────────────────────
+
+ public static @Nullable String resolveHql(
+ CharSequence queryCharseq, boolean isNative, Map<String, Object> namedParams) {
+ String raw = queryCharseq instanceof GString gstr
+ ? buildNamedParameterQueryFromGString(gstr, namedParams)
+ : queryCharseq != null ? queryCharseq.toString() : "";
+ String normalized = normalizeMultiLineQueryString(raw);
+ return isNative ? normalized : normalizeNonAliasedSelect(normalized);
+ }
+
+ // ─── Projection analysis ─────────────────────────────────────────────────
+
+ /**
+ * Returns the result target class for a query:
+ * the entity class when there is no explicit SELECT or a single entity projection,
+ * {@code Object.class} for a single scalar projection, or {@code Object[].class}
+ * for multiple projections.
+ */
+ public static Class<?> getTarget(CharSequence hql, Class<?> clazz) {
+ String normalized = normalizeNonAliasedSelect(hql == null ? null : hql.toString());
+ return switch (countHqlProjections(normalized)) {
+ case 0 -> clazz;
+ case 1 -> isPropertyProjection(normalized) ? Object.class : clazz;
+ default -> Object[].class;
+ };
+ }
+
+ /**
+ * Returns the number of top-level projections in the SELECT clause:
+ * 0 if no explicit SELECT, 1 for a single projection (including DISTINCT x or NEW map(…)),
+ * 2 for two or more comma-separated top-level projections.
+ *
+ * <p>Commas inside parentheses or string literals are ignored.
+ */
+ static int countHqlProjections(CharSequence hql) {
+ if (hql == null || hql.length() == 0) return 0;
+ String s = hql.toString().trim();
+ String lower = s.toLowerCase();
+ int selectIdx = lower.indexOf("select ");
+ if (selectIdx < 0) return 0;
+
+ int fromIdx = lower.indexOf(" from ", selectIdx);
+ String sel = s.substring(selectIdx + "select".length(), fromIdx < 0 ? s.length() : fromIdx).trim();
+ if (sel.isEmpty()) return 0;
+
+ // Strip leading DISTINCT/ALL
+ String selLower = sel.toLowerCase();
+ if (selLower.startsWith("distinct ")) sel = sel.substring("distinct ".length()).trim();
+ else if (selLower.startsWith("all ")) sel = sel.substring("all ".length()).trim();
+
+ // Count top-level commas, ignoring those inside parens or string literals
+ int depth = 0, commas = 0;
+ boolean inSingle = false, inDouble = false;
+ for (int i = 0; i < sel.length(); i++) {
+ char c = sel.charAt(i);
+ if (!inDouble && c == '\'') {
+ if (inSingle && i + 1 < sel.length() && sel.charAt(i + 1) == '\'') { i++; continue; } // escaped ''
+ inSingle = !inSingle;
+ } else if (!inSingle && c == '"') {
+ inDouble = !inDouble;
+ } else if (!inSingle && !inDouble) {
+ if (c == '(') depth++;
+ else if (c == ')' && depth > 0) depth--;
+ else if (c == ',' && depth == 0) commas++;
+ }
+ }
+ return commas == 0 ? 1 : 2;
+ }
+
+ // ─── HQL normalization ────────────────────────────────────────────────────
+
+ /**
+ * Injects a synthetic alias {@code "e"} into unaliased SELECT queries so that
+ * projection detection works uniformly. The FROM remainder is left intact.
+ * <p>
+ * Examples:
+ * {@code "select name from Person"} → {@code "select e.name from Person e"}<br>
+ * {@code "select Person from Person"} → {@code "select e from Person e"}
+ */
+ static @Nullable String normalizeNonAliasedSelect(String hql) {
+ if (hql == null) return null;
+ String s = hql.trim();
+ if (s.isEmpty()) return s;
+
+ String lower = s.toLowerCase();
+ int selectIdx = lower.indexOf("select ");
+ if (selectIdx < 0) return s; // no SELECT clause — nothing to normalize
+
+ int fromIdx = lower.indexOf(" from ", selectIdx);
+ if (fromIdx < 0) return s; // malformed — leave as-is
+
+ int selectStart = selectIdx + "select ".length();
+ String selectClauseOrig = s.substring(selectStart, fromIdx).trim();
+ String selectClauseLower = lower.substring(selectStart, fromIdx).trim();
+
+ // Parse entity name from the FROM head
+ int afterFrom = fromIdx + " from ".length();
+ int entityEnd = afterFrom;
+ while (entityEnd < s.length() && !Character.isWhitespace(s.charAt(entityEnd))) entityEnd++;
+ String entityName = s.substring(afterFrom, entityEnd);
+ if (entityName.isEmpty()) return s;
+
+ // Skip whitespace, then optional "as" keyword
+ int cur = entityEnd;
+ while (cur < s.length() && Character.isWhitespace(s.charAt(cur))) cur++;
+ if (cur + 2 <= s.length() && s.substring(cur, cur + 2).equalsIgnoreCase("as")) {
+ cur += 2;
+ while (cur < s.length() && Character.isWhitespace(s.charAt(cur))) cur++;
+ }
+
+ // Read the next token; a clause keyword means no user-defined alias is present
+ int tokenEnd = cur;
+ while (tokenEnd < s.length() && !Character.isWhitespace(s.charAt(tokenEnd))) tokenEnd++;
+ String token = s.substring(cur, tokenEnd).toLowerCase();
+ boolean hasAlias = !token.isEmpty()
+ && !Set.of("where", "join", "left", "right", "inner", "outer", "group", "order", "having")
+ .contains(token);
+ if (hasAlias) return s;
+
+ // Strip DISTINCT/ALL prefix before adjusting the projection
+ String prefix = "", projOrig = selectClauseOrig, projLower = selectClauseLower;
+ if (projLower.startsWith("distinct ")) {
+ prefix = "distinct ";
+ projOrig = selectClauseOrig.substring("distinct ".length()).trim();
+ projLower = projLower.substring("distinct ".length()).trim();
+ } else if (projLower.startsWith("all ")) {
+ prefix = "all ";
+ projOrig = selectClauseOrig.substring("all ".length()).trim();
+ projLower = projLower.substring("all ".length()).trim();
+ }
+
+ // Qualify the projection with the synthetic alias
+ String adjusted;
+ if (projLower.equals(entityName.toLowerCase())) {
+ adjusted = "e"; // "select Person from Person" → "select e"
+ } else if (!projLower.contains("(") && !projLower.contains(".") && !projLower.startsWith("new ")) {
+ adjusted = "e." + projOrig; // "select name from Person" → "select e.name"
+ } else {
+ adjusted = projOrig; // functions / constructor expr / already qualified
+ }
+
+ return "select " + prefix + adjusted + " from " + entityName + " e" + s.substring(entityEnd);
+ }
+
+ // ─── Private helpers ─────────────────────────────────────────────────────
+
+ private static boolean isPropertyProjection(CharSequence hql) {
+ if (hql == null) return false;
+ String s = hql.toString().toLowerCase().trim();
+ int selectIdx = s.indexOf("select ");
+ if (selectIdx < 0) return false;
+ int fromIdx = s.indexOf(" from ", selectIdx);
+ String clause = s.substring(selectIdx + "select ".length(), fromIdx < 0 ? s.length() : fromIdx).trim();
+ if (clause.startsWith("distinct ")) clause = clause.substring("distinct ".length()).trim();
+ else if (clause.startsWith("all ")) clause = clause.substring("all ".length()).trim();
+ return clause.contains(".");
+ }
+
+ private static String normalizeMultiLineQueryString(String query) {
+ if (query == null || query.indexOf('\n') == -1) return query;
+ return query.trim().replace("\n", " ");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static String buildNamedParameterQueryFromGString(
+ GString query, Map<String, Object> params) {
+ 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) {
+ String name = "p" + i;
+ sql.append(':').append(name);
+ params.put(name, 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
new file mode 100644
index 0000000000..6c8941381a
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy
@@ -0,0 +1,226 @@
+package org.grails.orm.hibernate.query
+
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import grails.persistence.Entity
+import org.hibernate.FlushMode
+import spock.lang.Unroll
+
+class HibernateHqlQuerySpec extends HibernateGormDatastoreSpec {
+
+ void setupSpec() {
+ manager.addAllDomainClasses([HibernateHqlQuerySpecBook, HibernateHqlQuerySpecAuthor])
+ }
+
+ def setup() {
+ def author = new HibernateHqlQuerySpecAuthor(name: "Tolkien").save(flush: true)
+ new HibernateHqlQuerySpecBook(title: "The Hobbit", pages: 310, author: author).save()
+ new HibernateHqlQuerySpecBook(title: "Fellowship", pages: 423, author: author).save()
+ new HibernateHqlQuerySpecBook(title: "The Two Towers", pages: 352, author: author).save(flush: true)
+ }
+
+ 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 session = sessionFactory.currentSession
+ def hqlQuery = HibernateHqlQuery.buildQuery(session, datastore, sessionFactory, entity, ctx)
+ if (args) hqlQuery.populateQuerySettings(new HashMap(args))
+ if (ctx.namedParams()) hqlQuery.populateQueryWithNamedArguments(new HashMap(ctx.namedParams()))
+ else if (positionalParams) hqlQuery.populateQueryWithIndexedArguments(positionalParams)
+ hqlQuery
+ }
+
+ // ─── countHqlProjections ────────────────────────────────────────────────
+
+ void "countHqlProjections returns 0 for null"() {
+ expect: HqlQueryContext.countHqlProjections(null) == 0
+ }
+
+ void "countHqlProjections returns 0 for empty string"() {
+ expect: HqlQueryContext.countHqlProjections("") == 0
+ }
+
+ void "countHqlProjections returns 0 when no SELECT clause"() {
+ expect: HqlQueryContext.countHqlProjections("from HibernateHqlQuerySpecBook") == 0
+ }
+
+ void "countHqlProjections returns 1 for single projection"() {
+ expect: HqlQueryContext.countHqlProjections("select b.title from HibernateHqlQuerySpecBook b") == 1
+ }
+
+ void "countHqlProjections returns 2 for multiple top-level projections"() {
+ expect: HqlQueryContext.countHqlProjections("select b.title, b.pages from HibernateHqlQuerySpecBook b") == 2
+ }
+
+ void "countHqlProjections ignores commas inside function calls"() {
+ expect: HqlQueryContext.countHqlProjections("select count(b.title) from HibernateHqlQuerySpecBook b") == 1
+ }
+
+ void "countHqlProjections handles DISTINCT single projection"() {
+ expect: HqlQueryContext.countHqlProjections("select distinct b.title from HibernateHqlQuerySpecBook b") == 1
+ }
+
+ void "countHqlProjections handles constructor expression as single projection"() {
+ expect: HqlQueryContext.countHqlProjections("select new map(b.title as t, b.pages as p) from HibernateHqlQuerySpecBook b") == 1
+ }
+
+ // ─── getTarget ──────────────────────────────────────────────────────────
+
+ void "getTarget returns entity class when no SELECT clause"() {
+ expect:
+ HqlQueryContext.getTarget("from HibernateHqlQuerySpecBook", HibernateHqlQuerySpecBook) == HibernateHqlQuerySpecBook
+ }
+
+ void "getTarget returns entity class for single entity projection"() {
+ expect:
+ HqlQueryContext.getTarget("select b from HibernateHqlQuerySpecBook b", HibernateHqlQuerySpecBook) == HibernateHqlQuerySpecBook
+ }
+
+ void "getTarget returns Object for single scalar projection"() {
+ expect:
+ HqlQueryContext.getTarget("select b.title from HibernateHqlQuerySpecBook b", HibernateHqlQuerySpecBook) == Object
+ }
+
+ void "getTarget returns Object array for multiple projections"() {
+ expect:
+ HqlQueryContext.getTarget("select b.title, b.pages from HibernateHqlQuerySpecBook b", HibernateHqlQuerySpecBook) == Object[].class
+ }
+
+ // ─── createHqlQuery + executeQuery ──────────────────────────────────────
+
+ void "createHqlQuery with plain HQL returns all results"() {
+ when:
+ def results = buildHqlQuery("from HibernateHqlQuerySpecBook").list()
+ then:
+ results.size() == 3
+ }
+
+ void "createHqlQuery with named parameters filters correctly"() {
+ when:
+ def results = buildHqlQuery("from HibernateHqlQuerySpecBook b where b.title = :title", [title: "The Hobbit"]).list()
+ then:
+ results.size() == 1
+ results[0].title == "The Hobbit"
+ }
+
+ void "createHqlQuery with positional parameters filters correctly"() {
+ when:
+ def results = buildHqlQuery("from HibernateHqlQuerySpecBook b where b.title = ?1", [:], ["Fellowship"]).list()
+ then:
+ results.size() == 1
+ results[0].title == "Fellowship"
+ }
+
+ void "createHqlQuery with max arg limits results"() {
+ when:
+ def results = buildHqlQuery("from HibernateHqlQuerySpecBook", [:], null, [max: 2]).list()
+ then:
+ results.size() == 2
+ }
+
+ void "createHqlQuery with offset arg skips results"() {
+ when:
+ def results = buildHqlQuery("from HibernateHqlQuerySpecBook order by title", [:], null, [offset: 2]).list()
+ then:
+ results.size() == 1
+ }
+
+ void "createHqlQuery with empty query string defaults to full entity query"() {
+ when:
+ def results = buildHqlQuery("").list()
+ then:
+ results.size() == 3
+ }
+
+ void "createHqlQuery executes update"() {
+ when:
+ int updated = buildHqlQuery("update HibernateHqlQuerySpecBook set pages = 999 where title = :t",
+ [t: "The Hobbit"], null, [:], true).executeUpdate()
+ then:
+ updated == 1
+ }
+
+ void "createHqlQuery with GString builds named parameters automatically"() {
+ given:
+ String titleVal = "The Two Towers"
+ 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 hqlQuery = HibernateHqlQuery.buildQuery(sessionFactory.currentSession, datastore, sessionFactory, entity, ctx)
+ if (ctx.namedParams()) hqlQuery.populateQueryWithNamedArguments(new HashMap(ctx.namedParams()))
+ def results = hqlQuery.list()
+ then:
+ 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()
+ then:
+ results.size() == 2
+ }
+
+ // ─── setFlushMode ───────────────────────────────────────────────────────
+
+ @Unroll
+ void "setFlushMode maps Hibernate #hibernateMode correctly"() {
+ when:
+ buildHqlQuery("from HibernateHqlQuerySpecBook").setFlushMode(hibernateMode)
+ then:
+ noExceptionThrown()
+ where:
+ hibernateMode << [FlushMode.AUTO, FlushMode.ALWAYS, FlushMode.COMMIT, FlushMode.MANUAL]
+ }
+
+ // ─── named parameter edge cases ─────────────────────────────────────────
+
+ void "populateQueryWithNamedArguments handles list parameter"() {
+ when:
+ def results = buildHqlQuery("from HibernateHqlQuerySpecBook b where b.title in (:titles)",
+ [titles: ["The Hobbit", "Fellowship"]]).list()
+ then:
+ results.size() == 2
+ }
+
+ void "populateQueryWithNamedArguments handles null value"() {
+ when:
+ def results = buildHqlQuery("from HibernateHqlQuerySpecBook b where b.title = :t", [t: null]).list()
+ then:
+ results.size() == 0
+ }
+
+ void "populateQueryWithNamedArguments throws for non-string key"() {
+ when:
+ buildHqlQuery("from HibernateHqlQuerySpecBook")
+ .populateQueryWithNamedArguments([(42): "value"])
+ then:
+ thrown(Exception)
+ }
+}
+
+@Entity
+class HibernateHqlQuerySpecBook {
+ String title
+ Integer pages
+ HibernateHqlQuerySpecAuthor author
+
+ static belongsTo = [author: HibernateHqlQuerySpecAuthor]
+
+ static constraints = {
+ title nullable: false
+ pages nullable: false
+ author nullable: true
+ }
+}
+
+@Entity
+class HibernateHqlQuerySpecAuthor {
+ String name
+
+ static hasMany = [books: HibernateHqlQuerySpecBook]
+
+ static constraints = {
+ name nullable: false
+ }
+}