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/04/15 20:48:34 UTC

(grails-core) 05/24: Refactor JPA query engine to support Hibernate 7 SQM requirements

This is an automated email from the ASF dual-hosted git repository.

borinquenkid pushed a commit to branch 8.0.x-hibernate7-dev
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 000712416c3c3c10c98f12eba64621dbfb51fbf6
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Sun Apr 12 15:23:29 2026 -0500

    Refactor JPA query engine to support Hibernate 7 SQM requirements
    
    - Introduce JpaQueryContext to manage query state, roots, and aliased expressions.
    - Extract JpaProjectionTranslator to centralize GORM-to-JPA projection mapping.
    - Refactor JpaCriteriaQueryCreator to use a multi-pass approach for alias discovery and query construction.
    - Update PredicateGenerator to support AbstractQuery (unified main query and subquery handling).
    - Add Hibernate7CountProjection for correct property-based count handling.
    - Fix alias order in HibernateCriteriaBuilder and related tests.
    - Add comprehensive unit specs for the new query engine components.
---
 .../grails/orm/HibernateCriteriaBuilder.java       |  14 +-
 .../hibernate/query/Hibernate7CountProjection.java |  36 ++
 .../hibernate/query/JpaCriteriaQueryCreator.java   | 234 ++++----
 .../orm/hibernate/query/JpaFromProvider.java       | 363 -------------
 .../hibernate/query/JpaProjectionTranslator.java   | 105 ++++
 .../orm/hibernate/query/JpaQueryContext.java       | 231 ++++++++
 .../orm/hibernate/query/PredicateGenerator.java    | 595 ++++++---------------
 .../JpaCriteriaQueryCreatorSpec.groovy             |  55 +-
 .../hibernatequery/JpaFromProviderSpec.groovy      | 253 ---------
 .../JpaProjectionTranslatorSpec.groovy             | 124 +++++
 .../hibernatequery/JpaQueryContextSpec.groovy      | 150 ++++++
 .../hibernatequery/PredicateGeneratorSpec.groovy   |  35 +-
 .../HibernateToManyCollectionPropertySpec.groovy   | 200 +++++--
 .../HibernateConnectionSourceFactorySpec.groovy    |  21 -
 14 files changed, 1172 insertions(+), 1244 deletions(-)

diff --git a/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
index 2b3afca5b6..829de67e43 100644
--- a/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
+++ b/grails-data-hibernate7/core/src/main/groovy/grails/orm/HibernateCriteriaBuilder.java
@@ -161,8 +161,18 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
         setDefaultFlushMode(GrailsHibernateTemplate.FLUSH_AUTO);
     }
 
+    public static final String ALIAS_SEPARATOR = ":";
+
     private static String getFullyQualifiedColumn(String propertyName, String alias) {
-        return (Objects.nonNull(alias) ? alias + "." : "") + propertyName;
+        return (Objects.nonNull(alias) ? alias + ALIAS_SEPARATOR : "") + propertyName;
+    }
+
+    public org.grails.datastore.mapping.query.api.Criteria exists(Closure  subquery) {
+        return exists(new grails.gorm.DetachedCriteria(targetClass).build(subquery));
+    }
+
+    public org.grails.datastore.mapping.query.api.Criteria notExists(Closure  subquery) {
+        return notExists(new grails.gorm.DetachedCriteria(targetClass).build(subquery));
     }
 
     public final void setDatastore(HibernateDatastore datastore) {
@@ -324,7 +334,7 @@ public class HibernateCriteriaBuilder extends GroovyObjectSupport implements Bui
      * @param alias The alias to use
      */
     public void count(String propertyName, String alias) {
-        hibernateQuery.projections().countDistinct(getFullyQualifiedColumn(propertyName, alias));
+        hibernateQuery.projections().add(new org.grails.orm.hibernate.query.Hibernate7CountProjection(getFullyQualifiedColumn(propertyName, alias)));
     }
 
     @Override
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/Hibernate7CountProjection.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/Hibernate7CountProjection.java
new file mode 100644
index 0000000000..6c36d9472d
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/Hibernate7CountProjection.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed 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 org.grails.datastore.mapping.query.Query;
+
+/**
+ * A {@link Query.CountProjection} that also includes a property name.
+ *
+ * @author graemerocher
+ * @since 7.0.0
+ */
+public class Hibernate7CountProjection extends Query.CountProjection {
+    private final String propertyName;
+
+    public Hibernate7CountProjection(String propertyName) {
+        this.propertyName = propertyName;
+    }
+
+    public String getPropertyName() {
+        return propertyName;
+    }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.java
index d69344e494..b1803a5a9b 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaCriteriaQueryCreator.java
@@ -1,50 +1,47 @@
 /*
- *  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
+ * Copyright 2024-2025 the original author or authors.
  *
- *    https://www.apache.org/licenses/LICENSE-2.0
+ * Licensed 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
  *
- *  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.
+ *      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 java.util.List;
-import java.util.Objects;
-import java.util.function.Function;
-
+import grails.gorm.DetachedCriteria;
 import jakarta.persistence.Tuple;
+import jakarta.persistence.criteria.AbstractQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
 import jakarta.persistence.criteria.CriteriaQuery;
 import jakarta.persistence.criteria.Expression;
 import jakarta.persistence.criteria.From;
-import jakarta.persistence.criteria.Order;
-import jakarta.persistence.criteria.Path;
-import jakarta.persistence.criteria.Predicate;
 import jakarta.persistence.criteria.Root;
 import jakarta.persistence.criteria.Selection;
-
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.query.Query;
 import org.hibernate.query.criteria.HibernateCriteriaBuilder;
 import org.hibernate.query.criteria.JpaCriteriaQuery;
-import org.hibernate.query.criteria.JpaExpression;
 import org.hibernate.query.criteria.JpaSubQuery;
-
 import org.springframework.core.convert.ConversionService;
 
-import grails.gorm.DetachedCriteria;
-import org.grails.datastore.mapping.model.PersistentEntity;
-import org.grails.datastore.mapping.query.Query;
+import java.util.List;
+import java.util.Objects;
 
-@SuppressWarnings("PMD.DataflowAnomalyAnalysis")
-public class JpaCriteriaQueryCreator {
+/**
+ * A class that creates a JPA {@link CriteriaQuery} from a GORM {@link Query} and {@link DetachedCriteria}.
+ *
+ * @author burt
+ * @author graemerocher
+ * @since 7.0.0
+ */
+public class JpaCriteriaQueryCreator<T> {
 
     private final Query.ProjectionList projections;
     private final HibernateCriteriaBuilder criteriaBuilder;
@@ -55,11 +52,11 @@ public class JpaCriteriaQueryCreator {
 
     public JpaCriteriaQueryCreator(
             Query.ProjectionList projections,
-            HibernateCriteriaBuilder criteriaBuilder,
+            CriteriaBuilder criteriaBuilder,
             PersistentEntity entity,
             DetachedCriteria<?> detachedCriteria,
             ConversionService conversionService) {
-        this(projections, criteriaBuilder, entity, detachedCriteria, conversionService, null);
+        this(projections, (HibernateCriteriaBuilder) criteriaBuilder, entity, detachedCriteria, conversionService, null);
     }
 
     public JpaCriteriaQueryCreator(
@@ -78,21 +75,32 @@ public class JpaCriteriaQueryCreator {
     }
 
     public JpaCriteriaQuery<?> createQuery() {
-
         var projectionList = collectProjections();
         var cq = createCriteriaQuery(projectionList);
         Class<?> javaClass = entity.getJavaClass();
         Root<?> root = cq.from(javaClass);
-        var tablesByName = new JpaFromProvider(
+        var context = new JpaQueryContext(
                 detachedCriteria,
                 projectionList,
                 hibernateQuery != null ? hibernateQuery.getAliases() : List.of(),
                 root);
-        assignProjections(projectionList, cq, tablesByName);
-        assignGroupBy(cq, tablesByName);
+        
+        // Pass 1: Discover all projection aliases without full resolution
+        for (Query.Projection projection : projectionList) {
+            if (projection instanceof Query.PropertyProjection propertyProjection) {
+                context.registerAliasFromPath(propertyProjection.getPropertyName());
+            } else if (projection instanceof Query.CountDistinctProjection countDistinctProjection) {
+                context.registerAliasFromPath(countDistinctProjection.getPropertyName());
+            }
+        }
 
-        assignOrderBy(cq, tablesByName);
-        assignCriteria(cq, root, tablesByName, entity);
+        var translator = new JpaProjectionTranslator(criteriaBuilder, context);
+
+        assignProjections(projectionList, cq, translator, context);
+        assignGroupBy(cq, context);
+
+        assignOrderBy(cq, context);
+        assignCriteria(cq, root, context, entity);
         return cq;
     }
 
@@ -100,15 +108,30 @@ public class JpaCriteriaQueryCreator {
         var projectionList = collectProjections();
         Class<?> javaClass = entity.getJavaClass();
         Root<?> root = subquery.from(javaClass);
-        var tablesByName = new JpaFromProvider(
+        var context = new JpaQueryContext(
                 detachedCriteria,
                 projectionList,
                 hibernateQuery != null ? hibernateQuery.getAliases() : List.of(),
                 root);
 
+        // Pass 1: Discover all projection aliases
+        for (Query.Projection projection : projectionList) {
+            if (projection instanceof Query.PropertyProjection propertyProjection) {
+                context.registerAliasFromPath(propertyProjection.getPropertyName());
+            } else if (projection instanceof Query.CountDistinctProjection countDistinctProjection) {
+                context.registerAliasFromPath(countDistinctProjection.getPropertyName());
+            }
+        }
+
+        if (projectionList.stream().anyMatch(Query.DistinctProjection.class::isInstance)) {
+            subquery.distinct(true);
+        }
+
+        var translator = new JpaProjectionTranslator(criteriaBuilder, context);
+
         var aliasedProjections = new java.util.concurrent.atomic.AtomicInteger(0);
         var projectionExpressions = projectionList.stream()
-                .map(projectionToJpaExpression(tablesByName))
+                .map(translator::translate)
                 .filter(Objects::nonNull)
                 .map(expr -> expr.alias("col_" + aliasedProjections.getAndIncrement()))
                 .toList();
@@ -117,78 +140,87 @@ public class JpaCriteriaQueryCreator {
         }
 
         Expression<?>[] groupByPaths = collectGroupProjections().stream()
-                .map(gp -> (Expression<?>) tablesByName.getFullyQualifiedPath(gp.getPropertyName()))
+                .map(gp -> context.getFullyQualifiedExpression(gp.getPropertyName()))
                 .filter(Objects::nonNull)
-                .toArray(Expression<?>[]::new);
+                .toArray(Expression[]::new);
         if (groupByPaths.length > 0) {
             subquery.groupBy(groupByPaths);
         }
 
-        List<Query.Criterion> criteriaList = detachedCriteria.getCriteria();
-        if (!criteriaList.isEmpty()) {
-            // Build predicates using a temporary CriteriaQuery since PredicateGenerator
-            // requires CriteriaQuery<?> for subquery creation in nested exists/in clauses.
-            // The predicates themselves are independent of the query type.
-            var tempCq = criteriaBuilder.createTupleQuery();
-            tempCq.from(javaClass);
-            Predicate[] predicates = new PredicateGenerator(conversionService)
-                    .getPredicates(criteriaBuilder, tempCq, root, criteriaList, tablesByName, entity);
-            subquery.where(criteriaBuilder.and(predicates));
-        }
+        assignCriteria(subquery, root, context, entity);
     }
 
     private List<Query.Projection> collectProjections() {
-        return projections.getProjectionList().stream()
-                .filter(new ProjectionPredicate())
-                .toList();
+        return projections.getProjectionList();
     }
 
     private JpaCriteriaQuery<?> createCriteriaQuery(List<Query.Projection> projections) {
-        var cq = projections.stream()
-                                .filter(it -> !(it instanceof Query.DistinctProjection ||
-                                        it instanceof Query.DistinctPropertyProjection))
-                                .toList()
-                                .size() > 1 ?
-                criteriaBuilder.createTupleQuery() :
-                criteriaBuilder.createQuery(Object.class);
-        projections.stream()
-                .filter(it -> it instanceof Query.DistinctProjection || it instanceof Query.DistinctPropertyProjection)
-                .findFirst()
-                .ifPresent(projection -> cq.distinct(true));
-        return cq;
+        if (projections.size() > 1) {
+            return (JpaCriteriaQuery<?>) criteriaBuilder.createTupleQuery();
+        } else if (projections.isEmpty()) {
+            return (JpaCriteriaQuery<?>) criteriaBuilder.createQuery(entity.getJavaClass());
+        } else {
+            var first = projections.get(0);
+            if (first instanceof Query.CountProjection || first instanceof Query.CountDistinctProjection) {
+                return (JpaCriteriaQuery<?>) criteriaBuilder.createQuery(Long.class);
+            } else if (first instanceof Query.PropertyProjection propertyProjection) {
+                PersistentEntity persistentEntity = entity.getMappingContext().getPersistentEntity(entity.getJavaClass().getName());
+                String propertyName = propertyProjection.getPropertyName();
+                if (propertyName.contains(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)) {
+                    propertyName = propertyName.split(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)[1];
+                }
+                var property = persistentEntity.getPropertyByName(propertyName);
+                if (property == null) {
+                    return (JpaCriteriaQuery<?>) criteriaBuilder.createQuery(Object.class);
+                }
+                return (JpaCriteriaQuery<?>) criteriaBuilder.createQuery(property.getType());
+            } else if (first instanceof Query.IdProjection) {
+                return (JpaCriteriaQuery<?>) criteriaBuilder.createQuery(entity.getIdentity().getType());
+            }
+            return (JpaCriteriaQuery<?>) criteriaBuilder.createQuery(entity.getJavaClass());
+        }
     }
 
     @SuppressWarnings("unchecked")
     private <T> void assignProjections(
-            List<Query.Projection> projections, CriteriaQuery<T> cq, JpaFromProvider tablesByName) {
+            List<Query.Projection> projections, CriteriaQuery<T> cq, JpaProjectionTranslator translator, JpaQueryContext context) {
+        if (projections.stream().anyMatch(Query.DistinctProjection.class::isInstance)) {
+            cq.distinct(true);
+        }
         var projectionExpressions = projections.stream()
-                .map(projectionToJpaExpression(tablesByName))
+                .map(translator::translate)
                 .filter(Objects::nonNull)
                 .toList();
         if (!projectionExpressions.isEmpty()) {
-            var tupleCriteriaQuery = (CriteriaQuery<Tuple>) cq;
-            tupleCriteriaQuery.select(criteriaBuilder.tuple(projectionExpressions.toArray(new Selection<?>[0])));
+            if (cq.getResultType().equals(Tuple.class)) {
+                cq.select((Selection<? extends T>) criteriaBuilder.tuple(projectionExpressions.toArray(new Selection<?>[0])));
+            } else {
+                cq.select((Selection<? extends T>) projectionExpressions.get(0));
+            }
         } else {
-            cq.select((Selection<? extends T>) tablesByName.getFullyQualifiedPath("root"));
+            // Default select root
+            cq.select((Selection<? extends T>) context.getRoot());
         }
     }
 
-    private void assignGroupBy(CriteriaQuery<?> cq, JpaFromProvider tablesByName) {
-        var groupByPaths = collectGroupProjections().stream()
-                .map(groupPropertyProjection ->
-                        tablesByName.getFullyQualifiedPath(groupPropertyProjection.getPropertyName()))
+    private void assignGroupBy(CriteriaQuery<?> cq, JpaQueryContext context) {
+        var groupByExpressions = collectGroupProjections().stream()
+                .map(groupPropertyProjection -> context.getFullyQualifiedExpression(groupPropertyProjection.getPropertyName()))
                 .filter(Objects::nonNull)
-                .toArray(Path<?>[]::new);
-        cq.groupBy(groupByPaths);
+                .toArray(Expression[]::new);
+        if (groupByExpressions.length > 0) {
+            cq.groupBy(groupByExpressions);
+        }
     }
 
     @SuppressWarnings("unchecked")
-    private void assignOrderBy(CriteriaQuery<?> cq, JpaFromProvider tablesByName) {
+    private void assignOrderBy(CriteriaQuery<?> cq, JpaQueryContext context) {
         List<Query.Order> orders = detachedCriteria.getOrders();
         if (!orders.isEmpty()) {
             var jpaOrders = orders.stream()
                     .map(order -> {
-                        Path<?> expression = tablesByName.getFullyQualifiedPath(order.getProperty());
+                        var propertyName = order.getProperty();
+                        Expression<?> expression = context.getFullyQualifiedExpression(propertyName);
                         if (order.isIgnoreCase() && expression.getJavaType().equals(String.class)) {
                             return order.getDirection().equals(Query.Order.Direction.ASC) ?
                                     criteriaBuilder.asc(criteriaBuilder.lower((Expression<String>) expression)) :
@@ -199,49 +231,21 @@ public class JpaCriteriaQueryCreator {
                                     criteriaBuilder.desc(expression);
                         }
                     })
-                    .toArray(Order[]::new);
+                    .toArray(jakarta.persistence.criteria.Order[]::new);
             cq.orderBy(jpaOrders);
         }
     }
 
     private void assignCriteria(
-            CriteriaQuery<?> cq, From<?, ?> root, JpaFromProvider tablesByName, PersistentEntity entity) {
+            AbstractQuery<?> cq, Root<?> root, JpaQueryContext context, PersistentEntity entity) {
         List<Query.Criterion> criteriaList = detachedCriteria.getCriteria();
         if (!criteriaList.isEmpty()) {
-            Predicate[] predicates = new PredicateGenerator(conversionService)
-                    .getPredicates(criteriaBuilder, cq, root, criteriaList, tablesByName, entity);
-            cq.where(criteriaBuilder.and(predicates));
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    private Function<Query.Projection, JpaExpression<?>> projectionToJpaExpression(JpaFromProvider tablesByName) {
-        return projection -> {
-            if (projection instanceof Query.CountProjection) {
-                return criteriaBuilder.count(tablesByName.getFullyQualifiedPath("root"));
-            } else if (projection instanceof Query.CountDistinctProjection countDistinctProjection) {
-                var propertyName = countDistinctProjection.getPropertyName();
-                return criteriaBuilder.countDistinct(tablesByName.getFullyQualifiedPath("root." + propertyName));
-            } else if (projection instanceof Query.IdProjection) {
-                return (JpaExpression<?>) tablesByName.getFullyQualifiedPath("root.id");
-            } else if (projection instanceof Query.DistinctProjection) {
-                return null;
-            } else {
-                var propertyName = ((Query.PropertyProjection) projection).getPropertyName();
-                Path<?> path = tablesByName.getFullyQualifiedPath(propertyName);
-                if (projection instanceof Query.MaxProjection) {
-                    return criteriaBuilder.max((Expression<? extends Number>) path);
-                } else if (projection instanceof Query.MinProjection) {
-                    return criteriaBuilder.min((Expression<? extends Number>) path);
-                } else if (projection instanceof Query.AvgProjection) {
-                    return criteriaBuilder.avg((Expression<? extends Number>) path);
-                } else if (projection instanceof Query.SumProjection) {
-                    return criteriaBuilder.sum((Expression<? extends Number>) path);
-                } else { // keep this last!!!
-                    return (JpaExpression<?>) path;
-                }
+            var predicateGenerator = new PredicateGenerator(criteriaBuilder, conversionService);
+            var predicate = predicateGenerator.generate(criteriaList, cq, root, context, entity);
+            if (predicate != null) {
+                cq.where(predicate);
             }
-        };
+        }
     }
 
     private List<Query.GroupPropertyProjection> collectGroupProjections() {
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java
deleted file mode 100644
index 5419ca6dfb..0000000000
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaFromProvider.java
+++ /dev/null
@@ -1,363 +0,0 @@
-/*
- *  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 java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.stream.Collectors;
-
-import jakarta.persistence.FetchType;
-import jakarta.persistence.criteria.From;
-import jakarta.persistence.criteria.JoinType;
-import jakarta.persistence.criteria.Path;
-
-import grails.gorm.DetachedCriteria;
-import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria;
-import org.grails.datastore.mapping.model.PersistentEntity;
-import org.grails.datastore.mapping.model.PersistentProperty;
-import org.grails.datastore.mapping.model.types.Association;
-import org.grails.datastore.mapping.model.types.Basic;
-import org.grails.datastore.mapping.query.Query;
-
-@SuppressWarnings({
-    "PMD.DataflowAnomalyAnalysis",
-    "PMD.ProperCloneImplementation",
-    "PMD.CloneMethodReturnTypeMustMatchClassName",
-    "PMD.CloneThrowsCloneNotSupportedException"
-})
-public class JpaFromProvider implements Cloneable {
-
-    private static final int SINGLE_PROPERTY = 1;
-    private static final String ROOT_ALIAS = "root";
-
-    private final Map<String, From<?, ?>> fromMap;
-
-    private JpaFromProvider(Map<String, From<?, ?>> fromMap) {
-        this.fromMap = new HashMap<>(fromMap);
-    }
-
-    public JpaFromProvider(DetachedCriteria<?> detachedCriteria, List<Query.Projection> projections, From<?, ?> root) {
-        this(detachedCriteria, projections, List.of(), root);
-    }
-
-    @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
-    public JpaFromProvider(
-            DetachedCriteria<?> detachedCriteria,
-            List<Query.Projection> projections,
-            List<HibernateAlias> aliases,
-            From<?, ?> root) {
-        fromMap = getFromsByName(detachedCriteria, projections, aliases, root);
-    }
-
-    @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
-    public JpaFromProvider(
-            JpaFromProvider parent,
-            DetachedCriteria<?> detachedCriteria,
-            List<Query.Projection> projections,
-            From<?, ?> root) {
-        fromMap = new HashMap<>(parent.fromMap);
-        fromMap.putAll(getFromsByName(detachedCriteria, projections, List.of(), root));
-    }
-
-    @SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
-    public JpaFromProvider(
-            JpaFromProvider parent,
-            PersistentEntity entity,
-            List<Query.Criterion> criteria,
-            List<Query.Projection> projections,
-            Map<String, jakarta.persistence.FetchType> fetchStrategies,
-            Map<String, jakarta.persistence.criteria.JoinType> joinTypes,
-            From<?, ?> root) {
-        fromMap = new HashMap<>(parent.fromMap);
-        fromMap.putAll(getFromsByName(entity, criteria, projections, List.of(), fetchStrategies, joinTypes, root));
-    }
-
-    public Map<String, From<?, ?>> getFromsByName() {
-        return fromMap;
-    }
-
-    public boolean hasAlias(String name) {
-        return fromMap.containsKey(name);
-    }
-
-    protected Map<String, From<?, ?>> getFromsByName(
-            DetachedCriteria<?> detachedCriteria,
-            List<Query.Projection> projections,
-            List<HibernateAlias> aliases,
-            From<?, ?> root) {
-        Map<String, From<?, ?>> froms = getFromsByName(
-                detachedCriteria.getPersistentEntity(),
-                detachedCriteria.getCriteria(),
-                projections,
-                aliases,
-                detachedCriteria.getFetchStrategies(),
-                detachedCriteria.getJoinTypes(),
-                root);
-        if (detachedCriteria.getAlias() != null) {
-            froms.put(detachedCriteria.getAlias(), root);
-        }
-        return froms;
-    }
-
-    protected Map<String, From<?, ?>> getFromsByName(
-            PersistentEntity entity,
-            List<Query.Criterion> criteria,
-            List<Query.Projection> projections,
-            List<HibernateAlias> aliases,
-            Map<String, FetchType> fetchStrategies,
-            Map<String, JoinType> joinTypes,
-            From<?, ?> root) {
-        var allCriteriaPaths =
-                criteria.stream().flatMap(c -> findPaths(c).stream()).toList();
-
-        var detachedAssociationCriteriaList = criteria.stream()
-                .map(new DetachedAssociationFunction())
-                .flatMap(List::stream)
-                .toList();
-
-        var aliasMap = createAliasMap(detachedAssociationCriteriaList);
-
-        // Also scan for HibernateAlias (basic collections)
-        Map<String, String> basicAliasMap = new HashMap<>();
-        Map<String, JoinType> basicJoinTypeMap = new HashMap<>();
-        for (HibernateAlias ha : aliases) {
-            basicAliasMap.put(ha.path(), ha.alias());
-            basicJoinTypeMap.put(ha.path(), ha.joinType());
-        }
-
-        var definedAliases = detachedAssociationCriteriaList.stream()
-                .map(DetachedAssociationCriteria::getAlias)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toSet());
-        definedAliases.addAll(basicAliasMap.values());
-
-        var associationProjectedPaths = projections.stream()
-                .filter(Query.PropertyProjection.class::isInstance)
-                .map(p -> ((Query.PropertyProjection) p).getPropertyName())
-                .filter(name -> name.contains("."))
-                .map(name -> name.substring(0, name.lastIndexOf('.')))
-                .collect(Collectors.toSet());
-
-        var criteriaPaths = allCriteriaPaths.stream()
-                .filter(p -> p.contains("."))
-                .map(p -> p.substring(0, p.lastIndexOf('.')))
-                .collect(Collectors.toSet());
-
-        var eagerPaths = fetchStrategies.entrySet().stream()
-                .filter(entry -> entry.getValue().equals(FetchType.EAGER))
-                .map(Map.Entry::getKey)
-                .collect(Collectors.toSet());
-
-        var collectionPaths = entity != null ?
-                entity.getPersistentProperties().stream()
-                        .filter(p -> p instanceof org.grails.datastore.mapping.model.types.Basic)
-                        .map(org.grails.datastore.mapping.model.PersistentProperty::getName)
-                        .collect(Collectors.toSet()) :
-                java.util.Collections.<String>emptySet();
-
-        java.util.Set<String> allPaths = new java.util.HashSet<>();
-        allPaths.addAll(aliasMap.keySet());
-        allPaths.addAll(basicAliasMap.keySet());
-        allPaths.addAll(associationProjectedPaths);
-        allPaths.addAll(criteriaPaths);
-        allPaths.addAll(eagerPaths);
-        allPaths.addAll(collectionPaths);
-
-        // Don't try to join segments that are already defined aliases
-        allPaths.removeAll(definedAliases);
-
-        // Expand paths to include all parents (e.g., "a.b.c" -> "a", "a.b", "a.b.c")
-        java.util.Set<String> expandedPaths = new java.util.HashSet<>();
-        for (String path : allPaths) {
-            String[] segments = path.split("\\.");
-            StringBuilder current = new StringBuilder();
-            for (String segment : segments) {
-                if (!current.isEmpty()) {
-                    current.append(".");
-                }
-                current.append(segment);
-                expandedPaths.add(current.toString());
-            }
-        }
-
-        // Re-calculate projected paths to include expanded segments for LEFT join logic
-        var finalProjectedPaths = expandedPaths.stream()
-                .filter(p -> associationProjectedPaths.stream().anyMatch(dp -> dp.equals(p) || dp.startsWith(p + ".")))
-                .collect(Collectors.toSet());
-
-        Map<String, From<?, ?>> fromsByPath = new HashMap<>();
-        fromsByPath.put(ROOT_ALIAS, root);
-
-        List<String> sortedPaths = expandedPaths.stream()
-                .sorted(java.util.Comparator.comparingInt(p -> p.split("\\.").length))
-                .toList();
-
-        for (String path : sortedPaths) {
-            if (fromsByPath.containsKey(path)) {
-                continue;
-            }
-            String parentPath = path.contains(".") ? path.substring(0, path.lastIndexOf('.')) : ROOT_ALIAS;
-            String leaf = path.contains(".") ? path.substring(path.lastIndexOf('.') + 1) : path;
-
-            From<?, ?> base = fromsByPath.get(parentPath);
-            if (base == null) {
-                continue;
-            }
-
-            // Hibernate 7: Ensure we only join associations or basic collections
-            if (leaf.equals("{alias}")) {
-                continue;
-            }
-            PersistentProperty property = entity != null ? entity.getPropertyByName(leaf) : null;
-            if (property != null && !(property instanceof Association || property instanceof Basic)) {
-                continue;
-            }
-            if (property == null && parentPath.equals(ROOT_ALIAS)) {
-                // Top-level property not found or is a special token, skip join
-                continue;
-            }
-
-            JoinType joinType = JoinType.INNER;
-            if (joinTypes.containsKey(path)) {
-                joinType = joinTypes.get(path);
-            } else if (basicJoinTypeMap.containsKey(path)) {
-                joinType = basicJoinTypeMap.get(path);
-            } else if (finalProjectedPaths.contains(path) ||
-                    eagerPaths.contains(path) ||
-                    collectionPaths.contains(path)) {
-                joinType = JoinType.LEFT;
-            }
-
-            var table = base.join(leaf, joinType);
-
-            boolean aliasApplied = false;
-            // If there's an alias for this path, map it to the alias too
-            var dac = aliasMap.get(path);
-            if (dac != null && dac.getAlias() != null) {
-                table.alias(dac.getAlias());
-                fromsByPath.put(dac.getAlias(), table);
-                aliasApplied = true;
-            }
-
-            String basicAlias = basicAliasMap.get(path);
-            if (basicAlias != null) {
-                table.alias(basicAlias);
-                fromsByPath.put(basicAlias, table);
-                aliasApplied = true;
-            }
-
-            if (!aliasApplied) {
-                table.alias(path);
-            }
-            fromsByPath.put(path, table);
-        }
-
-        return fromsByPath;
-    }
-
-    private java.util.Set<String> findPaths(Query.Criterion criterion) {
-        java.util.Set<String> paths = new java.util.HashSet<>();
-        if (criterion instanceof Query.PropertyNameCriterion pnc) {
-            paths.add(pnc.getProperty());
-        } else if (criterion instanceof Query.Junction junction) {
-            for (Query.Criterion c : junction.getCriteria()) {
-                paths.addAll(findPaths(c));
-            }
-        }
-        return paths;
-    }
-
-    private Map<String, DetachedCriteria<?>> createAliasMap(
-            List<DetachedAssociationCriteria<?>> detachedAssociationCriteriaList) {
-        // Use a merge function and a stable map type to avoid DuplicateKey exceptions when the same
-        // association path/alias appears multiple times (e.g., referenced in both predicate and sort).
-        // Keep the first occurrence to preserve deterministic aliasing.
-        return detachedAssociationCriteriaList.stream()
-                .map(new AliasMapEntryFunction())
-                .collect(Collectors.toMap(
-                        Map.Entry::getKey,
-                        Map.Entry::getValue,
-                        (existing, replacement) -> existing,
-                        java.util.LinkedHashMap::new));
-    }
-
-    public Path<?> getFullyQualifiedPath(String propertyName) {
-        if (Objects.isNull(propertyName) || propertyName.trim().isEmpty()) {
-            throw new IllegalArgumentException("propertyName cannot be null");
-        }
-
-        if (fromMap.containsKey(propertyName)) {
-            return fromMap.get(propertyName);
-        }
-
-        String[] parsed = propertyName.split("\\.");
-        if (parsed.length == SINGLE_PROPERTY) {
-            From<?, ?> root = fromMap.get(ROOT_ALIAS);
-            if (root != null) {
-                if (propertyName.equals(root.getJavaType().getSimpleName()) ||
-                        propertyName.equals(root.getJavaType().getName())) {
-                    return root;
-                }
-                return root.get(propertyName);
-            }
-        }
-
-        // Try to find the longest matching prefix in fromMap
-        for (int i = parsed.length; i >= 1; i--) {
-            String prefix = java.util.Arrays.stream(parsed, 0, i).collect(Collectors.joining("."));
-            if (fromMap.containsKey(prefix)) {
-                Path<?> path = fromMap.get(prefix);
-                if (path != null) {
-                    for (int j = i; j < parsed.length; j++) {
-                        path = path.get(parsed[j]);
-                        if (path == null) {
-                            break;
-                        }
-                    }
-                    if (path != null) {
-                        return path;
-                    }
-                }
-            }
-        }
-
-        // Fallback to root
-        Path<?> path = fromMap.get(ROOT_ALIAS);
-        if (path != null) {
-            for (String segment : parsed) {
-                path = path.get(segment);
-                if (path == null) {
-                    break;
-                }
-            }
-        }
-        return path;
-    }
-
-    @Override
-    public Object clone() {
-        return new JpaFromProvider(fromMap);
-    }
-
-    public void put(String tableName, From<?, ?> child) {
-        fromMap.put(tableName, child);
-    }
-}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaProjectionTranslator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaProjectionTranslator.java
new file mode 100644
index 0000000000..c72d908a8e
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaProjectionTranslator.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed 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 jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.Expression;
+import org.grails.datastore.mapping.query.Query;
+import org.hibernate.query.criteria.JpaExpression;
+
+/**
+ * A class that translates GORM projections to JPA expressions.
+ *
+ * @author burt
+ * @author graemerocher
+ * @since 7.0.0
+ */
+public class JpaProjectionTranslator {
+
+    private final CriteriaBuilder criteriaBuilder;
+    private final JpaQueryContext context;
+
+    public JpaProjectionTranslator(CriteriaBuilder criteriaBuilder, JpaQueryContext context) {
+        this.criteriaBuilder = criteriaBuilder;
+        this.context = context;
+    }
+
+    @SuppressWarnings("unchecked")
+    public JpaExpression<?> translate(Query.Projection projection) {
+        JpaExpression<?> jpaExpression;
+        String propertyName = null;
+        String alias = null;
+
+        if (projection instanceof Hibernate7CountProjection countProjection) {
+            propertyName = countProjection.getPropertyName();
+        } else if (projection instanceof Query.PropertyProjection propertyProjection) {
+            propertyName = propertyProjection.getPropertyName();
+        } else if (projection instanceof Query.CountDistinctProjection countDistinctProjection) {
+            propertyName = countDistinctProjection.getPropertyName();
+        }
+
+        if (propertyName != null && propertyName.contains(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)) {
+            String[] parts = propertyName.split(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR);
+            alias = parts[0];
+            propertyName = parts[1];
+        }
+
+        if (projection instanceof Query.CountProjection) {
+            Expression<?> pathExpr;
+            if (propertyName != null) {
+                pathExpr = context.getAliasedExpression(propertyName);
+                if (pathExpr == null) {
+                    pathExpr = context.getFullyQualifiedExpression("root." + propertyName);
+                }
+            } else {
+                pathExpr = context.getRoot();
+            }
+            jpaExpression = (JpaExpression<?>) criteriaBuilder.count(pathExpr);
+        } else if (projection instanceof Query.CountDistinctProjection) {
+            Expression<?> pathExpr = context.getAliasedExpression(propertyName);
+            if (pathExpr == null) {
+                pathExpr = context.getFullyQualifiedExpression("root." + propertyName);
+            }
+            jpaExpression = (JpaExpression<?>) criteriaBuilder.countDistinct(pathExpr);
+        } else if (projection instanceof Query.IdProjection) {
+            jpaExpression = (JpaExpression<?>) context.getFullyQualifiedPath("root.id");
+        } else if (projection instanceof Query.DistinctProjection) {
+            return null;
+        } else if (projection instanceof Query.PropertyProjection) {
+            Expression<?> expression = context.getFullyQualifiedExpression(propertyName);
+
+            if (projection instanceof Query.MaxProjection) {
+                jpaExpression = (JpaExpression<?>) criteriaBuilder.max((Expression<? extends Number>) expression);
+            } else if (projection instanceof Query.MinProjection) {
+                jpaExpression = (JpaExpression<?>) criteriaBuilder.min((Expression<? extends Number>) expression);
+            } else if (projection instanceof Query.AvgProjection) {
+                jpaExpression = (JpaExpression<?>) criteriaBuilder.avg((Expression<? extends Number>) expression);
+            } else if (projection instanceof Query.SumProjection) {
+                jpaExpression = (JpaExpression<?>) criteriaBuilder.sum((Expression<? extends Number>) expression);
+            } else {
+                jpaExpression = (JpaExpression<?>) expression;
+            }
+        } else {
+            throw new UnsupportedOperationException("Unsupported projection: " + projection.getClass().getName());
+        }
+
+        if (alias != null && jpaExpression != null) {
+            jpaExpression.alias(alias);
+            context.registerAlias(alias, jpaExpression);
+        }
+        return jpaExpression;
+    }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaQueryContext.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaQueryContext.java
new file mode 100644
index 0000000000..1f35fb706b
--- /dev/null
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/JpaQueryContext.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed 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 jakarta.persistence.criteria.Expression;
+import jakarta.persistence.criteria.From;
+import jakarta.persistence.criteria.Path;
+import org.hibernate.query.criteria.JpaExpression;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import grails.gorm.DetachedCriteria;
+import org.grails.datastore.mapping.model.PersistentEntity;
+import org.grails.datastore.mapping.query.Query;
+
+/**
+ * A class that provides the {@link From} and {@link Path} for a given alias or property path.
+ *
+ * @author burt
+ * @author graemerocher
+ * @since 7.0.0
+ */
+public class JpaQueryContext implements Cloneable {
+
+    private final Map<String, From<?, ?>> fromMap = new HashMap<>();
+    private final Map<String, Expression<?>> aliasMap = new HashMap<>();
+    private From<?, ?> root;
+
+    public JpaQueryContext() {
+    }
+
+    public JpaQueryContext(
+            DetachedCriteria<?> detachedCriteria,
+            List<Query.Projection> projections,
+            List<HibernateAlias> aliases,
+            From<?, ?> root) {
+        this.root = root;
+    }
+
+    /**
+     * Constructor for PredicateGenerator subqueries.
+     */
+    public JpaQueryContext(
+            JpaQueryContext parent,
+            PersistentEntity entity,
+            List<Query.Criterion> criteria,
+            List<Object> arguments,
+            Map<?, ?> fetchStrategies,
+            Map<?, ?> joinTypes,
+            From<?, ?> root) {
+        this.root = root;
+        if (parent != null) {
+            this.fromMap.putAll(parent.fromMap);
+            this.aliasMap.putAll(parent.aliasMap);
+        }
+    }
+
+    /**
+     * Constructor for simpler subqueries or clones.
+     */
+    public JpaQueryContext(
+            JpaQueryContext parent,
+            DetachedCriteria<?> detachedCriteria,
+            List<Object> aliases,
+            From<?, ?> root) {
+        this.root = root;
+        if (parent != null) {
+            this.fromMap.putAll(parent.fromMap);
+            this.aliasMap.putAll(parent.aliasMap);
+        }
+    }
+
+    public void setRoot(From<?, ?> root) {
+        this.root = root;
+    }
+
+    public From<?, ?> getRoot() {
+        return root;
+    }
+
+    public void addFrom(String path, From<?, ?> from) {
+        fromMap.put(path, from);
+    }
+
+    public From<?, ?> getFrom(String path) {
+        return fromMap.get(path);
+    }
+
+    public void registerAlias(String alias, Expression<?> expression) {
+        aliasMap.put(alias, expression);
+    }
+
+    public void registerAliasFromPath(String path) {
+        if (path != null && path.contains(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)) {
+            String alias = path.split(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)[0];
+            if (!aliasMap.containsKey(alias)) {
+                aliasMap.put(alias, null); // Placeholder to mark it as a known alias
+            }
+        }
+    }
+
+    public boolean hasAlias(String alias) {
+        if (alias == null) return false;
+        String key = alias;
+        if (key.contains(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)) {
+            key = key.split(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)[0];
+        }
+        return aliasMap.containsKey(key);
+    }
+
+    public Expression<?> getAliasedExpression(String alias) {
+        if (alias == null) return null;
+        String key = alias;
+        if (key.contains(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)) {
+            key = key.split(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)[0];
+        }
+        
+        if (aliasMap.containsKey(key)) {
+            return aliasMap.get(key);
+        }
+        if (key.startsWith("root.")) {
+            String stripped = key.substring(5);
+            if (aliasMap.containsKey(stripped)) {
+                return aliasMap.get(stripped);
+            }
+        }
+        return null;
+    }
+
+    public Expression<?> getFullyQualifiedExpression(String path) {
+        Expression<?> aliased = getAliasedExpression(path);
+        if (aliased != null) {
+            return aliased;
+        }
+
+        if (path.equals("root")) {
+            return root;
+        }
+        
+        String propertyName = path;
+        if (propertyName.startsWith("root.")) {
+            propertyName = propertyName.substring(5);
+        }
+        
+        String alias = null;
+        if (propertyName.contains(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)) {
+            String[] parts = propertyName.split(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR);
+            alias = parts[0];
+            propertyName = parts[1];
+        }
+
+        Expression<?> expression = getAliasedExpression(alias != null ? alias : propertyName);
+        if (expression == null) {
+            expression = getFullyQualifiedPath(propertyName);
+        }
+
+        if (alias != null && expression instanceof JpaExpression) {
+            ((JpaExpression<?>) expression).alias(alias);
+            registerAlias(alias, expression);
+        }
+        
+        return expression;
+    }
+
+    public Path<?> getFullyQualifiedPath(String path) {
+        if (hasAlias(path)) {
+            Expression<?> aliased = getAliasedExpression(path);
+            if (aliased instanceof Path<?>) {
+                return (Path<?>) aliased;
+            }
+            return null; // Known alias, not a path
+        }
+
+        String propertyName = path;
+        if (propertyName.contains(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)) {
+            propertyName = propertyName.split(grails.orm.HibernateCriteriaBuilder.ALIAS_SEPARATOR)[1];
+        }
+
+        if (propertyName.startsWith("root.")) {
+            propertyName = propertyName.substring(5);
+        }
+
+        if (hasAlias(propertyName)) {
+            Expression<?> aliased = getAliasedExpression(propertyName);
+            if (aliased instanceof Path<?>) {
+                return (Path<?>) aliased;
+            }
+            return null; // Known alias, not a path
+        }
+
+        if (propertyName.equals("root")) {
+            return root;
+        }
+        
+        String[] parts = propertyName.split("\\.");
+        if (hasAlias(parts[0])) {
+             return null;
+        }
+        
+        Path<?> p = root;
+        for (String part : parts) {
+            p = p.get(part);
+        }
+        return p;
+    }
+
+    @Override
+    public JpaQueryContext clone() {
+        try {
+            JpaQueryContext cloned = (JpaQueryContext) super.clone();
+            cloned.fromMap.putAll(this.fromMap);
+            cloned.aliasMap.putAll(this.aliasMap);
+            return cloned;
+        } catch (CloneNotSupportedException e) {
+            throw new AssertionError();
+        }
+    }
+}
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java
index ed194306f7..90b1dd45b2 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/PredicateGenerator.java
@@ -27,6 +27,7 @@ import java.util.stream.Stream;
 
 import groovy.util.logging.Slf4j;
 
+import jakarta.persistence.criteria.AbstractQuery;
 import jakarta.persistence.criteria.CriteriaBuilder;
 import jakarta.persistence.criteria.CriteriaQuery;
 import jakarta.persistence.criteria.Expression;
@@ -37,7 +38,9 @@ import jakarta.persistence.criteria.Root;
 import jakarta.persistence.criteria.Subquery;
 
 import org.hibernate.query.criteria.HibernateCriteriaBuilder;
+import org.hibernate.query.criteria.JpaExpression;
 import org.hibernate.query.criteria.JpaInPredicate;
+import org.hibernate.query.criteria.JpaSubQuery;
 import org.hibernate.query.sqm.tree.domain.SqmPath;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -67,96 +70,95 @@ public class PredicateGenerator {
     private static final Logger log = LoggerFactory.getLogger(PredicateGenerator.class);
 
     private final ConversionService conversionService;
+    private final HibernateCriteriaBuilder criteriaBuilder;
 
-    public PredicateGenerator(ConversionService conversionService) {
+    public PredicateGenerator(HibernateCriteriaBuilder criteriaBuilder, ConversionService conversionService) {
+        this.criteriaBuilder = criteriaBuilder;
         this.conversionService = conversionService;
     }
 
     public Predicate[] getPredicates(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
+            AbstractQuery<?> criteriaQuery,
             From<?, ?> root_,
             List<? extends Query.QueryElement> criteriaList,
-            JpaFromProvider fromsByProvider,
+            JpaQueryContext fromsByProvider,
             PersistentEntity entity) {
 
         List<Predicate> list = criteriaList.stream()
-                .map(criterion -> handleCriterion(cb, criteriaQuery, root_, fromsByProvider, entity, criterion))
+                .map(criterion -> handleCriterion(criteriaQuery, root_, fromsByProvider, entity, criterion))
                 .filter(Objects::nonNull)
                 .toList();
 
         if (list.isEmpty()) {
-            list = List.of(cb.equal(cb.literal(1), cb.literal(1)));
+            list = List.of(criteriaBuilder.equal(criteriaBuilder.literal(1), criteriaBuilder.literal(1)));
         }
         return list.toArray(new Predicate[0]);
     }
 
     private Predicate handleCriterion(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
+            AbstractQuery<?> criteriaQuery,
             From<?, ?> root,
-            JpaFromProvider fromsByProvider,
+            JpaQueryContext fromsByProvider,
             PersistentEntity entity,
             Query.QueryElement criterion) {
+
         if (criterion instanceof Query.Junction junction) {
-            return handleJunction(cb, criteriaQuery, root, fromsByProvider, entity, junction);
+            return handleJunction(criteriaQuery, root, fromsByProvider, entity, junction);
         } else if (criterion instanceof Query.DistinctProjection) {
-            return cb.conjunction();
+            return criteriaBuilder.conjunction();
         } else if (criterion instanceof DetachedAssociationCriteria<?> c) {
-            return handleAssociationCriteria(cb, criteriaQuery, fromsByProvider, c);
+            return handleAssociationCriteria(criteriaQuery, fromsByProvider, c);
         } else if (criterion instanceof HibernateAssociationQuery haq) {
-            return handleHibernateAssociationQuery(cb, criteriaQuery, fromsByProvider, haq);
+            return handleHibernateAssociationQuery(criteriaQuery, fromsByProvider, haq);
         } else if (criterion instanceof Query.PropertyCriterion pc) {
-            return handlePropertyCriterion(cb, criteriaQuery, root, fromsByProvider, entity, pc);
+            return handlePropertyCriterion(criteriaQuery, root, fromsByProvider, entity, pc);
         } else if (criterion instanceof Query.PropertyComparisonCriterion c) {
-            return handlePropertyComparisonCriterion(cb, fromsByProvider, c);
+            return handlePropertyComparisonCriterion(fromsByProvider, c);
         } else if (criterion instanceof Query.PropertyNameCriterion c) {
-            return handlePropertyNameCriterion(cb, fromsByProvider, c);
+            return handlePropertyNameCriterion(fromsByProvider, c);
         } else if (criterion instanceof Query.Exists c) {
             return handleExists(
-                    cb, criteriaQuery, root, fromsByProvider, c.getSubquery().getPersistentEntity(), c);
+                    criteriaQuery, root, fromsByProvider, c.getSubquery().getPersistentEntity(), c);
         } else if (criterion instanceof Query.NotExists c) {
             PersistentEntity childEntity = c.getSubquery().getPersistentEntity();
-            return cb.not(handleExists(
-                    cb, criteriaQuery, root, fromsByProvider, childEntity, new Query.Exists(c.getSubquery())));
+            return criteriaBuilder.not(handleExists(
+                    criteriaQuery, root, fromsByProvider, childEntity, new Query.Exists(c.getSubquery())));
         } else if (criterion instanceof HibernateAlias) {
-            return null; // Metadata only, handled by JpaFromProvider
+            return null; // Metadata only, handled by JpaQueryContext
         }
         throw new IllegalArgumentException("Unsupported criterion: " + criterion);
     }
 
     private Predicate handleJunction(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
+            AbstractQuery<?> criteriaQuery,
             From<?, ?> root_,
-            JpaFromProvider fromsByProvider,
+            JpaQueryContext fromsByProvider,
             PersistentEntity entity,
             Query.Junction junction) {
-        var predicates = getPredicates(cb, criteriaQuery, root_, junction.getCriteria(), fromsByProvider, entity);
+        var predicates = getPredicates(criteriaQuery, root_, junction.getCriteria(), fromsByProvider, entity);
         if (junction instanceof Query.Disjunction) {
-            return cb.or(predicates);
+            return criteriaBuilder.or(predicates);
         } else if (junction instanceof Query.Conjunction) {
-            return cb.and(predicates);
+            return criteriaBuilder.and(predicates);
         } else if (junction instanceof Query.Negation) {
             if (predicates.length != 1) {
                 log.error("Must have a single predicate behind a not");
                 throw new RuntimeException("Must have a single predicate behind a not");
             }
-            return cb.not(predicates[0]);
+            return criteriaBuilder.not(predicates[0]);
         }
         return null;
     }
 
     private Predicate handleAssociationCriteria(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
-            JpaFromProvider fromsByProvider,
+            AbstractQuery<?> criteriaQuery,
+            JpaQueryContext fromsByProvider,
             DetachedAssociationCriteria<?> c) {
 
         From<?, ?> child = (From<?, ?>) fromsByProvider.getFullyQualifiedPath(c.getAssociationPath());
         PersistentEntity associatedEntity = c.getAssociation().getAssociatedEntity();
 
-        JpaFromProvider childTablesByName = new JpaFromProvider(
+        JpaQueryContext childTablesByName = new JpaQueryContext(
                 fromsByProvider,
                 associatedEntity,
                 c.getCriteria(),
@@ -165,16 +167,15 @@ public class PredicateGenerator {
                 c.getJoinTypes(),
                 child);
 
-        return cb.and(getPredicates(cb, criteriaQuery, child, c.getCriteria(), childTablesByName, associatedEntity));
+        return criteriaBuilder.and(getPredicates(criteriaQuery, child, c.getCriteria(), childTablesByName, associatedEntity));
     }
 
     private Predicate handleHibernateAssociationQuery(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
-            JpaFromProvider fromsByProvider,
+            AbstractQuery<?> criteriaQuery,
+            JpaQueryContext fromsByProvider,
             HibernateAssociationQuery haq) {
         From<?, ?> child = (From<?, ?>) fromsByProvider.getFullyQualifiedPath(haq.associationPath);
-        JpaFromProvider childFroms = new JpaFromProvider(
+        JpaQueryContext childFroms = new JpaQueryContext(
                 fromsByProvider,
                 haq.getEntity(),
                 haq.getAssociationCriteria(),
@@ -182,15 +183,14 @@ public class PredicateGenerator {
                 java.util.Collections.emptyMap(),
                 java.util.Collections.emptyMap(),
                 child);
-        return cb.and(
-                getPredicates(cb, criteriaQuery, child, haq.getAssociationCriteria(), childFroms, haq.getEntity()));
+        return criteriaBuilder.and(
+                getPredicates(criteriaQuery, child, haq.getAssociationCriteria(), childFroms, haq.getEntity()));
     }
 
     private Predicate handlePropertyCriterion(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
+            AbstractQuery<?> criteriaQuery,
             From<?, ?> root,
-            JpaFromProvider fromsByProvider,
+            JpaQueryContext fromsByProvider,
             PersistentEntity entity,
             Query.PropertyCriterion pc) {
 
@@ -199,437 +199,152 @@ public class PredicateGenerator {
                 !propertyName.contains(".") &&
                 entity.getPropertyByName(propertyName) == null &&
                 !fromsByProvider.hasAlias(propertyName)) {
-            throw new ConfigurationException(
-                    "Property [" + propertyName + "] is not a valid property of class [" + entity.getName() + "]");
+            throw new ConfigurationException("Property [" + propertyName + "] is not a valid property of [" + entity.getName() + "]");
         }
 
-        var fullyQualifiedPath = fromsByProvider.getFullyQualifiedPath(pc.getProperty());
-
-        if (pc instanceof Query.NotIn c) {
-            return handleNotIn(cb, criteriaQuery, fromsByProvider, entity, c, fullyQualifiedPath);
-        } else if (pc instanceof Query.SubqueryCriterion c) {
-            return handleSubqueryCriterion(cb, criteriaQuery, fromsByProvider, c);
-        } else if (pc instanceof Query.In c) {
-            return handleIn(cb, criteriaQuery, fromsByProvider, entity, c, fullyQualifiedPath);
-        } else if (pc instanceof Query.ILike c) {
-            return cb.ilike(
-                    (Expression<String>) fullyQualifiedPath, c.getValue().toString());
-        } else if (pc instanceof Query.RLike c) {
-            return handleRLike(cb, fullyQualifiedPath, c);
-        } else if (pc instanceof Query.Like c) {
-            return cb.like((Expression<String>) fullyQualifiedPath, c.getValue().toString());
-        } else if (pc instanceof Query.Equals c) {
-            return cb.equal(fullyQualifiedPath, normalizeValue(c.getValue()));
-        } else if (pc instanceof Query.NotEquals c) {
-            return cb.or(cb.notEqual(fullyQualifiedPath, normalizeValue(c.getValue())), cb.isNull(fullyQualifiedPath));
-        } else if (pc instanceof Query.IdEquals c) {
-            return cb.equal(root.get("id"), normalizeValue(c.getValue()));
-        } else if (pc instanceof Query.GreaterThan c) {
-            if (c.getValue() == null) {
-                throw new ConfigurationException("Property [greaterThan] cannot be null");
-            }
-            if (c.getValue() instanceof Number || c.getValue() instanceof PropertyArithmetic) {
-                Expression<? extends Number> rhs = resolveNumericExpression(cb, root, c);
-                return rhs != null ? cb.gt((Expression<? extends Number>) fullyQualifiedPath, rhs) : cb.gt((Expression<? extends Number>) fullyQualifiedPath, (Number) getNumericValue(c));
+        Expression<?> propertyPath = fromsByProvider.getFullyQualifiedExpression(propertyName);
+
+        if (pc instanceof Query.Equals) {
+            return handleEquals(criteriaQuery, pc, propertyPath);
+        } else if (pc instanceof Query.NotEquals) {
+            return criteriaBuilder.not(handleEquals(criteriaQuery, pc, propertyPath));
+        } else if (pc instanceof Query.Like) {
+            return criteriaBuilder.like((Expression<String>) propertyPath, (String) convertValue(entity, propertyName, pc.getValue()));
+        } else if (pc instanceof Query.ILike) {
+            return criteriaBuilder.like(criteriaBuilder.lower((Expression<String>) propertyPath), ((String) convertValue(entity, propertyName, pc.getValue())).toLowerCase());
+        } else if (pc instanceof Query.RLike rLike) {
+            return criteriaBuilder.like((Expression<String>) propertyPath, (String) convertValue(entity, propertyName, pc.getValue()));
+        } else if (pc instanceof Query.GreaterThan) {
+            Object value = convertValue(entity, propertyName, pc.getValue());
+            if (value instanceof Number) {
+                return criteriaBuilder.gt((Expression<? extends Number>) propertyPath, (Number) value);
             } else {
-                return cb.greaterThan((Expression) fullyQualifiedPath, (Comparable) normalizeValue(c.getValue()));
+                return criteriaBuilder.greaterThan((Expression<? extends Comparable>) propertyPath, (Comparable) value);
             }
-        } else if (pc instanceof Query.GreaterThanEquals c) {
-            if (c.getValue() == null) {
-                throw new ConfigurationException("Property [greaterThanEquals] cannot be null");
-            }
-            if (c.getValue() instanceof Number || c.getValue() instanceof PropertyArithmetic) {
-                Expression<? extends Number> rhs = resolveNumericExpression(cb, root, c);
-                return rhs != null ? cb.ge((Expression<? extends Number>) fullyQualifiedPath, rhs) : cb.ge((Expression<? extends Number>) fullyQualifiedPath, (Number) getNumericValue(c));
+        } else if (pc instanceof Query.GreaterThanEquals) {
+            Object value = convertValue(entity, propertyName, pc.getValue());
+            if (value instanceof Number) {
+                return criteriaBuilder.ge((Expression<? extends Number>) propertyPath, (Number) value);
             } else {
-                return cb.greaterThanOrEqualTo((Expression) fullyQualifiedPath, (Comparable) normalizeValue(c.getValue()));
-            }
-        } else if (pc instanceof Query.LessThan c) {
-            if (c.getValue() == null) {
-                throw new ConfigurationException("Property [lessThan] cannot be null");
+                return criteriaBuilder.greaterThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Comparable) value);
             }
-            if (c.getValue() instanceof Number || c.getValue() instanceof PropertyArithmetic) {
-                Expression<? extends Number> rhs = resolveNumericExpression(cb, root, c);
-                return rhs != null ? cb.lt((Expression<? extends Number>) fullyQualifiedPath, rhs) : cb.lt((Expression<? extends Number>) fullyQualifiedPath, (Number) getNumericValue(c));
+        } else if (pc instanceof Query.LessThan) {
+            Object value = convertValue(entity, propertyName, pc.getValue());
+            if (value instanceof Number) {
+                return criteriaBuilder.lt((Expression<? extends Number>) propertyPath, (Number) value);
             } else {
-                return cb.lessThan((Expression) fullyQualifiedPath, (Comparable) normalizeValue(c.getValue()));
+                return criteriaBuilder.lessThan((Expression<? extends Comparable>) propertyPath, (Comparable) value);
             }
-        } else if (pc instanceof Query.LessThanEquals c) {
-            if (c.getValue() == null) {
-                throw new ConfigurationException("Property [lessThanEquals] cannot be null");
-            }
-            if (c.getValue() instanceof Number || c.getValue() instanceof PropertyArithmetic) {
-                Expression<? extends Number> rhs = resolveNumericExpression(cb, root, c);
-                return rhs != null ? cb.le((Expression<? extends Number>) fullyQualifiedPath, rhs) : cb.le((Expression<? extends Number>) fullyQualifiedPath, (Number) getNumericValue(c));
+        } else if (pc instanceof Query.LessThanEquals) {
+            Object value = convertValue(entity, propertyName, pc.getValue());
+            if (value instanceof Number) {
+                return criteriaBuilder.le((Expression<? extends Number>) propertyPath, (Number) value);
             } else {
-                return cb.lessThanOrEqualTo((Expression) fullyQualifiedPath, (Comparable) normalizeValue(c.getValue()));
-            }
-        } else if (pc instanceof Query.SizeEquals c) {
-            return cb.equal(cb.size((Expression) fullyQualifiedPath), normalizeValue(c.getValue()));
-        } else if (pc instanceof Query.SizeNotEquals c) {
-            return cb.notEqual(cb.size((Expression) fullyQualifiedPath), normalizeValue(c.getValue()));
-        } else if (pc instanceof Query.SizeGreaterThan c) {
-            return cb.gt(cb.size((Expression) fullyQualifiedPath), (Number) getNumericValue(c));
-        } else if (pc instanceof Query.SizeGreaterThanEquals c) {
-            return cb.ge(cb.size((Expression) fullyQualifiedPath), (Number) getNumericValue(c));
-        } else if (pc instanceof Query.SizeLessThan c) {
-            return cb.lt(cb.size((Expression) fullyQualifiedPath), (Number) getNumericValue(c));
-        } else if (pc instanceof Query.SizeLessThanEquals c) {
-            return cb.le(cb.size((Expression) fullyQualifiedPath), (Number) getNumericValue(c));
-        } else if (pc instanceof Query.Between c) {
-            return cb.between((Expression) fullyQualifiedPath, (Comparable) normalizeValue(c.getFrom()), (Comparable) normalizeValue(c.getTo()));
-        }
-        return null;
-    }
-
-    private Predicate handleNotIn(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
-            JpaFromProvider fromsByProvider,
-            PersistentEntity entity,
-            Query.NotIn c,
-            Path fullyQualifiedPath) {
-        var queryableCriteria = getQueryableCriteriaFromInCriteria(c);
-        if (Objects.nonNull(queryableCriteria)) {
-            return cb.not(getQueryableCriteriaValue(cb, criteriaQuery, fromsByProvider, entity, c, queryableCriteria));
-        } else if (Objects.nonNull(c.getSubquery()) &&
-                !c.getSubquery().getProjections().isEmpty()) {
-            Subquery subquery2 = criteriaQuery.subquery(Number.class);
-            PersistentEntity subEntity = c.getValue().getPersistentEntity();
-            Root from2 = subquery2.from(subEntity.getJavaClass());
-            JpaFromProvider newMap2 = (JpaFromProvider) fromsByProvider.clone();
-            var projection = c.getSubquery().getProjections().get(0);
-            if (projection instanceof Query.PropertyProjection pp) {
-                boolean distinct = projection instanceof Query.DistinctPropertyProjection;
-                Predicate[] predicates2 =
-                        getPredicates(cb, criteriaQuery, from2, c.getValue().getCriteria(), newMap2, subEntity);
-                subquery2
-                        .select(from2.get(pp.getPropertyName()))
-                        .distinct(distinct)
-                        .where(cb.and(predicates2));
-                return cb.not(cb.in(fullyQualifiedPath).value(subquery2));
-            } else if (projection instanceof Query.IdProjection) {
-                Predicate[] predicates2 =
-                        getPredicates(cb, criteriaQuery, from2, c.getValue().getCriteria(), newMap2, subEntity);
-                subquery2.select(from2).where(cb.and(predicates2));
-                return cb.not(cb.in(fullyQualifiedPath).value(subquery2));
-            }
-        }
-        return cb.not(cb.in(fullyQualifiedPath, c.getValue()));
-    }
-
-    private Predicate handleIn(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
-            JpaFromProvider fromsByProvider,
-            PersistentEntity entity,
-            Query.In c,
-            Path<?> fullyQualifiedPath) {
-        var queryableCriteria = getQueryableCriteriaFromInCriteria(c);
-        if (Objects.nonNull(queryableCriteria)) {
-            return getQueryableCriteriaValue(cb, criteriaQuery, fromsByProvider, entity, c, queryableCriteria);
-        } else if (!c.getValues().isEmpty()) {
-            if (c.getValues().iterator().next() instanceof GormEntity firstEntity) {
-                List<GormEntity> gormEntities = new ArrayList<>(c.getValues());
-                Path id = criteriaQuery.from(firstEntity.getClass()).get("id");
-                Collection newValues =
-                        gormEntities.stream().map(GormEntity::ident).toList();
-                return cb.in(id, newValues);
+                return criteriaBuilder.lessThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Comparable) value);
             }
-
-            // Hibernate 7: If the path is a collection, we must ensure it's correctly handled
-            if (fullyQualifiedPath instanceof SqmPath sqmPath &&
-                    sqmPath.getReferencedPathSource() instanceof jakarta.persistence.metamodel.PluralAttribute) {
-                // For basic collections, GORM's 'in' traditionally implies joining.
-                // We'll check if the path is already a join (From)
-                if (fullyQualifiedPath instanceof From) {
-                    return cb.in(fullyQualifiedPath, c.getValues());
-                }
-                // If not joined yet, we may need to use 'elements' or MEMBER OF
-                // but usually JpaFromProvider should have joined it if it was a property path
-                // that refers to a collection.
-            }
-
-            return cb.in(fullyQualifiedPath, c.getValues());
-        }
-        return null;
-    }
-
-    private Predicate handleRLike(HibernateCriteriaBuilder cb, Path fullyQualifiedPath, Query.RLike c) {
-        String pattern = c.getPattern().replaceAll("^/|/$", "");
-        return cb.equal(
-                cb.function(
-                        GrailsRLikeFunctionContributor.RLIKE, Boolean.class, fullyQualifiedPath, cb.literal(pattern)),
-                true);
-    }
-
-    private Predicate handleSubqueryCriterion(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
-            JpaFromProvider fromsByProvider,
-            Query.SubqueryCriterion c) {
-        
-        QueryableCriteria<?> subqueryable = c.getValue();
-        PersistentEntity subEntity = subqueryable.getPersistentEntity();
-        
-        var projection = findPropertyOrIdProjection(subqueryable);
-        var subProperty = findSubproperty(projection);
-        
-        Class<?> subqueryType = subEntity.getJavaClass();
-        if (projection instanceof Query.PropertyProjection pp) {
-            PersistentProperty prop = subEntity.getPropertyByName(pp.getPropertyName());
-            if (prop != null) subqueryType = prop.getType();
-        } else if (projection instanceof Query.IdProjection) {
-            if (subEntity.getIdentity() != null) subqueryType = subEntity.getIdentity().getType();
-        }
-
-        Subquery subquery = criteriaQuery.subquery(subqueryType);
-        Root from = subquery.from(subEntity.getJavaClass());
-        JpaFromProvider newMap = new JpaFromProvider(fromsByProvider, (DetachedCriteria<?>) subqueryable, java.util.Collections.emptyList(), from);
-        
-        Predicate[] predicates =
-                getPredicates(cb, criteriaQuery, from, c.getValue().getCriteria(), newMap, subEntity);
-        
-        Path path = fromsByProvider.getFullyQualifiedPath(c.getProperty());
-        Expression subExpression = (Expression) newMap.getFullyQualifiedPath(subProperty);
-
-        if (c instanceof Query.GreaterThanEqualsAll) {
-            subquery.select(subExpression).where(cb.and(predicates));
-            return cb.greaterThanOrEqualTo(path, cb.all(subquery));
-        } else if (c instanceof Query.GreaterThanAll) {
-            subquery.select(subExpression).where(cb.and(predicates));
-            return cb.greaterThan(path, cb.all(subquery));
-        } else if (c instanceof Query.LessThanEqualsAll) {
-            subquery.select(subExpression).where(cb.and(predicates));
-            return cb.lessThanOrEqualTo(path, cb.all(subquery));
-        } else if (c instanceof Query.LessThanAll) {
-            subquery.select(subExpression).where(cb.and(predicates));
-            return cb.lessThan(path, cb.all(subquery));
-        } else if (c instanceof Query.EqualsAll) {
-            subquery.select(subExpression).where(cb.and(predicates));
-            return cb.equal(path, cb.all(subquery));
-        } else if (c instanceof Query.GreaterThanEqualsSome) {
-            subquery.select(subExpression).where(cb.and(predicates));
-            return cb.greaterThanOrEqualTo(path, cb.some(subquery));
-        } else if (c instanceof Query.GreaterThanSome) {
-            subquery.select(subExpression).where(cb.and(predicates));
-            return cb.greaterThan(path, cb.some(subquery));
-        } else if (c instanceof Query.LessThanEqualsSome) {
-            subquery.select(subExpression).where(cb.and(predicates));
-            return cb.lessThanOrEqualTo(path, cb.some(subquery));
-        } else if (c instanceof Query.LessThanSome) {
-            subquery.select(subExpression).where(cb.and(predicates));
-            return cb.lessThan(path, cb.some(subquery));
-        }
-        return null;
-    }
-
-    private Predicate handlePropertyComparisonCriterion(
-            HibernateCriteriaBuilder cb, JpaFromProvider fromsByProvider, Query.PropertyComparisonCriterion c) {
-        Path path = fromsByProvider.getFullyQualifiedPath(c.getProperty());
-        Path otherPath = fromsByProvider.getFullyQualifiedPath(c.getOtherProperty());
-        if (!path.getJavaType().equals(otherPath.getJavaType())) {
-            jakarta.persistence.criteria.Path parentOfPath = path.getParentPath();
-            if (parentOfPath != null && parentOfPath.getJavaType().equals(otherPath.getJavaType())) {
-                path = parentOfPath;
-            } else {
-                jakarta.persistence.criteria.Path parentOfOther = otherPath.getParentPath();
-                if (parentOfOther != null && parentOfOther.getJavaType().equals(path.getJavaType())) {
-                    otherPath = parentOfOther;
-                }
+        } else if (pc instanceof Query.In) {
+            Collection<?> values = (Collection<?>) pc.getValue();
+            var in = criteriaBuilder.in(propertyPath);
+            for (Object value : values) {
+                ((CriteriaBuilder.In)in).value(convertValue(entity, propertyName, value));
             }
+            return in;
+        } else if (pc instanceof Query.Between between) {
+            return criteriaBuilder.between((Expression<? extends Comparable>) propertyPath, (Comparable) convertValue(entity, propertyName, between.getFrom()), (Comparable) convertValue(entity, propertyName, between.getTo()));
+        } else if (((Object)pc) instanceof Query.IsNull) {
+            return criteriaBuilder.isNull(propertyPath);
+        } else if (((Object)pc) instanceof Query.IsNotNull) {
+            return criteriaBuilder.isNotNull(propertyPath);
+        } else if (((Object)pc) instanceof Query.IsEmpty) {
+            return criteriaBuilder.isEmpty((Expression<Collection>) propertyPath);
+        } else if (((Object)pc) instanceof Query.IsNotEmpty) {
+            return criteriaBuilder.isNotEmpty((Expression<Collection>) propertyPath);
+        } else if (pc instanceof Query.SizeEquals sizeEquals) {
+            return criteriaBuilder.equal(criteriaBuilder.size((Expression<Collection>) propertyPath), (Integer) sizeEquals.getValue());
+        } else if (pc instanceof Query.SizeNotEquals sizeNotEquals) {
+            return criteriaBuilder.notEqual(criteriaBuilder.size((Expression<Collection>) propertyPath), (Integer) sizeNotEquals.getValue());
+        } else if (pc instanceof Query.SizeGreaterThan sizeGreaterThan) {
+            return criteriaBuilder.gt(criteriaBuilder.size((Expression<Collection>) propertyPath), (Integer) sizeGreaterThan.getValue());
+        } else if (pc instanceof Query.SizeGreaterThanEquals sizeGreaterThanEquals) {
+            return criteriaBuilder.ge(criteriaBuilder.size((Expression<Collection>) propertyPath), (Integer) sizeGreaterThanEquals.getValue());
+        } else if (pc instanceof Query.SizeLessThan sizeLessThan) {
+            return criteriaBuilder.lt(criteriaBuilder.size((Expression<Collection>) propertyPath), (Integer) sizeLessThan.getValue());
+        } else if (pc instanceof Query.SizeLessThanEquals sizeLessThanEquals) {
+            return criteriaBuilder.le(criteriaBuilder.size((Expression<Collection>) propertyPath), (Integer) sizeLessThanEquals.getValue());
         }
-        if (c instanceof Query.EqualsProperty) return cb.equal(path, otherPath);
-        if (c instanceof Query.NotEqualsProperty) return cb.notEqual(path, otherPath);
-        if (c instanceof Query.LessThanEqualsProperty) return cb.le(path, otherPath);
-        if (c instanceof Query.LessThanProperty) return cb.lt(path, otherPath);
-        if (c instanceof Query.GreaterThanEqualsProperty) return cb.ge(path, otherPath);
-        if (c instanceof Query.GreaterThanProperty) return cb.gt(path, otherPath);
-        return null;
-    }
-
-    private Predicate handlePropertyNameCriterion(
-            HibernateCriteriaBuilder cb, JpaFromProvider fromsByProvider, Query.PropertyNameCriterion c) {
-        Path path = fromsByProvider.getFullyQualifiedPath(c.getProperty());
-        if (c instanceof Query.IsNull) return cb.isNull(path);
-        if (c instanceof Query.IsNotNull) return cb.isNotNull(path);
-        if (c instanceof Query.IsEmpty) return cb.isEmpty(path);
-        if (c instanceof Query.IsNotEmpty) return cb.isNotEmpty(path);
-        return null;
-    }
-
-    private Predicate handleExists(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
-            From<?, ?> root_,
-            JpaFromProvider fromsByProvider,
-            PersistentEntity entity,
-            Query.Exists c) {
-        QueryableCriteria<?> subqueryable = c.getSubquery();
-        PersistentEntity subEntity = subqueryable.getPersistentEntity();
-        Subquery<Integer> subquery = criteriaQuery.subquery(Integer.class);
-        Root<?> subRoot = subquery.from(subEntity.getJavaClass());
 
-        JpaFromProvider subFromsProvider = new JpaFromProvider(
-                fromsByProvider,
-                subEntity,
-                subqueryable.getCriteria(),
-                java.util.Collections.emptyList(),
-                java.util.Collections.emptyMap(),
-                java.util.Collections.emptyMap(),
-                subRoot);
-
-        var predicates =
-                getPredicates(cb, criteriaQuery, subRoot, subqueryable.getCriteria(), subFromsProvider, subEntity);
-        var existsPredicate = getExistsPredicate(cb, root_, entity, subRoot);
-        Predicate[] allPredicates = existsPredicate != null ?
-                Stream.concat(Arrays.stream(predicates), Stream.of(existsPredicate))
-                        .toArray(Predicate[]::new) :
-                predicates;
-        subquery.select(cb.literal(1)).where(cb.and(allPredicates));
-        return cb.exists(subquery);
+        throw new UnsupportedOperationException("Unsupported criterion: " + pc.getClass().getName());
     }
 
-    private CriteriaBuilder.In getQueryableCriteriaValue(
-            HibernateCriteriaBuilder cb,
-            CriteriaQuery<?> criteriaQuery,
-            JpaFromProvider fromsByProvider,
-            PersistentEntity entity,
-            Query.PropertyNameCriterion criterion,
-            QueryableCriteria queryableCriteria) {
-        var projection = findPropertyOrIdProjection(queryableCriteria);
-        var subProperty = findSubproperty(projection);
-        var path = fromsByProvider.getFullyQualifiedPath(criterion.getProperty());
-        boolean isAssociation = isAssociation(entity, criterion.getProperty());
-        var in = findInPredicate(cb, projection, path, subProperty, isAssociation);
-
-        PersistentEntity subEntity = queryableCriteria.getPersistentEntity();
-        Class<?> subqueryType = subEntity.getJavaClass();
-        if (projection instanceof Query.PropertyProjection propertyProjection) {
-            PersistentProperty prop = subEntity.getPropertyByName(propertyProjection.getPropertyName());
-            if (prop != null) {
-                subqueryType = prop.getType();
-            } else if (propertyProjection.getPropertyName().contains(".")) {
-                // Handle aliased or nested properties in projections (e.g., "e1.id")
-                String propName = propertyProjection.getPropertyName();
-                String simplePropName = propName.substring(propName.lastIndexOf('.') + 1);
-                PersistentProperty simpleProp = subEntity.getPropertyByName(simplePropName);
-                if (simpleProp != null) {
-                    subqueryType = simpleProp.getType();
-                } else if ("id".equals(simplePropName)) {
-                    subqueryType = subEntity.getIdentity() != null ?
-                            subEntity.getIdentity().getType() :
-                            Long.class;
-                }
-            }
-        } else if (projection instanceof Query.IdProjection) {
-            subqueryType =
-                    subEntity.getIdentity() != null ? subEntity.getIdentity().getType() : Long.class;
-        } else if (isAssociation) {
-            subqueryType =
-                    subEntity.getIdentity() != null ? subEntity.getIdentity().getType() : Long.class;
+    private Predicate handlePropertyComparisonCriterion(JpaQueryContext fromsByProvider, Query.PropertyComparisonCriterion c) {
+        Expression<?> propertyPath = fromsByProvider.getFullyQualifiedExpression(c.getProperty());
+        Expression<?> otherPropertyPath = fromsByProvider.getFullyQualifiedExpression(c.getOtherProperty());
+
+        if (c instanceof Query.EqualsProperty) {
+            return criteriaBuilder.equal(propertyPath, otherPropertyPath);
+        } else if (c instanceof Query.NotEqualsProperty) {
+            return criteriaBuilder.notEqual(propertyPath, otherPropertyPath);
+        } else if (c instanceof Query.GreaterThanProperty) {
+            return criteriaBuilder.greaterThan((Expression<? extends Comparable>) propertyPath, (Expression<? extends Comparable>) otherPropertyPath);
+        } else if (c instanceof Query.GreaterThanEqualsProperty) {
+            return criteriaBuilder.greaterThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Expression<? extends Comparable>) otherPropertyPath);
+        } else if (c instanceof Query.LessThanProperty) {
+            return criteriaBuilder.lessThan((Expression<? extends Comparable>) propertyPath, (Expression<? extends Comparable>) otherPropertyPath);
+        } else if (c instanceof Query.LessThanEqualsProperty) {
+            return criteriaBuilder.lessThanOrEqualTo((Expression<? extends Comparable>) propertyPath, (Expression<? extends Comparable>) otherPropertyPath);
         }
 
-        var subquery = criteriaQuery.subquery(subqueryType);
-        var from = subquery.from(subEntity.getJavaClass());
-        var clonedProviderByName = new JpaFromProvider(
-                fromsByProvider, (DetachedCriteria<?>) queryableCriteria, java.util.Collections.emptyList(), from);
-        var predicates = getPredicates(
-                cb, criteriaQuery, from, queryableCriteria.getCriteria(), clonedProviderByName, subEntity);
-        subquery.select((Expression) clonedProviderByName.getFullyQualifiedPath(subProperty))
-                .distinct(true)
-                .where(cb.and(predicates));
-        return in.value(subquery);
+        throw new UnsupportedOperationException("Unsupported property comparison criterion: " + c.getClass().getName());
     }
 
-    private boolean isAssociation(PersistentEntity entity, String propertyName) {
-        if ("id".equals(propertyName) ||
-                (entity.getIdentity() != null &&
-                        propertyName.equals(entity.getIdentity().getName()))) {
-            return false;
+    private Predicate handlePropertyNameCriterion(JpaQueryContext fromsByProvider, Query.PropertyNameCriterion c) {
+        Expression<?> propertyPath = fromsByProvider.getFullyQualifiedExpression(c.getProperty());
+        if (((Object)c) instanceof Query.IsNull) {
+            return criteriaBuilder.isNull(propertyPath);
+        } else if (((Object)c) instanceof Query.IsNotNull) {
+            return criteriaBuilder.isNotNull(propertyPath);
+        } else if (((Object)c) instanceof Query.IsEmpty) {
+            return criteriaBuilder.isEmpty((Expression<Collection>) propertyPath);
+        } else if (((Object)c) instanceof Query.IsNotEmpty) {
+            return criteriaBuilder.isNotEmpty((Expression<Collection>) propertyPath);
         }
-        PersistentProperty prop = entity.getPropertyByName(propertyName);
-        return prop instanceof Association;
-    }
-
-    private Predicate getExistsPredicate(
-            HibernateCriteriaBuilder cb, From<?, ?> root_, PersistentEntity childPersistentEntity, Root subRoot) {
-        return childPersistentEntity.getAssociations().stream()
-                .filter(assoc -> assoc.getAssociatedEntity().getJavaClass().equals(root_.getJavaType()))
-                .findFirst()
-                .map(owner -> (Predicate) cb.equal(subRoot.get(owner.getName()), root_))
-                .orElse(null);
+        throw new UnsupportedOperationException("Unsupported property name criterion: " + c.getClass().getName());
     }
 
-    private JpaInPredicate findInPredicate(
-            HibernateCriteriaBuilder cb, Object projection, Path path, String subProperty, boolean isAssociation) {
-        if (projection instanceof Query.PropertyProjection || !isAssociation) {
-            return cb.in(path);
+    private Predicate handleEquals(AbstractQuery<?> criteriaQuery, Query.PropertyCriterion pc, Expression<?> propertyPath) {
+        if (pc.getValue() instanceof QueryableCriteria qc) {
+            Subquery<?> subquery = criteriaQuery.subquery(qc.getPersistentEntity().getJavaClass());
+            new JpaCriteriaQueryCreator(new Query.ProjectionList(), criteriaBuilder, qc.getPersistentEntity(), (DetachedCriteria) qc, conversionService).populateSubquery((JpaSubQuery)subquery);
+            return criteriaBuilder.equal(propertyPath, subquery);
         } else {
-            return cb.in(((SqmPath) path).get(subProperty));
+            return criteriaBuilder.equal(propertyPath, convertValue(null, pc.getProperty(), pc.getValue()));
         }
     }
 
-    private String findSubproperty(Object projection) {
-        return projection instanceof Query.PropertyProjection ?
-                ((Query.PropertyProjection) projection).getPropertyName() :
-                "id";
-    }
-
-    private Query.Projection findPropertyOrIdProjection(QueryableCriteria queryableCriteria) {
-        return (Query.Projection) queryableCriteria.getProjections().stream()
-                .filter(p -> p instanceof Query.PropertyProjection || p instanceof Query.IdProjection)
-                .findFirst()
-                .orElse(new Query.IdProjection());
-    }
-
-    private QueryableCriteria getQueryableCriteriaFromInCriteria(Query.Criterion criterion) {
-        return criterion instanceof Query.In ?
-                ((Query.In) criterion).getSubquery() :
-                ((Query.NotIn) criterion).getSubquery();
-    }
-
-    /**
-     * Normalizes a criterion value for use with JPA Criteria API.
-     * Hibernate 7's SqmCriteriaNodeBuilder requires strict Java types and cannot
-     * coerce Groovy types like GString. This method converts CharSequence (including
-     * GString) to String so Hibernate can process the value correctly.
-     */
-    private static Object normalizeValue(Object value) {
-        if (value instanceof CharSequence && !(value instanceof String)) {
-            return value.toString();
-        }
-        return value;
+    private Predicate handleExists(
+            AbstractQuery<?> criteriaQuery,
+            From<?, ?> root,
+            JpaQueryContext fromsByProvider,
+            PersistentEntity entity,
+            Query.Exists exists) {
+        QueryableCriteria subqueryCriteria = exists.getSubquery();
+        Subquery<?> subquery = criteriaQuery.subquery(entity.getJavaClass());
+        new JpaCriteriaQueryCreator(new Query.ProjectionList(), criteriaBuilder, entity, (DetachedCriteria) subqueryCriteria, conversionService).populateSubquery((JpaSubQuery)subquery);
+        return criteriaBuilder.exists(subquery);
     }
 
-    private Object getNumericValue(Query.PropertyCriterion criterion) {
-        Object value = criterion.getValue();
-        if (value instanceof Number) {
-            return value;
+    private Object convertValue(PersistentEntity entity, String propertyName, Object value) {
+        if (value == null) {
+            return null;
         }
-        if (value != null) {
-            try {
-                return conversionService.convert(value, Number.class);
-            } catch (org.springframework.core.convert.ConversionException e) {
-                // ignore and return as is
-            }
+        if (conversionService.canConvert(value.getClass(), Object.class)) {
+            return conversionService.convert(value, Object.class);
         }
         return value;
     }
 
-    @SuppressWarnings("unchecked")
-    private Expression<? extends Number> resolveNumericExpression(HibernateCriteriaBuilder cb, From<?, ?> root, Query.PropertyCriterion criterion) {
-        Object value = criterion.getValue();
-        if (!(value instanceof PropertyArithmetic)) {
-            return null;
-        }
-        PropertyArithmetic pa = (PropertyArithmetic) value;
-        Expression<Number> propertyPath = root.get(pa.propertyName());
-        return switch (pa.operator()) {
-            case MULTIPLY -> cb.prod(propertyPath, pa.operand());
-            case ADD -> cb.sum(propertyPath, pa.operand());
-            case SUBTRACT -> cb.diff(propertyPath, pa.operand());
-            case DIVIDE -> cb.quot(propertyPath, pa.operand());
-        };
+    public Predicate generate(List<Query.Criterion> criteriaList, AbstractQuery<?> cq, From<?, ?> root, JpaQueryContext tablesByName, PersistentEntity entity) {
+        Predicate[] predicates = getPredicates(cq, root, criteriaList, tablesByName, entity);
+        return criteriaBuilder.and(predicates);
     }
 }
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaCriteriaQueryCreatorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaCriteriaQueryCreatorSpec.groovy
index e312d3320a..908d9db156 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaCriteriaQueryCreatorSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaCriteriaQueryCreatorSpec.groovy
@@ -158,18 +158,59 @@ class JpaCriteriaQueryCreatorSpec extends HibernateGormDatastoreSpec {
         noExceptionThrown()
     }
 
-    def "test createQuery with HibernateAlias triggers join"() {
+    def "test createQuery with aliased projection"() {
         given:
         var entity = getPersistentEntity(JpaCriteriaQueryCreatorSpecPerson)
         var detachedCriteria = new DetachedCriteria(JpaCriteriaQueryCreatorSpecPerson)
         
-        // Mock HibernateQuery to provide an alias for a basic collection
-        def hibernateQuery = Mock(org.grails.orm.hibernate.query.HibernateQuery) {
-            getAliases() >> [new org.grails.orm.hibernate.query.HibernateAlias("nicknames", "n")]
-            getEntity() >> entity
-        }
+        var projections = new Query.ProjectionList()
+        // Property with alias is supported
+        projections.property("cnt:firstName")
+
+        var creator = new JpaCriteriaQueryCreator(projections, criteriaBuilder, entity, detachedCriteria, new DefaultConversionService())
+
+        when:
+        JpaCriteriaQuery<?> query = creator.createQuery()
+
+        then:
+        noExceptionThrown()
+        query != null
+    }
+
+    def "test createQuery with aliased group property and order by alias"() {
+        given:
+        var entity = getPersistentEntity(JpaCriteriaQueryCreatorSpecPerson)
+        var detachedCriteria = new DetachedCriteria(JpaCriteriaQueryCreatorSpecPerson)
+        
+        var projections = new Query.ProjectionList()
+        // Group by property with alias
+        projections.groupProperty("groupAlias:lastName")
+        
+        // Order by the alias
+        detachedCriteria.order(Query.Order.asc("groupAlias"))
 
-        var creator = new JpaCriteriaQueryCreator(new Query.ProjectionList(), criteriaBuilder, entity, detachedCriteria, new DefaultConversionService(), hibernateQuery)
+        var creator = new JpaCriteriaQueryCreator(projections, criteriaBuilder, entity, detachedCriteria, new DefaultConversionService())
+
+        when:
+        JpaCriteriaQuery<?> query = creator.createQuery()
+
+        then:
+        noExceptionThrown()
+        query != null
+    }
+
+    def "test createQuery with aliased countDistinct and order by alias"() {
+        given:
+        var entity = getPersistentEntity(JpaCriteriaQueryCreatorSpecPerson)
+        var detachedCriteria = new DetachedCriteria(JpaCriteriaQueryCreatorSpecPerson)
+        
+        var projections = new Query.ProjectionList()
+        projections.countDistinct("distinctCnt:firstName")
+        
+        // Order by the alias
+        detachedCriteria.order(Query.Order.asc("distinctCnt"))
+
+        var creator = new JpaCriteriaQueryCreator(projections, criteriaBuilder, entity, detachedCriteria, new DefaultConversionService())
 
         when:
         JpaCriteriaQuery<?> query = creator.createQuery()
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy
deleted file mode 100644
index a0fa4e7ceb..0000000000
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaFromProviderSpec.groovy
+++ /dev/null
@@ -1,253 +0,0 @@
-/*
- *  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 grails.gorm.specs.hibernatequery
-
-import org.hibernate.query.criteria.JpaCriteriaQuery
-
-import grails.gorm.DetachedCriteria
-import grails.gorm.specs.HibernateGormDatastoreSpec
-import jakarta.persistence.criteria.From
-import jakarta.persistence.criteria.Join
-import jakarta.persistence.criteria.Path
-import org.grails.orm.hibernate.query.JpaFromProvider
-import grails.gorm.annotation.Entity
-import org.grails.datastore.gorm.GormEntity
-
-class JpaFromProviderSpec extends HibernateGormDatastoreSpec {
-
-    void setupSpec() {
-        manager.addAllDomainClasses([JpaFromProviderSpecPerson, JpaFromProviderSpecPet, JpaFromProviderSpecFace])
-    }
-
-    private JpaFromProvider bare(Class clazz, From root) {
-        def dc = new DetachedCriteria(clazz)
-        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) {
-            getJavaType() >> String
-            alias(_) >> it
-        }
-        return new JpaFromProvider(dc, [], root)
-    }
-
-    def "getFromsByName returns root for 'root' key"() {
-        given:
-        From root = Mock(From) {
-            getJavaType() >> JpaFromProviderSpecPerson
-        }
-        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it }
-        
-        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
-
-        expect:
-        provider.getFromsByName().get("root") == root
-    }
-
-    def "getFullyQualifiedPath returns root for entity name if it matches root"() {
-        given:
-        From root = Mock(From) {
-            getJavaType() >> JpaFromProviderSpecPerson
-        }
-        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it }
-
-        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
-
-        expect:
-        provider.getFullyQualifiedPath("JpaFromProviderSpecPerson") == root
-    }
-
-    def "getFullyQualifiedPath returns root for 'root' prefix"() {
-        given:
-        Path idPath = Mock(Path)
-        From root = Mock(From) {
-            getJavaType() >> JpaFromProviderSpecPerson
-            get("id") >> idPath
-        }
-        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it }
-
-        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
-
-        expect:
-        provider.getFullyQualifiedPath("root.id") == idPath
-    }
-
-    def "getFullyQualifiedPath throws for null property name"() {
-        given:
-        From root = Mock(From)
-        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it }
-
-        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
-
-        when:
-        provider.getFullyQualifiedPath(null)
-
-        then:
-        thrown(IllegalArgumentException)
-    }
-
-    def "clone produces an independent copy that does not affect original"() {
-        given:
-        From root = Mock(From) {
-            getJavaType() >> JpaFromProviderSpecPerson
-        }
-        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it }
-
-        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
-        From extra = Mock(From)
-
-        when:
-        JpaFromProvider clone = provider.clone()
-        clone.put("extra", extra)
-
-        then:
-        clone.getFromsByName().containsKey("extra")
-        !provider.getFromsByName().containsKey("extra")
-    }
-
-    def "put overwrites an existing key"() {
-        given:
-        From root = Mock(From) {
-            getJavaType() >> JpaFromProviderSpecPerson
-        }
-        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it }
-
-        JpaFromProvider provider = bare(JpaFromProviderSpecPerson, root)
-        From newRoot = Mock(From)
-
-        when:
-        provider.put("root", newRoot)
-
-        then:
-        provider.getFromsByName().get("root") == newRoot
-    }
-
-    def "root alias registered via setAlias is available for dotted lookup"() {
-        given:
-        Path idPath = Mock(Path)
-        From root = Mock(From) {
-            getJavaType() >> JpaFromProviderSpecPerson
-            get("id") >> idPath
-        }
-        root.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it }
-
-        def dc = new DetachedCriteria(JpaFromProviderSpecPerson)
-        dc.setAlias("myAlias")
-        JpaFromProvider provider = new JpaFromProvider(dc, [], root)
-
-        when:
-        Path result = provider.getFullyQualifiedPath("myAlias.id")
-
-        then:
-        result == idPath
-    }
-
-    def "getFromsByName creates hierarchical joins for projection paths"() {
-        given:
-        def dc = new DetachedCriteria(JpaFromProviderSpecPerson)
-        From root = Mock(From) {
-            getJavaType() >> JpaFromProviderSpecPerson
-        }
-        // Stub for auto-joined basic collections
-        root.join("nicknames", _) >> Mock(Join) { alias(_) >> it }
-
-        Join teamJoin = Mock(Join) {
-            getJavaType() >> String
-            alias(_) >> it
-        }
-        Join clubJoin = Mock(Join) {
-            getJavaType() >> String
-            alias(_) >> it
-        }
-
-        and: "projections with nested paths"
-        def projections = [
-                new org.grails.datastore.mapping.query.Query.PropertyProjection("team.club.name")
-        ]
-
-        when:
-        JpaFromProvider provider = new JpaFromProvider(dc, projections, root)
-
-        then: "joins are created hierarchically"
-        1 * root.join("team", jakarta.persistence.criteria.JoinType.LEFT) >> teamJoin
-        1 * teamJoin.join("club", jakarta.persistence.criteria.JoinType.LEFT) >> clubJoin
-        0 * clubJoin.join(_, _)
-
-        and: "paths are registered in provider"
-        provider.getFullyQualifiedPath("team") == teamJoin
-        provider.getFullyQualifiedPath("team.club") == clubJoin
-    }
-
-    def "constructor with parent provider inherits froms and supports correlation"() {
-        given:
-        From outerRoot = Mock(From) { getJavaType() >> JpaFromProviderSpecPerson }
-        JpaFromProvider parent = bare(JpaFromProviderSpecPerson, outerRoot)
-
-        and: "subquery detached criteria"
-        def subDc = new DetachedCriteria(JpaFromProviderSpecPet)
-        From subRoot = Mock(From) { getJavaType() >> JpaFromProviderSpecPet }
-        subRoot.join(_ as String, _ as jakarta.persistence.criteria.JoinType) >> Mock(Join) { alias(_) >> it }
-
-        when:
-        JpaFromProvider subProvider = new JpaFromProvider(parent, subDc, [], subRoot)
-
-        then: "subquery provider has its own root"
-        subProvider.getFullyQualifiedPath("root") == subRoot
-
-        and: "subquery provider inherits outer paths"
-        subProvider.getFullyQualifiedPath("root") != outerRoot // subquery root shadows outer root
-    }
-
-    def "getFromsByName automatically joins basic collections"() {
-        given:
-        def dc = new DetachedCriteria(JpaFromProviderSpecPerson)
-        From root = Mock(From) {
-            getJavaType() >> JpaFromProviderSpecPerson
-        }
-        Join nicknamesJoin = Mock(Join) {
-            getJavaType() >> String
-            alias(_) >> it
-        }
-
-        when:
-        JpaFromProvider provider = new JpaFromProvider(dc, [], root)
-
-        then: "basic collection is joined automatically"
-        1 * root.join("nicknames", jakarta.persistence.criteria.JoinType.LEFT) >> nicknamesJoin
-
-        and: "path is registered"
-        provider.getFullyQualifiedPath("nicknames") == nicknamesJoin
-    }
-}
-
-@Entity
-class JpaFromProviderSpecPerson implements GormEntity<JpaFromProviderSpecPerson> {
-    Long id
-    String firstName
-    Set<String> nicknames
-    static hasMany = [nicknames: String]
-}
-
-@Entity
-class JpaFromProviderSpecPet implements GormEntity<JpaFromProviderSpecPet> {
-    Long id
-    JpaFromProviderSpecPerson owner
-}
-
-@Entity
-class JpaFromProviderSpecFace implements GormEntity<JpaFromProviderSpecFace> {
-    Long id
-}
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaProjectionTranslatorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaProjectionTranslatorSpec.groovy
new file mode 100644
index 0000000000..ccbedd7831
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaProjectionTranslatorSpec.groovy
@@ -0,0 +1,124 @@
+/*
+ *  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 grails.gorm.specs.hibernatequery
+
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import jakarta.persistence.criteria.Expression
+import jakarta.persistence.criteria.Root
+import org.grails.datastore.mapping.query.Query
+import org.grails.orm.hibernate.query.JpaProjectionTranslator
+import org.grails.orm.hibernate.query.JpaQueryContext
+import org.hibernate.query.criteria.JpaCriteriaQuery
+import grails.gorm.annotation.Entity
+import org.grails.datastore.gorm.GormEntity
+
+class JpaProjectionTranslatorSpec extends HibernateGormDatastoreSpec {
+
+    void setupSpec() {
+        manager.addAllDomainClasses([JpaProjectionTranslatorSpecPerson])
+    }
+
+    def "translate PropertyProjection"() {
+        given:
+        JpaCriteriaQuery cq = criteriaBuilder.createQuery(JpaProjectionTranslatorSpecPerson)
+        Root root = cq.from(JpaProjectionTranslatorSpecPerson)
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+        JpaProjectionTranslator translator = new JpaProjectionTranslator(criteriaBuilder, context)
+
+        when:
+        Expression result = translator.translate(new Query.PropertyProjection("firstName"))
+
+        then:
+        result != null
+        result.getJavaType() == String
+    }
+
+    def "translate aliased PropertyProjection"() {
+        given:
+        JpaCriteriaQuery cq = criteriaBuilder.createQuery(JpaProjectionTranslatorSpecPerson)
+        Root root = cq.from(JpaProjectionTranslatorSpecPerson)
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+        JpaProjectionTranslator translator = new JpaProjectionTranslator(criteriaBuilder, context)
+
+        when:
+        Expression result = translator.translate(new Query.PropertyProjection("cnt:firstName"))
+
+        then:
+        result != null
+        result.getAlias() == "cnt"
+        context.hasAlias("cnt")
+    }
+
+    def "translate CountProjection"() {
+        given:
+        JpaCriteriaQuery cq = criteriaBuilder.createQuery(Long)
+        Root root = cq.from(JpaProjectionTranslatorSpecPerson)
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+        JpaProjectionTranslator translator = new JpaProjectionTranslator(criteriaBuilder, context)
+
+        when:
+        Expression result = translator.translate(new org.grails.orm.hibernate.query.Hibernate7CountProjection("firstName"))
+
+        then:
+        result != null
+        result.getJavaType() == Long
+    }
+
+    def "translate CountDistinctProjection"() {
+        given:
+        JpaCriteriaQuery cq = criteriaBuilder.createQuery(Long)
+        Root root = cq.from(JpaProjectionTranslatorSpecPerson)
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+        JpaProjectionTranslator translator = new JpaProjectionTranslator(criteriaBuilder, context)
+
+        when:
+        Expression result = translator.translate(new Query.CountDistinctProjection("firstName"))
+
+        then:
+        result != null
+        result.getJavaType() == Long
+    }
+
+    def "translate MaxProjection"() {
+        given:
+        JpaCriteriaQuery cq = criteriaBuilder.createQuery(Integer)
+        Root root = cq.from(JpaProjectionTranslatorSpecPerson)
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+        JpaProjectionTranslator translator = new JpaProjectionTranslator(criteriaBuilder, context)
+
+        when:
+        Expression result = translator.translate(new Query.MaxProjection("age"))
+
+        then:
+        result != null
+        result.getJavaType() == Integer
+    }
+}
+
+@Entity
+class JpaProjectionTranslatorSpecPerson implements GormEntity<JpaProjectionTranslatorSpecPerson> {
+    Long id
+    String firstName
+    Integer age
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaQueryContextSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaQueryContextSpec.groovy
new file mode 100644
index 0000000000..b61a43f650
--- /dev/null
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/JpaQueryContextSpec.groovy
@@ -0,0 +1,150 @@
+/*
+ *  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 grails.gorm.specs.hibernatequery
+
+import grails.gorm.DetachedCriteria
+import grails.gorm.specs.HibernateGormDatastoreSpec
+import jakarta.persistence.criteria.Expression
+import jakarta.persistence.criteria.From
+import jakarta.persistence.criteria.Path
+import org.grails.orm.hibernate.query.JpaQueryContext
+import grails.gorm.annotation.Entity
+import org.grails.datastore.gorm.GormEntity
+import org.hibernate.query.criteria.JpaExpression
+
+class JpaQueryContextSpec extends HibernateGormDatastoreSpec {
+
+    void setupSpec() {
+        manager.addAllDomainClasses([JpaQueryContextSpecPerson])
+    }
+
+    def "getRoot returns the assigned root"() {
+        given:
+        From root = Mock(From)
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+
+        expect:
+        context.getRoot() == root
+    }
+
+    def "registerAlias and hasAlias"() {
+        given:
+        JpaQueryContext context = new JpaQueryContext()
+        Expression expr = Mock(Expression)
+
+        when:
+        context.registerAlias("myAlias", expr)
+
+        then:
+        context.hasAlias("myAlias")
+        context.getAliasedExpression("myAlias") == expr
+    }
+
+    def "registerAliasFromPath parses separator"() {
+        given:
+        JpaQueryContext context = new JpaQueryContext()
+
+        when:
+        context.registerAliasFromPath("cnt:firstName")
+
+        then:
+        context.hasAlias("cnt")
+        context.getAliasedExpression("cnt") == null // Registered as placeholder
+    }
+
+    def "getFullyQualifiedExpression handles root"() {
+        given:
+        From root = Mock(From)
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+
+        expect:
+        context.getFullyQualifiedExpression("root") == root
+    }
+
+    def "getFullyQualifiedExpression handles root prefix and alias registration"() {
+        given:
+        Path firstNamePath = Mock(Path)
+        From root = Mock(From) {
+            get("firstName") >> firstNamePath
+        }
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+
+        when:
+        Expression result = context.getFullyQualifiedExpression("root.firstName")
+
+        then:
+        result == firstNamePath
+    }
+
+    def "getFullyQualifiedExpression handles ALIAS_SEPARATOR"() {
+        given:
+        // Use a mock that implements both JpaExpression and Path since getFullyQualifiedPath expects Path
+        JpaExpression firstNameExpr = Mock(JpaExpression, additionalInterfaces: [Path])
+        
+        From root = Mock(From) {
+            get("firstName") >> (Path)firstNameExpr
+        }
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+
+        when:
+        Expression result = context.getFullyQualifiedExpression("cnt:firstName")
+
+        then:
+        result == firstNameExpr
+        1 * firstNameExpr.alias("cnt")
+        context.hasAlias("cnt")
+        context.getAliasedExpression("cnt") == firstNameExpr
+    }
+
+    def "getFullyQualifiedPath handles nested paths"() {
+        given:
+        Path cityPath = Mock(Path)
+        Path addressPath = Mock(Path) {
+            get("city") >> cityPath
+        }
+        From root = Mock(From) {
+            get("address") >> addressPath
+        }
+        JpaQueryContext context = new JpaQueryContext()
+        context.setRoot(root)
+
+        expect:
+        context.getFullyQualifiedPath("address.city") == cityPath
+    }
+
+    def "getFullyQualifiedPath returns null for non-path alias"() {
+        given:
+        Expression countExpr = Mock(Expression)
+        JpaQueryContext context = new JpaQueryContext()
+        context.registerAlias("cnt", countExpr)
+
+        expect:
+        context.getFullyQualifiedPath("cnt") == null
+    }
+}
+
+@Entity
+class JpaQueryContextSpecPerson implements GormEntity<JpaQueryContextSpecPerson> {
+    Long id
+    String firstName
+}
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy
index 8910f22448..76faa884e2 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/PredicateGeneratorSpec.groovy
@@ -23,11 +23,12 @@ import org.hibernate.query.criteria.HibernateCriteriaBuilder
 import grails.gorm.DetachedCriteria
 import grails.gorm.specs.HibernateGormDatastoreSpec
 import jakarta.persistence.criteria.CriteriaQuery
+import jakarta.persistence.criteria.Expression
 import jakarta.persistence.criteria.Root
 import org.grails.datastore.mapping.model.PersistentEntity
 import org.grails.datastore.mapping.query.Query
 
-import org.grails.orm.hibernate.query.JpaFromProvider
+import org.grails.orm.hibernate.query.JpaQueryContext
 import org.grails.orm.hibernate.query.PredicateGenerator
 import org.grails.orm.hibernate.query.PropertyArithmetic
 import grails.gorm.annotation.Entity
@@ -39,7 +40,7 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec {
     HibernateCriteriaBuilder cb
     CriteriaQuery query
     Root root
-    JpaFromProvider fromProvider
+    JpaQueryContext fromProvider
     PersistentEntity personEntity
 
     void setupSpec() {
@@ -51,7 +52,7 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec {
         query = cb.createQuery(PredicateGeneratorSpecPerson)
         root = query.from(PredicateGeneratorSpecPerson)
         personEntity = session.datastore.mappingContext.getPersistentEntity(PredicateGeneratorSpecPerson.name)
-        fromProvider = new JpaFromProvider(new DetachedCriteria(PredicateGeneratorSpecPerson),[], root)
+        fromProvider = new JpaQueryContext(new DetachedCriteria(PredicateGeneratorSpecPerson),[], root)
         predicateGenerator = new PredicateGenerator(session.datastore.mappingContext.conversionService)
     }
 
@@ -217,7 +218,7 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec {
         List criteria = [new Query.In("nicknames", ["Bob", "Alice"])]
         
         // Ensure nicknames is joined in fromProvider
-        fromProvider = new JpaFromProvider(new DetachedCriteria(PredicateGeneratorSpecPerson), [], root)
+        fromProvider = new JpaQueryContext(new DetachedCriteria(PredicateGeneratorSpecPerson), [], root)
 
         when:
         def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity)
@@ -476,6 +477,32 @@ class PredicateGeneratorSpec extends HibernateGormDatastoreSpec {
         then:
         predicates.length == 6
     }
+
+    def "getPredicates supports Subquery"() {
+        given:
+        jakarta.persistence.criteria.Subquery subquery = query.subquery(Long)
+        subquery.from(PredicateGeneratorSpecPerson)
+        List criteria = [new Query.Equals("firstName", "Bob")]
+
+        when:
+        def predicates = predicateGenerator.getPredicates(subquery, root, criteria, fromProvider, personEntity)
+
+        then:
+        predicates.length == 1
+    }
+
+    def "handlePropertyCriterion resolves aliased expression"() {
+        given:
+        Expression aliasedExpr = Mock(Expression)
+        fromProvider.registerAlias("myAlias", aliasedExpr)
+        List criteria = [new Query.Equals("myAlias", "Bob")]
+
+        when:
+        def predicates = predicateGenerator.getPredicates(cb, query, root, criteria, fromProvider, personEntity)
+
+        then:
+        predicates.length == 1
+    }
 }
 
 @Entity
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyCollectionPropertySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyCollectionPropertySpec.groovy
index 6891f7aa24..25d3e7d56f 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyCollectionPropertySpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/hibernate/HibernateToManyCollectionPropertySpec.groovy
@@ -1,82 +1,204 @@
+/*
+ *  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.cfg.domainbinding.hibernate
 
+import org.grails.datastore.mapping.model.ClassMapping
+import org.grails.datastore.mapping.model.PersistentProperty
+import org.grails.datastore.mapping.model.PersistentEntity
+import org.grails.datastore.mapping.model.PropertyMapping
+import org.grails.orm.hibernate.cfg.PersistentEntityNamingStrategy
+import org.grails.orm.hibernate.cfg.PropertyConfig
 import org.hibernate.type.StandardBasicTypes
-import org.hibernate.boot.model.naming.ImplicitNamingStrategy
-import org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl
+import org.hibernate.mapping.Collection
 import spock.lang.Specification
 
 class HibernateToManyCollectionPropertySpec extends Specification {
 
-    void "test getElementTypeName uses componentType when available"() {
+    static class TestPropertyMapping implements PropertyMapping<PropertyConfig> {
+        private final GrailsHibernatePersistentEntity owner
+        private final PropertyConfig mapping
+
+        TestPropertyMapping(GrailsHibernatePersistentEntity owner, PropertyConfig mapping) {
+            this.owner = owner
+            this.mapping = mapping
+        }
+
+        @Override
+        ClassMapping getClassMapping() { owner.getMapping() }
+
+        @Override
+        PropertyConfig getMappedForm() { mapping }
+    }
+
+    static class TestToManyCollectionProperty implements HibernateToManyCollectionProperty {
+        private final GrailsHibernatePersistentEntity owner
+        private final String name
+        private final PropertyConfig mapping
+        Class<?> componentType
+        String typeName
+        Collection hibernateCollection
+
+        TestToManyCollectionProperty(GrailsHibernatePersistentEntity owner, String name, PropertyConfig mapping) {
+            this.owner = owner
+            this.name = name
+            this.mapping = mapping
+        }
+
+        @Override
+        Collection getHibernateCollection() { hibernateCollection }
+
+        @Override
+        void setHibernateCollection(Collection collection) { this.hibernateCollection = collection }
+
+        @Override
+        PropertyConfig getHibernateMappedForm() { mapping }
+
+        @Override
+        String getName() { name }
+
+        @Override
+        GrailsHibernatePersistentEntity getHibernateOwner() { owner }
+
+        @Override
+        Class<?> getComponentType() { componentType }
+
+        @Override
+        String getTypeName() { typeName }
+
+        @Override
+        PersistentEntity getOwner() { owner }
+
+        @Override
+        PropertyMapping<PropertyConfig> getMapping() {
+            return new TestPropertyMapping(owner, mapping)
+        }
+
+        @Override
+        Class getType() { java.util.Collection.class }
+
+        @Override
+        boolean isNullable() { true }
+
+        @Override
+        boolean isInherited() { false }
+
+        @Override
+        org.grails.datastore.mapping.reflect.EntityReflector.PropertyReader getReader() { null }
+
+        @Override
+        org.grails.datastore.mapping.reflect.EntityReflector.PropertyWriter getWriter() { null }
+
+        @Override
+        String getCapitilizedName() { name.substring(0, 1).toUpperCase() + name.substring(1) }
+
+        @Override
+        boolean isBidirectionalToManyMap() { false }
+
+        @Override
+        boolean isCircular() { false }
+
+        @Override
+        boolean isOwningSide() { true }
+
+        @Override
+        boolean isBidirectional() { false }
+
+        @Override
+        PersistentEntity getAssociatedEntity() { null }
+
+        @Override
+        PersistentProperty getInverseSide() { null }
+    }
+
+    def "test getElementTypeName uses componentType when available"() {
         given:
-        def property = Spy(HibernateToManyCollectionProperty)
-        property.getComponentType() >> String.class
-        property.getTypeName(String.class) >> "custom_string_type"
+        def property = new TestToManyCollectionProperty(Mock(GrailsHibernatePersistentEntity), "tags", new PropertyConfig())
+        property.setComponentType(String.class)
 
         expect:
-        property.getElementTypeName() == "custom_string_type"
+        property.getElementTypeName() == String.class.name
     }
 
-    void "test getElementTypeName falls back to getTypeName when componentType is null"() {
+    def "test getElementTypeName falls back to getTypeName when componentType is null"() {
         given:
-        def property = Spy(HibernateToManyCollectionProperty)
-        property.getComponentType() >> null
-        property.getTypeName() >> "fallback_type"
+        def property = new TestToManyCollectionProperty(Mock(GrailsHibernatePersistentEntity), "tags", new PropertyConfig())
+        property.setComponentType(null)
+        property.setTypeName("fallback_type")
 
         expect:
         property.getElementTypeName() == "fallback_type"
     }
 
-    void "test getElementTypeName falls back to STRING when typeName is null"() {
+    def "test getElementTypeName falls back to STRING when typeName is null"() {
         given:
-        def property = Spy(HibernateToManyCollectionProperty)
-        property.getComponentType() >> null
-        property.getTypeName() >> null
+        def property = new TestToManyCollectionProperty(Mock(GrailsHibernatePersistentEntity), "tags", new PropertyConfig())
+        property.setComponentType(null)
+        property.setTypeName(null)
 
         expect:
         property.getElementTypeName() == StandardBasicTypes.STRING.getName()
     }
 
-    void "test getElementTypeName falls back to STRING when typeName is Object"() {
+    def "test getElementTypeName falls back to STRING when typeName is Object"() {
         given:
-        def property = Spy(HibernateToManyCollectionProperty)
-        property.getComponentType() >> Object.class
-        property.getTypeName(Object.class) >> Object.class.name
+        def property = new TestToManyCollectionProperty(Mock(GrailsHibernatePersistentEntity), "tags", new PropertyConfig())
+        property.setComponentType(null)
+        property.setTypeName(Object.class.name)
 
         expect:
         property.getElementTypeName() == StandardBasicTypes.STRING.getName()
     }
 
-    void "test getRole with path"() {
+    def "test getRole with path"() {
         given:
-        def property = Spy(HibernateToManyCollectionProperty)
-        property.getOwner() >> [getName: { "MyEntity" }] as org.grails.datastore.mapping.model.PersistentEntity
-        property.getName() >> "tags"
+        GrailsHibernatePersistentEntity owner = Mock(GrailsHibernatePersistentEntity) {
+            getName() >> "com.example.Book"
+        }
+        def property = new TestToManyCollectionProperty(owner, "tags", new PropertyConfig())
 
         expect:
-        property.getRole("foo.bar") == "MyEntity.foo.bar.tags"
-        property.getRole(null) == "MyEntity.tags"
+        property.getRole("") == "com.example.Book.tags"
     }
 
-    void "test getIndexColumnName fallback"() {
+    def "test getIndexColumnName fallback"() {
         given:
-        def property = Spy(HibernateToManyCollectionProperty)
-        property.getName() >> "tags"
-        def namingStrategy = ImplicitNamingStrategyJpaCompliantImpl.INSTANCE
+        def property = new TestToManyCollectionProperty(Mock(GrailsHibernatePersistentEntity), "tags", new PropertyConfig())
+        PersistentEntityNamingStrategy namingStrategy = Mock(PersistentEntityNamingStrategy)
 
-        expect:
-        // By default ImplicitNamingStrategyJpaCompliantImpl uses the property name directly for the index
-        // but HibernateToManyProperty adds _idx suffix in the default implementation
-        property.getIndexColumnName(namingStrategy) == "tags_idx"
+        when:
+        String indexColumn = property.getIndexColumnName(namingStrategy)
+
+        then:
+        1 * namingStrategy.resolveColumnName("tags") >> "tags_column"
+        indexColumn == "tags_column_idx"
     }
 
-    void "test getMapElementName fallback"() {
+    def "test getMapElementName fallback"() {
         given:
-        def property = Spy(HibernateToManyCollectionProperty)
-        property.getName() >> "tags"
-        def namingStrategy = ImplicitNamingStrategyJpaCompliantImpl.INSTANCE
+        def property = new TestToManyCollectionProperty(Mock(GrailsHibernatePersistentEntity), "tags", new PropertyConfig())
+        PersistentEntityNamingStrategy namingStrategy = Mock(PersistentEntityNamingStrategy)
 
-        expect:
-        property.getMapElementName(namingStrategy) == "tags_elt"
+        when:
+        String mapElement = property.getMapElementName(namingStrategy)
+
+        then:
+        1 * namingStrategy.resolveColumnName("tags") >> "tags_column"
+        mapElement == "tags_column_elt"
     }
 }
diff --git a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactorySpec.groovy b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactorySpec.groovy
index b35cc46c44..3ed15425f2 100644
--- a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactorySpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/HibernateConnectionSourceFactorySpec.groovy
@@ -270,27 +270,6 @@ class HibernateConnectionSourceFactorySpec extends HibernateGormDatastoreSpec {
         config != null
     }
 
-    void "buildConfiguration with a naming strategy configures it on the configuration"() {
-        given:
-        def factory = new HibernateConnectionSourceFactory(Foo)
-        def settings = new HibernateConnectionSourceSettings()
-        settings.hibernate.naming_strategy = PhysicalNamingStrategyStandardImpl
-        def dsConfig = DatastoreUtils.createPropertyResolver([
-            'dataSource.url'         : "jdbc:h2:mem:grailsDB;LOCK_TIMEOUT=10000",
-            'dataSource.dbCreate'    : 'update',
-            'dataSource.dialect'     : H2Dialect.name,
-            'hibernate.hbm2ddl.auto' : 'create',
-        ])
-        def dsCs = new org.grails.datastore.gorm.jdbc.connections.DataSourceConnectionSourceFactory()
-            .create(ConnectionSource.DEFAULT, dsConfig)
-
-        when:
-        def config = factory.buildConfiguration(ConnectionSource.DEFAULT, dsCs, settings)
-
-        then:
-        config != null
-    }
-
     void "buildConfiguration with annotatedClasses, annotatedPackages, packagesToScan"() {
         given:
         def factory = new HibernateConnectionSourceFactory(Foo)