From 3ecc602852c68dc02a984c6fb4d79268a7bbdc47 Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Thu, 23 Sep 2021 00:20:57 +0200 Subject: [PATCH] Implement tuple count emulation --- .../hibernate/dialect/CockroachDialect.java | 5 + .../org/hibernate/dialect/DerbyDialect.java | 4 +- .../java/org/hibernate/dialect/Dialect.java | 2 +- .../java/org/hibernate/dialect/H2Dialect.java | 8 +- .../org/hibernate/dialect/HSQLDialect.java | 5 + .../org/hibernate/dialect/MySQLDialect.java | 5 + .../hibernate/dialect/PostgreSQLDialect.java | 5 + .../function/CommonFunctionFactory.java | 12 +- .../dialect/function/CountFunction.java | 140 ++++++++++++++++++ .../internal/ToOneAttributeMapping.java | 4 - .../sqm/internal/SqmCriteriaNodeBuilder.java | 2 +- .../hibernate/sql/ast/SqlAstTranslator.java | 6 + .../sql/ast/spi/AbstractSqlAstTranslator.java | 5 + .../ast/tree/from/CompositeTableGroup.java | 12 -- .../EntityGraphAttributeResolutionTest.java | 2 +- 15 files changed, 187 insertions(+), 30 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index 3edf375eda..6dd60d3964 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -215,6 +215,11 @@ public class CockroachDialect extends Dialect { return NullOrdering.SMALLEST; } + @Override + public boolean supportsTupleCounts() { + return true; + } + @Override public boolean requiresParensForTupleDistinctCounts() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java index 717cedd549..f768723170 100755 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java @@ -21,7 +21,6 @@ import org.hibernate.dialect.identity.IdentityColumnSupport; import org.hibernate.dialect.pagination.AbstractLimitHandler; import org.hibernate.dialect.pagination.DerbyLimitHandler; import org.hibernate.dialect.pagination.LimitHandler; -import org.hibernate.dialect.sequence.DB2SequenceSupport; import org.hibernate.dialect.sequence.DerbySequenceSupport; import org.hibernate.dialect.sequence.SequenceSupport; import org.hibernate.engine.jdbc.Size; @@ -53,7 +52,6 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorDerbyDatabaseImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorNoOpImpl; import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; -import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.jdbc.DecimalTypeDescriptor; import org.hibernate.type.descriptor.jdbc.JdbcTypeDescriptor; import org.hibernate.type.descriptor.jdbc.SmallIntTypeDescriptor; @@ -185,7 +183,7 @@ public class DerbyDialect extends Dialect { super.initializeFunctionRegistry( queryEngine ); // Derby needs an actual argument type for aggregates like SUM, AVG, MIN, MAX to determine the result type - CommonFunctionFactory.aggregates( queryEngine, SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER ); + CommonFunctionFactory.aggregates( this, queryEngine, SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER ); CommonFunctionFactory.concat_pipeOperator( queryEngine ); CommonFunctionFactory.cot( queryEngine ); 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 60d3d9f6c9..d14c12efc8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -476,7 +476,7 @@ public abstract class Dialect implements ConversionContext { //aggregate functions, supported on every database - CommonFunctionFactory.aggregates( queryEngine, SqlAstNodeRenderingMode.DEFAULT ); + CommonFunctionFactory.aggregates( this, queryEngine, SqlAstNodeRenderingMode.DEFAULT ); //the ANSI SQL-defined aggregate functions any() and every() are only //supported on one database, but can be emulated using sum() and case, diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 9f336fb300..b08366306c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -46,7 +46,6 @@ import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorH2DatabaseImpl; import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl; @@ -157,7 +156,7 @@ public class H2Dialect extends Dialect { super.initializeFunctionRegistry( queryEngine ); // H2 needs an actual argument type for aggregates like SUM, AVG, MIN, MAX to determine the result type - CommonFunctionFactory.aggregates( queryEngine, SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER ); + CommonFunctionFactory.aggregates( this, queryEngine, SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER ); CommonFunctionFactory.pi( queryEngine ); CommonFunctionFactory.cot( queryEngine ); @@ -416,6 +415,11 @@ public class H2Dialect extends Dialect { return false; } + @Override + public boolean supportsTupleCounts() { + return true; + } + @Override public boolean requiresParensForTupleDistinctCounts() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index e8ec42332c..28cb07cda5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -650,6 +650,11 @@ public class HSQLDialect extends Dialect { return String.valueOf( bool ); } + @Override + public boolean supportsTupleCounts() { + return true; + } + @Override public boolean supportsTupleDistinctCounts() { // from v. 2.2.9 is added support for COUNT(DISTINCT ...) with multiple arguments diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 96ed44c0f8..40cd97b6c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -494,6 +494,11 @@ public class MySQLDialect extends Dialect { return getMySQLVersion() >= 570; } + @Override + public boolean supportsTupleCounts() { + return true; + } + @Override public boolean supportsUnionAll() { return getMySQLVersion() >= 500; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index f2f78c11c9..1bbc52901e 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -531,6 +531,11 @@ public class PostgreSQLDialect extends Dialect { return "select now()"; } + @Override + public boolean supportsTupleCounts() { + return true; + } + @Override public boolean requiresParensForTupleDistinctCounts() { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java index 25bfa9b4f5..38f8725e34 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CommonFunctionFactory.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Arrays; import java.util.function.Supplier; +import org.hibernate.dialect.Dialect; import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.model.domain.AllowableFunctionReturnType; @@ -1483,7 +1484,10 @@ public class CommonFunctionFactory { .register(); } - public static void aggregates(QueryEngine queryEngine, SqlAstNodeRenderingMode inferenceArgumentRenderingMode) { + public static void aggregates( + Dialect dialect, + QueryEngine queryEngine, + SqlAstNodeRenderingMode inferenceArgumentRenderingMode) { queryEngine.getSqmFunctionRegistry().namedAggregateDescriptorBuilder( "max" ) .setArgumentRenderingMode( inferenceArgumentRenderingMode ) .setExactArgumentCount( 1 ) @@ -1603,11 +1607,7 @@ public class CommonFunctionFactory { .setExactArgumentCount( 1 ) .register(); - queryEngine.getSqmFunctionRegistry().namedAggregateDescriptorBuilder( "count" ) - .setInvariantType( StandardBasicTypes.LONG ) - .setExactArgumentCount( 1 ) - .setArgumentListSignature( "([distinct ]{arg|*})" ) - .register(); + queryEngine.getSqmFunctionRegistry().register( CountFunction.FUNCTION_NAME, new CountFunction( dialect ) ); } public static void math(QueryEngine queryEngine) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java new file mode 100644 index 0000000000..ad8c56f0fb --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/CountFunction.java @@ -0,0 +1,140 @@ +/* + * 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.dialect.function; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.dialect.Dialect; +import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.function.FunctionKind; +import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.sql.internal.AbstractSqmPathInterpretation; +import org.hibernate.query.sqm.sql.internal.EntityValuedPathInterpretation; +import org.hibernate.query.sqm.sql.internal.NonAggregatedCompositeValuedPathInterpretation; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; +import org.hibernate.sql.ast.tree.expression.Distinct; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.NullnessLiteral; +import org.hibernate.sql.ast.tree.expression.QueryLiteral; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; +import org.hibernate.sql.ast.tree.expression.Star; +import org.hibernate.sql.ast.tree.predicate.Junction; +import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.type.StandardBasicTypes; + +/** + * @author Christian Beikov + */ +public class CountFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + + public static final String FUNCTION_NAME = "count"; + private final Dialect dialect; + + public CountFunction(Dialect dialect) { + super( + FUNCTION_NAME, + FunctionKind.AGGREGATE, + StandardArgumentsValidators.exactly( 1 ), + StandardFunctionReturnTypeResolvers.invariant( StandardBasicTypes.LONG ) + ); + this.dialect = dialect; + } + + @Override + public void render(SqlAppender sqlAppender, List sqlAstArguments, SqlAstTranslator walker) { + render( sqlAppender, sqlAstArguments, null, walker ); + } + + @Override + public void render( + SqlAppender sqlAppender, + List sqlAstArguments, + Predicate filter, + SqlAstTranslator translator) { + final boolean caseWrapper = filter != null && !translator.supportsFilterClause(); + final SqlAstNode arg = sqlAstArguments.get( 0 ); + final SqlAstNode realArg; + sqlAppender.appendSql( "count(" ); + if ( arg instanceof Distinct ) { + sqlAppender.appendSql( "distinct " ); + final Expression distinctArg = ( (Distinct) arg ).getExpression(); + // todo (6.0): emulate tuple count distinct if necessary + realArg = distinctArg; + } + else { + // If the table group supports inner joins, this means that it is non-optional, + // which means we can omit the tuples and instead use count(*) + final SqlTuple tuple; + if ( ( arg instanceof EntityValuedPathInterpretation || arg instanceof NonAggregatedCompositeValuedPathInterpretation ) + && ( (AbstractSqmPathInterpretation) arg ).getTableGroup().canUseInnerJoins() ) { + realArg = Star.INSTANCE; + } + else if ( !dialect.supportsTupleCounts() && ( tuple = SqlTupleContainer.getSqlTuple( arg ) ) != null ) { + final List expressions = tuple.getExpressions(); + if ( expressions.size() == 1 ) { + realArg = expressions.get( 0 ); + } + else { + final List whenFragments = new ArrayList<>( 1 ); + final Junction junction = new Junction( Junction.Nature.DISJUNCTION ); + for ( Expression expression : expressions ) { + junction.add( new NullnessPredicate( expression ) ); + } + whenFragments.add( + new CaseSearchedExpression.WhenFragment( + junction, + new NullnessLiteral( StandardBasicTypes.INTEGER ) + ) + ); + realArg = new CaseSearchedExpression( + StandardBasicTypes.INTEGER, + whenFragments, + new QueryLiteral<>( 1, StandardBasicTypes.INTEGER ) + ); + } + } + else { + realArg = arg; + } + } + if ( caseWrapper ) { + sqlAppender.appendSql( "case when " ); + filter.accept( translator ); + sqlAppender.appendSql( " then " ); + if ( realArg instanceof Star ) { + sqlAppender.appendSql( "1" ); + } + else { + translator.render( realArg, SqlAstNodeRenderingMode.DEFAULT ); + } + sqlAppender.appendSql( " else null end" ); + } + else { + translator.render( realArg, SqlAstNodeRenderingMode.DEFAULT ); + } + sqlAppender.appendSql( ')' ); + if ( filter != null && !caseWrapper ) { + sqlAppender.appendSql( " filter (where " ); + filter.accept( translator ); + sqlAppender.appendSql( ')' ); + } + } + + @Override + public String getArgumentListSignature() { + return "([distinct ]{arg|*})"; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index ca100c901b..2e00eccac8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java @@ -935,10 +935,6 @@ public class ToOneAttributeMapping if ( !canUseParentTableGroup ) { return false; } -// // Special case for resolving the table group for entity valued paths -// if ( np == navigablePath ) { -// return true; -// } NavigablePath path = np.getParent(); // Fast path if ( path != null && navigablePath.equals( path ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index a71b21af01..ee662b9c29 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -1835,7 +1835,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, return new SqmComparisonPredicate( (SqmExpression) x, ComparisonOperator.LESS_THAN_OR_EQUAL, - (SqmExpression) y, + value( y, (SqmExpression) x ), this ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java index 9ef8892811..5ce136d9a8 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/SqlAstTranslator.java @@ -10,6 +10,7 @@ import java.util.Set; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.SqlAstNode; +import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -28,6 +29,11 @@ public interface SqlAstTranslator extends SqlAstWalker */ boolean supportsFilterClause(); + /** + * Returns the current query part that is translated. + */ + QueryPart getCurrentQueryPart(); + /** * Not the best spot for this. Its the table names collected while walking the SQL AST. * Its ok here because the translator is consider a one-time-use. It just needs to be called 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 75eabfe5f5..3e301f0e4e 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 @@ -530,6 +530,11 @@ public abstract class AbstractSqlAstTranslator implemen return queryPartStack; } + @Override + public QueryPart getCurrentQueryPart() { + return queryPartStack.getCurrent(); + } + @Override public T translate(JdbcParameterBindings jdbcParameterBindings, QueryOptions queryOptions) { try { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/CompositeTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/CompositeTableGroup.java index 1f4579fd41..5eed0a2dd8 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/CompositeTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/CompositeTableGroup.java @@ -55,18 +55,6 @@ public class CompositeTableGroup implements VirtualTableGroup { @Override public boolean isFetched() { -// if ( fetched ) { -// return true; -// } -// // We also consider it "fetched" if it contains fetched joins -// if ( tableGroupJoins != null ) { -// for ( TableGroupJoin tableGroupJoin : tableGroupJoins ) { -// if ( tableGroupJoin.getJoinedGroup().isFetched() ) { -// return true; -// } -// } -// } -// return false; return fetched; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/graphs/EntityGraphAttributeResolutionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/graphs/EntityGraphAttributeResolutionTest.java index b31425edea..adb1caee2a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/graphs/EntityGraphAttributeResolutionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/graphs/EntityGraphAttributeResolutionTest.java @@ -109,7 +109,7 @@ public class EntityGraphAttributeResolutionTest extends BaseEntityManagerFunctio attributeNodes = { @NamedAttributeNode("permissions") }) - @Table(name = "groups") // Name 'group' not accepted by H2 + @Table( name = "t_group") // Name 'group' not accepted by H2 public static class Group { public static final String ENTITY_GRAPH = "group-with-permissions";