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]
