Copilot commented on code in PR #16548:
URL: https://github.com/apache/grails-core/pull/16548#discussion_r4207836653


##########
grails-data-hibernate7/docs/src/docs/asciidoc/querying/criteria.adoc:
##########
@@ -217,6 +217,69 @@ Inside an association block, such as `pets { ... }`, the 
condition restricts the
 
 A <<querying-detachedCriteria,detached criteria>> accepts `sqlRestriction` too.
 
+==== SQL Projections
+
+
+The `projections` node also accepts native SQL, for a value that only SQL 
computes, such as a date truncated to the day.
+
+[source,java]
+----
+// Box is a domain class...
+class Box {
+    int width
+    int height
+}

Review Comment:
   The later `boxesPerDay` example references `{alias}.packed_at`, but this 
sample domain declares no `packedAt` property, so following the documented 
example produces a missing-column error. Add the property to keep the example 
self-contained.



##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SqlProjection.java:
##########
@@ -0,0 +1,209 @@
+/*
+ *  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.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.hibernate.type.BasicTypeReference;
+import org.hibernate.type.Type;
+
+import org.grails.datastore.mapping.query.Query;
+
+/**
+ * Projection that selects the value of a native SQL expression, created by 
{@code sqlProjection} or
+ * {@code sqlGroupProjection} in a criteria query. In the SQL, {@code {alias}} 
stands for the table alias of the
+ * queried entity.
+ *
+ * <p>The SQL of a {@code sqlProjection} may select several columns, separated 
by commas, each one optionally
+ * followed by {@code as} and its column alias, as on Hibernate 5. It is split 
into one projection per column, so
+ * every column is a selection of its own.</p>
+ *
+ * @since 8.0.0
+ */
+public class SqlProjection extends Query.Projection {
+
+    private static final Pattern TRAILING_ALIAS = Pattern.compile(
+            
"(?is)^(.*?)\\s+as\\s+(?:\"([\\w$]+)\"|`([\\w$]+)`|\\[([\\w$]+)]|([\\w$]+))\\s*$");
+
+    private final String sql;
+    private final String columnAlias;
+    private final Object declaredType;
+    private final Class<?> type;
+
+    /**
+     * @param sql the SQL expression of the projected column, without its 
column alias
+     * @param columnAlias the alias of the projected column
+     * @param type the type of the projected value: an {@code 
org.hibernate.type.StandardBasicTypes} constant, an
+     *     {@code org.hibernate.type.Type}, a Java class or {@code null} for a 
value of any type
+     * @throws IllegalArgumentException if the SQL is empty or the type is 
none of these
+     */
+    public SqlProjection(String sql, String columnAlias, Object type) {
+        if (sql == null || sql.isBlank()) {
+            throw new IllegalArgumentException("The SQL of a sqlProjection 
must not be empty");
+        }
+        this.sql = sql;
+        this.columnAlias = columnAlias;
+        this.declaredType = type;
+        this.type = javaType(type);
+    }
+
+    /**
+     * Returns the SQL expression of the projected column, without its column 
alias.
+     */
+    public String getSql() {
+        return sql;
+    }
+
+    /**
+     * Returns the alias of the projected column, which names it in an {@code 
order} of the criteria query.
+     */
+    public String getColumnAlias() {
+        return columnAlias;
+    }
+
+    /**
+     * Returns the Java type of the projected value.
+     */
+    public Class<?> getType() {
+        return type;
+    }
+
+    /**
+     * Returns the type of the projected value as it was given: an {@code 
org.hibernate.type.StandardBasicTypes}
+     * constant or an {@code org.hibernate.type.Type}, which also tell how the 
value is read, a Java class or
+     * {@code null}.
+     */
+    public Object getDeclaredType() {
+        return declaredType;
+    }
+
+    /**
+     * Splits the SQL of a {@code sqlProjection} or {@code sqlGroupProjection} 
into one projection per column.
+     *
+     * @param sql the SQL selecting the columns, separated by commas
+     * @param columnAliases the alias of each column, in order
+     * @param types the type of each column, in order: an {@code 
org.hibernate.type.StandardBasicTypes} constant, an
+     *     {@code org.hibernate.type.Type} or a Java class
+     * @return the projection of each column
+     * @throws IllegalArgumentException if the number of columns, aliases and 
types differ
+     */
+    public static List<SqlProjection> of(String sql, List<String> 
columnAliases, List<?> types) {
+        if (sql == null || sql.isBlank()) {
+            throw new IllegalArgumentException("The SQL of a sqlProjection 
must not be empty");
+        }
+        if (columnAliases == null || types == null || columnAliases.size() != 
types.size()) {
+            throw new IllegalArgumentException("A sqlProjection needs as many 
types as column aliases: " + sql);
+        }
+        List<String> columns = splitColumns(sql);
+        if (columns.size() != columnAliases.size()) {
+            throw new IllegalArgumentException("The SQL of a sqlProjection 
selects " + columns.size() +
+                    " columns but " + columnAliases.size() + " column aliases 
were given: " + sql);
+        }
+        List<SqlProjection> projections = new ArrayList<>(columns.size());
+        for (int i = 0; i < columns.size(); i++) {
+            String alias = columnAliases.get(i);
+            projections.add(new SqlProjection(withoutAlias(columns.get(i), 
alias), alias, types.get(i)));
+        }
+        return projections;
+    }
+
+    /**
+     * Returns the Java type of a projected value given as an {@code 
org.hibernate.type.StandardBasicTypes} constant,
+     * an {@code org.hibernate.type.Type} or a Java class.
+     */
+    static Class<?> javaType(Object type) {
+        if (type == null) {
+            return Object.class;
+        }
+        if (type instanceof Class<?> javaClass) {
+            return javaClass;
+        }
+        if (type instanceof BasicTypeReference<?> reference) {
+            return reference.getJavaType();
+        }
+        if (type instanceof Type hibernateType) {
+            return hibernateType.getReturnedClass();

Review Comment:
   The new contract explicitly accepts an `org.hibernate.type.Type`, but the 
added tests cover only `BasicTypeReference` constants and Java classes, leaving 
this branch and the translator's `BasicType` preservation path unverified. Add 
an end-to-end criteria test with a resolved Hibernate `BasicType`—ideally one 
with value conversion—to verify that this advertised input form is retained 
through result extraction.



##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SqlProjection.java:
##########
@@ -0,0 +1,209 @@
+/*
+ *  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.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.hibernate.type.BasicTypeReference;
+import org.hibernate.type.Type;
+
+import org.grails.datastore.mapping.query.Query;
+
+/**
+ * Projection that selects the value of a native SQL expression, created by 
{@code sqlProjection} or
+ * {@code sqlGroupProjection} in a criteria query. In the SQL, {@code {alias}} 
stands for the table alias of the
+ * queried entity.
+ *
+ * <p>The SQL of a {@code sqlProjection} may select several columns, separated 
by commas, each one optionally
+ * followed by {@code as} and its column alias, as on Hibernate 5. It is split 
into one projection per column, so
+ * every column is a selection of its own.</p>
+ *
+ * @since 8.0.0
+ */
+public class SqlProjection extends Query.Projection {
+
+    private static final Pattern TRAILING_ALIAS = Pattern.compile(
+            
"(?is)^(.*?)\\s+as\\s+(?:\"([\\w$]+)\"|`([\\w$]+)`|\\[([\\w$]+)]|([\\w$]+))\\s*$");

Review Comment:
   The matcher only permits `\w`/`$` inside quoted aliases, so valid quoted 
identifiers such as `"total height"` or `[total-height]` are not stripped. 
Those aliases remain as `... AS ...` inside the self-rendering expression; 
using the advertised alias in a group/order then repeats an `AS` clause where 
it is invalid SQL. Please parse full quoted identifier bodies (including 
doubled-delimiter escapes) and compare the unquoted value with `columnAlias`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to