jamesfredley commented on code in PR #16719: URL: https://github.com/apache/grails-core/pull/16719#discussion_r4417491074
########## grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlText.java: ########## @@ -0,0 +1,289 @@ +/* + * 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.Locale; +import java.util.Set; + +import jakarta.annotation.Nullable; + +/** HQL text and its top-level projection analysis, without changing the text sent to Hibernate. */ +public final class HqlText { Review Comment: Rebased onto current 8.0.x in b7bb164afd. The separate class is gone. A leading parenthesized group is read inside the existing HqlText, and normalized() remains the whole original text. `(select b.title from Book b) union select 'extra' from Book` is unchanged. ########## grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/HqlText.java: ########## @@ -0,0 +1,289 @@ +/* + * 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.Locale; +import java.util.Set; + +import jakarta.annotation.Nullable; + +/** HQL text and its top-level projection analysis, without changing the text sent to Hibernate. */ +public final class HqlText { + + private final String hql; + private final String text; + private final String lower; + private final int selectIndex; + private final int fromIndex; + + public HqlText(@Nullable CharSequence hql) { + this(hql, true); + } + + private HqlText(@Nullable CharSequence hql, boolean analyzeRoot) { + this.hql = hql == null ? null : hql.toString(); + this.text = this.hql == null ? "" : analyzeRoot ? unwrapRootQuery(this.hql) : this.hql.trim(); + this.lower = text.toLowerCase(Locale.ROOT); + this.selectIndex = indexOfTopLevel(lower, HibernateQueryArgument.HQL_SELECT.value() + " ", 0); + this.fromIndex = indexOfTopLevel(lower, " " + HibernateQueryArgument.HQL_FROM.value() + " ", + Math.max(selectIndex, 0)); + } + + public @Nullable String hql() { + return hql; + } + + public Class<?> getTarget(Class<?> entityClass) { + return switch (countHqlProjections()) { + case 0 -> entityClass; + case 1 -> { + String clause = getSingleProjectionClause(); + if (clause.startsWith("count(") || clause.startsWith("sum(") || + clause.startsWith("avg(") || clause.startsWith("min(") || + clause.startsWith("max(")) { + yield null; + } + String rootAlias = getRootAlias(); + boolean rootProjection = rootAlias != null ? + clause.equals(rootAlias.toLowerCase(Locale.ROOT)) : + clause.equals(getEntityName().toLowerCase(Locale.ROOT)); + yield rootProjection ? entityClass : Object.class; + } + default -> Object[].class; + }; + } + + public int countHqlProjections() { + String clause = getSingleProjectionClause(); + if (clause == null || clause.isEmpty()) return 0; + return getCommas(clause) == 0 ? 1 : 2; + } + + public @Nullable String getSingleProjectionClause() { + if (selectIndex < 0) return null; + return extractSelectClause(lower, selectIndex, fromIndex); + } + + static String extractSelectClause(String text, int selectIndex, int fromIndex) { + String clause = text.substring(selectIndex + HibernateQueryArgument.HQL_SELECT.value().length(), + fromIndex < 0 ? text.length() : fromIndex).trim(); + if (clause.startsWith(HibernateQueryArgument.HQL_DISTINCT.value() + " ")) { + clause = clause.substring(HibernateQueryArgument.HQL_DISTINCT.value().length() + 1).trim(); + } else if (clause.startsWith(HibernateQueryArgument.HQL_ALL.value() + " ")) { + clause = clause.substring(HibernateQueryArgument.HQL_ALL.value().length() + 1).trim(); + } + return clause; + } + + static int getCommas(String clause) { + int depth = 0; + int commas = 0; + int index = 0; + while (index < clause.length()) { + int skipped = skipIgnored(clause, index); + if (skipped != index) { + index = skipped; + continue; + } + char current = clause.charAt(index); + if (current == '(') { + depth++; + } else if (current == ')' && depth > 0) { + depth--; + } else if (current == ',' && depth == 0) { + commas++; + } + index++; + } + return commas; + } + + private static String unwrapRootQuery(String hql) { + String text = hql.trim(); + while (text.startsWith("(")) { + int depth = 0; + int closing = -1; + int index = 0; + while (index < text.length()) { + int skipped = skipIgnored(text, index); + if (skipped != index) { + index = skipped; + continue; + } + char current = text.charAt(index); + if (current == '(') { + depth++; + } else if (current == ')') { + depth--; + if (depth == 0) { + closing = index; + break; + } + } + index++; + } + if (closing < 0) return text; + int next = skipWhitespaceAndComments(text, closing + 1); + if (next < text.length() && !isSetOperation(text, next)) return text; Review Comment: The first group is analyzed whatever follows it. `(select b.title from HqlResultTypeBook b) limit 1` is covered in HqlSelectResultTypeSpec and returns a String. ########## grails-data-hibernate7/core/src/test/groovy/grails/gorm/tests/HqlSelectResultTypeSpec.groovy: ########## @@ -83,5 +108,7 @@ class HqlResultTypeAuthor { class HqlResultTypeBook implements HibernateEntity<HqlResultTypeBook> { String title + int pages Review Comment: The unused pages property and its setup values are gone. -- 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]
