From 42b3d70d641df89784148f17c084d5a76868e587 Mon Sep 17 00:00:00 2001 From: Rob Green Date: Sat, 14 Jun 2025 07:38:48 -0400 Subject: [PATCH 1/3] HHH-8370 - Optimize sql server IN predicate using derived table --- .../java/org/hibernate/dialect/Dialect.java | 21 ++++ .../hibernate/dialect/SQLServerDialect.java | 6 +- .../sql/ast/spi/AbstractSqlAstTranslator.java | 47 +++++++++ .../SQLServerDialectCompositeTest.java | 98 +++++++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index f566d03caf27..cf7406d16b72 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -6208,4 +6208,25 @@ public boolean supportsRowValueConstructorSyntaxInInSubQuery() { return supportsRowValueConstructorSyntaxInInList(); } + /** + * If the dialect supports {@link org.hibernate.dialect.Dialect#supportsRowValueConstructorSyntax() row values}, + * does it allow using them in a derived table within an EXISTS predicate that emulates an IN-list? + *

+ * For example: + *

+	 *     SELECT * FROM EntityTable T
+	 *     WHERE EXISTS (
+	 *         SELECT 1 FROM (VALUES (?, ?), (?, ?)) AS V(FIELD1, FIELD2)
+	 *         WHERE T.FIELD1 = V.FIELD1 AND T.FIELD2 = V.FIELD2
+	 *     )
+	 * 
+ * This pattern avoids the SQL length and performance issues of large disjunctions, + * and emulates tuple-based IN-list comparisons using a derived VALUES table. + * + * @return True if this SQL dialect is known to support row value constructors in + * derived tables for EXISTS-based IN-list emulation; false otherwise. + */ + public boolean supportsRowValueConstructorSyntaxInDerivedTableInList() { + return false; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index cce6a6d51412..378049c60d0e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -1245,7 +1245,7 @@ public boolean supportsSimpleQueryGrouping() { @Override public boolean supportsRowValueConstructorSyntax() { - return false; + return getVersion().isSameOrAfter( 10 ); } @Override @@ -1263,4 +1263,8 @@ public boolean supportsRowValueConstructorSyntaxInInList() { return false; } + @Override + public boolean supportsRowValueConstructorSyntaxInDerivedTableInList() { + return getVersion().isSameOrAfter( 10 ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index d140524d8bd0..20148c30e6fd 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -21,6 +21,7 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.hibernate.AssertionFailure; import org.hibernate.Internal; @@ -7642,6 +7643,52 @@ public void visitInListPredicate(InListPredicate inListPredicate) { getSqlTuple( listExpression ) .getExpressions().get( 0 ); } + else if ( dialect.supportsRowValueConstructorSyntaxInDerivedTableInList() ) { + if ( inListPredicate.isNegated() ) { + appendSql( "not " ); + } + appendSql( "exists (select 1 from (values " ); + + List listTuples = listExpressions.stream() + .map( SqlTupleContainer::getSqlTuple ) + .toList(); + for ( int i = 0; i < listTuples.size(); i++ ) { + if ( i > 0 ) { + appendSql( ", " ); + } + + appendSql( OPEN_PARENTHESIS ); + renderCommaSeparatedSelectExpression( listTuples.get(i).getExpressions() ); + appendSql( CLOSE_PARENTHESIS ); + } + + List expressions = lhsTuple.getExpressions() + .stream() + .filter( expression -> expression.getColumnReference() != null ) + .collect( Collectors.toList() ); + appendSql( ") as v(" ); + for ( int i = 0; i < expressions.size(); i++ ) { + if ( i > 0 ) { + appendSql( ", " ); + } + appendSql( expressions.get( i ).getColumnReference().getColumnExpression() ); + } + + appendSql( ") where " ); + for ( int i = 0; i < expressions.size(); i++ ) { + if ( i > 0 ) { + appendSql( " and " ); + } + + Expression expression = expressions.get( i ); + expression.accept( this ); + appendSql( " = v." ); + appendSql( expression.getColumnReference().getColumnExpression() ); + } + + appendSql( CLOSE_PARENTHESIS ); + return; + } else if ( !dialect.supportsRowValueConstructorSyntaxInInList() ) { final ComparisonOperator comparisonOperator = inListPredicate.isNegated() ? ComparisonOperator.NOT_EQUAL : diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java new file mode 100644 index 000000000000..ec63f6dd6cf3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java @@ -0,0 +1,98 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.dialect.functional; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.orm.test.jpa.CompositeId; +import org.hibernate.orm.test.jpa.EntityWithCompositeId; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Rob Green + */ +@JiraKey( value = "HHH-8370" ) +@RequiresDialect( value = SQLServerDialect.class, majorVersion = 10 ) +@Jpa( + annotatedClasses = { EntityWithCompositeId.class }, + integrationSettings = { + @Setting( name = AvailableSettings.USE_SQL_COMMENTS, value = "true" ), + }, + useCollectingStatementInspector = true +) +public class SQLServerDialectCompositeTest { + @Test + public void testCompositeQueryWithInPredicate(EntityManagerFactoryScope scope) { + final SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + sqlStatementInterceptor.clear(); + + List compositeIds = new ArrayList<>(); + compositeIds.add( new CompositeId( 1,2 ) ); + compositeIds.add( new CompositeId( 3,4 ) ); + + scope.inTransaction( entityManager -> { + entityManager.createQuery( "SELECT e FROM EntityWithCompositeId e WHERE e.id IN (:ids)" ) + .setParameter( "ids", compositeIds ) + .getResultList(); + } + ); + + var query = sqlStatementInterceptor.getSqlQueries().get( 0 ); + assertTrue(query.endsWith( "where exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" )); + } + + @Test + public void testCompositeQueryWithNotInPredicate(EntityManagerFactoryScope scope) { + final SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + sqlStatementInterceptor.clear(); + + List compositeIds = new ArrayList<>(); + compositeIds.add( new CompositeId( 1,2 ) ); + compositeIds.add( new CompositeId( 3,4 ) ); + + scope.inTransaction( entityManager -> { + entityManager.createQuery( "SELECT e FROM EntityWithCompositeId e WHERE e.id NOT IN (:ids)" ) + .setParameter( "ids", compositeIds ) + .getResultList(); + } + ); + + var query = sqlStatementInterceptor.getSqlQueries().get( 0 ); + assertTrue(query.endsWith( "where not exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" )); + } + + @Test + public void testCompositeQueryWithMultiplePredicatesIncludingIn(EntityManagerFactoryScope scope) { + final SQLStatementInspector sqlStatementInterceptor = scope.getCollectingStatementInspector(); + sqlStatementInterceptor.clear(); + + List compositeIds = new ArrayList<>(); + compositeIds.add( new CompositeId( 1,2 ) ); + compositeIds.add( new CompositeId( 3,4 ) ); + + scope.inTransaction( entityManager -> { + entityManager.createQuery("SELECT e FROM EntityWithCompositeId e WHERE e.description = :description AND e.id IN (:ids)") + .setParameter( "ids", compositeIds ) + .setParameter( "description", "test" ) + .getResultList(); + } + ); + + var query = sqlStatementInterceptor.getSqlQueries().get( 0 ); + assertTrue(query.endsWith( "where ewci1_0.description=? and exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" )); + } +} From a517221a237e2ce24b0950fdad17f34b78223313 Mon Sep 17 00:00:00 2001 From: Rob Green Date: Sat, 14 Jun 2025 07:42:32 -0400 Subject: [PATCH 2/3] HHH-8370 - spacing --- .../dialect/functional/SQLServerDialectCompositeTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java index ec63f6dd6cf3..930b217f33af 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java @@ -52,7 +52,7 @@ public void testCompositeQueryWithInPredicate(EntityManagerFactoryScope scope) { ); var query = sqlStatementInterceptor.getSqlQueries().get( 0 ); - assertTrue(query.endsWith( "where exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" )); + assertTrue( query.endsWith( "where exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" ) ); } @Test @@ -72,7 +72,7 @@ public void testCompositeQueryWithNotInPredicate(EntityManagerFactoryScope scope ); var query = sqlStatementInterceptor.getSqlQueries().get( 0 ); - assertTrue(query.endsWith( "where not exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" )); + assertTrue( query.endsWith( "where not exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" ) ); } @Test @@ -85,7 +85,7 @@ public void testCompositeQueryWithMultiplePredicatesIncludingIn(EntityManagerFac compositeIds.add( new CompositeId( 3,4 ) ); scope.inTransaction( entityManager -> { - entityManager.createQuery("SELECT e FROM EntityWithCompositeId e WHERE e.description = :description AND e.id IN (:ids)") + entityManager.createQuery( "SELECT e FROM EntityWithCompositeId e WHERE e.description = :description AND e.id IN (:ids)" ) .setParameter( "ids", compositeIds ) .setParameter( "description", "test" ) .getResultList(); @@ -93,6 +93,6 @@ public void testCompositeQueryWithMultiplePredicatesIncludingIn(EntityManagerFac ); var query = sqlStatementInterceptor.getSqlQueries().get( 0 ); - assertTrue(query.endsWith( "where ewci1_0.description=? and exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" )); + assertTrue( query.endsWith( "where ewci1_0.description=? and exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" ) ); } } From 07b44a62a48714f20dfbae199ad43d4a039dc5d6 Mon Sep 17 00:00:00 2001 From: Rob Green Date: Mon, 16 Jun 2025 09:18:43 -0400 Subject: [PATCH 3/3] HHH-8370 remove support for row value constructor and adjust naming of new support --- .../java/org/hibernate/dialect/Dialect.java | 11 ++----- .../hibernate/dialect/SQLServerDialect.java | 4 +-- .../sql/ast/spi/AbstractSqlAstTranslator.java | 32 ++++++------------- .../SQLServerDialectCompositeTest.java | 6 ++-- 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index cf7406d16b72..3ba29c0df717 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -6209,8 +6209,8 @@ public boolean supportsRowValueConstructorSyntaxInInSubQuery() { } /** - * If the dialect supports {@link org.hibernate.dialect.Dialect#supportsRowValueConstructorSyntax() row values}, - * does it allow using them in a derived table within an EXISTS predicate that emulates an IN-list? + * This pattern avoids the SQL length and performance issues of large disjunctions, + * and emulates tuple-based IN-list comparisons using a derived VALUES table. *

* For example: *

@@ -6220,13 +6220,8 @@ public boolean supportsRowValueConstructorSyntaxInInSubQuery() {
 	 *         WHERE T.FIELD1 = V.FIELD1 AND T.FIELD2 = V.FIELD2
 	 *     )
 	 * 
- * This pattern avoids the SQL length and performance issues of large disjunctions, - * and emulates tuple-based IN-list comparisons using a derived VALUES table. - * - * @return True if this SQL dialect is known to support row value constructors in - * derived tables for EXISTS-based IN-list emulation; false otherwise. */ - public boolean supportsRowValueConstructorSyntaxInDerivedTableInList() { + public boolean supportsValuesListForInListExistsEmulation() { return false; } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java index 378049c60d0e..48480f626c4d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -1245,7 +1245,7 @@ public boolean supportsSimpleQueryGrouping() { @Override public boolean supportsRowValueConstructorSyntax() { - return getVersion().isSameOrAfter( 10 ); + return false; } @Override @@ -1264,7 +1264,7 @@ public boolean supportsRowValueConstructorSyntaxInInList() { } @Override - public boolean supportsRowValueConstructorSyntaxInDerivedTableInList() { + public boolean supportsValuesListForInListExistsEmulation() { return getVersion().isSameOrAfter( 10 ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 20148c30e6fd..c0c122b50a24 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -21,7 +21,6 @@ import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; import org.hibernate.AssertionFailure; import org.hibernate.Internal; @@ -7643,49 +7642,38 @@ public void visitInListPredicate(InListPredicate inListPredicate) { getSqlTuple( listExpression ) .getExpressions().get( 0 ); } - else if ( dialect.supportsRowValueConstructorSyntaxInDerivedTableInList() ) { + else if ( dialect.supportsValuesListForInListExistsEmulation() ) { if ( inListPredicate.isNegated() ) { appendSql( "not " ); } - appendSql( "exists (select 1 from (values " ); - List listTuples = listExpressions.stream() - .map( SqlTupleContainer::getSqlTuple ) - .toList(); - for ( int i = 0; i < listTuples.size(); i++ ) { + appendSql( "exists (select 1 from (values " ); + for ( int i = 0; i < listExpressions.size(); i++ ) { if ( i > 0 ) { appendSql( ", " ); } - appendSql( OPEN_PARENTHESIS ); - renderCommaSeparatedSelectExpression( listTuples.get(i).getExpressions() ); + renderCommaSeparatedSelectExpression( List.of( listExpressions.get( i ) ) ); appendSql( CLOSE_PARENTHESIS ); } - - List expressions = lhsTuple.getExpressions() - .stream() - .filter( expression -> expression.getColumnReference() != null ) - .collect( Collectors.toList() ); appendSql( ") as v(" ); + + final List expressions = lhsTuple.getExpressions(); for ( int i = 0; i < expressions.size(); i++ ) { if ( i > 0 ) { appendSql( ", " ); } - appendSql( expressions.get( i ).getColumnReference().getColumnExpression() ); + appendSql( "col_" + i ); } - appendSql( ") where " ); + for ( int i = 0; i < expressions.size(); i++ ) { if ( i > 0 ) { appendSql( " and " ); } - - Expression expression = expressions.get( i ); - expression.accept( this ); - appendSql( " = v." ); - appendSql( expression.getColumnReference().getColumnExpression() ); + expressions.get( i ).accept( this ); + appendSql( " = v.col_" + i ); } - appendSql( CLOSE_PARENTHESIS ); return; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java index 930b217f33af..fe043b4e07e5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/functional/SQLServerDialectCompositeTest.java @@ -52,7 +52,7 @@ public void testCompositeQueryWithInPredicate(EntityManagerFactoryScope scope) { ); var query = sqlStatementInterceptor.getSqlQueries().get( 0 ); - assertTrue( query.endsWith( "where exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" ) ); + assertTrue( query.endsWith( "where exists (select 1 from (values (?,?), (?,?)) as v(col_0, col_1) where ewci1_0.id1 = v.col_0 and ewci1_0.id2 = v.col_1)" ) ); } @Test @@ -72,7 +72,7 @@ public void testCompositeQueryWithNotInPredicate(EntityManagerFactoryScope scope ); var query = sqlStatementInterceptor.getSqlQueries().get( 0 ); - assertTrue( query.endsWith( "where not exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" ) ); + assertTrue( query.endsWith( "where not exists (select 1 from (values (?,?), (?,?)) as v(col_0, col_1) where ewci1_0.id1 = v.col_0 and ewci1_0.id2 = v.col_1)" ) ); } @Test @@ -93,6 +93,6 @@ public void testCompositeQueryWithMultiplePredicatesIncludingIn(EntityManagerFac ); var query = sqlStatementInterceptor.getSqlQueries().get( 0 ); - assertTrue( query.endsWith( "where ewci1_0.description=? and exists (select 1 from (values (?,?), (?,?)) as v(id1, id2) where ewci1_0.id1 = v.id1 and ewci1_0.id2 = v.id2)" ) ); + assertTrue( query.endsWith( "where ewci1_0.description=? and exists (select 1 from (values (?,?), (?,?)) as v(col_0, col_1) where ewci1_0.id1 = v.col_0 and ewci1_0.id2 = v.col_1)" ) ); } }