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