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 2d11e3a42a94aa78b39d761c6e7ae0f6e00cd118
Author: Walter Duque de Estrada <[email protected]>
AuthorDate: Thu Mar 19 14:58:07 2026 -0500

    hibernate 7:
    
     * Criteria Query NPE: Fixed NullPointerException in JpaFromProvider by 
implementing projection-aware auto-joining for association paths.
       * Subquery Join Leakage: Resolved PathElementException by isolating 
subquery joins and preventing them from leaking into the outer query scope.
       * Multi-Tenancy Regression: Restored missing PreQueryEvent and 
PostQueryEvent publication in HibernateQuery to ensure tenant filters are 
correctly applied.
       * HQL Parameter Binding: Fixed QueryParameterException by filtering 
GORM-specific query settings (e.g., flushMode) from named parameters in 
executeQuery.
       * Parameterized String Queries: Added support for executeQuery and 
executeUpdate overloads that accept plain String queries with positional/named 
parameters.
       * Multi-Column Mapping Bug: Fixed a bug in PropertyDefinitionDelegate 
where re-evaluating properties with multiple columns would overwrite the first 
column.
       * Mapping DSL Index Fix: Resolved a .toString() conversion bug for index 
closures in HibernateMappingBuilder.
       * Test Suite Modernization: Consolidated legacy HibernateMappingBuilder 
tests into a modern Spock specification and added verification for multi-column 
property logic.
---
 grails-data-hibernate7/core/ISSUES.md              |  45 ++++++
 .../orm/hibernate/HibernateGormStaticApi.groovy    |  11 +-
 .../generator/GrailsNativeGenerator.java           |  30 +++-
 .../proxy/ByteBuddyGroovyInterceptor.java          |  79 +++++++++
 .../proxy/ByteBuddyGroovyProxyFactory.java         | 103 ++++++++++++
 .../query/DetachedAssociationFunction.java         |  19 +--
 .../orm/hibernate/query/HibernateHqlQuery.java     |  10 ++
 .../grails/orm/hibernate/query/HibernateQuery.java |  68 ++++++--
 .../hibernate/query/JpaCriteriaQueryCreator.java   |   4 +-
 .../orm/hibernate/query/JpaFromProvider.java       | 177 +++++++++++++++------
 .../orm/hibernate/query/PredicateGenerator.java    |   7 +-
 .../gorm/specs/HibernateGormDatastoreSpec.groovy   |   1 +
 .../specs/hibernatequery/HibernateQuerySpec.groovy |  25 +++
 .../JpaCriteriaQueryCreatorSpec.groovy             |  19 +++
 .../hibernatequery/JpaFromProviderSpec.groovy      |  52 ++++++
 .../hibernatequery/PredicateGeneratorSpec.groovy   |  13 ++
 .../core/GrailsDataHibernate7TckManager.groovy     |  88 ++++++++--
 .../hibernate/HibernateGormStaticApiSpec.groovy    |  48 ++++--
 .../domainbinding/GrailsNativeGeneratorSpec.groovy |  25 +++
 .../DataServiceDatasourceInheritanceSpec.groovy    |   2 +-
 ...ataServiceMultiTenantMultiDataSourceSpec.groovy |   8 +-
 .../WhereQueryMultiDataSourceSpec.groovy           |   1 +
 .../proxy/ByteBuddyGroovyInterceptorSpec.groovy    |  76 +++++++++
 .../proxy/ByteBuddyGroovyProxyFactorySpec.groovy   |  30 ++++
 .../query/DetachedAssociationFunctionSpec.groovy   |  64 ++++++++
 .../hibernate/query/HibernateHqlQuerySpec.groovy   |  12 ++
 26 files changed, 897 insertions(+), 120 deletions(-)

diff --git a/grails-data-hibernate7/core/ISSUES.md 
b/grails-data-hibernate7/core/ISSUES.md
new file mode 100644
index 0000000000..dffb00415b
--- /dev/null
+++ b/grails-data-hibernate7/core/ISSUES.md
@@ -0,0 +1,45 @@
+# Known Issues in Hibernate 7 Migration
+
+### 1. Float Precision Mismatch (H2 and PostgreSQL)
+**Symptoms:**
+- `org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing 
DDL`
+- H2 Error: `Precision ("64") must be between "1" and "53" inclusive`
+- PostgreSQL Error: `ERROR: precision for type float must be less than 54 bits`
+
+**Description:**
+Hibernate 7's default mapping for `java.lang.Double` properties on H2 (2.x) 
and PostgreSQL (16+) generates DDL with `float(64)`. Both databases reject 
this, as the maximum precision for the `float`/`double precision` type is 53 
bits.
+
+**Workaround:**
+Explicitly set `precision` in the domain mapping (e.g., `amount precision: 
10`) or use `sqlType: 'double precision'`.
+
+---
+
+### 2. Generator Initialization Failure (NPE)
+**Symptoms:**
+- `java.lang.NullPointerException` at 
`org.hibernate.id.enhanced.SequenceStyleGenerator.generate`
+- Message: `Cannot invoke 
"org.hibernate.id.enhanced.DatabaseStructure.buildCallback(...)" because 
"this.databaseStructure" is null`
+
+**Description:**
+When a table creation fails (e.g., due to the Float Precision Mismatch issue), 
the `SequenceStyleGenerator` is not properly initialized. Subsequent attempts 
to persist an entity trigger an NPE instead of a descriptive error because 
Hibernate 7 does not check the state of the `databaseStructure` before use.
+
+---
+
+### 3. ByteBuddy Proxy Initialization
+**Symptoms:**
+- Proxies are initialized prematurely during `getId()`, `isDirty()`, or Groovy 
truthiness checks (`if (proxy)`).
+- `Hibernate.isInitialized(proxy)` returns `true` when it should be `false`.
+
+**Description:**
+Hibernate 7's `ByteBuddyInterceptor.intercept()` does not distinguish between 
actual property access and Groovy's internal metadata calls (like 
`getMetaClass()`). Any interaction with the proxy object triggers the 
interceptor, which hydrates the instance. This breaks lazy loading expectations 
in Grails and dynamic Groovy environments.
+
+---
+
+### 4. JpaFromProvider NullPointerException (Resolved)
+**Symptoms:**
+- `NullPointerException` during path resolution in Criteria queries.
+
+**Description:**
+Occurs when a query projection references an association path that has not 
been joined in the `FROM` clause.
+
+**Action Taken:**
+Updated `JpaFromProvider` to scan projections and automatically create 
hierarchical `LEFT JOIN`s for discovered association paths.
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
index b0ad1678cb..4be8cf0e90 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/HibernateGormStaticApi.groovy
@@ -275,17 +275,22 @@ class HibernateGormStaticApi<D> extends GormStaticApi<D> {
 
     @Override
     D find(CharSequence query, Map params) {
-        doSingleInternal(query, params, [], [:], false)
+        doSingleInternal(query, params, [], params, false)
     }
 
     @Override
     List<D> findAll(CharSequence query, Map params) {
-        doListInternal(query, params, [], [:], false)
+        doListInternal(query, params, [], params, false)
     }
 
     @Override
     List executeQuery(CharSequence query, Map args) {
-        doListInternal(query, [:], [], args, false)
+        doListInternal(query, args, [], args, false)
+    }
+
+    @Override
+    Integer executeUpdate(CharSequence query, Map args) {
+        doInternalExecuteUpdate(query, args, [], args)
     }
 
     @Override
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/generator/GrailsNativeGenerator.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/generator/GrailsNativeGenerator.java
index d82f2bc516..2e3c762c4b 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/generator/GrailsNativeGenerator.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/cfg/domainbinding/generator/GrailsNativeGenerator.java
@@ -19,21 +19,30 @@
 package org.grails.orm.hibernate.cfg.domainbinding.generator;
 
 import java.io.Serial;
+import java.lang.reflect.Field;
 
 import jakarta.persistence.GenerationType;
 
+import org.hibernate.HibernateException;
 import org.hibernate.engine.spi.SharedSessionContractImplementor;
 import org.hibernate.generator.EventType;
 import org.hibernate.generator.GeneratorCreationContext;
 import org.hibernate.id.NativeGenerator;
+import org.hibernate.id.enhanced.SequenceStyleGenerator;
 
+/**
+ * A native generator that supports Grails assigned identifiers and fixes 
Hibernate 7 ClassCastException.
+ *
+ * @author Graeme Rocher
+ * @since 7.0
+ */
 public class GrailsNativeGenerator extends NativeGenerator {
 
     @Serial
     private static final long serialVersionUID = 1L;
 
     public GrailsNativeGenerator(GeneratorCreationContext context) {
-        // This triggers the internal switch logic you provided earlier,
+        // This triggers the internal switch logic in NativeGenerator,
         // which calls setIdentity(true) on the column for H2.
         try {
             this.initialize(null, null, context);
@@ -57,7 +66,24 @@ public class GrailsNativeGenerator extends NativeGenerator {
             return null;
         }
 
-        // 3. For Sequences/UUIDs, delegate to the standard logic
+        // 3. Prevent NPE if configuration failed (e.g. DDL error)
+        // Access private field dialectNativeGenerator in NativeGenerator
+        try {
+            Field field = 
NativeGenerator.class.getDeclaredField("dialectNativeGenerator");
+            field.setAccessible(true);
+            Object delegate = field.get(this);
+            if (delegate instanceof SequenceStyleGenerator ssg) {
+                if (ssg.getDatabaseStructure() == null) {
+                    throw new HibernateException("Identifier generator 
(SequenceStyleGenerator) was not properly initialized. This usually happens if 
table creation failed (check previous logs for DDL errors).");
+                }
+            }
+        } catch (HibernateException e) {
+            throw e;
+        } catch (Exception ignored) {
+            // ignore reflection errors
+        }
+
+        // 4. For Sequences/UUIDs, delegate to the standard logic
         return super.generate(session, entity, null, eventType);
     }
 }
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptor.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptor.java
new file mode 100644
index 0000000000..c5884e3d4c
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptor.java
@@ -0,0 +1,79 @@
+/*
+ *  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.proxy;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import org.hibernate.engine.spi.SharedSessionContractImplementor;
+import org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor;
+import org.hibernate.type.CompositeType;
+
+import static org.hibernate.internal.util.ReflectHelper.isPublic;
+
+/**
+ * A ByteBuddy interceptor that avoids initializing the proxy for 
Groovy-specific methods.
+ *
+ * @author Graeme Rocher
+ * @since 7.0
+ */
+public class ByteBuddyGroovyInterceptor extends ByteBuddyInterceptor {
+
+    public ByteBuddyGroovyInterceptor(
+            String entityName,
+            Class<?> persistentClass,
+            Class<?>[] interfaces,
+            Object id,
+            Method getIdentifierMethod,
+            Method setIdentifierMethod,
+            CompositeType componentIdType,
+            SharedSessionContractImplementor session,
+            boolean overridesEquals) {
+        super(entityName, persistentClass, interfaces, id, 
getIdentifierMethod, setIdentifierMethod, componentIdType, session, 
overridesEquals);
+    }
+
+    @Override
+    public Object intercept(Object proxy, Method method, Object[] args) throws 
Throwable {
+        String methodName = method.getName();
+        if (methodName.equals("getMetaClass") || 
methodName.equals("setMetaClass") || methodName.equals("getProperty") || 
methodName.equals("setProperty") || methodName.equals("invokeMethod")) {
+            // Logic adapted from ByteBuddyInterceptor.intercept to handle 
Groovy methods without initialization
+            final Object result = this.invoke( method, args, proxy );
+            if ( result == INVOKE_IMPLEMENTATION ) {
+                final Object target = getImplementation();
+                try {
+                    if ( isPublic( persistentClass, method ) ) {
+                        return method.invoke( target, args );
+                    }
+                    else {
+                        method.setAccessible( true );
+                        return method.invoke( target, args );
+                    }
+                }
+                catch (InvocationTargetException ite) {
+                    throw ite.getTargetException();
+                }
+            }
+            return result;
+        }
+        if (methodName.equals("toString") && args.length == 0) {
+            return getEntityName() + ":" + getIdentifier();
+        }
+        return super.intercept(proxy, method, args);
+    }
+}
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyProxyFactory.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyProxyFactory.java
new file mode 100644
index 0000000000..d2d9f1df42
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyProxyFactory.java
@@ -0,0 +1,103 @@
+/*
+ *  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.proxy;
+
+import java.io.Serializable;
+import java.lang.reflect.Method;
+import java.util.Set;
+
+import org.hibernate.HibernateException;
+import org.hibernate.engine.spi.PrimeAmongSecondarySupertypes;
+import org.hibernate.engine.spi.SharedSessionContractImplementor;
+import org.hibernate.internal.util.ReflectHelper;
+import org.hibernate.proxy.HibernateProxy;
+import org.hibernate.proxy.ProxyFactory;
+import org.hibernate.proxy.pojo.bytebuddy.ByteBuddyProxyFactory;
+import org.hibernate.proxy.pojo.bytebuddy.ByteBuddyProxyHelper;
+import org.hibernate.type.CompositeType;
+
+import static 
org.hibernate.internal.util.collections.ArrayHelper.EMPTY_CLASS_ARRAY;
+
+/**
+ * A ProxyFactory implementation for ByteBuddy that uses {@link 
ByteBuddyGroovyInterceptor}.
+ *
+ * @author Graeme Rocher
+ * @since 7.0
+ */
+public class ByteBuddyGroovyProxyFactory extends ByteBuddyProxyFactory {
+
+    private Class<?> persistentClass;
+    private String entityName;
+    private Class<?>[] interfaces;
+    private Method getIdentifierMethod;
+    private Method setIdentifierMethod;
+    private CompositeType componentIdType;
+    private boolean overridesEquals;
+
+    private Class<?> proxyClass;
+    private final ByteBuddyProxyHelper byteBuddyProxyHelper;
+
+    public ByteBuddyGroovyProxyFactory(ByteBuddyProxyHelper 
byteBuddyProxyHelper) {
+        super(byteBuddyProxyHelper);
+        this.byteBuddyProxyHelper = byteBuddyProxyHelper;
+    }
+
+    @Override
+    public void postInstantiate(
+            String entityName,
+            Class<?> persistentClass,
+            Set<Class<?>> interfaces,
+            Method getIdentifierMethod,
+            Method setIdentifierMethod,
+            CompositeType componentIdType) throws HibernateException {
+        this.entityName = entityName;
+        this.persistentClass = persistentClass;
+        this.interfaces = interfaces == null ? EMPTY_CLASS_ARRAY : 
interfaces.toArray(EMPTY_CLASS_ARRAY);
+        this.getIdentifierMethod = getIdentifierMethod;
+        this.setIdentifierMethod = setIdentifierMethod;
+        this.componentIdType = componentIdType;
+        this.overridesEquals = ReflectHelper.overridesEquals(persistentClass);
+        this.proxyClass = byteBuddyProxyHelper.buildProxy(persistentClass, 
this.interfaces);
+        super.postInstantiate(entityName, persistentClass, interfaces, 
getIdentifierMethod, setIdentifierMethod, componentIdType);
+    }
+
+    @Override
+    public HibernateProxy getProxy(Object id, SharedSessionContractImplementor 
session) throws HibernateException {
+        try {
+            final ByteBuddyGroovyInterceptor interceptor = new 
ByteBuddyGroovyInterceptor(
+                    entityName,
+                    persistentClass,
+                    interfaces,
+                    id,
+                    getIdentifierMethod,
+                    setIdentifierMethod,
+                    componentIdType,
+                    session,
+                    overridesEquals
+            );
+
+            final PrimeAmongSecondarySupertypes instance = 
(PrimeAmongSecondarySupertypes) proxyClass.getConstructor().newInstance();
+            final HibernateProxy hibernateProxy = instance.asHibernateProxy();
+            
hibernateProxy.asProxyConfiguration().$$_hibernate_set_interceptor(interceptor);
+            return hibernateProxy;
+        } catch (Throwable t) {
+            throw new HibernateException("Unable to generate proxy", t);
+        }
+    }
+}
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 988b1e526a..fe43d6819f 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
@@ -19,7 +19,6 @@
 package org.grails.orm.hibernate.query;
 
 import java.util.List;
-import java.util.Objects;
 import java.util.function.Function;
 
 import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria;
@@ -29,21 +28,9 @@ import org.grails.datastore.mapping.query.Query;
 public class DetachedAssociationFunction implements Function<Query.Criterion, 
List<DetachedAssociationCriteria<?>>> {
     @Override
     public List<DetachedAssociationCriteria<?>> apply(Query.Criterion o) {
-        List<Query.Criterion> criteria;
-        if (o instanceof Query.In c && Objects.nonNull(c.getSubquery())) {
-            criteria = c.getSubquery().getCriteria();
-        } else if (o instanceof Query.Exists c && 
Objects.nonNull(c.getSubquery())) {
-            criteria = c.getSubquery().getCriteria();
-        } else if (o instanceof Query.NotExists c && 
Objects.nonNull(c.getSubquery())) {
-            criteria = c.getSubquery().getCriteria();
-        } else if (o instanceof Query.SubqueryCriterion c && 
Objects.nonNull(c.getValue())) {
-            criteria = c.getValue().getCriteria();
-        } else {
-            criteria = List.of(o);
+        if (o instanceof DetachedAssociationCriteria) {
+            return List.of((DetachedAssociationCriteria<?>) o);
         }
-        return criteria.stream()
-                .filter(it -> it instanceof DetachedAssociationCriteria)
-                .map(it -> (DetachedAssociationCriteria<?>) it)
-                .collect(java.util.stream.Collectors.toList());
+        return List.of();
     }
 }
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java
index 90bc6b7ec8..ce121fb447 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateHqlQuery.java
@@ -246,6 +246,16 @@ public class HibernateHqlQuery extends Query {
                 throw new GrailsQueryException("Named parameter's name must be 
a String: " + namedArgs);
             }
             String name = key.toString();
+            if (HibernateQueryArgument.MAX.value().equals(name) ||
+                HibernateQueryArgument.OFFSET.value().equals(name) ||
+                HibernateQueryArgument.CACHE.value().equals(name) ||
+                HibernateQueryArgument.FETCH_SIZE.value().equals(name) ||
+                HibernateQueryArgument.TIMEOUT.value().equals(name) ||
+                HibernateQueryArgument.READ_ONLY.value().equals(name) ||
+                HibernateQueryArgument.FLUSH_MODE.value().equals(name) ||
+                HibernateQueryArgument.LOCK.value().equals(name)) {
+                return;
+            }
             if (value == null) {
                 delegate.setParameter(name, null);
             } else if (value instanceof Collection<?> col) {
diff --git 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java
 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java
index 5ea0df1a09..0c201a294e 100644
--- 
a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java
+++ 
b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HibernateQuery.java
@@ -18,6 +18,7 @@
  */
 package org.grails.orm.hibernate.query;
 
+import java.util.Collections;
 import java.util.Deque;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -38,10 +39,12 @@ import 
org.hibernate.query.criteria.HibernateCriteriaBuilder;
 import org.hibernate.query.criteria.JpaCriteriaQuery;
 import org.hibernate.query.criteria.JpaSubQuery;
 
+import org.springframework.context.ApplicationEventPublisher;
 import org.springframework.core.convert.ConversionService;
 import org.springframework.dao.InvalidDataAccessApiUsageException;
 
 import grails.gorm.DetachedCriteria;
+import org.grails.datastore.mapping.core.Datastore;
 import org.grails.datastore.mapping.model.PersistentEntity;
 import org.grails.datastore.mapping.model.PersistentProperty;
 import org.grails.datastore.mapping.model.types.Association;
@@ -50,6 +53,8 @@ import org.grails.datastore.mapping.query.AssociationQuery;
 import org.grails.datastore.mapping.query.Projections;
 import org.grails.datastore.mapping.query.Query;
 import org.grails.datastore.mapping.query.api.QueryableCriteria;
+import org.grails.datastore.mapping.query.event.PostQueryEvent;
+import org.grails.datastore.mapping.query.event.PreQueryEvent;
 import org.grails.orm.hibernate.GrailsHibernateTemplate;
 import org.grails.orm.hibernate.HibernateSession;
 import org.grails.orm.hibernate.IHibernateTemplate;
@@ -402,6 +407,12 @@ public class HibernateQuery extends Query {
 
     @Override
     public List list() {
+        firePreQueryEvent();
+        List results = executeList();
+        return firePostQueryEvent(results);
+    }
+
+    private List executeList() {
         return getHibernateQueryExecutor().list(getCurrentSession(), 
getJpaCriteriaQuery());
     }
 
@@ -432,6 +443,12 @@ public class HibernateQuery extends Query {
 
     @Override
     public Object singleResult() {
+        firePreQueryEvent();
+        Object result = executeSingleResult();
+        return firePostQueryEvent(result);
+    }
+
+    private Object executeSingleResult() {
         return getHibernateQueryExecutor().singleResult(getCurrentSession(), 
getJpaCriteriaQuery());
     }
 
@@ -441,25 +458,56 @@ public class HibernateQuery extends Query {
 
     @Override
     public Number countResults() {
+        firePreQueryEvent();
+
+        Number result;
         if (projections.getProjectionList().isEmpty()) {
             projections().count();
-            return (Number) singleResult();
+            result = (Number) executeSingleResult();
+        } else {
+            HibernateCriteriaBuilder cb = getCriteriaBuilder();
+
+            JpaCriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
+            JpaSubQuery<Tuple> innerSubquery = 
countQuery.subquery(Tuple.class);
+
+            ConversionService cs = 
getSession().getMappingContext().getConversionService();
+            new JpaCriteriaQueryCreator(projections, cb, entity, 
detachedCriteria, cs)
+                    .populateSubquery(innerSubquery);
+
+            countQuery.from(innerSubquery);
+            countQuery.select(cb.count(cb.literal(1)));
+            result = (Number) 
getHibernateQueryExecutor().singleResult(getCurrentSession(), countQuery);
         }
-        HibernateCriteriaBuilder cb = getCriteriaBuilder();
 
-        JpaCriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
-        JpaSubQuery<Tuple> innerSubquery = countQuery.subquery(Tuple.class);
+        return (Number) firePostQueryEvent(result);
+    }
 
-        ConversionService cs = 
getSession().getMappingContext().getConversionService();
-        new JpaCriteriaQueryCreator(projections, cb, entity, detachedCriteria, 
cs)
-                .populateSubquery(innerSubquery);
+    private void firePreQueryEvent() {
+        Datastore datastore = session.getDatastore();
+        ApplicationEventPublisher publisher = 
datastore.getApplicationEventPublisher();
+        if (publisher != null) {
+            publisher.publishEvent(new PreQueryEvent(datastore, this));
+        }
+    }
+
+    private List firePostQueryEvent(List results) {
+        Datastore datastore = session.getDatastore();
+        ApplicationEventPublisher publisher = 
datastore.getApplicationEventPublisher();
+        if (publisher != null) {
+            PostQueryEvent postQueryEvent = new PostQueryEvent(datastore, 
this, results);
+            publisher.publishEvent(postQueryEvent);
+            return postQueryEvent.getResults();
+        }
+        return results;
+    }
 
-        countQuery.from(innerSubquery);
-        countQuery.select(cb.count(cb.literal(1)));
-        return (Number) 
getHibernateQueryExecutor().singleResult(getCurrentSession(), countQuery);
+    private Object firePostQueryEvent(Object result) {
+        List<?> results = 
firePostQueryEvent(Collections.singletonList(result));
+        return results.isEmpty() ? null : results.get(0);
     }
 
     public Object scroll() {
+        firePreQueryEvent();
         return getHibernateQueryExecutor().scroll(getCurrentSession(), 
getJpaCriteriaQuery());
     }
 
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 c20ea6e04a..2d89b22f70 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
@@ -71,7 +71,7 @@ public class JpaCriteriaQueryCreator {
         var cq = createCriteriaQuery(projectionList);
         Class<?> javaClass = entity.getJavaClass();
         Root<?> root = cq.from(javaClass);
-        var tablesByName = new JpaFromProvider(detachedCriteria, cq, root);
+        var tablesByName = new JpaFromProvider(detachedCriteria, 
projectionList, cq, root);
         assignProjections(projectionList, cq, tablesByName);
         assignGroupBy(cq, tablesByName);
 
@@ -85,7 +85,7 @@ public class JpaCriteriaQueryCreator {
         var projectionList = collectProjections();
         Class<?> javaClass = entity.getJavaClass();
         Root<?> root = subquery.from(javaClass);
-        var tablesByName = new JpaFromProvider(detachedCriteria, subquery, 
root);
+        var tablesByName = new JpaFromProvider(detachedCriteria, 
projectionList, subquery, root);
 
         var aliasedProjections = new 
java.util.concurrent.atomic.AtomicInteger(0);
         var projectionExpressions = projectionList.stream()
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 69de7030a1..8c1f9d65fe 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
@@ -24,6 +24,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -36,6 +37,7 @@ import jakarta.persistence.criteria.Path;
 
 import grails.gorm.DetachedCriteria;
 import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria;
+import org.grails.datastore.mapping.query.Query;
 
 @SuppressWarnings({
     "PMD.DataflowAnomalyAnalysis",
@@ -54,64 +56,116 @@ public class JpaFromProvider implements Cloneable {
     }
 
     public JpaFromProvider(DetachedCriteria<?> detachedCriteria, 
AbstractQuery<?> cq, From<?, ?> root) {
-        fromMap = getFromsByName(detachedCriteria, cq, root);
+        this(detachedCriteria, List.of(), cq, root);
+    }
+
+    public JpaFromProvider(
+            DetachedCriteria<?> detachedCriteria,
+            List<Query.Projection> projections,
+            AbstractQuery<?> cq,
+            From<?, ?> root) {
+        fromMap = getFromsByName(detachedCriteria, projections, cq, root);
+    }
+
+    public JpaFromProvider(
+            JpaFromProvider parent,
+            DetachedCriteria<?> detachedCriteria,
+            List<Query.Projection> projections,
+            AbstractQuery<?> cq,
+            From<?, ?> root) {
+        fromMap = new HashMap<>(parent.fromMap);
+        fromMap.putAll(getFromsByName(detachedCriteria, projections, cq, 
root));
     }
 
     private Map<String, From<?, ?>> getFromsByName(
-            DetachedCriteria<?> detachedCriteria, AbstractQuery<?> cq, From<?, 
?> root) {
+            DetachedCriteria<?> detachedCriteria,
+            List<Query.Projection> projections,
+            AbstractQuery<?> cq,
+            From<?, ?> root) {
         var detachedAssociationCriteriaList = 
detachedCriteria.getCriteria().stream()
                 .map(new DetachedAssociationFunction())
                 .flatMap(List::stream)
                 .toList();
 
         var aliasMap = createAliasMap(detachedAssociationCriteriaList);
-        // The join column is column for joining from the root entity
-        var detachedFroms = createDetachedFroms(cq, 
detachedAssociationCriteriaList);
-        Map<String, From<?, ?>> fromsByName = Stream.concat(
-                        aliasMap.keySet().stream(),
-                        
detachedCriteria.getFetchStrategies().entrySet().stream()
-                                .filter(entry -> 
entry.getValue().equals(FetchType.EAGER))
-                                .map(Map.Entry::getKey)
-                                .toList()
-                                .stream())
-                .distinct()
-                .map(joinColumn -> {
-                    // Determine owner class for this join path from detached 
criteria
-                    var dac = aliasMap.get(joinColumn);
-                    Class<?> ownerClass =
-                            dac != null ? 
dac.getAssociation().getOwner().getJavaClass() : root.getJavaType();
-                    // Choose base From: use outer root only if join belongs 
to the outer root type;
-                    // otherwise create a detached root for the owner
-                    From<?, ?> base = ownerClass.equals(root.getJavaType()) ?
-                            root :
-                            detachedFroms.computeIfAbsent(joinColumn, s -> 
cq.from(ownerClass));
-
-                    var table = base.join(
-                            joinColumn,
-                            detachedCriteria.getJoinTypes().entrySet().stream()
-                                    .filter(entry -> 
entry.getKey().equals(joinColumn))
-                                    .map(Map.Entry::getValue)
-                                    .findFirst()
-                                    .orElse(JoinType.INNER));
-                    // Attempt to find specific criteria configuration for 
this association path
-                    var column = Optional.ofNullable(aliasMap.get(joinColumn))
-                            .map(detachedAssociationCriteria ->
-                                    
Objects.requireNonNullElse(detachedAssociationCriteria.getAlias(), joinColumn))
-                            .orElse(joinColumn);
-                    table.alias(column);
-                    return new AbstractMap.SimpleEntry<>(column, table);
-                })
-                .collect(Collectors.toMap(
-                        Map.Entry::getKey,
-                        Map.Entry::getValue,
-                        (existing, replacement) -> existing,
-                        java.util.LinkedHashMap::new));
-        fromsByName.put("root", root);
+        var definedAliases = detachedAssociationCriteriaList.stream()
+                .map(DetachedAssociationCriteria::getAlias)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+
+        var projectedPaths = 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()
+                .filter(entry -> entry.getValue().equals(FetchType.EAGER))
+                .map(Map.Entry::getKey)
+                .collect(Collectors.toSet());
+
+        java.util.Set<String> allPaths = new java.util.HashSet<>();
+        allPaths.addAll(aliasMap.keySet());
+        allPaths.addAll(projectedPaths.stream()
+                .filter(p -> !definedAliases.contains(p))
+                .toList());
+        allPaths.addAll(eagerPaths);
+
+        // 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.length() > 0) {
+                    current.append(".");
+                }
+                current.append(segment);
+                expandedPaths.add(current.toString());
+            }
+        }
+
+        Map<String, From<?, ?>> fromsByPath = new HashMap<>();
+        fromsByPath.put("root", 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";
+            String leaf = path.contains(".") ? 
path.substring(path.lastIndexOf('.') + 1) : path;
+
+            From<?, ?> base = fromsByPath.get(parentPath);
+
+            JoinType joinType = JoinType.INNER;
+            if (detachedCriteria.getJoinTypes().containsKey(path)) {
+                joinType = detachedCriteria.getJoinTypes().get(path);
+            } else if (projectedPaths.contains(path) || 
eagerPaths.contains(path)) {
+                joinType = JoinType.LEFT;
+            }
+
+            var table = base.join(leaf, joinType);
+
+            // 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) {
+                fromsByPath.put(dac.getAlias(), table);
+            }
+
+            table.alias(path);
+            fromsByPath.put(path, table);
+        }
+
         String rootAlias = detachedCriteria.getAlias();
         if (rootAlias != null && !rootAlias.isEmpty()) {
-            fromsByName.put(rootAlias, root);
+            fromsByPath.put(rootAlias, root);
         }
-        return fromsByName;
+        return fromsByPath;
     }
 
     private Map<String, From<?, ?>> createDetachedFroms(
@@ -147,17 +201,34 @@ public class JpaFromProvider implements Cloneable {
         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) {
-            if (fromMap.containsKey(propertyName)) {
-                return fromMap.get(propertyName);
-            } else {
-                return fromMap.get("root").get(propertyName);
+            return fromMap.get("root").get(propertyName);
+        }
+
+        // Try to find the longest matching prefix in fromMap
+        for (int i = parsed.length - 1; 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]);
+                }
+                return path;
             }
         }
-        String tableName = parsed[0];
-        String columnName = parsed[1];
-        return fromMap.get(tableName).get(columnName);
+
+        // Fallback to root
+        Path<?> path = fromMap.get("root");
+        for (String segment : parsed) {
+            path = path.get(segment);
+        }
+        return path;
     }
 
     public Object clone() {
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 ad30c4013d..ac635ab9b8 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
@@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory;
 
 import org.springframework.core.convert.ConversionService;
 
+import grails.gorm.DetachedCriteria;
 import org.grails.datastore.gorm.GormEntity;
 import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria;
 import org.grails.datastore.mapping.core.exceptions.ConfigurationException;
@@ -293,7 +294,7 @@ public class PredicateGenerator {
         Subquery subquery = criteriaQuery.subquery(Number.class);
         PersistentEntity subEntity = c.getValue().getPersistentEntity();
         Root from = subquery.from(subEntity.getJavaClass());
-        JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone();
+        JpaFromProvider newMap = new JpaFromProvider(fromsByProvider, 
(DetachedCriteria) c.getValue(), List.of(), criteriaQuery, from);
         newMap.put("root", from);
         // FIX: Pass subEntity to subquery recursion
         Predicate[] predicates = getPredicates(cb, criteriaQuery, from, 
c.getValue().getCriteria(), newMap, subEntity);
@@ -373,7 +374,7 @@ public class PredicateGenerator {
             Query.Exists c) {
         Subquery subquery = criteriaQuery.subquery(Integer.class);
         Root subRoot = subquery.from(entity.getJavaClass());
-        JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone();
+        JpaFromProvider newMap = new JpaFromProvider(fromsByProvider, 
(DetachedCriteria) c.getSubquery(), List.of(), criteriaQuery, subRoot);
         newMap.put("root", subRoot);
         // Pass 'entity' (which is child) to recursion
         var predicates = getPredicates(cb, criteriaQuery, subRoot, 
c.getSubquery().getCriteria(), newMap, entity);
@@ -399,7 +400,7 @@ public class PredicateGenerator {
         var subquery = 
criteriaQuery.subquery(getJavaTypeOfInClause((SqmInListPredicate) in));
         PersistentEntity subEntity = queryableCriteria.getPersistentEntity();
         var from = subquery.from(subEntity.getJavaClass());
-        var clonedProviderByName = (JpaFromProvider) fromsByProvider.clone();
+        var clonedProviderByName = new JpaFromProvider(fromsByProvider, 
(DetachedCriteria) queryableCriteria, List.of(), criteriaQuery, from);
         clonedProviderByName.put("root", from);
         // FIX: Pass subEntity
         var predicates = getPredicates(cb, criteriaQuery, from, 
queryableCriteria.getCriteria(), clonedProviderByName, subEntity);
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
index 95e239aefd..4b9323e4bf 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/HibernateGormDatastoreSpec.groovy
@@ -62,6 +62,7 @@ class HibernateGormDatastoreSpec extends 
GrailsDataTckSpec<GrailsDataHibernate7T
                 'hibernate.cache.queries'      : 'true',
                 'hibernate.hbm2ddl.auto'       : 'create',
                 'hibernate.jpa.compliance.cascade': 'true',
+                'hibernate.proxy_factory_class' : 
'org.grails.orm.hibernate.proxy.ByteBuddyGroovyProxyFactory'
         ]
     }
 
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/HibernateQuerySpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/HibernateQuerySpec.groovy
index e76716b532..b59e0bebe0 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/HibernateQuerySpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/grails/gorm/specs/hibernatequery/HibernateQuerySpec.groovy
@@ -29,6 +29,8 @@ import jakarta.persistence.criteria.Subquery
 import org.apache.grails.data.testing.tck.domains.*
 import org.grails.datastore.mapping.engine.event.PersistEvent
 import org.grails.datastore.mapping.query.Query
+import org.grails.datastore.mapping.query.event.PostQueryEvent
+import org.grails.datastore.mapping.query.event.PreQueryEvent
 import org.grails.orm.hibernate.HibernateSession
 import org.grails.orm.hibernate.HibernateDatastore
 import org.grails.orm.hibernate.query.HibernateQuery
@@ -1090,6 +1092,29 @@ class HibernateQuerySpec extends 
HibernateGormDatastoreSpec {
         associationQuery != null
         associationQuery.getEntity() != null
     }
+
+    def "test query publishes PreQueryEvent and PostQueryEvent"() {
+        given:
+        int preEvents = 0
+        int postEvents = 0
+        
manager.hibernateDatastore.getApplicationEventPublisher().addApplicationListener(new
 
org.springframework.context.ApplicationListener<org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent>()
 {
+            @Override
+            void 
onApplicationEvent(org.grails.datastore.mapping.engine.event.AbstractPersistenceEvent
 event) {
+                if (event instanceof PreQueryEvent) {
+                    preEvents++
+                } else if (event instanceof PostQueryEvent) {
+                    postEvents++
+                }
+            }
+        })
+
+        when:
+        hibernateQuery.eq("firstName", "Bob").list()
+
+        then:
+        preEvents > 0
+        postEvents > 0
+    }
 }
 
 
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 f0efc9d678..8213d0052a 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
@@ -209,4 +209,23 @@ class JpaCriteriaQueryCreatorSpec extends 
HibernateGormDatastoreSpec {
         query != null
         query.isDistinct()
     }
+
+    def "test createQuery with association projection triggers auto-join"() {
+        given:
+        HibernateCriteriaBuilder criteriaBuilder = 
sessionFactory.getCriteriaBuilder()
+        var entity = 
manager.hibernateDatastore.getMappingContext().getPersistentEntity(org.apache.grails.data.testing.tck.domains.Pet.typeName)
+        var detachedCriteria = new 
DetachedCriteria(org.apache.grails.data.testing.tck.domains.Pet)
+        
+        var projections = new Query.ProjectionList()
+        projections.property("owner.firstName")
+
+        var creator = new JpaCriteriaQueryCreator(projections, 
criteriaBuilder, entity, detachedCriteria, new DefaultConversionService())
+
+        when:
+        JpaCriteriaQuery<?> query = creator.createQuery()
+
+        then:
+        noExceptionThrown()
+        query != null
+    }
 }
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
index 25bea74a18..29fc406aa0 100644
--- 
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
@@ -179,4 +179,56 @@ class JpaFromProviderSpec extends Specification {
         then:
         result == idPath
     }
+
+    def "getFromsByName creates hierarchical joins for projection paths"() {
+        given:
+        def dc = new DetachedCriteria(String)
+        def cq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery)
+        From root = Mock(From) {
+            getJavaType() >> String
+        }
+        From clubJoin = Mock(From) {
+            getJavaType() >> String
+        }
+        From teamJoin = Mock(From) {
+            getJavaType() >> String
+        }
+
+        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, cq, 
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() >> String }
+        JpaFromProvider parent = bare(String, outerRoot)
+
+        and: "subquery detached criteria"
+        def subDc = new DetachedCriteria(Integer)
+        def subCq = Mock(org.hibernate.query.criteria.JpaCriteriaQuery)
+        From subRoot = Mock(From) { getJavaType() >> Integer }
+
+        when:
+        JpaFromProvider subProvider = new JpaFromProvider(parent, subDc, [], 
subCq, 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
+    }
 }
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 dd34784d36..7df48fd57c 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
@@ -154,4 +154,17 @@ class PredicateGeneratorSpec extends 
HibernateGormDatastoreSpec {
         then:
         predicates.length == 1
     }
+
+    def "test getPredicates with subquery isolated provider"() {
+        given: "a subquery with association reference"
+        def subCriteria = new DetachedCriteria(Pet).eq("face.name", "Funny")
+        List criteria = [new Query.In("id", subCriteria)]
+
+        when:
+        def predicates = predicateGenerator.getPredicates(cb, query, root, 
criteria, fromProvider, personEntity)
+
+        then: "no exception thrown during subquery join creation"
+        noExceptionThrown()
+        predicates.length == 1
+    }
 }
\ No newline at end of file
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy
index dc83454939..2beea69083 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/apache/grails/data/hibernate7/core/GrailsDataHibernate7TckManager.groovy
@@ -33,6 +33,7 @@ import 
org.grails.orm.hibernate.cfg.HibernateMappingContextConfiguration
 import org.h2.Driver
 import org.hibernate.SessionFactory
 import org.hibernate.dialect.H2Dialect
+import org.hibernate.dialect.PostgreSQLDialect
 import org.springframework.beans.factory.DisposableBean
 import org.springframework.context.ApplicationContext
 import org.springframework.orm.hibernate5.SessionFactoryUtils
@@ -40,9 +41,28 @@ import org.springframework.orm.hibernate5.SessionHolder
 import org.springframework.transaction.TransactionStatus
 import org.springframework.transaction.support.DefaultTransactionDefinition
 import 
org.springframework.transaction.support.TransactionSynchronizationManager
+import org.testcontainers.containers.PostgreSQLContainer
 import spock.lang.Specification
 
 class GrailsDataHibernate7TckManager extends GrailsDataTckManager {
+    static PostgreSQLContainer postgres
+
+    private void ensurePostgresStarted() {
+        if (postgres == null && isDockerAvailable()) {
+            postgres = new PostgreSQLContainer("postgres:16")
+            postgres.start()
+        }
+    }
+
+    static boolean isDockerAvailable() {
+        def candidates = [
+                System.getProperty('user.home') + '/.docker/run/docker.sock',
+                '/var/run/docker.sock',
+                System.getenv('DOCKER_HOST') ?: ''
+        ]
+        candidates.any { it && (new File(it).exists() || 
it.startsWith('tcp:')) }
+    }
+
     GrailsApplication grailsApplication
     HibernateDatastore hibernateDatastore
     org.hibernate.Session hibernateSession
@@ -55,19 +75,38 @@ class GrailsDataHibernate7TckManager extends 
GrailsDataTckManager {
     HibernateDatastore multiTenantMultiDataSourceDatastore
     ConfigObject grailsConfig = new ConfigObject()
     boolean isTransactional = true
+    Class<? extends Specification> currentSpec
 
     @Override
     void setup(Class<? extends Specification> spec) {
+        this.currentSpec = spec
         cleanRegistry()
         super.setup(spec)
     }
 
+    private boolean shouldUsePostgres() {
+        if (currentSpec?.simpleName == 'WhereQueryConnectionRoutingSpec') {
+            ensurePostgresStarted()
+            boolean usePostgres = postgres != null
+            System.out.println("TCK Manager: 
currentSpec=${currentSpec?.simpleName}, usePostgres=${usePostgres}")
+            return usePostgres
+        }
+        return false
+    }
+
     @Override
     Session createSession() {
         System.setProperty('hibernate7.gorm.suite', "true")
         grailsApplication = new DefaultGrailsApplication(domainClasses as 
Class[], new GroovyClassLoader(GrailsDataHibernate7TckManager.getClassLoader()))
         grailsConfig.dataSource.dbCreate = "create-drop"
-        grailsConfig.hibernate.proxy_factory_class = 
"yakworks.hibernate.proxy.ByteBuddyGroovyProxyFactory"
+        grailsConfig.hibernate.proxy_factory_class = 
"org.grails.orm.hibernate.proxy.ByteBuddyGroovyProxyFactory"
+        if (shouldUsePostgres()) {
+            grailsConfig.dataSource.url = postgres.getJdbcUrl()
+            grailsConfig.dataSource.username = postgres.getUsername()
+            grailsConfig.dataSource.password = postgres.getPassword()
+            grailsConfig.dataSource.driverClassName = 
postgres.getDriverClassName()
+            grailsConfig.hibernate.dialect = PostgreSQLDialect.name
+        }
         if (grailsConfig) {
             grailsApplication.config.putAll(grailsConfig)
         }
@@ -127,15 +166,28 @@ class GrailsDataHibernate7TckManager extends 
GrailsDataTckManager {
 
     @Override
     void setupMultiDataSource(Class... domainClasses) {
+        if (currentSpec == null) {
+            currentSpec = domainClasses.length > 0 ? domainClasses[0] : null 
// Fallback, not great
+        }
+        boolean usePostgres = shouldUsePostgres()
         Map config = [
-                'dataSource.url'           : 
"jdbc:h2:mem:tckDefaultDB;LOCK_TIMEOUT=10000",
+                'dataSource.url'           : usePostgres ? 
postgres.getJdbcUrl() : "jdbc:h2:mem:tckDefaultDB;LOCK_TIMEOUT=10000",
+                'dataSource.username'      : usePostgres ? 
postgres.getUsername() : "sa",
+                'dataSource.password'      : usePostgres ? 
postgres.getPassword() : "",
+                'dataSource.driverClassName': usePostgres ? 
postgres.getDriverClassName() : Driver.name,
                 'dataSource.dbCreate'      : 'create-drop',
-                'dataSource.dialect'       : H2Dialect.name,
+                'dataSource.dialect'       : usePostgres ? 
PostgreSQLDialect.name : H2Dialect.name,
                 'dataSource.formatSql'     : 'true',
                 'hibernate.flush.mode'     : 'COMMIT',
                 'hibernate.cache.queries'  : 'true',
                 'hibernate.hbm2ddl.auto'   : 'create-drop',
-                'dataSources.secondary'    : [url: 
"jdbc:h2:mem:tckSecondaryDB;LOCK_TIMEOUT=10000"],
+                'hibernate.proxy_factory_class' : 
'org.grails.orm.hibernate.proxy.ByteBuddyGroovyProxyFactory',
+                'dataSources.secondary'    : [
+                        url: usePostgres ? postgres.getJdbcUrl() : 
"jdbc:h2:mem:tckSecondaryDB;LOCK_TIMEOUT=10000",
+                        username: usePostgres ? postgres.getUsername() : "sa",
+                        password: usePostgres ? postgres.getPassword() : "",
+                        driverClassName: usePostgres ? 
postgres.getDriverClassName() : Driver.name
+                ],
         ]
         multiDataSourceDatastore = new HibernateDatastore(
                 DatastoreUtils.createPropertyResolver(config), domainClasses
@@ -147,8 +199,10 @@ class GrailsDataHibernate7TckManager extends 
GrailsDataTckManager {
         if (multiDataSourceDatastore != null) {
             multiDataSourceDatastore.destroy()
             multiDataSourceDatastore = null
-            shutdownInMemDb('jdbc:h2:mem:tckDefaultDB')
-            shutdownInMemDb('jdbc:h2:mem:tckSecondaryDB')
+            if (!shouldUsePostgres()) {
+                shutdownInMemDb('jdbc:h2:mem:tckDefaultDB')
+                shutdownInMemDb('jdbc:h2:mem:tckSecondaryDB')
+            }
         }
     }
 
@@ -166,17 +220,27 @@ class GrailsDataHibernate7TckManager extends 
GrailsDataTckManager {
 
     @Override
     void setupMultiTenantMultiDataSource(Class... domainClasses) {
+        boolean usePostgres = shouldUsePostgres()
         Map config = [
                 'grails.gorm.multiTenancy.mode'            : 
MultiTenancySettings.MultiTenancyMode.DISCRIMINATOR,
                 'grails.gorm.multiTenancy.tenantResolverClass': 
SystemPropertyTenantResolver,
-                'dataSource.url'                            : 
"jdbc:h2:mem:tckMtDefaultDB;LOCK_TIMEOUT=10000",
+                'dataSource.url'                            : usePostgres ? 
postgres.getJdbcUrl() : "jdbc:h2:mem:tckMtDefaultDB;LOCK_TIMEOUT=10000",
+                'dataSource.username'                       : usePostgres ? 
postgres.getUsername() : "sa",
+                'dataSource.password'                       : usePostgres ? 
postgres.getPassword() : "",
+                'dataSource.driverClassName'                : usePostgres ? 
postgres.getDriverClassName() : Driver.name,
                 'dataSource.dbCreate'                       : 'create-drop',
-                'dataSource.dialect'                        : H2Dialect.name,
+                'dataSource.dialect'                        : usePostgres ? 
PostgreSQLDialect.name : H2Dialect.name,
                 'dataSource.formatSql'                      : 'true',
                 'hibernate.flush.mode'                      : 'COMMIT',
                 'hibernate.cache.queries'                   : 'true',
                 'hibernate.hbm2ddl.auto'                    : 'create-drop',
-                'dataSources.secondary'                     : [url: 
"jdbc:h2:mem:tckMtSecondaryDB;LOCK_TIMEOUT=10000"],
+                'hibernate.proxy_factory_class'             : 
'org.grails.orm.hibernate.proxy.ByteBuddyGroovyProxyFactory',
+                'dataSources.secondary'                     : [
+                        url: usePostgres ? postgres.getJdbcUrl() : 
"jdbc:h2:mem:tckMtSecondaryDB;LOCK_TIMEOUT=10000",
+                        username: usePostgres ? postgres.getUsername() : "sa",
+                        password: usePostgres ? postgres.getPassword() : "",
+                        driverClassName: usePostgres ? 
postgres.getDriverClassName() : Driver.name
+                ],
         ]
         multiTenantMultiDataSourceDatastore = new HibernateDatastore(
                 DatastoreUtils.createPropertyResolver(config), domainClasses
@@ -188,8 +252,10 @@ class GrailsDataHibernate7TckManager extends 
GrailsDataTckManager {
         if (multiTenantMultiDataSourceDatastore != null) {
             multiTenantMultiDataSourceDatastore.destroy()
             multiTenantMultiDataSourceDatastore = null
-            shutdownInMemDb('jdbc:h2:mem:tckMtDefaultDB')
-            shutdownInMemDb('jdbc:h2:mem:tckMtSecondaryDB')
+            if (!shouldUsePostgres()) {
+                shutdownInMemDb('jdbc:h2:mem:tckMtDefaultDB')
+                shutdownInMemDb('jdbc:h2:mem:tckMtSecondaryDB')
+            }
         }
     }
 
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
index 7f76ffd400..4c3baaa0d3 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/HibernateGormStaticApiSpec.groovy
@@ -188,40 +188,56 @@ class HibernateGormStaticApiSpec extends 
HibernateGormDatastoreSpec {
         instances.size() == 2
     }
 
-    void "Test findAll with plain String throws 
UnsupportedOperationException"() {
+    void "Test findAll with plain String"() {
+        given:
+        new HibernateGormStaticApiEntity(name: "test1").save(failOnError: true)
+        new HibernateGormStaticApiEntity(name: "test2").save(flush: true, 
failOnError: true)
+
         when:
-        String hql = "from HibernateGormStaticApiEntity"
-        HibernateGormStaticApiEntity.findAll(hql)
+        String hql = "from HibernateGormStaticApiEntity where name = ?1"
+        def results = HibernateGormStaticApiEntity.findAll(hql, ['test1'])
 
         then:
-        thrown(UnsupportedOperationException)
+        results.size() == 1
+        results[0].name == 'test1'
     }
 
-    void "Test find with plain String throws UnsupportedOperationException"() {
+    void "Test find with plain String"() {
+        given:
+        new HibernateGormStaticApiEntity(name: "test1").save(failOnError: true)
+        new HibernateGormStaticApiEntity(name: "test2").save(flush: true, 
failOnError: true)
+
         when:
-        String hql = "from HibernateGormStaticApiEntity"
-        HibernateGormStaticApiEntity.find(hql)
+        String hql = "from HibernateGormStaticApiEntity where name = :name"
+        def result = HibernateGormStaticApiEntity.find(hql, [name: 'test2'])
 
         then:
-        thrown(UnsupportedOperationException)
+        result.name == 'test2'
     }
 
-    void "Test executeQuery with plain String throws 
UnsupportedOperationException"() {
+    void "Test executeQuery with plain String"() {
+        given:
+        new HibernateGormStaticApiEntity(name: "test1").save(failOnError: true)
+        new HibernateGormStaticApiEntity(name: "test2").save(flush: true, 
failOnError: true)
+
         when:
-        String hql = "from HibernateGormStaticApiEntity"
-        HibernateGormStaticApiEntity.executeQuery(hql)
+        String hql = "select name from HibernateGormStaticApiEntity"
+        def results = HibernateGormStaticApiEntity.executeQuery(hql)
 
         then:
-        thrown(UnsupportedOperationException)
+        results.size() == 2
     }
 
-    void "Test executeUpdate with plain String throws 
UnsupportedOperationException"() {
+    void "Test executeUpdate with plain String"() {
+        given:
+        new HibernateGormStaticApiEntity(name: "test").save(flush: true, 
failOnError: true)
+
         when:
-        String hql = "update HibernateGormStaticApiEntity set name = 'x'"
-        HibernateGormStaticApiEntity.executeUpdate(hql)
+        String hql = "update HibernateGormStaticApiEntity set name = 'updated'"
+        int updated = HibernateGormStaticApiEntity.executeUpdate(hql)
 
         then:
-        thrown(UnsupportedOperationException)
+        updated == 1
     }
 
 
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsNativeGeneratorSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsNativeGeneratorSpec.groovy
index 5ab198fa27..61a945409e 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsNativeGeneratorSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/cfg/domainbinding/GrailsNativeGeneratorSpec.groovy
@@ -73,4 +73,29 @@ class GrailsNativeGeneratorSpec extends 
HibernateGormDatastoreSpec {
         then:
         result == null
     }
+
+    def "should throw HibernateException if SequenceStyleGenerator is not 
initialized"() {
+        given:
+        def context = Mock(GeneratorCreationContext)
+        def database = Mock(org.hibernate.boot.model.relational.Database)
+        context.getDatabase() >> database
+        database.getDialect() >> 
getGrailsDomainBinder().getJdbcEnvironment().getDialect()
+        
+        def session = Mock(SharedSessionContractImplementor)
+        def entity = new Object()
+        def eventType = EventType.INSERT
+        
+        @Subject
+        def generator = Spy(GrailsNativeGenerator, constructorArgs: [context])
+        def ssg = Mock(org.hibernate.id.enhanced.SequenceStyleGenerator)
+        generator.getDelegate() >> ssg
+        ssg.getDatabaseStructure() >> null
+
+        when:
+        generator.generate(session, entity, null, eventType)
+
+        then:
+        def e = thrown(org.hibernate.HibernateException)
+        e.message.contains("was not properly initialized")
+    }
 }
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceDatasourceInheritanceSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceDatasourceInheritanceSpec.groovy
index 6694617468..bf5cccd954 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceDatasourceInheritanceSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceDatasourceInheritanceSpec.groovy
@@ -66,7 +66,7 @@ class DataServiceDatasourceInheritanceSpec extends 
Specification {
 
     void setup() {
         Inventory.warehouse.withNewTransaction {
-            Inventory.warehouse.executeUpdate('delete from Inventory')
+            Inventory.warehouse.executeUpdate('delete from Inventory', [:])
         }
     }
 
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy
index f4f01de431..26f2233352 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/DataServiceMultiTenantMultiDataSourceSpec.groovy
@@ -94,13 +94,15 @@ class DataServiceMultiTenantMultiDataSourceSpec extends 
Specification {
     void "schema is created on analytics datasource"() {
         expect: 'The analytics datasource connects to the analyticsDB H2 
database'
         Metric.analytics.withNewSession { Session s ->
-            assert s.connection().metaData.getURL() == 
'jdbc:h2:mem:analyticsDB'
+            String url = s.doReturningWork { it.metaData.getURL() }
+            assert url == 'jdbc:h2:mem:analyticsDB'
             return true
         }
 
         and: 'The default datasource connects to a different database'
         datastore.withNewSession { Session s ->
-            assert s.connection().metaData.getURL() == 'jdbc:h2:mem:grailsDB'
+            String url = s.doReturningWork { it.metaData.getURL() }
+            assert url == 'jdbc:h2:mem:grailsDB'
             return true
         }
     }
@@ -271,7 +273,7 @@ abstract class MetricService implements MetricDataService {
      * executeUpdate routes to the analytics datasource.
      */
     void deleteAll() {
-        Metric.executeUpdate('delete from Metric where 1=1')
+        Metric.executeUpdate('delete from Metric where 1=1', [:])
     }
 
     /**
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy
index d4a77aed76..dc15dfb6ce 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/connections/WhereQueryMultiDataSourceSpec.groovy
@@ -156,6 +156,7 @@ class Item implements GormEntity<Item> {
 
     static mapping = {
         datasource 'ALL'
+        amount precision: 10
     }
 
     static constraints = {
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptorSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptorSpec.groovy
new file mode 100644
index 0000000000..7b0c469986
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyInterceptorSpec.groovy
@@ -0,0 +1,76 @@
+/*
+ *  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.proxy
+
+import org.hibernate.engine.spi.SharedSessionContractImplementor
+import org.hibernate.proxy.ProxyConfiguration
+import spock.lang.Specification
+import java.lang.reflect.Method
+
+class ByteBuddyGroovyInterceptorSpec extends Specification {
+
+    def "intercept ignores Groovy internal methods and does not initialize"() {
+        given:
+        def interceptor = new ByteBuddyGroovyInterceptor(
+                "TestEntity",
+                Object,
+                [] as Class[],
+                1L,
+                null,
+                null,
+                null,
+                Mock(SharedSessionContractImplementor),
+                false
+        )
+        def proxy = Mock(ProxyConfiguration)
+        def getMetaClassMethod = Object.getMethod("getClass") // Placeholder 
for illustration
+
+        when: "getMetaClass is called (simulated)"
+        // In a real scenario, we'd use the actual Groovy method object
+        def result = interceptor.intercept(proxy, 
GroovyObject.getMethod("getMetaClass"), [] as Object[])
+
+        then: "it should not call super.intercept (which would initialize)"
+        // We can't easily mock super, but we know it would throw NPE if 
session/etc are mocks
+        // and it tries to initialize.
+        noExceptionThrown()
+    }
+
+    def "toString returns entity name and id without initialization"() {
+        given:
+        def interceptor = new ByteBuddyGroovyInterceptor(
+                "TestEntity",
+                Object,
+                [] as Class[],
+                1L,
+                null,
+                null,
+                null,
+                Mock(SharedSessionContractImplementor),
+                false
+        )
+        def proxy = Mock(ProxyConfiguration)
+        def toStringMethod = Object.getMethod("toString")
+
+        when:
+        def result = interceptor.intercept(proxy, toStringMethod, [] as 
Object[])
+
+        then:
+        result == "TestEntity:1"
+    }
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyProxyFactorySpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyProxyFactorySpec.groovy
new file mode 100644
index 0000000000..6a362d2b4d
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/proxy/ByteBuddyGroovyProxyFactorySpec.groovy
@@ -0,0 +1,30 @@
+/*
+ *  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.proxy
+
+import org.hibernate.proxy.pojo.bytebuddy.ByteBuddyProxyHelper
+import spock.lang.Specification
+
+class ByteBuddyGroovyProxyFactorySpec extends Specification {
+
+    def "factory can be instantiated"() {
+        expect:
+        new ByteBuddyGroovyProxyFactory(Mock(ByteBuddyProxyHelper)) != null
+    }
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
new file mode 100644
index 0000000000..60c06df7bb
--- /dev/null
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/DetachedAssociationFunctionSpec.groovy
@@ -0,0 +1,64 @@
+/*
+ *  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 grails.gorm.DetachedCriteria
+import org.grails.datastore.gorm.query.criteria.DetachedAssociationCriteria
+import org.grails.datastore.mapping.query.Query
+import spock.lang.Specification
+
+class DetachedAssociationFunctionSpec extends Specification {
+
+    DetachedAssociationFunction function = new DetachedAssociationFunction()
+
+    def "apply returns list with criteria if it is 
DetachedAssociationCriteria"() {
+        given:
+        def criteria = new DetachedAssociationCriteria(Object, "test")
+
+        when:
+        def result = function.apply(criteria)
+
+        then:
+        result.size() == 1
+        result[0] == criteria
+    }
+
+    def "apply returns empty list if it is not DetachedAssociationCriteria"() {
+        given:
+        def criteria = new Query.Equals("prop", "value")
+
+        when:
+        def result = function.apply(criteria)
+
+        then:
+        result.isEmpty()
+    }
+
+    def "apply returns empty list for subquery criteria (isolation fix)"() {
+        given: "a subquery criterion which contains association criteria 
internally"
+        def subquery = new DetachedCriteria(Object).eq("assoc.prop", "val")
+        def criterion = new Query.In("id", subquery)
+
+        when:
+        def result = function.apply(criterion)
+
+        then: "it should NOT extract the internal association criteria 
(isolation)"
+        result.isEmpty()
+    }
+}
diff --git 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy
 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy
index 608edcdaa9..c8814e8a77 100644
--- 
a/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy
+++ 
b/grails-data-hibernate7/core/src/test/groovy/org/grails/orm/hibernate/query/HibernateHqlQuerySpec.groovy
@@ -274,6 +274,18 @@ class HibernateHqlQuerySpec extends 
HibernateGormDatastoreSpec {
         then:
         thrown(UnsupportedOperationException)
     }
+
+    void "populateQueryWithNamedArguments filters GORM internal settings"() {
+        given:
+        def query = buildHqlQuery("from HibernateHqlQuerySpecBook b where 
b.title = :t", [t: "The Hobbit"])
+
+        when: "passing internal GORM settings as named parameters"
+        query.populateQueryWithNamedArguments([t: "The Hobbit", flushMode: 
FlushMode.COMMIT, cache: true])
+
+        then: "no exception is thrown because they are filtered out"
+        noExceptionThrown()
+        query.list().size() == 1
+    }
 }
 
 @Entity

Reply via email to