diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/pagination/LimitHandler.java b/hibernate-core/src/main/java/org/hibernate/dialect/pagination/LimitHandler.java index 936c5f49bdb5..06e0153656a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/pagination/LimitHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/pagination/LimitHandler.java @@ -9,6 +9,7 @@ import org.hibernate.query.spi.Limit; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.spi.ParameterMarkerStrategy; /** * Contract defining dialect-specific limit and offset handling. @@ -41,10 +42,21 @@ public interface LimitHandler { String processSql(String sql, Limit limit); + @Deprecated // Never called directly by Hibernate ORM default String processSql(String sql, Limit limit, QueryOptions queryOptions) { return processSql( sql, limit ); } + /** + * Currently all Dialects with non-standard native parameter renderers invariably rely on {@link OffsetFetchLimitHandler}, + * so the method below is currently only overridden in it. However, if other {@link LimitHandler}s are involved in the future + * (e.g. {@link Limit} related parameters need to be inserted after select), code refactoring is required (HHH-18624). + */ + // This is the one called directly by Hibernate ORM + default String processSql(String sql, int jdbcParameterBindingsCnt, ParameterMarkerStrategy parameterMarkerStrategy, Limit limit, QueryOptions queryOptions) { + return processSql( sql, limit ); + } + int bindLimitParametersAtStartOfQuery(Limit limit, PreparedStatement statement, int index) throws SQLException; int bindLimitParametersAtEndOfQuery(Limit limit, PreparedStatement statement, int index) throws SQLException; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/pagination/OffsetFetchLimitHandler.java b/hibernate-core/src/main/java/org/hibernate/dialect/pagination/OffsetFetchLimitHandler.java index 0697d1c0cac5..577de737e7ab 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/pagination/OffsetFetchLimitHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/pagination/OffsetFetchLimitHandler.java @@ -5,6 +5,10 @@ package org.hibernate.dialect.pagination; import org.hibernate.query.spi.Limit; +import org.hibernate.query.spi.QueryOptions; +import org.hibernate.sql.ast.internal.ParameterMarkerStrategyStandard; +import org.hibernate.sql.ast.spi.ParameterMarkerStrategy; +import org.hibernate.type.descriptor.jdbc.IntegerJdbcType; /** * A {@link LimitHandler} for databases which support the @@ -25,7 +29,12 @@ public OffsetFetchLimitHandler(boolean variableLimit) { @Override public String processSql(String sql, Limit limit) { + return processSql( sql, 0, ParameterMarkerStrategyStandard.INSTANCE, limit, QueryOptions.NONE ); + } + + @Override + public String processSql(String sql, int jdbcParameterBindingsCnt, ParameterMarkerStrategy parameterMarkerStrategy, Limit limit, QueryOptions queryOptions) { boolean hasFirstRow = hasFirstRow(limit); boolean hasMaxRows = hasMaxRows(limit); @@ -40,7 +49,7 @@ public String processSql(String sql, Limit limit) { if ( hasFirstRow ) { offsetFetch.append( " offset " ); if ( supportsVariableLimit() ) { - offsetFetch.append( "?" ); + offsetFetch.append( parameterMarkerStrategy.createMarker( ++jdbcParameterBindingsCnt, IntegerJdbcType.INSTANCE ) ); } else { offsetFetch.append( limit.getFirstRow() ); @@ -58,7 +67,7 @@ public String processSql(String sql, Limit limit) { offsetFetch.append( " fetch first " ); } if ( supportsVariableLimit() ) { - offsetFetch.append( "?" ); + offsetFetch.append( parameterMarkerStrategy.createMarker( ++jdbcParameterBindingsCnt, IntegerJdbcType.INSTANCE ) ); } else { offsetFetch.append( getMaxOrLimit( limit ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index ccacc8d58916..05058956f9da 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -95,6 +95,10 @@ import org.hibernate.query.sql.spi.ParameterInterpretation; import org.hibernate.query.sql.spi.ParameterOccurrence; import org.hibernate.query.sql.spi.SelectInterpretationsKey; +import org.hibernate.service.ServiceRegistry; +import org.hibernate.service.spi.ServiceRegistryImplementor; +import org.hibernate.sql.ast.internal.ParameterMarkerStrategyStandard; +import org.hibernate.sql.ast.spi.ParameterMarkerStrategy; import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.spi.Callback; import org.hibernate.sql.results.graph.Fetchable; @@ -486,13 +490,20 @@ private ParameterInterpretation resolveParameterInterpretation( private static ParameterInterpretationImpl parameterInterpretation( String sqlString, SharedSessionContractImplementor session) { - final ParameterRecognizerImpl parameterRecognizer = new ParameterRecognizerImpl(); - session.getFactory().getServiceRegistry() + final ServiceRegistryImplementor serviceRegistry = session.getFactory().getServiceRegistry(); + final ParameterMarkerStrategy parameterMarkerStrategy = getNullSafeParameterMarkerStrategy( serviceRegistry ); + final ParameterRecognizerImpl parameterRecognizer = new ParameterRecognizerImpl( parameterMarkerStrategy ); + serviceRegistry .requireService( NativeQueryInterpreter.class ) .recognizeParameters( sqlString, parameterRecognizer ); return new ParameterInterpretationImpl( parameterRecognizer ); } + private static ParameterMarkerStrategy getNullSafeParameterMarkerStrategy(ServiceRegistry serviceRegistry) { + final ParameterMarkerStrategy parameterMarkerStrategy = serviceRegistry.getService( ParameterMarkerStrategy.class ); + return parameterMarkerStrategy == null ? ParameterMarkerStrategyStandard.INSTANCE : parameterMarkerStrategy; + } + protected void applyOptions(NamedNativeQueryMemento memento) { super.applyOptions( memento ); @@ -882,37 +893,55 @@ protected String expandParameterLists() { StringBuilder sql = null; + final ParameterMarkerStrategy parameterMarkerStrategy = getNullSafeParameterMarkerStrategy( factory.getServiceRegistry() ); + // Handle parameter lists int offset = 0; - for ( ParameterOccurrence occurrence : parameterOccurrences ) { + int expandedParameterPosition = 1; + for ( int originalParameterPosition = 1; originalParameterPosition <= parameterOccurrences.size(); originalParameterPosition++ ) { + final ParameterOccurrence occurrence = parameterOccurrences.get( originalParameterPosition - 1 ); final QueryParameterImplementor queryParameter = occurrence.parameter(); final QueryParameterBinding binding = parameterBindings.getBinding( queryParameter ); + String occurenceReplacement = null; + int expandedParameterPositionIncrement = 1; if ( binding.isMultiValued() ) { final int bindValueCount = binding.getBindValues().size(); logTooManyExpressions( inExprLimit, bindValueCount, dialect, queryParameter ); - final int sourcePosition = occurrence.sourcePosition(); - if ( sourcePosition >= 0 ) { + if ( occurrence.sourcePosition() >= 0 ) { // check if placeholder is already immediately enclosed in parentheses // (ignoring whitespace) - final boolean isEnclosedInParens = isEnclosedInParens( sourcePosition ); + final boolean isEnclosedInParens = isEnclosedInParens( occurrence ); // short-circuit for performance when only 1 value and the // placeholder is already enclosed in parentheses... - if ( bindValueCount != 1 || !isEnclosedInParens ) { - if ( sql == null ) { - sql = new StringBuilder( sqlString.length() + 20 ); - sql.append( sqlString ); - } + if ( bindValueCount != 1 || !isEnclosedInParens || expandedParameterPosition != originalParameterPosition) { final int bindValueMaxCount = determineBindValueMaxCount( paddingEnabled, inExprLimit, bindValueCount ); - final String expansionListAsString = - expandList( bindValueMaxCount, isEnclosedInParens ); - final int start = sourcePosition + offset; - final int end = start + 1; - sql.replace( start, end, expansionListAsString ); - offset += expansionListAsString.length() - 1; + occurenceReplacement = + expandList( bindValueMaxCount, isEnclosedInParens, parameterMarkerStrategy, expandedParameterPosition ); + expandedParameterPositionIncrement = bindValueCount; } } } + else if ( expandedParameterPosition != originalParameterPosition ) { + final String oldParameterMarker = parameterMarkerStrategy.createMarker( originalParameterPosition, + null ); + final String newParameterMarker = parameterMarkerStrategy.createMarker( expandedParameterPosition, + null ); + if ( !oldParameterMarker.equals( newParameterMarker ) ) { + occurenceReplacement = newParameterMarker; + } + } + if (occurenceReplacement != null) { + final int start = occurrence.sourcePosition() + offset; + final int end = start + occurrence.length(); + if ( sql == null ) { + sql = new StringBuilder( sqlString.length() + 20 ); + sql.append( sqlString ); + } + sql.replace( start, end, occurenceReplacement ); + offset += occurenceReplacement.length() - occurrence.length(); + } + expandedParameterPosition += expandedParameterPositionIncrement; } return sql == null ? sqlString : sql.toString(); } @@ -932,41 +961,32 @@ private static void logTooManyExpressions( } } - private static String expandList(int bindValueMaxCount, boolean isEnclosedInParens) { + private static String expandList(int bindValueMaxCount, boolean isEnclosedInParens, ParameterMarkerStrategy parameterMarkerStrategy, int parameterStartPosition) { // HHH-8901 if ( bindValueMaxCount == 0 ) { return isEnclosedInParens ? "null" : "(null)"; } else { - // Shift 1 bit instead of multiplication by 2 - final char[] chars; - if ( isEnclosedInParens ) { - chars = new char[(bindValueMaxCount << 1) - 1]; - chars[0] = '?'; - for ( int i = 1; i < bindValueMaxCount; i++ ) { - final int index = i << 1; - chars[index - 1] = ','; - chars[index] = '?'; - } + final String firstParameterMarker = parameterMarkerStrategy.createMarker( parameterStartPosition, null ); + final int estimatedLength = bindValueMaxCount * ( firstParameterMarker.length() + 1 ) - 1 + ( isEnclosedInParens ? 0 : 2 ); + final StringBuilder stringBuilder = new StringBuilder( estimatedLength ); + if ( ! isEnclosedInParens ) { + stringBuilder.append( '(' ); } - else { - chars = new char[(bindValueMaxCount << 1) + 1]; - chars[0] = '('; - chars[1] = '?'; - for ( int i = 1; i < bindValueMaxCount; i++ ) { - final int index = i << 1; - chars[index] = ','; - chars[index + 1] = '?'; - } - chars[chars.length - 1] = ')'; + stringBuilder.append( firstParameterMarker ); + for ( int i = 1; i < bindValueMaxCount; i++ ) { + stringBuilder.append( ',' ).append( parameterMarkerStrategy.createMarker( parameterStartPosition + i, null ) ); + } + if ( ! isEnclosedInParens ) { + stringBuilder.append( ')' ); } - return new String( chars ); + return stringBuilder.toString(); } } - private boolean isEnclosedInParens(int sourcePosition) { + private boolean isEnclosedInParens(ParameterOccurrence occurrence) { boolean isEnclosedInParens = true; - for ( int i = sourcePosition - 1; i >= 0; i-- ) { + for ( int i = occurrence.sourcePosition() - 1; i >= 0; i-- ) { final char ch = sqlString.charAt( i ); if ( !isWhitespace( ch ) ) { isEnclosedInParens = ch == '('; @@ -974,7 +994,7 @@ private boolean isEnclosedInParens(int sourcePosition) { } } if ( isEnclosedInParens ) { - for ( int i = sourcePosition + 1; i < sqlString.length(); i++ ) { + for ( int i = occurrence.sourcePosition() + occurrence.length(); i < sqlString.length(); i++ ) { final char ch = sqlString.charAt( i ); if ( !isWhitespace( ch ) ) { isEnclosedInParens = ch == ')'; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ParameterRecognizerImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ParameterRecognizerImpl.java index d8561de73130..901c03f06a32 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ParameterRecognizerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ParameterRecognizerImpl.java @@ -18,6 +18,7 @@ import org.hibernate.query.spi.QueryParameterImplementor; import org.hibernate.query.sql.spi.ParameterOccurrence; import org.hibernate.query.sql.spi.ParameterRecognizer; +import org.hibernate.sql.ast.spi.ParameterMarkerStrategy; /** * @author Steve Ebersole @@ -36,11 +37,17 @@ private enum ParameterStyle { private int ordinalParameterImplicitPosition; + private int nextParameterMarkerPosition; + private final ParameterMarkerStrategy parameterMarkerStrategy; + private List parameterList; private final StringBuilder sqlStringBuffer = new StringBuilder(); - public ParameterRecognizerImpl() { + public ParameterRecognizerImpl(ParameterMarkerStrategy parameterMarkerStrategy) { + assert parameterMarkerStrategy != null; ordinalParameterImplicitPosition = 1; + nextParameterMarkerPosition = 1; + this.parameterMarkerStrategy = parameterMarkerStrategy; } @Override @@ -57,13 +64,13 @@ public void complete() { if ( first ) { throw new ParameterLabelException( "Ordinal parameter labels start from '?" + position + "'" - + " (ordinal parameters must be labelled from '?1')" + + " (ordinal parameters must be labelled from '?1')" ); } else { throw new ParameterLabelException( "Gap between '?" + previous + "' and '?" + position + "' in ordinal parameter labels" - + " (ordinal parameters must be labelled sequentially)" + + " (ordinal parameters must be labelled sequentially)" ); } } @@ -117,12 +124,7 @@ else if ( parameterStyle != ParameterStyle.JDBC ) { positionalQueryParameters.put( implicitPosition, parameter ); } - if ( parameterList == null ) { - parameterList = new ArrayList<>(); - } - - parameterList.add( new ParameterOccurrence( parameter, sqlStringBuffer.length() ) ); - sqlStringBuffer.append( "?" ); + recognizeParameter( parameter ); } @Override @@ -148,12 +150,7 @@ else if ( parameterStyle != ParameterStyle.NAMED ) { namedQueryParameters.put( name, parameter ); } - if ( parameterList == null ) { - parameterList = new ArrayList<>(); - } - - parameterList.add( new ParameterOccurrence( parameter, sqlStringBuffer.length() ) ); - sqlStringBuffer.append( "?" ); + recognizeParameter( parameter ); } @Override @@ -183,16 +180,21 @@ else if ( parameterStyle != ParameterStyle.NAMED ) { positionalQueryParameters.put( position, parameter ); } - if ( parameterList == null ) { - parameterList = new ArrayList<>(); - } - - parameterList.add( new ParameterOccurrence( parameter, sqlStringBuffer.length() ) ); - sqlStringBuffer.append( "?" ); + recognizeParameter( parameter ); } @Override public void other(char character) { sqlStringBuffer.append( character ); } + + private void recognizeParameter(QueryParameterImplementor parameter) { + final String marker = parameterMarkerStrategy.createMarker( nextParameterMarkerPosition++, null ); + final int markerLength = marker.length(); + if ( parameterList == null ) { + parameterList = new ArrayList<>(); + } + sqlStringBuffer.append( marker ); + parameterList.add( new ParameterOccurrence( parameter, sqlStringBuffer.length() - markerLength, markerLength ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/ParameterOccurrence.java b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/ParameterOccurrence.java index c1b421c0c841..e2518588aee5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/ParameterOccurrence.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/ParameterOccurrence.java @@ -9,5 +9,9 @@ /** * @author Christian Beikov */ -public record ParameterOccurrence(QueryParameterImplementor parameter, int sourcePosition) { +public record ParameterOccurrence(QueryParameterImplementor parameter, int sourcePosition, int length) { + @Deprecated(forRemoval = true) + public ParameterOccurrence(QueryParameterImplementor parameter, int sourcePosition) { + this( parameter, sourcePosition, 1 ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java index 9f8dfb979251..12395381f69e 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java @@ -27,6 +27,7 @@ import org.hibernate.query.spi.QueryOptions; import org.hibernate.resource.jdbc.spi.JdbcSessionContext; import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor; +import org.hibernate.sql.ast.spi.ParameterMarkerStrategy; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcLockStrategy; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; @@ -91,7 +92,17 @@ public DeferredResultSetAccess( limit = queryOptions.getLimit(); final boolean hasLimit = isHasLimit( jdbcSelect ); limitHandler = hasLimit ? NoopLimitHandler.NO_LIMIT : dialect.getLimitHandler(); - final String sqlWithLimit = hasLimit ? sql : limitHandler.processSql( sql, limit, queryOptions ); + + final String sqlWithLimit; + if ( hasLimit ) { + sqlWithLimit = sql; + } + else { + final int jdbcBindingsCnt = jdbcParameterBindings.getBindings().size(); + final ParameterMarkerStrategy parameterMarkerStrategy = + executionContext.getSession().getFactory().getServiceRegistry().requireService( ParameterMarkerStrategy.class ); + sqlWithLimit = limitHandler.processSql( sql, jdbcBindingsCnt, parameterMarkerStrategy, limit, queryOptions ); + } final LockOptions lockOptions = queryOptions.getLockOptions(); final JdbcLockStrategy jdbcLockStrategy = jdbcSelect.getLockStrategy(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/NativeParameterMarkerStrategyTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/NativeParameterMarkerStrategyTests.java new file mode 100644 index 000000000000..3ac69011e9de --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/NativeParameterMarkerStrategyTests.java @@ -0,0 +1,178 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.sql.ast; + +import java.util.List; + +import org.hibernate.query.NativeQuery; +import org.hibernate.query.sql.spi.NativeQueryImplementor; +import org.hibernate.sql.ast.spi.ParameterMarkerStrategy; +import org.hibernate.type.descriptor.jdbc.IntegerJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DialectContext; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SessionFactoryScopeAware; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Nathan Xu + */ +@ServiceRegistry( services = @ServiceRegistry.Service( + role = ParameterMarkerStrategy.class, + impl = NativeParameterMarkerStrategyTests.DialectParameterMarkerStrategy.class +) ) +@DomainModel( annotatedClasses = NativeParameterMarkerStrategyTests.Book.class ) +@SessionFactory( useCollectingStatementInspector = true ) +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsNonStandardNativeParameterRendering.class ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16283" ) +class NativeParameterMarkerStrategyTests implements SessionFactoryScopeAware { + + private enum ParameterStyle { + JDBC, + ORDINAL, + NAMED + } + + public static class DialectParameterMarkerStrategy implements ParameterMarkerStrategy { + + public static final DialectParameterMarkerStrategy INSTANCE = new DialectParameterMarkerStrategy(); + + @Override + public String createMarker(int position, JdbcType jdbcType) { + return DialectContext.getDialect().getNativeParameterMarkerStrategy().createMarker( position, jdbcType ); + } + } + + private SessionFactoryScope scope; + private SQLStatementInspector statementInspector; + + @Override + public void injectSessionFactoryScope(SessionFactoryScope scope) { + this.scope = scope; + } + + @BeforeEach + void setUp() { + statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + } + + @ParameterizedTest + @EnumSource(ParameterStyle.class) + void testHappyPath(ParameterStyle style) { + scope.inTransaction( (session) -> { + final NativeQueryImplementor nativeQuery; + final var parameterValue = "War and Peace"; + if ( style == ParameterStyle.NAMED ) { + nativeQuery = session.createNativeQuery( "select * from books b where b.title = :title", Book.class ) + .setParameter( "title", parameterValue ); + } else { + nativeQuery = session.createNativeQuery( "select * from books b where b.title = " + ( style == ParameterStyle.ORDINAL ? "?1" : "?" ), Book.class ) + .setParameter( 1, parameterValue ); + }; + nativeQuery.list(); + assertNativeQueryContainsMarkers( 1 ); + } ); + } + + @ParameterizedTest + @EnumSource(ParameterStyle.class) + void testParameterExpansion(ParameterStyle style) { + final var parameterValue = List.of( "Moby-Dick", "Don Quixote", "In Search of Lost Time" ); + + scope.inTransaction( (session) -> { + final NativeQuery nativeQuery; + if ( style == ParameterStyle.NAMED ) { + nativeQuery = session.createNativeQuery( "select * from books b where b.title in :titles", Book.class ) + .setParameterList( "titles", parameterValue ); + } else { + nativeQuery = session.createNativeQuery( "select * from books b where b.title in " + ( style == ParameterStyle.ORDINAL ? "?1" : "?" ), Book.class ) + .setParameterList( 1, parameterValue ); + }; + nativeQuery.list(); + assertNativeQueryContainsMarkers( parameterValue.size() ); + } ); + } + + @ParameterizedTest + @EnumSource(ParameterStyle.class) + void testLimitHandler(ParameterStyle style) { + scope.inTransaction( (session) -> { + final NativeQueryImplementor nativeQuery; + final var parameterValue = "Herman Melville"; + if ( style == ParameterStyle.NAMED ) { + nativeQuery = session.createNativeQuery( "select * from books b where b.author = :author", Book.class ) + .setParameter( "author", parameterValue ); + } else { + nativeQuery = session.createNativeQuery( "select * from books b where b.author = " + ( style == ParameterStyle.ORDINAL ? "?1" : "?" ), Book.class ) + .setParameter( 1, parameterValue ); + }; + nativeQuery.setFirstResult( 2 ).setMaxResults( 1 ).list(); + + assertNativeQueryContainsMarkers( 3 ); + } ); + } + + @ParameterizedTest + @EnumSource(ParameterStyle.class) + void test_parameterExpansionAndLimitHandler(ParameterStyle style) { + final var parameterValue = List.of( "Moby-Dick", "Don Quixote", "In Search of Lost Time" ); + + scope.inTransaction( (session) -> { + final NativeQueryImplementor nativeQuery; + if ( style == ParameterStyle.NAMED ) { + nativeQuery = session.createNativeQuery( "select * from books b where b.title in :titles", Book.class ) + .setParameterList( "titles", parameterValue ); + } else { + nativeQuery = session.createNativeQuery( "select * from books b where b.title in " + ( style == ParameterStyle.ORDINAL ? "?1" : "?" ), Book.class ) + .setParameterList( 1, parameterValue ); + }; + nativeQuery.setFirstResult( 1 ).setMaxResults( 3 ).list(); + + assertNativeQueryContainsMarkers( parameterValue.size() + 2 ); + } ); + } + + private void assertNativeQueryContainsMarkers(int expectedMarkerNum) { + + final var strategy = DialectParameterMarkerStrategy.INSTANCE; + + final var expectedMarkers = new String[expectedMarkerNum]; + for ( int i = 1; i <= expectedMarkerNum; i++ ) { + expectedMarkers[i - 1] = strategy.createMarker( i, IntegerJdbcType.INSTANCE ); + } + + final var unexpectedMarker = strategy.createMarker( expectedMarkerNum + 1, IntegerJdbcType.INSTANCE ); + + assertThat( statementInspector.getSqlQueries() ) + .singleElement() + .satisfies( query -> assertThat( query ).contains( expectedMarkers ).doesNotContain( unexpectedMarker ) ); + } + + @Entity + @Table(name = "books") + static class Book { + @Id + int id; + String title; + String author; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/ParameterMarkerStrategyTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/ParameterMarkerStrategyTests.java index 3258f8b8ee1b..f278e2ff3cad 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/ParameterMarkerStrategyTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/ast/ParameterMarkerStrategyTests.java @@ -4,7 +4,6 @@ */ package org.hibernate.orm.test.sql.ast; -import java.util.List; import org.hibernate.annotations.Filter; import org.hibernate.annotations.FilterDef; @@ -16,7 +15,6 @@ 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.FailureExpected; import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.testing.orm.junit.ServiceRegistry; @@ -124,35 +122,6 @@ public void testMutations(SessionFactoryScope scope) { } ); } - @Test - @FailureExpected - @Jira( "https://hibernate.atlassian.net/browse/HHH-16283" ) - public void testNativeQuery(SessionFactoryScope scope) { - final SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); - - statementInspector.clear(); - scope.inTransaction( (session) -> { - session.createNativeQuery( "select count(1) from filtered_entity e where e.region = :region" ) - .setParameter( "region", "ABC" ) - .uniqueResult(); - - assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); - assertThat( count( statementInspector.getSqlQueries().get( 0 ), "?" ) ).isEqualTo( 1 ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "?1" ); - } ); - - statementInspector.clear(); - scope.inTransaction( (session) -> { - session.createNativeQuery( "select count(1) from filtered_entity e where e.region in (:region)" ) - .setParameterList( "region", List.of( "ABC", "DEF" ) ) - .uniqueResult(); - - assertThat( statementInspector.getSqlQueries() ).hasSize( 1 ); - assertThat( count( statementInspector.getSqlQueries().get( 0 ), "?" ) ).isEqualTo( 1 ); - assertThat( statementInspector.getSqlQueries().get( 0 ) ).contains( "?1" ); - } ); - } - @AfterEach public void cleanUpTestData(SessionFactoryScope scope) { scope.getSessionFactory().getSchemaManager().truncate(); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index b27b78efe6d3..e98def7f1f6c 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -81,6 +81,7 @@ import org.hibernate.query.sqm.function.SqmFunctionDescriptor; import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.ast.internal.ParameterMarkerStrategyStandard; import org.hibernate.sql.ast.spi.StringBuilderSqlAppender; import org.hibernate.testing.boot.BootstrapContextImpl; import org.hibernate.type.SqlTypes; @@ -1035,6 +1036,13 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsNonStandardNativeParameterRendering implements DialectFeatureCheck { + @Override + public boolean apply(Dialect dialect) { + return !ParameterMarkerStrategyStandard.isStandardRenderer( dialect.getNativeParameterMarkerStrategy() ); + } + } + private static final HashMap FUNCTION_REGISTRIES = new HashMap<>(); public static boolean definesFunction(Dialect dialect, String functionName) {