This is an automated email from the ASF dual-hosted git repository. borinquenkid pushed a commit to branch 8.0.x-hibernate7 in repository https://gitbox.apache.org/repos/asf/grails-core.git
commit 337f79fff9022f4b43a3253ed895e938119ee6e0 Author: Walter Duque de Estrada <[email protected]> AuthorDate: Fri Feb 20 09:52:44 2026 -0600 Refactor PredicateGenerator for readability and document clause hierarchy --- grails-data-hibernate7/core/CLAUSE_HIERARCHY.md | 62 +++ .../orm/hibernate/query/PredicateGenerator.java | 491 ++++++++++----------- 2 files changed, 292 insertions(+), 261 deletions(-) diff --git a/grails-data-hibernate7/core/CLAUSE_HIERARCHY.md b/grails-data-hibernate7/core/CLAUSE_HIERARCHY.md new file mode 100644 index 0000000000..26bc600dfc --- /dev/null +++ b/grails-data-hibernate7/core/CLAUSE_HIERARCHY.md @@ -0,0 +1,62 @@ +# Criteria and Projection Hierarchy in PredicateGenerator + +| Class used in `instanceof` | Class Hierarchy (from `Query.java` or GORM) | +| :--- | :--- | +| `Query.Junction` | `Query.Junction` -> `Query.Criterion` | +| `Query.Disjunction` | `Query.Disjunction` -> `Query.Junction` -> `Query.Criterion` | +| `Query.Conjunction` | `Query.Conjunction` -> `Query.Junction` -> `Query.Criterion` | +| `Query.Negation` | `Query.Negation` -> `Query.Junction` -> `Query.Criterion` | +| `Query.DistinctProjection` | `Query.DistinctProjection` -> `Query.Projection` | +| `DetachedAssociationCriteria` | `DetachedAssociationCriteria` -> `AbstractDetachedCriteria` -> `Criteria` | +| `Query.PropertyCriterion` | `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.Equals` | `Query.Equals` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.NotEquals` | `Query.NotEquals` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.IdEquals` | `Query.IdEquals` -> `Query.Criterion` | +| `Query.GreaterThan` | `Query.GreaterThan` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.GreaterThanEquals` | `Query.GreaterThanEquals` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.LessThan` | `Query.LessThan` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.LessThanEquals` | `Query.LessThanEquals` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.SizeEquals` | `Query.SizeEquals` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.SizeNotEquals` | `Query.SizeNotEquals` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.SizeGreaterThan` | `Query.SizeGreaterThan` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.SizeGreaterThanEquals` | `Query.SizeGreaterThanEquals` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.SizeLessThan` | `Query.SizeLessThan` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.SizeLessThanEquals` | `Query.SizeLessThanEquals` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.Between` | `Query.Between` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.ILike` | `Query.ILike` -> `Query.Like` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.RLike` | `Query.RLike` -> `Query.Like` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.Like` | `Query.Like` -> `Query.PropertyCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.In` | `Query.In` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.NotIn` | `Query.NotIn` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.SubqueryCriterion` | `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.GreaterThanEqualsAll` | `Query.GreaterThanEqualsAll` -> `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.GreaterThanAll` | `Query.GreaterThanAll` -> `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.LessThanEqualsAll` | `Query.LessThanEqualsAll` -> `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.LessThanAll` | `Query.LessThanAll` -> `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.EqualsAll` | `Query.EqualsAll` -> `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.GreaterThanEqualsSome` | `Query.GreaterThanEqualsSome` -> `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.GreaterThanSome` | `Query.GreaterThanSome` -> `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.LessThanEqualsSome` | `Query.LessThanEqualsSome` -> `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.LessThanSome` | `Query.LessThanSome` -> `Query.SubqueryCriterion` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.IsNull` | `Query.IsNull` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.IsNotNull` | `Query.IsNotNull` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.IsEmpty` | `Query.IsEmpty` -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.IsNotEmpty` | `Query.IsNotEmpty` -> `Query.PropertyNameCriterion" -> `Query.Criterion` | +| `Query.EqualsProperty` | `Query.EqualsProperty` -> `Query.PropertyComparisonCriterion" -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.NotEqualsProperty` | `Query.NotEqualsProperty` -> `Query.PropertyComparisonCriterion" -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.LessThanEqualsProperty` | `Query.LessThanEqualsProperty` -> `Query.PropertyComparisonCriterion" -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.LessThanProperty` | `Query.LessThanProperty` -> `Query.PropertyComparisonCriterion" -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.GreaterThanEqualsProperty` | `Query.GreaterThanEqualsProperty" -> `Query.PropertyComparisonCriterion" -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.GreaterThanProperty` | `Query.GreaterThanProperty" -> `Query.PropertyComparisonCriterion" -> `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.Exists` | `Query.Exists` -> `Query.Criterion` | +| `Query.NotExists` | `Query.NotExists` -> `Query.Criterion` | +| `Query.PropertyNameCriterion` | `Query.PropertyNameCriterion` -> `Query.Criterion` | +| `Query.PropertyProjection` | `Query.PropertyProjection` -> `Query.Projection` | +| `Query.DistinctPropertyProjection` | `Query.DistinctPropertyProjection` -> `Query.PropertyProjection` -> `Query.Projection` | +| `Query.IdProjection` | `Query.IdProjection` -> `Query.Projection` | +| `Query.CountProjection` | `Query.CountProjection` -> `Query.Projection` | +| `Query.CountDistinctProjection` | `Query.CountDistinctProjection` -> `Query.PropertyProjection` -> `Query.Projection` | +| `Query.MaxProjection` | `Query.MaxProjection` -> `Query.PropertyProjection` -> `Query.Projection` | +| `Query.MinProjection` | `Query.MinProjection` -> `Query.PropertyProjection` -> `Query.Projection` | +| `Query.AvgProjection` | `Query.AvgProjection` -> `Query.PropertyProjection` -> `Query.Projection` | +| `Query.SumProjection` | `Query.SumProjection` -> `Query.PropertyProjection` -> `Query.Projection` | 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 40826b4f27..aafd7bccf9 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 @@ -19,7 +19,6 @@ import org.grails.datastore.mapping.model.types.Association; import org.grails.datastore.mapping.query.Query; import org.grails.datastore.mapping.query.api.QueryableCriteria; import org.hibernate.query.criteria.HibernateCriteriaBuilder; -import org.hibernate.query.criteria.JpaFunction; import org.hibernate.query.criteria.JpaInPredicate; import org.hibernate.query.criteria.JpaPredicate; import org.hibernate.query.sqm.tree.domain.SqmPath; @@ -27,280 +26,264 @@ import org.hibernate.query.sqm.tree.predicate.SqmInListPredicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.time.Instant; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; - @Slf4j public class PredicateGenerator { private static final Logger log = LoggerFactory.getLogger(PredicateGenerator.class); public Predicate[] getPredicates(HibernateCriteriaBuilder cb, - CriteriaQuery criteriaQuery, - From root_, - List criteriaList, JpaFromProvider fromsByProvider, PersistentEntity entity) { + CriteriaQuery criteriaQuery, + From root_, + List criteriaList, + JpaFromProvider fromsByProvider, + PersistentEntity entity) { + List<Predicate> list = ((List<Object>) criteriaList).stream() + .map(criterion -> handleCriterion(cb, criteriaQuery, root_, fromsByProvider, entity, criterion)) + .filter(Objects::nonNull) + .toList(); - List<Predicate> list = criteriaList.stream(). - map((Object criterion) -> { - if (criterion instanceof Query.Junction junction) { - var criterionList = junction.getCriteria(); - var predicates = (Predicate[])this.getPredicates(cb, criteriaQuery, root_, (List)criterionList, fromsByProvider, entity); - if (junction instanceof Query.Disjunction) { - return cb.or(predicates); - } else if (junction instanceof Query.Conjunction) { - return cb.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]); - } - } else if (criterion instanceof Query.DistinctProjection) { - // this returns always true - return cb.conjunction(); - } else if (criterion instanceof DetachedAssociationCriteria<?> c) { - Join child = root_.join(c.getAssociationPath(), JoinType.LEFT); - List<Query.Criterion> criterionList = c.getCriteria(); - JpaFromProvider childTablesByName = (JpaFromProvider) fromsByProvider.clone(); - childTablesByName.put("root", child); - return cb.and((Predicate[])this.getPredicates(cb, criteriaQuery, child, (List)criterionList, childTablesByName, entity)); - } else if (criterion instanceof Query.PropertyCriterion pc) { - var fullyQualifiedPath = fromsByProvider.getFullyQualifiedPath(pc.getProperty()); - - if (criterion instanceof Query.NotIn c) { - var queryableCriteria = getQueryableCriteriaFromInCriteria((Query.Criterion)criterion); - if (Objects.nonNull(queryableCriteria)) { - CriteriaBuilder.In value = getQueryableCriteriaValue(cb, criteriaQuery, fromsByProvider, entity, (Query.PropertyNameCriterion)criterion, queryableCriteria); - return cb.not(value); - } else if (Objects.nonNull(c.getSubquery()) - && !c.getSubquery().getProjections().isEmpty() - && c.getSubquery().getProjections().get(0) instanceof Query.PropertyProjection - ) { - Subquery subquery2 = criteriaQuery.subquery(Number.class); - Root from2 = subquery2.from(c.getValue().getPersistentEntity().getJavaClass()); - List subCriteria2 = c.getValue().getCriteria(); - Query.PropertyProjection projection = (Query.PropertyProjection) c.getSubquery().getProjections().get(0); - boolean distinct = projection instanceof Query.DistinctPropertyProjection; - JpaFromProvider newMap2 = (JpaFromProvider) fromsByProvider.clone(); - Predicate[] predicates2 = (Predicate[])this.getPredicates(cb, criteriaQuery, from2, (List)subCriteria2, newMap2, entity); - subquery2.select(from2.get(projection.getPropertyName())).distinct(distinct).where(cb.and(predicates2)); - return cb.not(cb.in(fullyQualifiedPath).value(subquery2)); - } else if ( Objects.nonNull(c.getSubquery()) - && !c.getSubquery().getProjections().isEmpty() - && c.getSubquery().getProjections().get(0) instanceof Query.IdProjection - ) { - Subquery subquery2 = criteriaQuery.subquery(Number.class); - Root from2 = subquery2.from(c.getValue().getPersistentEntity().getJavaClass()); - List subCriteria2 = c.getValue().getCriteria(); - JpaFromProvider newMap2 = (JpaFromProvider) fromsByProvider.clone(); - Predicate[] predicates2 = (Predicate[])this.getPredicates(cb, criteriaQuery, from2, (List)subCriteria2, newMap2, entity); - subquery2.select(from2).where(cb.and(predicates2)); - return cb.not(cb.in(root_.get("id")).value(subquery2)); - } else { - return cb.not(cb.in(fullyQualifiedPath, c.getValue())); - } - } else if (criterion instanceof Query.SubqueryCriterion c) { - Subquery subquery = criteriaQuery.subquery(Number.class); - Root from = subquery.from(c.getValue().getPersistentEntity().getJavaClass()); - List subCriteria = c.getValue().getCriteria(); - JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone(); - newMap.put("root", from); - Predicate[] predicates = (Predicate[])this.getPredicates(cb, criteriaQuery, from, (List)subCriteria, newMap, entity); - if (c instanceof Query.GreaterThanEqualsAll sc) { - subquery.select(cb.max(from.get(c.getProperty()))).where(cb.and(predicates)); - return cb.greaterThanOrEqualTo(fromsByProvider.getFullyQualifiedPath(sc.getProperty()), subquery); - } else if (c instanceof Query.GreaterThanAll sc) { - subquery.select(cb.max(from.get(c.getProperty()))).where(cb.and(predicates)); - return cb.greaterThan(fromsByProvider.getFullyQualifiedPath(sc.getProperty()), subquery); - } else if (c instanceof Query.LessThanEqualsAll sc) { - subquery.select(cb.min(from.get(c.getProperty()))).where(cb.and(predicates)); - return cb.lessThanOrEqualTo(fromsByProvider.getFullyQualifiedPath(sc.getProperty()), subquery); - } else if (c instanceof Query.LessThanAll sc) { - subquery.select(cb.min(from.get(c.getProperty()))).where(cb.and(predicates)); - return cb.lessThan(fromsByProvider.getFullyQualifiedPath(sc.getProperty()), subquery); - } else if (c instanceof Query.EqualsAll sc) { - subquery.select(from.get(c.getProperty())).where(cb.and(predicates)); - return cb.equal(fromsByProvider.getFullyQualifiedPath(sc.getProperty()), subquery); - } else if (c instanceof Query.GreaterThanEqualsSome sc) { - subquery.select(cb.max(from.get(c.getProperty()))).where(cb.or(predicates)); - return cb.greaterThanOrEqualTo(fromsByProvider.getFullyQualifiedPath(sc.getProperty()), subquery); - } else if (c instanceof Query.GreaterThanSome sc) { - subquery.select(cb.max(from.get(c.getProperty()))).where(cb.or(predicates)); - return cb.greaterThan(fromsByProvider.getFullyQualifiedPath(sc.getProperty()), subquery); - } else if (c instanceof Query.LessThanEqualsSome sc) { - subquery.select(cb.min(from.get(c.getProperty()))).where(cb.or(predicates)); - return cb.lessThanOrEqualTo(fromsByProvider.getFullyQualifiedPath(sc.getProperty()), subquery); - } else if (c instanceof Query.LessThanSome sc) { - subquery.select(cb.min(from.get(c.getProperty()))).where(cb.or(predicates)); - return cb.lessThan(fromsByProvider.getFullyQualifiedPath(sc.getProperty()), subquery); - } - } else if (criterion instanceof Query.In c) { - var queryableCriteria = getQueryableCriteriaFromInCriteria((Query.Criterion)criterion); - if (Objects.nonNull(queryableCriteria)) { - CriteriaBuilder.In value = getQueryableCriteriaValue(cb, criteriaQuery, fromsByProvider, entity, (Query.PropertyNameCriterion)criterion, queryableCriteria); - return value; - } else if (!c.getValues().isEmpty()) { - boolean areGormEntities = c.getValues().stream().allMatch(GormEntity.class::isInstance); - if (areGormEntities) { - List<GormEntity> gormEntities = new ArrayList<>(c.getValues()); - Path id = criteriaQuery.from(gormEntities.get(0).getClass()).get("id"); - Collection newValues = gormEntities.stream().map(GormEntity::ident).toList(); - return cb.in(id, newValues); - } - return cb.in(fullyQualifiedPath, c.getValues()); - } - } else if (criterion instanceof Query.ILike c) { - return cb.ilike(fullyQualifiedPath, c.getValue().toString()); - } else if (criterion instanceof Query.RLike c) { - String pattern = c.getPattern(); - pattern = pattern.replaceAll("^/|/$", ""); - return cb.equal(cb.function( - GrailsRLikeFunctionContributor.RLIKE, // The name we registered - Boolean.class, // Expected return type - fullyQualifiedPath, // The property path - cb.literal(pattern)), true); - } else if (criterion instanceof Query.Like c) { - return cb.like(fullyQualifiedPath, c.getValue().toString()); - } else if (criterion instanceof Query.Equals c) { - return cb.equal(fullyQualifiedPath, c.getValue()); - } else if (criterion instanceof Query.NotEquals c) { - var notEqualToValue = cb.notEqual(fromsByProvider.getFullyQualifiedPath(c.getProperty()), c.getValue()); - var isNull = cb.isNull(fullyQualifiedPath); - return cb.or(notEqualToValue, isNull); - } else if (criterion instanceof Query.IdEquals c) { - return cb.equal(root_.get("id"), c.getValue()); - } else if (criterion instanceof Query.GreaterThan c) { - return cb.gt(fullyQualifiedPath, getNumericValue(c)); - } else if (criterion instanceof Query.GreaterThanEquals c) { - return cb.ge(fullyQualifiedPath, getNumericValue(c)); - } else if (criterion instanceof Query.LessThan c) { - return cb.lt(fullyQualifiedPath, getNumericValue(c)); - } else if (criterion instanceof Query.LessThanEquals c) { - return cb.le(fullyQualifiedPath, getNumericValue(c)); - } else if (criterion instanceof Query.SizeEquals c) { - return cb.equal(cb.size(fullyQualifiedPath), c.getValue()); - } else if (criterion instanceof Query.SizeNotEquals c) { - return cb.notEqual(cb.size(fullyQualifiedPath), c.getValue()); - } else if (criterion instanceof Query.SizeGreaterThan c) { - return cb.gt(cb.size(fullyQualifiedPath), getNumericValue(c)); - } else if (criterion instanceof Query.SizeGreaterThanEquals c) { - return cb.ge(cb.size(fullyQualifiedPath), getNumericValue(c)); - } else if (criterion instanceof Query.SizeLessThan c) { - return cb.lt(cb.size(fullyQualifiedPath), getNumericValue(c)); - } else if (criterion instanceof Query.SizeLessThanEquals c) { - return cb.le(cb.size(fullyQualifiedPath), getNumericValue(c)); - } else if (criterion instanceof Query.Between c) { - return cb.between(fullyQualifiedPath, (Comparable) c.getFrom(), (Comparable) c.getTo()); - } - } else if (criterion instanceof Query.PropertyComparisonCriterion c) { - if (criterion instanceof Query.EqualsProperty) { - return cb.equal(fromsByProvider.getFullyQualifiedPath(c.getProperty()), root_.get(c.getOtherProperty())); - } else if (criterion instanceof Query.NotEqualsProperty) { - return cb.notEqual(fromsByProvider.getFullyQualifiedPath(c.getProperty()), root_.get(c.getOtherProperty())); - } else if (criterion instanceof Query.LessThanEqualsProperty) { - return cb.le(fromsByProvider.getFullyQualifiedPath(c.getProperty()), root_.get(c.getOtherProperty())); - } else if (criterion instanceof Query.LessThanProperty) { - return cb.lt(fromsByProvider.getFullyQualifiedPath(c.getProperty()), root_.get(c.getOtherProperty())); - } else if (criterion instanceof Query.GreaterThanEqualsProperty) { - return cb.ge(fromsByProvider.getFullyQualifiedPath(c.getProperty()), root_.get(c.getOtherProperty())); - } else if (criterion instanceof Query.GreaterThanProperty) { - return cb.gt(fromsByProvider.getFullyQualifiedPath(c.getProperty()), root_.get(c.getOtherProperty())); - } - } else if (criterion instanceof Query.PropertyNameCriterion c) { - if (criterion instanceof Query.IsNull) { - return cb.isNull(fromsByProvider.getFullyQualifiedPath(c.getProperty())); - } else if (criterion instanceof Query.IsNotNull) { - return cb.isNotNull(fromsByProvider.getFullyQualifiedPath(c.getProperty())); - } else if (criterion instanceof Query.IsEmpty) { - return cb.isEmpty(fromsByProvider.getFullyQualifiedPath(c.getProperty())); - } else if (criterion instanceof Query.IsNotEmpty) { - return cb.isNotEmpty(fromsByProvider.getFullyQualifiedPath(c.getProperty())); - } - } else if (criterion instanceof Query.Exists c) { - Subquery subquery = criteriaQuery.subquery(Integer.class); - PersistentEntity childPersistentEntity = c.getSubquery().getPersistentEntity(); - Root subRoot = subquery.from(childPersistentEntity.getJavaClass()); + if (list.isEmpty()) { + list = List.of(cb.equal(cb.literal(1), cb.literal(1))); + } + return list.toArray(new Predicate[0]); + } - JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone(); - newMap.put("root", subRoot); - var predicates = (Predicate[])this.getPredicates(cb, criteriaQuery, subRoot, (List)c.getSubquery().getCriteria(), newMap, entity); + private Predicate handleCriterion(HibernateCriteriaBuilder cb, CriteriaQuery criteriaQuery, From root_, JpaFromProvider fromsByProvider, PersistentEntity entity, Object criterion) { + if (criterion instanceof Query.Junction junction) { + return handleJunction(cb, criteriaQuery, root_, fromsByProvider, entity, junction); + } else if (criterion instanceof Query.DistinctProjection) { + return cb.conjunction(); + } else if (criterion instanceof DetachedAssociationCriteria<?> c) { + return handleAssociationCriteria(cb, criteriaQuery, root_, fromsByProvider, entity, c); + } else if (criterion instanceof Query.PropertyCriterion pc) { + return handlePropertyCriterion(cb, criteriaQuery, root_, fromsByProvider, entity, pc); + } else if (criterion instanceof Query.PropertyComparisonCriterion c) { + return handlePropertyComparisonCriterion(cb, fromsByProvider, root_, c); + } else if (criterion instanceof Query.PropertyNameCriterion c) { + return handlePropertyNameCriterion(cb, fromsByProvider, c); + } else if (criterion instanceof Query.Exists c) { + return handleExists(cb, criteriaQuery, root_, fromsByProvider, entity, c); + } else if (criterion instanceof Query.NotExists c) { + return cb.not(handleExists(cb, criteriaQuery, root_, fromsByProvider, entity, new Query.Exists(c.getSubquery()))); + } + throw new IllegalArgumentException("Unsupported criterion: " + criterion); + } - var existsPredicate = getExistsPredicate(cb, root_, childPersistentEntity, subRoot); - Predicate[] allPredicates = Stream.concat( - Arrays.stream(predicates), - Stream.of(existsPredicate) - ).toArray(Predicate[]::new); + private Predicate handleJunction(HibernateCriteriaBuilder cb, CriteriaQuery criteriaQuery, From root_, JpaFromProvider fromsByProvider, PersistentEntity entity, Query.Junction junction) { + var predicates = getPredicates(cb, criteriaQuery, root_, junction.getCriteria(), fromsByProvider, entity); + if (junction instanceof Query.Disjunction) { + return cb.or(predicates); + } else if (junction instanceof Query.Conjunction) { + return cb.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 null; + } - subquery.select(cb.literal(1)).where(cb.and(allPredicates)); - JpaPredicate exists = cb.exists(subquery); - return exists; - } else if (criterion instanceof Query.NotExists c) { - Subquery subquery = criteriaQuery.subquery(Integer.class); - PersistentEntity childPersistentEntity = c.getSubquery().getPersistentEntity(); - Root subRoot = subquery.from(childPersistentEntity.getJavaClass()); + private Predicate handleAssociationCriteria(HibernateCriteriaBuilder cb, CriteriaQuery criteriaQuery, From root_, JpaFromProvider fromsByProvider, PersistentEntity entity, DetachedAssociationCriteria<?> c) { + Join child = root_.join(c.getAssociationPath(), JoinType.LEFT); + JpaFromProvider childTablesByName = (JpaFromProvider) fromsByProvider.clone(); + childTablesByName.put("root", child); + return cb.and(getPredicates(cb, criteriaQuery, child, c.getCriteria(), childTablesByName, entity)); + } - JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone(); - newMap.put("root", subRoot); - var predicates = (Predicate[])this.getPredicates(cb, criteriaQuery, subRoot, (List)c.getSubquery().getCriteria(), newMap, entity); + private Predicate handlePropertyCriterion(HibernateCriteriaBuilder cb, CriteriaQuery criteriaQuery, From root_, JpaFromProvider fromsByProvider, PersistentEntity entity, Query.PropertyCriterion pc) { + var fullyQualifiedPath = fromsByProvider.getFullyQualifiedPath(pc.getProperty()); - var existsPredicate = getExistsPredicate(cb, root_, childPersistentEntity, subRoot); - Predicate[] allPredicates = Stream.concat( - Arrays.stream(predicates), - Stream.of(existsPredicate) - ).toArray(Predicate[]::new); + 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, entity, 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(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(fullyQualifiedPath, c.getValue().toString()); + } else if (pc instanceof Query.Equals c) { + return cb.equal(fullyQualifiedPath, c.getValue()); + } else if (pc instanceof Query.NotEquals c) { + return cb.or(cb.notEqual(fullyQualifiedPath, c.getValue()), cb.isNull(fullyQualifiedPath)); + } else if (pc instanceof Query.IdEquals c) { + return cb.equal(root_.get("id"), c.getValue()); + } else if (pc instanceof Query.GreaterThan c) { + return cb.gt(fullyQualifiedPath, getNumericValue(c)); + } else if (pc instanceof Query.GreaterThanEquals c) { + return cb.ge(fullyQualifiedPath, getNumericValue(c)); + } else if (pc instanceof Query.LessThan c) { + return cb.lt(fullyQualifiedPath, getNumericValue(c)); + } else if (pc instanceof Query.LessThanEquals c) { + return cb.le(fullyQualifiedPath, getNumericValue(c)); + } else if (pc instanceof Query.SizeEquals c) { + return cb.equal(cb.size(fullyQualifiedPath), c.getValue()); + } else if (pc instanceof Query.SizeNotEquals c) { + return cb.notEqual(cb.size(fullyQualifiedPath), c.getValue()); + } else if (pc instanceof Query.SizeGreaterThan c) { + return cb.gt(cb.size(fullyQualifiedPath), getNumericValue(c)); + } else if (pc instanceof Query.SizeGreaterThanEquals c) { + return cb.ge(cb.size(fullyQualifiedPath), getNumericValue(c)); + } else if (pc instanceof Query.SizeLessThan c) { + return cb.lt(cb.size(fullyQualifiedPath), getNumericValue(c)); + } else if (pc instanceof Query.SizeLessThanEquals c) { + return cb.le(cb.size(fullyQualifiedPath), getNumericValue(c)); + } else if (pc instanceof Query.Between c) { + return cb.between(fullyQualifiedPath, (Comparable) c.getFrom(), (Comparable) c.getTo()); + } + return null; + } - subquery.select(cb.literal(1)).where(cb.and(allPredicates)); - JpaPredicate exists = cb.exists(subquery); - return cb.not(exists); - } + 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); + Root from2 = subquery2.from(c.getValue().getPersistentEntity().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, entity); + 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, entity); + subquery2.select(from2).where(cb.and(predicates2)); + return cb.not(cb.in(fullyQualifiedPath).value(subquery2)); + } + } + return cb.not(cb.in(fullyQualifiedPath, c.getValue())); + } - throw new IllegalArgumentException("Unsupported criterion: " + criterion); - }).filter(Objects::nonNull).toList(); - if (list.isEmpty()) { - list = List.of(cb.equal(cb.literal(1),cb.literal(1))); + 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()) { + boolean areGormEntities = c.getValues().stream().allMatch(GormEntity.class::isInstance); + if (areGormEntities) { + List<GormEntity> gormEntities = new ArrayList<>(c.getValues()); + Path id = criteriaQuery.from(gormEntities.get(0).getClass()).get("id"); + Collection newValues = gormEntities.stream().map(GormEntity::ident).toList(); + return cb.in(id, newValues); + } + return cb.in(fullyQualifiedPath, c.getValues()); } - return list.toArray(new Predicate[0]); + 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, PersistentEntity entity, Query.SubqueryCriterion c) { + Subquery subquery = criteriaQuery.subquery(Number.class); + Root from = subquery.from(c.getValue().getPersistentEntity().getJavaClass()); + JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone(); + newMap.put("root", from); + Predicate[] predicates = getPredicates(cb, criteriaQuery, from, c.getValue().getCriteria(), newMap, entity); + Path path = fromsByProvider.getFullyQualifiedPath(c.getProperty()); + + if (c instanceof Query.GreaterThanEqualsAll) { + subquery.select(cb.max(from.get(c.getProperty()))).where(cb.and(predicates)); + return cb.greaterThanOrEqualTo(path, subquery); + } else if (c instanceof Query.GreaterThanAll) { + subquery.select(cb.max(from.get(c.getProperty()))).where(cb.and(predicates)); + return cb.greaterThan(path, subquery); + } else if (c instanceof Query.LessThanEqualsAll) { + subquery.select(cb.min(from.get(c.getProperty()))).where(cb.and(predicates)); + return cb.lessThanOrEqualTo(path, subquery); + } else if (c instanceof Query.LessThanAll) { + subquery.select(cb.min(from.get(c.getProperty()))).where(cb.and(predicates)); + return cb.lessThan(path, subquery); + } else if (c instanceof Query.EqualsAll) { + subquery.select(from.get(c.getProperty())).where(cb.and(predicates)); + return cb.equal(path, subquery); + } else if (c instanceof Query.GreaterThanEqualsSome) { + subquery.select(cb.max(from.get(c.getProperty()))).where(cb.or(predicates)); + return cb.greaterThanOrEqualTo(path, subquery); + } else if (c instanceof Query.GreaterThanSome) { + subquery.select(cb.max(from.get(c.getProperty()))).where(cb.or(predicates)); + return cb.greaterThan(path, subquery); + } else if (c instanceof Query.LessThanEqualsSome) { + subquery.select(cb.min(from.get(c.getProperty()))).where(cb.or(predicates)); + return cb.lessThanOrEqualTo(path, subquery); + } else if (c instanceof Query.LessThanSome) { + subquery.select(cb.min(from.get(c.getProperty()))).where(cb.or(predicates)); + return cb.lessThan(path, subquery); + } + return null; + } + + private Predicate handlePropertyComparisonCriterion(HibernateCriteriaBuilder cb, JpaFromProvider fromsByProvider, From root_, Query.PropertyComparisonCriterion c) { + Path path = fromsByProvider.getFullyQualifiedPath(c.getProperty()); + Path otherPath = root_.get(c.getOtherProperty()); + 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) { + Subquery subquery = criteriaQuery.subquery(Integer.class); + PersistentEntity childPersistentEntity = c.getSubquery().getPersistentEntity(); + Root subRoot = subquery.from(childPersistentEntity.getJavaClass()); + JpaFromProvider newMap = (JpaFromProvider) fromsByProvider.clone(); + newMap.put("root", subRoot); + var predicates = getPredicates(cb, criteriaQuery, subRoot, c.getSubquery().getCriteria(), newMap, entity); + var existsPredicate = getExistsPredicate(cb, root_, childPersistentEntity, subRoot); + Predicate[] allPredicates = Stream.concat(Arrays.stream(predicates), Stream.of(existsPredicate)).toArray(Predicate[]::new); + subquery.select(cb.literal(1)).where(cb.and(allPredicates)); + return cb.exists(subquery); } 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 = getPathFromInCriterion(fromsByProvider, criterion); + var path = fromsByProvider.getFullyQualifiedPath(criterion.getProperty()); var in = findInPredicate(cb, projection, path, subProperty); var subquery = criteriaQuery.subquery(getJavaTypeOfInClause((SqmInListPredicate) in)); var from = subquery.from(queryableCriteria.getPersistentEntity().getJavaClass()); - var subCriteria = queryableCriteria.getCriteria(); var clonedProviderByName = (JpaFromProvider) fromsByProvider.clone(); clonedProviderByName.put("root", from); - var predicates = this.getPredicates(cb, criteriaQuery, from, subCriteria, clonedProviderByName, entity); - subquery.select(clonedProviderByName.getFullyQualifiedPath(subProperty)) - .distinct(true) - .where(cb.and(predicates)); - CriteriaBuilder.In value = in.value(subquery); - return value; + var predicates = getPredicates(cb, criteriaQuery, from, queryableCriteria.getCriteria(), clonedProviderByName, entity); + subquery.select(clonedProviderByName.getFullyQualifiedPath(subProperty)).distinct(true).where(cb.and(predicates)); + return in.value(subquery); } private Predicate getExistsPredicate(HibernateCriteriaBuilder cb, From root_, PersistentEntity childPersistentEntity, Root subRoot) { - Association owner = childPersistentEntity - .getAssociations() - .stream() + Association owner = childPersistentEntity.getAssociations().stream() .filter(assoc -> assoc.getAssociatedEntity().getJavaClass().equals(root_.getJavaType())) .findFirst().orElseThrow(); - Predicate existsPredicate = cb.equal(subRoot.get(owner.getName()), root_); - return existsPredicate; + return cb.equal(subRoot.get(owner.getName()), root_); } @SuppressWarnings("rawtypes") @@ -309,46 +292,32 @@ public class PredicateGenerator { } private String findSubproperty(Object projection) { - return projection instanceof Query.PropertyProjection ? ((Query.PropertyProjection) projection).getPropertyName() :"id" ; + return projection instanceof Query.PropertyProjection ? ((Query.PropertyProjection) projection).getPropertyName() : "id"; } @SuppressWarnings("unchecked") private Query.Projection findPropertyOrIdProjection(QueryableCriteria queryableCriteria) { - return (Query.Projection ) queryableCriteria.getProjections() - .stream(). - filter(projection1 -> projection1 instanceof Query.PropertyProjection || projection1 instanceof Query.IdProjection) - .findFirst() - .orElse(new Query.IdProjection()); + return (Query.Projection) queryableCriteria.getProjections().stream() + .filter(p -> p instanceof Query.PropertyProjection || p instanceof Query.IdProjection) + .findFirst().orElse(new Query.IdProjection()); } @SuppressWarnings("rawtypes") private QueryableCriteria getQueryableCriteriaFromInCriteria(Query.Criterion criterion) { - return criterion instanceof Query.In ? ((Query.In) criterion).getSubquery() : ((Query.NotIn) criterion).getSubquery(); - } - - @SuppressWarnings("rawtypes") - private Path getPathFromInCriterion(JpaFromProvider tablesByName, Query.PropertyNameCriterion criterion) { - return tablesByName.getFullyQualifiedPath(criterion.getProperty()); + return criterion instanceof Query.In ? ((Query.In) criterion).getSubquery() : ((Query.NotIn) criterion).getSubquery(); } - @SuppressWarnings("rawtypes") private Class getJavaTypeOfInClause(SqmInListPredicate predicate) { - return Optional.ofNullable(predicate.getTestExpression() - .getExpressible()) - .map(expressible -> expressible.getExpressibleJavaType().getJavaTypeClass()) + return Optional.ofNullable(predicate.getTestExpression().getExpressible()) + .map(expressible -> expressible.getExpressibleJavaType().getJavaTypeClass()) .orElse(null); } private Number getNumericValue(Query.PropertyCriterion criterion) { Object value = criterion.getValue(); - if (value instanceof Number) { - return (Number) value; - } - String operationName = criterion.getClass().getSimpleName(); - throw new ConfigurationException( - String.format("Operation '%s' on property '%s' only accepts a numeric value, but received a %s", - operationName, criterion.getProperty(), (value == null ? "null" : value.getClass().getName()))); + if (value instanceof Number num) return num; + throw new ConfigurationException(String.format("Operation '%s' on property '%s' only accepts a numeric value, but received a %s", + criterion.getClass().getSimpleName(), criterion.getProperty(), (value == null ? "null" : value.getClass().getName()))); } - }
