HHH-16229 - Consider pluggability for rendering "JDBC" parameters

This commit is contained in:
Steve Ebersole 2023-02-27 09:39:51 -06:00
parent 6ed48ffff5
commit 4cc8f04b73
9 changed files with 285 additions and 88 deletions

View File

@ -38,6 +38,7 @@ import org.hibernate.query.sqm.mutation.internal.SqmMultiTableMutationStrategyPr
import org.hibernate.resource.beans.spi.ManagedBeanRegistryInitiator;
import org.hibernate.resource.transaction.internal.TransactionCoordinatorBuilderInitiator;
import org.hibernate.service.internal.SessionFactoryServiceRegistryFactoryInitiator;
import org.hibernate.sql.ast.internal.JdbcParameterRendererInitiator;
import org.hibernate.sql.results.jdbc.internal.JdbcValuesMappingProducerProviderInitiator;
import org.hibernate.tool.schema.internal.SchemaManagementToolInitiator;
import org.hibernate.tool.schema.internal.script.SqlScriptExtractorInitiator;
@ -99,6 +100,7 @@ public final class StandardServiceInitiators {
serviceInitiators.add( JdbcValuesMappingProducerProviderInitiator.INSTANCE );
serviceInitiators.add( SqmMultiTableMutationStrategyProviderInitiator.INSTANCE );
serviceInitiators.add( JdbcParameterRendererInitiator.INSTANCE );
serviceInitiators.trimToSize();

View File

@ -0,0 +1,33 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.sql.ast.internal;
import java.util.Map;
import org.hibernate.boot.registry.StandardServiceInitiator;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.sql.ast.spi.JdbcParameterRenderer;
/**
* @author Steve Ebersole
*/
public class JdbcParameterRendererInitiator implements StandardServiceInitiator<JdbcParameterRenderer> {
/**
* Singleton access
*/
public static final JdbcParameterRendererInitiator INSTANCE = new JdbcParameterRendererInitiator();
@Override
public JdbcParameterRenderer initiateService(Map<String, Object> configurationValues, ServiceRegistryImplementor registry) {
return JdbcParameterRendererStandard.INSTANCE;
}
@Override
public Class<JdbcParameterRenderer> getServiceInitiated() {
return JdbcParameterRenderer.class;
}
}

View File

@ -0,0 +1,27 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.sql.ast.internal;
import org.hibernate.dialect.Dialect;
import org.hibernate.sql.ast.spi.JdbcParameterRenderer;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.type.descriptor.jdbc.JdbcType;
/**
* @author Steve Ebersole
*/
public class JdbcParameterRendererStandard implements JdbcParameterRenderer {
/**
* Singleton access
*/
public static final JdbcParameterRendererStandard INSTANCE = new JdbcParameterRendererStandard();
@Override
public void renderJdbcParameter(int position, JdbcType jdbcType, SqlAppender appender, Dialect dialect) {
jdbcType.appendWriteExpression( "?", appender, dialect );
}
}

View File

@ -95,6 +95,7 @@ import org.hibernate.sql.ast.tree.cte.CteStatement;
import org.hibernate.sql.ast.tree.cte.CteTableGroup;
import org.hibernate.sql.ast.tree.cte.SearchClauseSpecification;
import org.hibernate.sql.ast.tree.delete.DeleteStatement;
import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression;
import org.hibernate.sql.ast.tree.expression.Any;
import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression;
import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression;
@ -102,7 +103,6 @@ import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression;
import org.hibernate.sql.ast.tree.expression.CastTarget;
import org.hibernate.sql.ast.tree.expression.Collation;
import org.hibernate.sql.ast.tree.expression.ColumnReference;
import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression;
import org.hibernate.sql.ast.tree.expression.Distinct;
import org.hibernate.sql.ast.tree.expression.Duration;
import org.hibernate.sql.ast.tree.expression.DurationUnit;
@ -192,7 +192,6 @@ import org.hibernate.sql.model.internal.TableInsertStandard;
import org.hibernate.sql.model.internal.TableUpdateCustomSql;
import org.hibernate.sql.model.internal.TableUpdateStandard;
import org.hibernate.sql.results.internal.SqlSelectionImpl;
import org.hibernate.sql.results.jdbc.internal.JdbcValuesMappingProducerStandard;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer;
import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider;
import org.hibernate.type.BasicPluralType;
@ -268,6 +267,12 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
private final List<JdbcParameterBinder> parameterBinders = new ArrayList<>();
private final JdbcParametersImpl jdbcParameters = new JdbcParametersImpl();
private JdbcParameterBindings jdbcParameterBindings;
private Map<JdbcParameter, JdbcParameterBinding> appliedParameterBindings = Collections.emptyMap();
private SqlAstNodeRenderingMode parameterRenderingMode = SqlAstNodeRenderingMode.DEFAULT;
private final JdbcParameterRenderer jdbcParameterRenderer;
private final Set<FilterJdbcParameter> filterJdbcParameters = new HashSet<>();
@ -302,16 +307,19 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
private transient BasicType<String> stringType;
private transient BasicType<Boolean> booleanType;
private SqlAstNodeRenderingMode parameterRenderingMode = SqlAstNodeRenderingMode.DEFAULT;
private Map<JdbcParameter, JdbcParameterBinding> appliedParameterBindings = Collections.emptyMap();
private JdbcParameterBindings jdbcParameterBindings;
private LockOptions lockOptions;
private Limit limit;
private JdbcParameter offsetParameter;
private JdbcParameter limitParameter;
private ForUpdateClause forUpdate;
protected AbstractSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) {
this.sessionFactory = sessionFactory;
this.dialect = sessionFactory.getJdbcServices().getDialect();
this.statementStack.push( statement );
this.jdbcParameterRenderer = sessionFactory.getServiceRegistry().getService( JdbcParameterRenderer.class );
}
private static Clause matchWithClause(Clause clause) {
if ( clause == Clause.WITH ) {
return Clause.WITH;
@ -323,12 +331,6 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
return dialect;
}
protected AbstractSqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) {
this.sessionFactory = sessionFactory;
this.dialect = sessionFactory.getJdbcServices().getDialect();
this.statementStack.push( statement );
}
@Override
public SessionFactoryImplementor getSessionFactory() {
return sessionFactory;
@ -499,7 +501,7 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
@Override
public boolean supportsFilterClause() {
// By default we report false because not many dialects support this
// By default, we report false because not many dialects support this
return false;
}
@ -6178,26 +6180,37 @@ public abstract class AbstractSqlAstTranslator<T extends JdbcOperation> implemen
public void visitParameter(JdbcParameter jdbcParameter) {
switch ( getParameterRenderingMode() ) {
case NO_UNTYPED:
case NO_PLAIN_PARAMETER:
case NO_PLAIN_PARAMETER: {
renderCasted( jdbcParameter );
break;
}
case INLINE_PARAMETERS:
case INLINE_ALL_PARAMETERS:
case INLINE_ALL_PARAMETERS: {
renderExpressionAsLiteral( jdbcParameter, jdbcParameterBindings );
break;
}
case DEFAULT:
default:
jdbcParameter.getExpressionType()
.getJdbcMappings()
.get( 0 )
.getJdbcType()
.appendWriteExpression( "?", this, getDialect() );
parameterBinders.add( jdbcParameter.getParameterBinder() );
jdbcParameters.addParameter( jdbcParameter );
default: {
visitParameterAsParameter( jdbcParameter );
break;
}
}
}
protected void visitParameterAsParameter(JdbcParameter jdbcParameter) {
renderParameterAsParameter( jdbcParameter );
parameterBinders.add( jdbcParameter.getParameterBinder() );
jdbcParameters.addParameter( jdbcParameter );
}
protected void renderParameterAsParameter(JdbcParameter jdbcParameter) {
jdbcParameterRenderer.renderJdbcParameter(
parameterBinders.size() + 1,
jdbcParameter.getExpressionType().getJdbcMappings().get( 0 ).getJdbcType(),
this,
getDialect()
);
}
@Override
public void render(SqlAstNode sqlAstNode, SqlAstNodeRenderingMode renderingMode) {

View File

@ -0,0 +1,29 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.sql.ast.spi;
import org.hibernate.dialect.Dialect;
import org.hibernate.service.Service;
import org.hibernate.type.descriptor.jdbc.JdbcType;
/**
* Extension point, intended for use from Hibernate Reactive, to render JDBC
* parameter placeholders into the SQL query string being generated.
*
* @author Steve Ebersole
*/
public interface JdbcParameterRenderer extends Service {
/**
* Render the parameter for the given position
*
* @param position The 1-based position of the parameter.
* @param jdbcType The type of the parameter
* @param appender The appender where the parameter should be rendered
* @param dialect The Dialect in use within the SessionFactory
*/
void renderJdbcParameter(int position, JdbcType jdbcType, SqlAppender appender, Dialect dialect);
}

View File

@ -9,6 +9,7 @@ package org.hibernate.orm.test.dialect.function;
import java.util.ArrayList;
import java.util.List;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.SQLServerDialect;
import org.hibernate.dialect.SybaseDialect;
@ -30,90 +31,95 @@ import org.hibernate.type.descriptor.jdbc.CharJdbcType;
import org.hibernate.type.internal.BasicTypeImpl;
import org.hibernate.type.spi.TypeConfiguration;
import org.junit.Test;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.ServiceRegistryScope;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* TODO : javadoc
*
* @author Steve Ebersole
* @author Christian Beikov
*/
@ServiceRegistry
public class AnsiTrimEmulationFunctionTest {
private static final String trimSource = "a.column";
@Test
public void testBasicSqlServerProcessing() {
public void testBasicSqlServerProcessing(ServiceRegistryScope scope) {
Dialect dialect = new SQLServerDialect();
TrimFunction function = new TrimFunction( dialect, new TypeConfiguration() );
performBasicSpaceTrimmingTests( dialect, function );
performBasicSpaceTrimmingTests( dialect, scope.getRegistry(), function );
final String expectedTrimPrep = "replace(replace(a.column,' ','#%#%'),'-',' ')";
final String expectedPostTrimPrefix = "replace(replace(";
final String expectedPostTrimSuffix = ",' ','-'),'#%#%',' ')";
// -> trim(LEADING '-' FROM a.column)
String rendered = render( dialect, function, TrimSpec.LEADING, '-', trimSource );
String rendered = render( dialect, scope.getRegistry(), function, TrimSpec.LEADING, '-', trimSource );
String expected = expectedPostTrimPrefix + "ltrim(" + expectedTrimPrep + ")" + expectedPostTrimSuffix;
assertEquals( expected, rendered );
// -> trim(TRAILING '-' FROM a.column)
rendered = render( dialect, function, TrimSpec.TRAILING, '-', trimSource );
rendered = render( dialect, scope.getRegistry(), function, TrimSpec.TRAILING, '-', trimSource );
expected = expectedPostTrimPrefix + "rtrim(" + expectedTrimPrep + ")" + expectedPostTrimSuffix;
assertEquals( expected, rendered );
// -> trim(BOTH '-' FROM a.column)
rendered = render( dialect, function, TrimSpec.BOTH, '-', trimSource );
rendered = render( dialect, scope.getRegistry(), function, TrimSpec.BOTH, '-', trimSource );
expected = expectedPostTrimPrefix + "ltrim(rtrim(" + expectedTrimPrep + "))" + expectedPostTrimSuffix;
assertEquals( expected, rendered );
}
@Test
public void testBasicSybaseProcessing() {
public void testBasicSybaseProcessing(ServiceRegistryScope scope) {
Dialect dialect = new SybaseDialect();
TrimFunction function = new TrimFunction( dialect, new TypeConfiguration() );
performBasicSpaceTrimmingTests( dialect, function );
performBasicSpaceTrimmingTests( dialect, scope.getRegistry(), function );
final String expectedTrimPrep = "str_replace(str_replace(a.column,' ','#%#%'),'-',' ')";
final String expectedPostTrimPrefix = "str_replace(str_replace(";
final String expectedPostTrimSuffix = ",' ','-'),'#%#%',' ')";
// -> trim(LEADING '-' FROM a.column)
String rendered = render( dialect, function, TrimSpec.LEADING, '-', trimSource );
String rendered = render( dialect, scope.getRegistry(), function, TrimSpec.LEADING, '-', trimSource );
String expected = expectedPostTrimPrefix + "ltrim(" + expectedTrimPrep + ")" + expectedPostTrimSuffix;
assertEquals( expected, rendered );
// -> trim(TRAILING '-' FROM a.column)
rendered = render( dialect, function, TrimSpec.TRAILING, '-', trimSource );
rendered = render( dialect, scope.getRegistry(), function, TrimSpec.TRAILING, '-', trimSource );
expected = expectedPostTrimPrefix + "rtrim(" + expectedTrimPrep + ")" + expectedPostTrimSuffix;
assertEquals( expected, rendered );
// -> trim(BOTH '-' FROM a.column)
rendered = render( dialect, function, TrimSpec.BOTH, '-', trimSource );
rendered = render( dialect, scope.getRegistry(), function, TrimSpec.BOTH, '-', trimSource );
expected = expectedPostTrimPrefix + "ltrim(rtrim(" + expectedTrimPrep + "))" + expectedPostTrimSuffix;
assertEquals( expected, rendered );
}
private void performBasicSpaceTrimmingTests(Dialect dialect, TrimFunction function) {
private void performBasicSpaceTrimmingTests(Dialect dialect, StandardServiceRegistry registry, TrimFunction function) {
// -> trim(a.column)
String rendered = render( dialect, function, TrimSpec.BOTH, ' ', trimSource );
String rendered = render( dialect, registry, function, TrimSpec.BOTH, ' ', trimSource );
assertEquals( "ltrim(rtrim(a.column))", rendered );
// -> trim(LEADING FROM a.column)
rendered = render( dialect, function, TrimSpec.LEADING, ' ', trimSource );
rendered = render( dialect, registry, function, TrimSpec.LEADING, ' ', trimSource );
assertEquals( "ltrim(a.column)", rendered );
// -> trim(TRAILING FROM a.column)
rendered = render( dialect, function, TrimSpec.TRAILING, ' ', trimSource );
rendered = render( dialect, registry, function, TrimSpec.TRAILING, ' ', trimSource );
assertEquals( "rtrim(a.column)", rendered );
}
private String render(
Dialect dialect,
StandardServiceRegistry registry,
TrimFunction function,
TrimSpec trimSpec,
char trimCharacter,
@ -121,6 +127,7 @@ public class AnsiTrimEmulationFunctionTest {
SessionFactoryImplementor factory = Mockito.mock( SessionFactoryImplementor.class );
JdbcServices jdbcServices = Mockito.mock( JdbcServices.class );
Mockito.doReturn( jdbcServices ).when( factory ).getJdbcServices();
Mockito.doReturn( registry ).when( factory ).getServiceRegistry();
Mockito.doReturn( dialect ).when( jdbcServices ).getDialect();
StandardSqlAstTranslator<JdbcOperation> walker = new StandardSqlAstTranslator<>(
factory,

View File

@ -0,0 +1,62 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.orm.test.sql.ast;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.H2Dialect;
import org.hibernate.sql.ast.spi.JdbcParameterRenderer;
import org.hibernate.sql.ast.spi.SqlAppender;
import org.hibernate.type.descriptor.jdbc.JdbcType;
import org.hibernate.testing.jdbc.SQLStatementInspector;
import org.hibernate.testing.orm.domain.gambit.EntityOfBasics;
import org.hibernate.testing.orm.junit.DomainModel;
import org.hibernate.testing.orm.junit.RequiresDialect;
import org.hibernate.testing.orm.junit.ServiceRegistry;
import org.hibernate.testing.orm.junit.SessionFactory;
import org.hibernate.testing.orm.junit.SessionFactoryScope;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @implNote Restricted to H2 as there is nothing intrinsically Dialect specific here,
* though each database has specific syntax for labelled parameters
*
* @author Steve Ebersole
*/
@ServiceRegistry( services = @ServiceRegistry.Service(
role = JdbcParameterRenderer.class,
impl = JdbcParameterRendererTests.JdbcParameterRendererImpl.class
) )
@DomainModel( annotatedClasses = EntityOfBasics.class )
@SessionFactory( useCollectingStatementInspector = true )
@RequiresDialect( H2Dialect.class )
public class JdbcParameterRendererTests {
@Test
public void basicTest(SessionFactoryScope scope) {
final String queryString = "select e from EntityOfBasics e where e.id = :id";
final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
statementInspector.clear();
scope.inTransaction( (session) -> {
session.createSelectionQuery( queryString, EntityOfBasics.class ).setParameter( "id", 1 ).list();
} );
assertThat( statementInspector.getSqlQueries() ).hasSize( 1 );
final String sql = statementInspector.getSqlQueries().get( 0 );
assertThat( sql ).contains( "?1" );
}
public static class JdbcParameterRendererImpl implements JdbcParameterRenderer {
@Override
public void renderJdbcParameter(int position, JdbcType jdbcType, SqlAppender appender, Dialect dialect) {
jdbcType.appendWriteExpression( "?" + position, appender, dialect );
}
}
}

View File

@ -71,27 +71,12 @@ import static org.hamcrest.MatcherAssert.assertThat;
public class SmokeTests {
@Test
public void testSimpleHqlInterpretation(SessionFactoryScope scope) {
scope.inTransaction(
session -> {
final QueryImplementor<String> query = session.createQuery(
scope.inTransaction( (session) -> {
final SelectStatement sqlAst = SqlAstHelper.translateHqlSelectQuery(
"select e.name from SimpleEntity e",
String.class
String.class,
session
);
final SqmQueryImplementor<String> hqlQuery = (SqmQueryImplementor<String>) query;
final SqmSelectStatement<String> sqmStatement = (SqmSelectStatement<String>) hqlQuery.getSqmStatement();
final StandardSqmTranslator<SelectStatement> sqmConverter = new StandardSqmTranslator<>(
sqmStatement,
hqlQuery.getQueryOptions(),
( (QuerySqmImpl<?>) hqlQuery ).getDomainParameterXref(),
query.getParameterBindings(),
session.getLoadQueryInfluencers(),
scope.getSessionFactory(),
true
);
final SqmTranslation<SelectStatement> sqmInterpretation = sqmConverter.translate();
final SelectStatement sqlAst = sqmInterpretation.getSqlAst();
final FromClause fromClause = sqlAst.getQuerySpec().getFromClause();
assertThat( fromClause.getRoots().size(), is( 1 ) );
@ -125,8 +110,7 @@ public class SmokeTests {
jdbcSelectOperation.getSqlString(),
is( "select s1_0.name from mapping_simple_entity s1_0" )
);
}
);
} );
}
@Test

View File

@ -0,0 +1,40 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
*/
package org.hibernate.orm.test.sql.ast;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.query.hql.spi.SqmQueryImplementor;
import org.hibernate.query.spi.QueryImplementor;
import org.hibernate.query.sqm.internal.QuerySqmImpl;
import org.hibernate.query.sqm.sql.SqmTranslation;
import org.hibernate.query.sqm.sql.internal.StandardSqmTranslator;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.hibernate.sql.ast.tree.select.SelectStatement;
/**
* @author Steve Ebersole
*/
public class SqlAstHelper {
public static SelectStatement translateHqlSelectQuery(String hql, Class<?> returnType, SessionImplementor session) {
final QueryImplementor<?> query = session.createQuery( hql, returnType );
final QuerySqmImpl<?> hqlQuery = (QuerySqmImpl<?>) query;
final SqmSelectStatement<?> sqmStatement = (SqmSelectStatement<?>) hqlQuery.getSqmStatement();
final StandardSqmTranslator<SelectStatement> sqmConverter = new StandardSqmTranslator<>(
sqmStatement,
hqlQuery.getQueryOptions(),
hqlQuery.getDomainParameterXref(),
query.getParameterBindings(),
session.getLoadQueryInfluencers(),
session.getFactory(),
true
);
final SqmTranslation<SelectStatement> sqmInterpretation = sqmConverter.translate();
return sqmInterpretation.getSqlAst();
}
}