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/10/22 23:07:17 UTC

(grails-core) branch feat/criteria-handler-context-8.1.x updated: Hibernate 7: let a handler order by the expression a sqlProjection records under its alias

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

borinquenkid pushed a commit to branch feat/criteria-handler-context-8.1.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git


The following commit(s) were added to refs/heads/feat/criteria-handler-context-8.1.x by this push:
     new 1e47f7ed5d Hibernate 7: let a handler order by the expression a sqlProjection records under its alias
1e47f7ed5d is described below

commit 1e47f7ed5d5986dbd8694e9530e108a4610878e1
Author: Walter Duque de Estrada <wb...@mac.com>
AuthorDate: Thu Oct 22 15:11:29 2026 -0500

    Hibernate 7: let a handler order by the expression a sqlProjection records under its alias
    
    A custom order that names a sqlProjection alias could not reach that expression: the translator records it with registerSelectionAlias, and getFullyQualifiedPath and getFullyQualifiedExpression resolve property paths only. QueryPathContext now exposes getSelectionAlias(String), which JpaQueryContext already had, so an order handler orders by a projected value the way the built-in alias order does; predicate property resolution is unchanged. HibernateCriteriaBuilderSqlProjectionSpec or [...]
---
 .../orm/hibernate/query/JpaQueryContext.java       |  1 +
 .../orm/hibernate/query/QueryPathContext.java      |  7 +++++
 ...ibernateCriteriaBuilderSqlProjectionSpec.groovy | 35 ++++++++++++++++++++++
 .../docs/src/docs/asciidoc/querying/criteria.adoc  |  3 +-
 4 files changed, 45 insertions(+), 1 deletion(-)

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
index bf180c3c55..16f01501ef 100644
--- 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
@@ -149,6 +149,7 @@ public class JpaQueryContext implements Cloneable, QueryPathContext {
      * @param alias the alias of the selection
      * @return the selection, or {@code null} if no selection of this query has the alias
      */
+    @Override
     public Expression<?> getSelectionAlias(String alias) {
         return selectionAliases.get(alias);
     }
diff --git a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/QueryPathContext.java b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/QueryPathContext.java
index 5758615bac..7a7a3df751 100644
--- a/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/QueryPathContext.java
+++ b/grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/QueryPathContext.java
@@ -46,4 +46,11 @@ public interface QueryPathContext {
      * @return the expression for it: a path, or the expression an alias or projection stands for
      */
     Expression<?> getFullyQualifiedExpression(String path);
+
+    /**
+     * @param alias the column alias of a {@code sqlProjection} of the query
+     * @return the projected expression recorded under that alias, so that a handler can order by it, or {@code null}
+     *     when no projection of the query carries the alias
+     */
+    Expression<?> getSelectionAlias(String alias);
 }
diff --git a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSqlProjectionSpec.groovy b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSqlProjectionSpec.groovy
index fb0d025f13..aca33a9420 100644
--- a/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSqlProjectionSpec.groovy
+++ b/grails-data-hibernate7/core/src/test/groovy/grails/orm/HibernateCriteriaBuilderSqlProjectionSpec.groovy
@@ -32,6 +32,8 @@ import spock.lang.Specification
 
 import grails.gorm.annotation.Entity
 import grails.gorm.transactions.Rollback
+import org.grails.orm.hibernate.query.PredicateGenerator
+import org.grails.datastore.mapping.query.Query
 import org.grails.datastore.mapping.core.DatastoreUtils
 import org.grails.orm.hibernate.HibernateDatastore
 import org.grails.orm.hibernate.cfg.Settings
@@ -125,6 +127,31 @@ class HibernateCriteriaBuilderSqlProjectionSpec extends Specification {
         rows*.toList().sort { it[0] } == [[2, 3L, 24L], [4, 1L, 9L]]
     }
 
+    @Rollback
+    void 'an order handler orders by the expression a SQL projection records under its alias'() {
+        given: "a handler that orders by the selection an alias stands for, descending"
+        saveBoxes()
+        PredicateGenerator.registerOrderHandler(SqlProjectionAliasOrder) { query, root, cb, order, context ->
+            cb.desc(context.getSelectionAlias(order.property))
+        }
+
+        when:
+        List rows = SqlProjectionBox.createCriteria().list {
+            projections {
+                sqlProjection 'width * 10 as scaled', 'scaled', INTEGER
+            }
+            order(new SqlProjectionAliasOrder('scaled'))
+        }
+
+        then: "the rows come in descending order of the projection, and the SQL orders by the select item, not by a column"
+        rows == rows.sort(false).reverse()
+        rows.size() == SqlProjectionBox.count()
+        sqlCapture.statements.any { it.contains('select width * 10 from') && it.contains('order by 1 desc') }
+
+        cleanup:
+        PredicateGenerator.clearCustomOrderHandlers()
+    }
+
     @Rollback
     void 'the results are ordered by the alias of a SQL projection'() {
         given:
@@ -512,3 +539,11 @@ class SqlProjectionBox {
     Date packedAt
     SqlProjectionShelf shelf
 }
+
+/** An order that a registered handler converts, naming the column alias of a SQL projection. */
+class SqlProjectionAliasOrder extends Query.Order {
+
+    SqlProjectionAliasOrder(String alias) {
+        super(alias)
+    }
+}
diff --git a/grails-data-hibernate7/docs/src/docs/asciidoc/querying/criteria.adoc b/grails-data-hibernate7/docs/src/docs/asciidoc/querying/criteria.adoc
index 6c6d315da1..0b3000fba2 100644
--- a/grails-data-hibernate7/docs/src/docs/asciidoc/querying/criteria.adoc
+++ b/grails-data-hibernate7/docs/src/docs/asciidoc/querying/criteria.adoc
@@ -424,7 +424,8 @@ The handlers convert them. A criterion handler is given the query, its root, the
 criterion and the query's `QueryPathContext`. Through the context, `getFullyQualifiedPath` resolves a property path
 the way the built-in criteria do: `alias.property`, for an alias created with `createAlias`, or `association.property`
 becomes a path on the join GORM created for that association, so a custom criterion and a built-in one on the
-same association restrict the same row rather than each joining the association again. An order handler is given
+same association restrict the same row rather than each joining the association again, and `getSelectionAlias`
+returns the expression a `sqlProjection` records under its column alias, so an order can sort by a projected value. An order handler is given
 the query, its root, the criteria builder, the order and the context, and returns the JPA order, or `null` to leave
 the order out. A query whose result is a count leaves every order out by itself, so no handler runs for it: