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


##########
grails-data-hibernate7/core/src/main/groovy/org/apache/grails/data/hibernate7/core/querytail/QueryTailStatementInspector.groovy:
##########
@@ -0,0 +1,354 @@
+/*
+ *  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.boot.registry.StandardServiceRegistryBuilder
+import org.hibernate.boot.registry.selector.spi.StrategySelectionException
+import org.hibernate.boot.registry.selector.spi.StrategySelector
+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.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
+import org.grails.orm.hibernate.cfg.HibernateMappingContextConfiguration
+
+/**
+ * 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. The inspector the session factory would run without the 
capture keeps running,
+ * wherever the application configured it: with {@code 
hibernate.session_factory.statement_inspector},
+ * on the configuration with {@link Configuration#setStatementInspector}, or 
in the settings of the
+ * service registry, as a {@code hibernate.configClass} may. 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, even one it sets as the configuration builds the session 
factory.</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
+
+    private SessionFactoryObserver observer
+    private StatementInspector delegate
+    private boolean wrapped
+    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
+     */
+    QueryTailStatementInspector(String dataSourceName, File file) {
+        this.dataSourceName = dataSourceName
+        this.file = file
+    }
+
+    /**
+     * Installs the inspector on the configuration of a session factory when 
capture is enabled, as
+     * the observer and the statement inspector the configuration puts in 
place of those of the
+     * session factory when it builds the session factory.
+     *
+     * @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
+     */
+    static QueryTailStatementInspector install(String dataSourceName, 
QueryTailSettings settings,
+            HibernateMappingContextConfiguration configuration) {
+        if (settings == null || !settings.enabled) {
+            return null
+        }
+        QueryTailStatementInspector inspector = new 
QueryTailStatementInspector(dataSourceName, settings.resolveFile())
+        configuration.queryTailStatementInspector = inspector
+        inspector
+    }
+
+    /**
+     * Takes the place of the statement inspector the session factory would 
run, and runs it: the
+     * one set on the configuration, which Hibernate prefers, else the one the 
settings of the
+     * service registry name, where the properties of the configuration are 
applied over the
+     * settings the registry builder came with. The configuration calls this 
as it builds the
+     * session factory, once every setting is in place.
+     *
+     * @param registry the builder of the service registry the session factory 
is built with
+     * @param configuration the configuration the session factory is built from
+     * @throws ConfigurationException when the inspector the application 
configured cannot be created
+     */
+    void wrap(StandardServiceRegistryBuilder registry, Configuration 
configuration) {
+        if (!wrapped) {
+            delegate = configuration.statementInspector ?: 
resolve(registry.bootstrapServiceRegistry.getService(StrategySelector),
+                    
registry.settings.get(AvailableSettings.STATEMENT_INSPECTOR))
+            wrapped = true
+        }
+        registry.applySetting(AvailableSettings.STATEMENT_INSPECTOR, this)
+        configuration.statementInspector = this
+    }
+
+    /**
+     * Takes the place of the session factory observer the configuration 
holds, and tells that one,
+     * set by the application, of every event first. The configuration calls 
this as it builds the
+     * session factory, after the application had every chance to set its own, 
even as the build
+     * began.
+     *
+     * @param configuration the configuration the session factory is built from
+     */
+    void observe(Configuration configuration) {
+        SessionFactoryObserver configured = 
configuration.sessionFactoryObserver
+        // a configuration built again holds this inspector already, in front 
of the observer it had
+        if (configured !== this) {
+            observer = configured
+        }
+        configuration.sessionFactoryObserver = this
+    }
+
+    /**
+     * @return the inspector the session factory would run without the 
capture, or null when it
+     * would run none; known once the session factory is built
+     */
+    StatementInspector getDelegate() {
+        delegate
+    }
+
+    /**
+     * @return the session factory observer the application configured, or 
null when it configured
+     * none; known once the session factory is built
+     */
+    SessionFactoryObserver getObserver() {
+        observer
+    }
+
+    /**
+     * Resolves the inspector an application configured as Hibernate does: an 
instance, a class, a
+     * class name or a name registered with the strategy selector of the 
service registry.
+     *
+     * @param strategies the strategy selector of the service registry the 
session factory is built with
+     * @param configured the configured value, or null
+     * @return the inspector, or null when nothing was configured
+     * @throws ConfigurationException when the configured inspector cannot be 
loaded or created
+     */
+    @PackageScope
+    static StatementInspector resolve(StrategySelector strategies, Object 
configured) {
+        if (configured == null) {
+            return null
+        }
+        String name = configured instanceof Class ? ((Class) configured).name 
: configured.toString()
+        try {
+            strategies.resolveStrategy(StatementInspector, configured)
+        }
+        catch (StrategySelectionException e) {
+            throw new ConfigurationException("Cannot load the statement 
inspector [${name}] configured with " +
+                    AvailableSettings.STATEMENT_INSPECTOR, e)
+        }
+        catch (ClassCastException e) {
+            // the selector creates whatever class is named, and only the cast 
of what it creates tells it is no inspector
+            throw new ConfigurationException("The statement inspector 
[${name}] configured with " +
+                    "${AvailableSettings.STATEMENT_INSPECTOR} does not 
implement ${StatementInspector.name}", e)
+        }
+    }
+
+    @Override
+    String inspect(String sql) {
+        String executed = sql
+        if (delegate != null) {
+            String inspected = delegate.inspect(sql)
+            if (inspected != null) {
+                executed = inspected
+            }
+        }
+        QueryTailCapture current = openCapture()
+        if (current != null) {
+            current.record(dataSourceName, executed)
+        }
+        executed
+    }
+
+    /**
+     * Opens the capture, and records the statements of the session factory in 
the syntax of its dialect.
+     */
+    @Override
+    void sessionFactoryCreated(SessionFactory factory) {
+        if (observer != null) {
+            observer.sessionFactoryCreated(factory)
+        }
+        QueryTailCapture current = openCapture()
+        if (current != null && factory instanceof SessionFactoryImplementor) {
+            current.syntax = syntaxOf(factory.jdbcServices.dialect)
+        }
+    }
+
+    /**
+     * The syntax the statements of a dialect are redacted in: that of its 
database, where the
+     * redactor knows the database's lexical rules. Cloud Spanner's PostgreSQL 
dialect is not known,
+     * as the parser of a Spanner PostgreSQL database has not been verified, 
and neither is SQL
+     * Anywhere's, a Transact-SQL dialect whose database reads {@code //} as a 
comment; the Informix
+     * dialect among the community dialects is known. Any other dialect, such 
as SQLite's, Firebird's
+     * or a dialect that is not Hibernate's, is redacted as for a database 
that is not known: a
+     * statement is then recorded only when every known syntax reads it the 
same way.
+     *
+     * @param dialect the dialect of a session factory
+     * @return the syntax its statements are redacted in
+     */
+    static QueryTailSyntax syntaxOf(Dialect dialect) {
+        if (dialect instanceof SpannerPostgreSQLDialect || 
extendsNamed(dialect, COMMUNITY_SQL_ANYWHERE)) {
+            return QueryTailSyntax.UNKNOWN
+        }
+        if (dialect instanceof AbstractTransactSQLDialect) {
+            return QueryTailSyntax.TRANSACT_SQL
+        }
+        if (dialect instanceof SpannerDialect) {
+            return QueryTailSyntax.GOOGLE_SQL
+        }
+        if (dialect instanceof MySQLDialect) {
+            return QueryTailSyntax.MYSQL
+        }
+        if (dialect instanceof CockroachDialect) {
+            return QueryTailSyntax.COCKROACH
+        }
+        if (dialect instanceof PostgreSQLDialect) {
+            return QueryTailSyntax.POSTGRESQL
+        }
+        if (dialect instanceof H2Dialect) {
+            return QueryTailSyntax.H2
+        }
+        if (dialect instanceof OracleDialect) {
+            return QueryTailSyntax.ORACLE
+        }
+        if (dialect instanceof DB2Dialect) {
+            return QueryTailSyntax.DB2
+        }
+        if (dialect instanceof HSQLDialect) {
+            return QueryTailSyntax.HSQLDB
+        }
+        if (dialect instanceof HANADialect) {
+            return QueryTailSyntax.HANA
+        }
+        if (extendsNamed(dialect, COMMUNITY_INFORMIX)) {
+            return QueryTailSyntax.INFORMIX
+        }
+        QueryTailSyntax.UNKNOWN
+    }
+
+    /** @return whether a dialect is, or extends, the named class, which may 
not be on the class path */
+    private static boolean extendsNamed(Dialect dialect, String className) {
+        for (Class<?> type = dialect.getClass(); type != null; type = 
type.superclass) {
+            if (type.name == className) {
+                return true
+            }
+        }
+        false
+    }
+
+    @Override
+    void sessionFactoryClosing(SessionFactory factory) {
+        if (observer != null) {
+            observer.sessionFactoryClosing(factory)
+        }
+    }
+
+    /**
+     * Closes the capture.
+     */
+    @Override
+    void sessionFactoryClosed(SessionFactory factory) {
+        try {
+            if (observer != null) {
+                observer.sessionFactoryClosed(factory)
+            }
+        }
+        finally {
+            close()
+        }
+    }
+
+    /**
+     * Stops recording and closes the capture; a capture that was never opened 
stays unopened.
+     */
+    synchronized void close() {

Review Comment:
   One configuration that builds two session factories reuses this inspector 
and its one capture. Closing either factory closes that capture. The other 
factory keeps running queries and nothing is written. Building again after 
close reuses the closed capture.
   
   Give each factory its own capture, or install a new inspector for each 
build. Hibernate 5 has the same gap. Please build two factories, close one, and 
assert the other still records.



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