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 95835198eb445675fe57a012ef466526f44d3e1f
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Sun Mar 22 18:44:29 2026 -0500

    hibernate 7:
      * Query Engine Precision:
           * Replaced inefficient IN subqueries with true SQL JOINs for 
association criteria.
           * Enabled full support for user-specified JoinType (INNER, LEFT, 
etc.) across all criteria APIs.
           * Resolved ClassCastException and IllegalArgumentException issues 
when navigating deeply nested association paths or querying into @Embedded 
components.
           * Fixed correlated subquery support (exists) to correctly handle its 
own independent join providers.
---
 grails-data-hibernate7/core/ISSUES.md              |   5 +
 .../groovy/grails/orm/CriteriaMethodInvoker.java   |  34 +++++-
 .../grails/orm/HibernateCriteriaBuilder.java       |  17 ++-
 .../grails/orm/hibernate/GrailsSessionContext.java |   9 +-
 .../generator/GrailsIncrementGenerator.java        |  76 +++++++-----
 .../query/DetachedAssociationFunction.java         |   6 +
 .../grails/orm/hibernate/query/HibernateAlias.java |   8 +-
 .../orm/hibernate/query/JpaFromProvider.java       | 135 +++++++++++++++++----
 .../orm/hibernate/query/PredicateGenerator.java    |  53 ++++++--
 .../grails/orm/CriteriaMethodInvokerSpec.groovy    |  23 ++--
 10 files changed, 276 insertions(+), 90 deletions(-)

diff --git a/grails-data-hibernate7/core/ISSUES.md 
b/grails-data-hibernate7/core/ISSUES.md
index e9194b2bc3..8874cbb009 100644
--- a/grails-data-hibernate7/core/ISSUES.md
+++ b/grails-data-hibernate7/core/ISSUES.md
@@ -15,6 +15,11 @@ In Grails 7:
 - `HibernateDatastore.getDatastoreForConnection` supports `"dataSource"`, 
`ConnectionSource.DEFAULT` ("default"), and `ConnectionSource.OLD_DEFAULT` 
("DEFAULT").
 - Raw `"default"` and `"DEFAULT"` strings in H7 production code and key tests 
have been replaced with `ConnectionSource.DEFAULT`.
 - `HibernateMappingContextConfiguration` and `HibernateDatastore` correctly 
use `ConnectionSource.DEFAULT` for the primary datasource name.
+- **Fixed Issues in GORM Querying (Hibernate 7):**
+    - Refactored `JpaFromProvider` to correctly handle root aliases and 
hierarchical joins for dot-notated projection/criteria paths.
+    - Fixed `ClassCastException` in `PredicateGenerator` by ensuring all 
association paths are properly pre-joined in the JPA metamodel.
+    - Enhanced `PredicateGenerator.handleExists` to properly support 
correlated subqueries with their own join providers.
+    - Ensured basic collection joins (e.g., `nicknames`) are automatically 
handled during query construction.
 
 **Risk & Potential Propagation:**
 - This change might affect other GORM modules (Neo4j, MongoDB, etc.) if they 
rely on the uppercase `"DEFAULT"` string literal and don't yet support the 
lowercase `"default"`.
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
 
b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
index 1eb07c07bd..68ba31de38 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/grails/orm/CriteriaMethodInvoker.java
@@ -34,7 +34,12 @@ import org.springframework.beans.BeanUtils;
 
 import grails.gorm.DetachedCriteria;
 import grails.gorm.PagedResultList;
+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.query.Query;
+import 
org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentEntity;
 import org.grails.orm.hibernate.query.HibernateQuery;
 import org.grails.orm.hibernate.query.HibernateQueryArgument;
 
@@ -188,18 +193,38 @@ public class CriteriaMethodInvoker {
 
             if (attribute.isAssociation()) {
                 Class<?> oldTargetClass = builder.getTargetClass();
-                
builder.setTargetClass(builder.getClassForAssociationType(attribute));
+                Class<?> associationClass = 
builder.getClassForAssociationType(attribute);
+                builder.setTargetClass(associationClass);
                 JoinType joinType;
                 if (hasMoreThanOneArg) {
                     joinType = builder.convertFromInt((Integer) args[0]);
-                } else if (builder.getTargetClass().equals(oldTargetClass)) {
+                } else if (associationClass.equals(oldTargetClass)) {
                     joinType = JoinType.LEFT; // default to left join if 
joining on the same table
                 } else {
                     joinType = builder.convertFromInt(0);
                 }
 
                 hibernateQuery.join(name, joinType);
-                hibernateQuery.in(name, new 
DetachedCriteria<>(builder.getTargetClass()).build(callable));
+
+                PersistentEntity parentEntity =
+                        
hibernateQuery.getSession().getMappingContext().getPersistentEntity(oldTargetClass.getName());
+                PersistentProperty property = 
parentEntity.getPropertyByName(name);
+                if (property instanceof Association association) {
+                    DetachedAssociationCriteria associationCriteria =
+                            new DetachedAssociationCriteria(associationClass, 
association);
+                    DetachedCriteria oldDetachedCriteria = 
hibernateQuery.getDetachedCriteria();
+                    hibernateQuery.setDetachedCriteria(associationCriteria);
+                    try {
+                        invokeClosureNode(callable);
+                    } finally {
+                        
hibernateQuery.setDetachedCriteria(oldDetachedCriteria);
+                    }
+                    hibernateQuery.add((Query.Criterion) associationCriteria);
+                } else {
+                    // Fallback for non-GORM associations if any
+                    hibernateQuery.in(name, new 
DetachedCriteria<>(associationClass).build(callable));
+                }
+
                 builder.setTargetClass(oldTargetClass);
 
                 return name;
@@ -238,7 +263,8 @@ public class CriteriaMethodInvoker {
                             && args[0] instanceof String s
                             && args[1] instanceof String a
                             && args[2] instanceof Number jt) {
-                        return builder.createAlias(s, a, jt.intValue());
+                        builder.createAlias(s, a, jt.intValue());
+                        return builder;
                     }
                     return name;
                 case IS_NULL, IS_NOT_NULL, IS_EMPTY, IS_NOT_EMPTY:
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 ceba4af686..fb8d7eccd4 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
@@ -168,13 +168,26 @@ public class HibernateCriteriaBuilder extends 
GroovyObjectSupport implements Bui
         }
     }
 
-    public Criteria createAlias(String associationPath, String alias) {
+    public org.grails.datastore.mapping.query.api.Criteria createAlias(String 
associationPath, String alias) {
         var prop = 
hibernateQuery.getEntity().getPropertyByName(associationPath);
         if (prop instanceof org.grails.datastore.mapping.model.types.Basic) {
-            hibernateQuery.addAlias(new 
org.grails.orm.hibernate.query.HibernateAlias(associationPath, alias));
+            hibernateQuery.addAlias(new 
org.grails.orm.hibernate.query.HibernateAlias(associationPath, alias, 
JoinType.INNER));
             return this;
         }
         hibernateQuery.getDetachedCriteria().createAlias(associationPath, 
alias);
+        hibernateQuery.getDetachedCriteria().join(associationPath, 
JoinType.INNER);
+        return this;
+    }
+
+    public org.grails.datastore.mapping.query.api.Criteria createAlias(String 
associationPath, String alias, int joinType) {
+        var prop = 
hibernateQuery.getEntity().getPropertyByName(associationPath);
+        JoinType convertedJoinType = convertFromInt(joinType);
+        if (prop instanceof org.grails.datastore.mapping.model.types.Basic) {
+            hibernateQuery.addAlias(new 
org.grails.orm.hibernate.query.HibernateAlias(associationPath, alias, 
convertedJoinType));
+            return this;
+        }
+        hibernateQuery.getDetachedCriteria().createAlias(associationPath, 
alias);
+        hibernateQuery.getDetachedCriteria().join(associationPath, 
convertedJoinType);
         return this;
     }
 
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsSessionContext.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsSessionContext.java
index 82b18747ff..f292dd0108 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsSessionContext.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/GrailsSessionContext.java
@@ -79,6 +79,7 @@ public class GrailsSessionContext implements 
CurrentSessionContext {
     }
 
     /** Retrieve the Spring-managed Session for the current thread, if any. */
+    @Override
     public Session currentSession() throws HibernateException {
         Object value = 
TransactionSynchronizationManager.getResource(sessionFactory);
         if (value instanceof Session) {
@@ -87,16 +88,16 @@ public class GrailsSessionContext implements 
CurrentSessionContext {
 
         if (value instanceof SessionHolder sessionHolder) {
             Session session = sessionHolder.getSession();
-            if (TransactionSynchronizationManager.isSynchronizationActive() &&
-                    !sessionHolder.isSynchronizedWithTransaction()) {
+            if (TransactionSynchronizationManager.isSynchronizationActive()
+                    && !sessionHolder.isSynchronizedWithTransaction()) {
                 TransactionSynchronizationManager.registerSynchronization(
                         createSpringSessionSynchronization(sessionHolder));
                 sessionHolder.setSynchronizedWithTransaction(true);
                 // Switch to FlushMode.AUTO, as we have to assume a 
thread-bound Session
                 // with FlushMode.MANUAL, which needs to allow flushing within 
the transaction.
                 FlushMode flushMode = session.getHibernateFlushMode();
-                if (flushMode.equals(FlushMode.MANUAL) &&
-                        
!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
+                if (flushMode.equals(FlushMode.MANUAL)
+                        && 
!TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
                     session.setHibernateFlushMode(FlushMode.AUTO);
                     sessionHolder.setPreviousFlushMode(flushMode);
                 }
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/generator/GrailsIncrementGenerator.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/generator/GrailsIncrementGenerator.java
index bc11819d80..172247aa5d 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/generator/GrailsIncrementGenerator.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/generator/GrailsIncrementGenerator.java
@@ -19,8 +19,10 @@
 package org.grails.orm.hibernate.cfg.domainbinding.generator;
 
 import java.io.Serial;
+import java.util.Optional;
 import java.util.Properties;
 
+import org.hibernate.boot.model.naming.Identifier;
 import org.hibernate.boot.model.relational.SqlStringGenerationContext;
 import 
org.hibernate.boot.model.relational.internal.SqlStringGenerationContextImpl;
 import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
@@ -49,44 +51,52 @@ public class GrailsIncrementGenerator extends 
IncrementGenerator {
             GrailsHibernatePersistentEntity domainClass,
             PersistentEntityNamingStrategy namingStrategy) {
 
+        configure(context, buildParams(context, mappedId, domainClass, 
namingStrategy));
+        initialize(buildSqlContext(context));
+    }
+
+    private Properties buildParams(
+            GeneratorCreationContext context,
+            Identity mappedId,
+            GrailsHibernatePersistentEntity domainClass,
+            PersistentEntityNamingStrategy namingStrategy) {
+
         Properties params = new Properties();
-        if (mappedId != null && mappedId.getProperties() != null) {
-            params.putAll(mappedId.getProperties());
-        }
+        
Optional.ofNullable(mappedId).map(Identity::getProperties).ifPresent(params::putAll);
 
-        // Use the entity's naming-strategy-aware table resolution:
-        // handles explicit mapping, table-per-hierarchy root, and 
PhysicalNamingStrategy fallback.
         params.put(TABLES, domainClass.getTableName(namingStrategy));
+        params.put(COLUMN, resolveColumnName(context, mappedId));
 
-        org.grails.orm.hibernate.cfg.Mapping mapping = 
domainClass.getHibernateMappedForm();
-        if (mapping != null && mapping.getTable() != null) {
-            if (mapping.getTable().getCatalog() != null)
-                params.put(CATALOG, mapping.getTable().getCatalog());
-            if (mapping.getTable().getSchema() != null)
-                params.put(SCHEMA, mapping.getTable().getSchema());
-        }
+        Optional.ofNullable(domainClass.getHibernateMappedForm())
+                .map(org.grails.orm.hibernate.cfg.Mapping::getTable)
+                .ifPresent(table -> {
+                    if (table.getCatalog() != null) params.put(CATALOG, 
table.getCatalog());
+                    if (table.getSchema() != null) params.put(SCHEMA, 
table.getSchema());
+                });
+
+        return params;
+    }
 
-        // Resolve column name — fall back to "id" if the property path is 
dotted (composite)
-        String columnName = context.getProperty().getName();
-        if (columnName == null || columnName.contains(".")) {
-            columnName = (mappedId != null &&
-                            mappedId.getName() != null &&
-                            !mappedId.getName().contains(".")) ?
-                    mappedId.getName() :
-                    "id";
+    private String resolveColumnName(GeneratorCreationContext context, 
Identity mappedId) {
+        String propertyName = context.getProperty().getName();
+        if (propertyName != null && !propertyName.contains(".")) {
+            return propertyName;
         }
-        params.put(COLUMN, columnName);
-
-        // Delegate to the standard configure() — sets returnClass, column, 
physicalTableNames
-        configure(context, params);
-
-        // Build SqlStringGenerationContext and initialize the SQL query
-        JdbcEnvironment jdbcEnvironment = 
context.getDatabase().getJdbcEnvironment();
-        var physicalName = 
context.getDatabase().getDefaultNamespace().getPhysicalName();
-        String catalog = physicalName.catalog() != null ? 
physicalName.catalog().getCanonicalName() : null;
-        String schema = physicalName.schema() != null ? 
physicalName.schema().getCanonicalName() : null;
-        SqlStringGenerationContext sqlContext =
-                SqlStringGenerationContextImpl.fromExplicit(jdbcEnvironment, 
context.getDatabase(), catalog, schema);
-        initialize(sqlContext);
+        return Optional.ofNullable(mappedId)
+                .map(Identity::getName)
+                .filter(name -> !name.contains("."))
+                .orElse("id");
+    }
+
+    private SqlStringGenerationContext 
buildSqlContext(GeneratorCreationContext context) {
+        var database = context.getDatabase();
+        var physicalName = database.getDefaultNamespace().getPhysicalName();
+
+        return SqlStringGenerationContextImpl.fromExplicit(
+                database.getJdbcEnvironment(),
+                database,
+                
Optional.ofNullable(physicalName.catalog()).map(Identifier::getCanonicalName).orElse(null),
+                
Optional.ofNullable(physicalName.schema()).map(Identifier::getCanonicalName).orElse(null)
+        );
     }
 }
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunction.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunction.java
index fe43d6819f..e8fd19a73a 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunction.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunction.java
@@ -30,6 +30,12 @@ public class DetachedAssociationFunction implements 
Function<Query.Criterion, Li
     public List<DetachedAssociationCriteria<?>> apply(Query.Criterion o) {
         if (o instanceof DetachedAssociationCriteria) {
             return List.of((DetachedAssociationCriteria<?>) o);
+        } else if (o instanceof Query.Junction junction) {
+            java.util.List<DetachedAssociationCriteria<?>> result = new 
java.util.ArrayList<>();
+            for (Query.Criterion criterion : junction.getCriteria()) {
+                result.addAll(apply(criterion));
+            }
+            return result;
         }
         return List.of();
     }
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java
index bd3fac0f6d..a1c9c10263 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateAlias.java
@@ -18,6 +18,8 @@
  */
 package org.grails.orm.hibernate.query;
 
+import jakarta.persistence.criteria.JoinType;
+
 import org.grails.datastore.mapping.query.Query;
 
 /**
@@ -25,6 +27,10 @@ import org.grails.datastore.mapping.query.Query;
  *
  * @author walterduquedeestrada
  */
-public record HibernateAlias(String path, String alias) implements 
Query.Criterion, Query.QueryElement {
+public record HibernateAlias(String path, String alias, JoinType joinType) 
implements Query.Criterion, Query.QueryElement {
+
+    public HibernateAlias(String path, String alias) {
+        this(path, alias, JoinType.INNER);
+    }
 
 }
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
index 7bc699eae9..2ae820e7f8 100644
--- 
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
@@ -31,6 +31,7 @@ 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.query.Query;
 
 @SuppressWarnings({
@@ -72,6 +73,19 @@ public class JpaFromProvider implements Cloneable {
         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;
     }
@@ -85,7 +99,33 @@ public class JpaFromProvider implements Cloneable {
             List<Query.Projection> projections,
             List<HibernateAlias> aliases,
             From<?, ?> root) {
-        var detachedAssociationCriteriaList = 
detachedCriteria.getCriteria().stream()
+        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();
@@ -94,8 +134,10 @@ public class JpaFromProvider implements Cloneable {
 
         // 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()
@@ -104,32 +146,39 @@ public class JpaFromProvider implements Cloneable {
                 .collect(Collectors.toSet());
         definedAliases.addAll(basicAliasMap.values());
 
-        var directProjectedPaths = projections.stream()
+        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 eagerPaths = 
detachedCriteria.getFetchStrategies().entrySet().stream()
+        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 = 
detachedCriteria.getPersistentEntity().getPersistentProperties().stream()
+        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());
+                .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(directProjectedPaths.stream()
-                .filter(p -> !definedAliases.contains(p))
-                .toList());
+        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) {
@@ -146,7 +195,7 @@ public class JpaFromProvider implements Cloneable {
 
         // Re-calculate projected paths to include expanded segments for LEFT 
join logic
         var finalProjectedPaths = expandedPaths.stream()
-                .filter(p -> directProjectedPaths.stream().anyMatch(dp -> 
dp.equals(p) || dp.startsWith(p + ".")))
+                .filter(p -> associationProjectedPaths.stream().anyMatch(dp -> 
dp.equals(p) || dp.startsWith(p + ".")))
                 .collect(Collectors.toSet());
 
         Map<String, From<?, ?>> fromsByPath = new HashMap<>();
@@ -164,10 +213,15 @@ public class JpaFromProvider implements Cloneable {
             String leaf = path.contains(".") ? 
path.substring(path.lastIndexOf('.') + 1) : path;
 
             From<?, ?> base = fromsByPath.get(parentPath);
+            if (base == null) {
+                continue;
+            }
 
             JoinType joinType = JoinType.INNER;
-            if (detachedCriteria.getJoinTypes().containsKey(path)) {
-                joinType = detachedCriteria.getJoinTypes().get(path);
+            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)) {
@@ -176,28 +230,43 @@ public class JpaFromProvider implements Cloneable {
 
             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;
             }
 
-            table.alias(path);
+            if (!aliasApplied) {
+                table.alias(path);
+            }
             fromsByPath.put(path, table);
         }
 
-        String rootAlias = detachedCriteria.getAlias();
-        if (rootAlias != null && !rootAlias.isEmpty()) {
-            fromsByPath.put(rootAlias, root);
-        }
         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, DetachedAssociationCriteria<?>> createAliasMap(
             List<DetachedAssociationCriteria<?>> 
detachedAssociationCriteriaList) {
         // Use a merge function and a stable map type to avoid DuplicateKey 
exceptions when the same
@@ -224,29 +293,43 @@ public class JpaFromProvider implements Cloneable {
         String[] parsed = propertyName.split("\\.");
         if (parsed.length == SINGLE_PROPERTY) {
             From<?, ?> root = fromMap.get("root");
-            if (propertyName.equals(root.getJavaType().getSimpleName())
-                    || propertyName.equals(root.getJavaType().getName())) {
-                return root;
+            if (root != null) {
+                if (propertyName.equals(root.getJavaType().getSimpleName())
+                        || propertyName.equals(root.getJavaType().getName())) {
+                    return root;
+                }
+                return root.get(propertyName);
             }
-            return root.get(propertyName);
         }
 
         // Try to find the longest matching prefix in fromMap
-        for (int i = parsed.length - 1; i >= 1; i--) {
+        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);
-                for (int j = i; j < parsed.length; j++) {
-                    path = path.get(parsed[j]);
+                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;
+                    }
                 }
-                return path;
             }
         }
 
         // Fallback to root
         Path<?> path = fromMap.get("root");
-        for (String segment : parsed) {
-            path = path.get(segment);
+        if (path != null) {
+            for (String segment : parsed) {
+                path = path.get(segment);
+                if (path == null) {
+                    break;
+                }
+            }
         }
         return path;
     }
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 e9a20cbd27..07985dee5b 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
@@ -136,12 +136,18 @@ public class PredicateGenerator {
             JpaFromProvider fromsByProvider,
             DetachedAssociationCriteria<?> c) {
 
-        var child = root.join(c.getAssociationPath(), JoinType.LEFT);
-        JpaFromProvider childTablesByName = (JpaFromProvider) 
fromsByProvider.clone();
-        childTablesByName.put("root", child);
-
+        From<?, ?> child = (From<?, ?>) 
fromsByProvider.getFullyQualifiedPath(c.getAssociationPath());
         PersistentEntity associatedEntity = 
c.getAssociation().getAssociatedEntity();
 
+        JpaFromProvider childTablesByName = new JpaFromProvider(
+                fromsByProvider,
+                associatedEntity,
+                c.getCriteria(),
+                java.util.Collections.<Query.Projection>emptyList(),
+                c.getFetchStrategies(),
+                c.getJoinTypes(),
+                child);
+
         return cb.and(getPredicates(cb, criteriaQuery, child, c.getCriteria(), 
childTablesByName, associatedEntity));
     }
 
@@ -151,9 +157,15 @@ public class PredicateGenerator {
             From<?, ?> root,
             JpaFromProvider fromsByProvider,
             HibernateAssociationQuery haq) {
-        var child = root.join(haq.associationPath, JoinType.LEFT);
-        JpaFromProvider childFroms = (JpaFromProvider) fromsByProvider.clone();
-        childFroms.put("root", child);
+        From<?, ?> child = (From<?, ?>) 
fromsByProvider.getFullyQualifiedPath(haq.associationPath);
+        JpaFromProvider childFroms = new JpaFromProvider(
+                fromsByProvider,
+                haq.getEntity(),
+                haq.getAssociationCriteria(),
+                java.util.Collections.<Query.Projection>emptyList(),
+                java.util.Collections.<String, 
jakarta.persistence.FetchType>emptyMap(),
+                java.util.Collections.<String, 
jakarta.persistence.criteria.JoinType>emptyMap(),
+                child);
         return cb.and(
                 getPredicates(cb, criteriaQuery, child, 
haq.getAssociationCriteria(), childFroms, haq.getEntity()));
     }
@@ -389,12 +401,27 @@ public class PredicateGenerator {
             JpaFromProvider fromsByProvider,
             PersistentEntity entity,
             Query.Exists c) {
-        Subquery subquery = criteriaQuery.subquery(Integer.class);
-        Root subRoot = subquery.from(entity.getJavaClass());
-        JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone();
-        newMap.put("root", subRoot);
-        var predicates =
-                getPredicates(cb, criteriaQuery, subRoot, 
c.getSubquery().getCriteria(), newMap, entity);
+        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.<Query.Projection>emptyList(),
+                java.util.Collections.<String, 
jakarta.persistence.FetchType>emptyMap(),
+                java.util.Collections.<String, 
jakarta.persistence.criteria.JoinType>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))
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
index b21b1050e4..d35c864e61 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/grails/orm/CriteriaMethodInvokerSpec.groovy
@@ -19,14 +19,12 @@
 
 package grails.orm
 
-import groovy.lang.Closure
-import groovy.lang.MissingMethodException
+
+import grails.gorm.DetachedCriteria
+import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria
 import org.grails.orm.hibernate.query.HibernateQuery
-import org.grails.orm.hibernate.HibernateSession
-import org.hibernate.Session
+
 import org.hibernate.SessionFactory
-import org.springframework.orm.hibernate5.SessionHolder
-import 
org.springframework.transaction.support.TransactionSynchronizationManager
 import spock.lang.Specification
 
 class CriteriaMethodInvokerSpec extends Specification {
@@ -34,10 +32,14 @@ class CriteriaMethodInvokerSpec extends Specification {
     HibernateCriteriaBuilder builder = Mock(HibernateCriteriaBuilder)
     HibernateQuery query = Mock(HibernateQuery)
     SessionFactory sessionFactory = Mock(SessionFactory)
+    org.grails.orm.hibernate.HibernateSession session = 
Mock(org.grails.orm.hibernate.HibernateSession)
+    org.grails.datastore.mapping.model.MappingContext mappingContext = 
Mock(org.grails.datastore.mapping.model.MappingContext)
     CriteriaMethodInvoker invoker = new CriteriaMethodInvoker(builder)
 
     def setup() {
         builder.getHibernateQuery() >> query
+        query.getSession() >> session
+        session.getMappingContext() >> mappingContext
         _ * builder.isPaginationEnabledList() >> false
     }
 
@@ -146,6 +148,8 @@ class CriteriaMethodInvokerSpec extends Specification {
     void "test invokeMethod handles association query"() {
         given:
         def closure = { eq("amount", 10) }
+        def association = 
Mock(org.grails.datastore.mapping.model.types.Association)
+        def persistentEntity = 
Mock(org.grails.orm.hibernate.cfg.domainbinding.hibernate.HibernatePersistentEntity)
 
         when:
         invoker.invokeMethod("transactions", [closure] as Object[])
@@ -164,7 +168,12 @@ class CriteriaMethodInvokerSpec extends Specification {
         }
         1 * builder.getClassForAssociationType(_) >> InvokerTransaction
         1 * query.join("transactions", _)
-        1 * query.in("transactions", _)
+        1 * mappingContext.getPersistentEntity(InvokerAccount.name) >> 
persistentEntity
+        1 * persistentEntity.getPropertyByName("transactions") >> association
+        1 * query.getDetachedCriteria() >> Mock(DetachedCriteria)
+        1 * query.setDetachedCriteria(_ as DetachedAssociationCriteria)
+        1 * query.setDetachedCriteria(_ as DetachedCriteria)
+        1 * query.add(_ as DetachedAssociationCriteria)
     }
 
     void "test invokeMethod handles and/or/not junctions"() {


Reply via email to