jamesfredley commented on code in PR #16735:
URL: https://github.com/apache/grails-core/pull/16735#discussion_r4362780417


##########
grails-data-hibernate7/core/src/main/groovy/org/grails/orm/hibernate/query/SqlText.java:
##########
@@ -0,0 +1,506 @@
+/*
+ *  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;
+
+/**
+ * Lexical reading of SQL or HQL text, so that every scanner of this package 
skips the same things the same way:
+ * string literals and quoted identifiers, where a doubled quote stands for 
the quote itself; block comments; and, as
+ * the {@link SqlSyntax} of the text has them, line comments, {@code [bracket 
identifiers]}, where {@code ]]} stands for
+ * a bracket, {@code $tag$...$tag$} and {@code q'[...]'} strings, backslash 
escapes and nested block comments.
+ * Parentheses, and the square brackets of a syntax without bracket 
identifiers, nest. The text is read once, when the instance is created: each 
character is classified as plain text, quoted
+ * text or comment, and each plain character carries its nesting depth, so 
every question asked afterwards walks
+ * those two arrays and never rescans the text. The text is scanned, never 
parsed: this answers where something is,
+ * not what it means.
+ *
+ * @since 8.0.1
+ */
+final class SqlText {
+
+    private static final byte PLAIN = 0;
+    private static final byte QUOTED = 1;
+    private static final byte BLOCK_COMMENT = 2;
+    private static final byte LINE_COMMENT = 3;
+
+    private static final Pattern DOLLAR_QUOTE_TAG = 
Pattern.compile("\\$(?:[\\p{L}_][\\p{L}\\p{N}_]*)?\\$");
+
+    private final String text;
+    private final SqlSyntax syntax;
+    /** The kind of each character: plain text, inside a literal or quoted 
identifier, or inside a comment. */
+    private final byte[] kind;
+    /**
+     * The nesting depth of each plain character: the depth a {@code (} opens 
and a {@code )} closes, so the two
+     * parentheses of a pair carry the same depth, and the depth enclosing any 
other character. Square brackets
+     * nest the same way where they do not quote an identifier.
+     */
+    private final int[] depth;
+
+    private SqlText(String text, SqlSyntax syntax) {
+        this.text = text;
+        this.syntax = syntax;
+        int length = text.length();
+        this.kind = new byte[length];
+        this.depth = new int[length];
+        int d = 0;
+        int i = 0;
+        while (i < length) {
+            int after = unitEnd(i);
+            if (after > i) {
+                byte unitKind = unitKind(i);
+                for (int j = i; j < after; j++) {
+                    kind[j] = unitKind;
+                }
+                i = after;
+                continue;
+            }
+            char c = text.charAt(i);
+            if (c == '(' || c == '[') {
+                d++;
+                depth[i] = d;
+            } else if (c == ')' || c == ']') {
+                depth[i] = d;
+                if (d > 0) {
+                    d--;
+                }
+            } else {
+                depth[i] = d;
+            }
+            i++;
+        }
+    }
+
+    /** Reads the text in the given syntax. */
+    static SqlText of(String text, SqlSyntax syntax) {
+        return new SqlText(text, syntax);
+    }
+
+    /** Reads the text as HQL. */
+    static SqlText hql(String text) {
+        return new SqlText(text, SqlSyntax.HQL);
+    }
+
+    /**
+     * Returns the index of {@code keyword} at or after {@code fromIndex}, 
outside string literals, quoted identifiers,
+     * comments and parentheses, where it starts a word: {@code select} in 
{@code preselect} or in {@code b.select} is
+     * part of a name. A keyword that starts with a space starts a word. The 
keyword is matched as written, so a
+     * case-insensitive search passes the text in lower case.
+     *
+     * @return the index, or -1 when the keyword is not at the top level of 
the text
+     */
+    int indexOfTopLevel(String keyword, int fromIndex) {
+        for (int i = Math.max(fromIndex, 0); i < text.length(); i++) {
+            if (kind[i] == PLAIN && depth[i] == 0 && text.startsWith(keyword, 
i) && startsWord(i)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Splits the text at the separators outside string literals, quoted 
identifiers, comments and parentheses, and
+     * trims each part. A text without the separator is one part.
+     */
+    List<String> splitTopLevel(char separator) {
+        List<String> parts = new ArrayList<>();
+        int start = 0;
+        for (int i = 0; i < text.length(); i++) {
+            if (isTopLevel(i, separator)) {
+                parts.add(text.substring(start, i).trim());
+                start = i + 1;
+            }
+        }
+        parts.add(text.substring(start).trim());
+        return parts;
+    }
+
+    /** Returns how many times {@code separator} occurs at the top level of 
the whole text. */
+    int countTopLevel(char separator) {
+        return countTopLevel(separator, 0, text.length());
+    }
+
+    /** Returns how many times {@code separator} occurs at the top level 
between {@code from} and {@code to}. */
+    int countTopLevel(char separator, int from, int to) {
+        int count = 0;
+        for (int i = Math.max(from, 0); i < Math.min(to, text.length()); i++) {
+            if (isTopLevel(i, separator)) {
+                count++;
+            }
+        }
+        return count;
+    }
+
+    /**
+     * Returns the indexes of {@code c} outside string literals, quoted 
identifiers and comments, at any depth. Where
+     * the syntax reads {@code ??} as a literal question mark, a pair of them 
is not a placeholder.
+     */
+    List<Integer> plainIndexes(char c) {
+        List<Integer> indexes = new ArrayList<>();
+        boolean pairEscaped = c == '?' && 
syntax.has(SqlSyntax.Feature.DOUBLE_QUESTION_MARK_ESCAPE);
+        for (int i = 0; i < text.length(); i++) {
+            if (kind[i] == PLAIN && text.charAt(i) == c) {
+                if (pairEscaped && i + 1 < text.length() && kind[i + 1] == 
PLAIN && text.charAt(i + 1) == c) {
+                    i++;
+                    continue;
+                }
+                indexes.add(i);
+            }
+        }
+        return indexes;
+    }
+
+    /** Returns whether the text ends in a line comment, which would comment 
out anything appended on the same line. */
+    boolean endsInLineComment() {
+        return text.length() > 0 && kind[text.length() - 1] == LINE_COMMENT;
+    }
+
+    /**
+     * Returns the text without a trailing {@code as alias} that names the 
given alias, and without the comments
+     * and whitespace after it; or the text as it is when it does not end in 
that alias. The alias may be quoted the
+     * way the syntax quotes an identifier, and then hold any character, and 
it is compared without its quotes, in
+     * any case.
+     */
+    String withoutTrailingAlias(String alias) {
+        int end = endOfContent();
+        int aliasStart = startOfTrailingIdentifier(end);
+        if (aliasStart < 0 || !alias.equalsIgnoreCase(unquoted(aliasStart, 
end))) {
+            return text;
+        }
+        int asEnd = startOfSeparator(aliasStart);
+        if ((asEnd == aliasStart && kind[aliasStart] != QUOTED) || asEnd < 2 
|| kind[asEnd - 2] != PLAIN ||
+                kind[asEnd - 1] != PLAIN || !text.regionMatches(true, asEnd - 
2, "as", 0, 2)) {
+            return text;
+        }
+        int asStart = asEnd - 2;
+        int expressionEnd = startOfSeparator(asStart);
+        // the as is a word of its own: after whitespace or a comment, or 
right after a closing parenthesis or quote
+        if (expressionEnd == 0 || (expressionEnd == asStart && 
!closesOperand(asStart - 1))) {
+            return text;
+        }
+        return text.substring(0, expressionEnd);
+    }
+
+    /** Returns whether the character at {@code index} closes an operand: a 
parenthesis or bracket, or the end of a quoted text. */
+    private boolean closesOperand(int index) {
+        return kind[index] == QUOTED || (kind[index] == PLAIN && 
(text.charAt(index) == ')' || text.charAt(index) == ']'));
+    }
+
+    /**
+     * Returns the text without the quotes that make all of it one identifier, 
in which a doubled closing quote
+     * stands for the quote; or the text as it is when it is not one quoted 
identifier, a string literal included.
+     */
+    String unquoted() {
+        return unquoted(0, text.length());
+    }
+
+    /** Returns the text between {@code start} and {@code end} without the 
quotes that make all of it one identifier. */
+    private String unquoted(int start, int end) {
+        if (end - start < 2 || kind[start] != QUOTED || unitEnd(start) != end) 
{
+            return text.substring(start, end);
+        }
+        char opening = text.charAt(start);
+        char closing = opening == '[' ? ']' : opening;
+        boolean identifier = opening == '"' || opening == '`' ||

Review Comment:
   On MySQL, `GROUP BY 'total'` groups by the string constant, not the alias 
`total`. `assignGroupBy()` now unquotes that constant and rewrites 
`sqlGroupProjection` to `GROUP BY sum(height)`, which MySQL rejects.
   
   Strip quotes from an `AS` alias. Do not treat a quoted group-by item as an 
alias. Please group by `'total'` and assert the SQL still contains that string.



-- 
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