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


##########
grails-datamapping-core/src/cli/groovy/org/apache/grails/data/core/cli/querytail/StatementCollapser.groovy:
##########
@@ -0,0 +1,131 @@
+/*
+ *  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.apache.grails.data.core.cli.querytail
+
+import java.util.regex.Pattern
+
+import groovy.transform.CompileStatic
+
+/**
+ * Shortens a statement to the part that identifies it: the table a select 
reads and its where
+ * clause, the table an update changes and its where clause, the table an 
insert writes to. The
+ * column lists Hibernate generates are long and say nothing about which code 
ran the statement,
+ * and dropping them lets the same query count as one statement wherever it is 
run from. A Cypher
+ * statement is shown whole: its pattern names what it reads or writes, and 
GORM generates nothing
+ * long in it. So is a MongoDB command, which is recorded as its name, its 
collection and the
+ * document that shapes it.
+ *
+ * @since 8.1
+ */
+@CompileStatic
+class StatementCollapser {
+
+    private static final Pattern COMMENT = Pattern.compile('/\\*.*?\\*/', 
Pattern.DOTALL)
+    private static final Pattern WHITESPACE = Pattern.compile('\\s+')
+    private static final String ELLIPSIS = '...'
+
+    /**
+     * @param sql a statement
+     * @return the shortened statement, or the statement with its comments and 
extra whitespace
+     * removed when it is not a SQL select, update or insert
+     */
+    static String collapse(String sql) {
+        String text = normalize(sql)
+        if (text.isEmpty() || StatementType.isCypher(text)) {
+            return text
+        }
+        switch (StatementType.of(text)) {
+            case StatementType.SELECT:
+                return collapseSelect(text)
+            case StatementType.UPDATE:
+                return collapseUpdate(text)
+            case StatementType.INSERT:
+                return collapseInsert(text)
+            default:
+                return text
+        }
+    }
+
+    /**
+     * @param sql a statement
+     * @return the statement without comments, its whitespace collapsed to 
single spaces
+     */
+    static String normalize(String sql) {
+        if (sql == null) {
+            return ''
+        }
+        WHITESPACE.matcher(COMMENT.matcher(sql).replaceAll(' ')).replaceAll(' 
').trim()
+    }
+
+    private static String collapseSelect(String text) {
+        int from = topLevelIndexOf(text, ' from ')
+        from < 0 ? text : "select 
${ELLIPSIS}${text.substring(from)}".toString()
+    }
+
+    private static String collapseUpdate(String text) {
+        int set = topLevelIndexOf(text, ' set ')
+        if (set < 0) {
+            return text
+        }
+        int where = topLevelIndexOf(text, ' where ')
+        String head = text.substring(0, set)
+        where > set ? "${head} ${ELLIPSIS}${text.substring(where)}".toString() 
: "${head} ${ELLIPSIS}".toString()
+    }
+
+    private static String collapseInsert(String text) {
+        int columns = topLevelIndexOf(text, '(')
+        int values = topLevelIndexOf(text, ' values ')
+        int cut = columns < 0 ? values : (values < 0 ? columns : 
Math.min(columns, values))
+        cut < 0 ? text : "${text.substring(0, cut).trim()} 
${ELLIPSIS}".toString()
+    }
+
+    /**
+     * Finds a keyword outside parentheses and string literals, so that a 
subquery or a value
+     * never stands in for the outer statement's clause.
+     */
+    static int topLevelIndexOf(String text, String needle) {
+        String haystack = text.toLowerCase(Locale.ROOT)
+        if (haystack.length() != text.length()) {
+            haystack = text
+        }
+        int depth = 0
+        boolean quoted = false
+        for (int i = 0; i < haystack.length(); i++) {
+            char c = haystack.charAt(i)
+            if (c == ('\'' as char)) {

Review Comment:
   `update "order set lines" set title=? where id=?` and `update "order set 
headers" set title=? where id=?` collapse into one statement. The scan skips 
single-quoted strings and still matches ` set ` inside a quoted identifier.
   
   Skip quoted identifiers, including a doubled quote, before matching a clause.



##########
grails-data-hibernate7/core/src/main/groovy/org/apache/grails/data/hibernate7/core/querytail/QueryTailStatementInspector.groovy:
##########
@@ -0,0 +1,319 @@
+/*
+ *  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.apache.grails.data.hibernate7.core.querytail
+
+import groovy.transform.CompileStatic
+import groovy.transform.PackageScope
+import org.hibernate.SessionFactory
+import org.hibernate.SessionFactoryObserver
+import org.hibernate.cfg.AvailableSettings
+import org.hibernate.cfg.Configuration
+import org.hibernate.dialect.AbstractTransactSQLDialect
+import org.hibernate.dialect.CockroachDialect
+import org.hibernate.dialect.DB2Dialect
+import org.hibernate.dialect.Dialect
+import org.hibernate.dialect.H2Dialect
+import org.hibernate.dialect.HANADialect
+import org.hibernate.dialect.HSQLDialect
+import org.hibernate.dialect.MySQLDialect
+import org.hibernate.dialect.OracleDialect
+import org.hibernate.dialect.PostgreSQLDialect
+import org.hibernate.dialect.SpannerDialect
+import org.hibernate.dialect.SpannerPostgreSQLDialect
+import org.hibernate.engine.spi.SessionFactoryImplementor
+import org.hibernate.resource.jdbc.spi.StatementInspector
+import org.springframework.beans.BeanUtils
+import org.springframework.util.ClassUtils
+
+import org.apache.grails.data.core.querytail.QueryTailCapture
+import org.apache.grails.data.core.querytail.QueryTailSettings
+import org.apache.grails.data.core.querytail.QueryTailSyntax
+import org.grails.datastore.mapping.core.exceptions.ConfigurationException
+
+/**
+ * The statement inspector GORM installs on a session factory when query-tail 
capture is enabled. It
+ * records every statement Hibernate is about to run to the {@link 
QueryTailCapture} of the
+ * application, without its values, and returns the statement as it found it: 
the database runs
+ * it unchanged. An inspector the application configured itself through
+ * {@code hibernate.session_factory.statement_inspector} keeps running: it 
sees the statement
+ * first, and the statement it returns is the one run and recorded.
+ *
+ * <p>The session factory owns the capture. The inspector observes it: it 
opens the capture when
+ * the session factory is created, takes from its dialect how the database 
quotes values, and
+ * closes the capture when the session factory closes, so a session factory 
that fails to start
+ * leaves no capture open either. An observer the application set on the 
configuration keeps
+ * being called.</p>
+ *
+ * <p>Nothing of this is installed unless {@link 
QueryTailSettings#SETTING_ENABLED} is set, so an
+ * application that does not capture pays nothing per statement.</p>
+ *
+ * @since 8.1
+ */
+@CompileStatic
+class QueryTailStatementInspector implements StatementInspector, 
SessionFactoryObserver {
+
+    @Serial
+    private static final long serialVersionUID = 1L
+
+    /** The community dialects, which are not a dependency, that the inspector 
tells apart by name. */
+    private static final String COMMUNITY_INFORMIX = 
'org.hibernate.community.dialect.InformixDialect'
+    private static final String COMMUNITY_SQL_ANYWHERE = 
'org.hibernate.community.dialect.SybaseAnywhereDialect'
+
+    /** The connection source name statements are recorded under. */
+    final String dataSourceName
+
+    /** The file statements are recorded to. */
+    final File file
+
+    /** The inspector the application configured, or null. */
+    final StatementInspector delegate
+
+    /** The session factory observer the application configured, or null. */
+    final SessionFactoryObserver observer
+
+    private transient volatile QueryTailCapture capture
+    private transient boolean closed
+
+    /**
+     * @param dataSourceName the connection source the inspected session 
factory belongs to
+     * @param file the capture file statements are recorded to
+     * @param delegate the inspector the application configured, or null
+     * @param observer the session factory observer the application 
configured, or null
+     */
+    QueryTailStatementInspector(String dataSourceName, File file, 
StatementInspector delegate,
+            SessionFactoryObserver observer) {
+        this.dataSourceName = dataSourceName
+        this.file = file
+        this.delegate = delegate
+        this.observer = observer
+    }
+
+    /**
+     * Installs the inspector on the configuration of a session factory when 
capture is enabled, as
+     * its statement inspector and as the observer of the session factory it 
builds.
+     *
+     * @param dataSourceName the connection source the session factory is 
built for
+     * @param settings the query-tail settings of the connection source
+     * @param configuration the configuration the session factory is built from
+     * @return the installed inspector, or null when capture is not enabled
+     * @throws ConfigurationException when the inspector the application 
configured cannot be created
+     */
+    static QueryTailStatementInspector install(String dataSourceName, 
QueryTailSettings settings,
+            Configuration configuration) {
+        if (settings == null || !settings.enabled) {
+            return null
+        }
+        Properties properties = configuration.properties
+        StatementInspector configured = 
resolve(properties.get(AvailableSettings.STATEMENT_INSPECTOR))

Review Comment:
   If `hibernate.configClass` calls `setStatementInspector`, Hibernate keeps 
that inspector apart from the property this code sets. The capture file opens 
and no statement is written.
   
   Wrap the inspector already set, and install the wrapper with 
`setStatementInspector`. The Hibernate 5 installer has the same gap.



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