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 <[email protected]>
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
+    }
+}

Reply via email to