Implement handling of top level limit/offset/fetch clause when collection fetches are involved

This commit is contained in:
Christian Beikov 2022-01-04 17:32:56 +01:00
parent afe9b6744e
commit 6e8f344a9f
4 changed files with 85 additions and 10 deletions

View File

@ -2954,6 +2954,21 @@ public class HQLTest extends BaseEntityManagerFunctionalTestCase {
}); });
} }
@Test
public void test_hql_bad_fetch_first_example() {
doInJPA(this::entityManagerFactory, entityManager -> {
List<Phone> wrongCalls = entityManager.createQuery(
"select p " +
"from Phone p " +
"join fetch p.calls " +
"order by p " +
"fetch first 50 percent rows only",
Phone.class)
.getResultList();
});
}
@Test @Test
public void test_hql_read_only_entities_example() { public void test_hql_read_only_entities_example() {

View File

@ -37,6 +37,7 @@ import org.hibernate.query.ImmutableEntityUpdateQueryHandlingMode;
import org.hibernate.query.Query; import org.hibernate.query.Query;
import org.hibernate.query.QueryTypeMismatchException; import org.hibernate.query.QueryTypeMismatchException;
import org.hibernate.query.SemanticException; import org.hibernate.query.SemanticException;
import org.hibernate.query.criteria.JpaExpression;
import org.hibernate.query.hql.internal.NamedHqlQueryMementoImpl; import org.hibernate.query.hql.internal.NamedHqlQueryMementoImpl;
import org.hibernate.query.hql.internal.QuerySplitter; import org.hibernate.query.hql.internal.QuerySplitter;
import org.hibernate.query.hql.spi.HqlQueryImplementor; import org.hibernate.query.hql.spi.HqlQueryImplementor;
@ -69,6 +70,7 @@ import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement;
import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.domain.SqmPath;
import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter;
import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper;
import org.hibernate.query.sqm.tree.expression.SqmLiteral;
import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.expression.SqmParameter;
import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.from.SqmRoot;
import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement;
@ -678,7 +680,7 @@ public class QuerySqmImpl<R>
getSession().prepareForQueryExecution( requiresTxn( getLockOptions().findGreatestLockMode() ) ); getSession().prepareForQueryExecution( requiresTxn( getLockOptions().findGreatestLockMode() ) );
final boolean containsCollectionFetches = selectStatement.containsCollectionFetches(); final boolean containsCollectionFetches = selectStatement.containsCollectionFetches();
final boolean hasLimit = queryOptions.hasLimit(); final boolean hasLimit = queryOptions.hasLimit() || selectStatement.getFetch() != null || selectStatement.getOffset() != null;
final boolean needsDistincting = containsCollectionFetches && ( final boolean needsDistincting = containsCollectionFetches && (
selectStatement.usesDistinct() || selectStatement.usesDistinct() ||
queryOptions.getGraph() != null || queryOptions.getGraph() != null ||
@ -723,10 +725,10 @@ public class QuerySqmImpl<R>
int includedCount = -1; int includedCount = -1;
// NOTE : firstRow is zero-based // NOTE : firstRow is zero-based
final int first = !hasLimit || queryOptions.getLimit().getFirstRow() == null final int first = !hasLimit || queryOptions.getLimit().getFirstRow() == null
? 0 ? getIntegerLiteral( selectStatement.getOffset(), 0 )
: queryOptions.getLimit().getFirstRow(); : queryOptions.getLimit().getFirstRow();
final int max = !hasLimit || queryOptions.getLimit().getMaxRows() == null final int max = !hasLimit || queryOptions.getLimit().getMaxRows() == null
? -1 ? getMaxRows( selectStatement, list.size() )
: queryOptions.getLimit().getMaxRows(); : queryOptions.getLimit().getMaxRows();
final List<R> tmp = new ArrayList<>( list.size() ); final List<R> tmp = new ArrayList<>( list.size() );
final IdentitySet<R> distinction = new IdentitySet<>( list.size() ); final IdentitySet<R> distinction = new IdentitySet<>( list.size() );
@ -749,6 +751,52 @@ public class QuerySqmImpl<R>
return list; return list;
} }
private int getMaxRows(SqmSelectStatement<?> selectStatement, int size) {
final JpaExpression<Number> expression = selectStatement.getFetch();
if ( expression == null ) {
return -1;
}
final Number fetchValue;
if ( expression instanceof SqmLiteral<?> ) {
fetchValue = ( (SqmLiteral<Number>) expression ).getLiteralValue();
}
else if ( expression instanceof SqmParameter<?> ) {
fetchValue = getParameterValue( (Parameter<Number>) expression );
if ( fetchValue == null ) {
return -1;
}
}
else {
throw new IllegalArgumentException( "Can't get max rows value from: " + expression );
}
// Note that we can never have ties because this is only used when we de-duplicate results
switch ( selectStatement.getFetchClauseType() ) {
case ROWS_ONLY:
case ROWS_WITH_TIES:
return fetchValue.intValue();
case PERCENT_ONLY:
case PERCENT_WITH_TIES:
return (int) Math.ceil( ( ( (double) size ) * fetchValue.doubleValue() ) / 100d );
}
throw new UnsupportedOperationException( "Unsupported fetch clause type: " + selectStatement.getFetchClauseType() );
}
private int getIntegerLiteral(JpaExpression<Number> expression, int defaultValue) {
if ( expression == null ) {
return defaultValue;
}
if ( expression instanceof SqmLiteral<?> ) {
return ( (SqmLiteral<Number>) expression ).getLiteralValue().intValue();
}
else if ( expression instanceof SqmParameter<?> ) {
final Number parameterValue = getParameterValue( (Parameter<Number>) expression );
return parameterValue == null ? defaultValue : parameterValue.intValue();
}
throw new IllegalArgumentException( "Can't get integer literal value from: " + expression );
}
private boolean requiresTxn(LockMode lockMode) { private boolean requiresTxn(LockMode lockMode) {
return lockMode != null && lockMode.greaterThan( LockMode.READ ); return lockMode != null && lockMode.greaterThan( LockMode.READ );
} }

View File

@ -361,6 +361,7 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
private boolean resolvingCircularFetch; private boolean resolvingCircularFetch;
private ForeignKeyDescriptor.Nature currentlyResolvingForeignKeySide; private ForeignKeyDescriptor.Nature currentlyResolvingForeignKeySide;
private SqmQueryPart<?> currentSqmQueryPart; private SqmQueryPart<?> currentSqmQueryPart;
private boolean containsCollectionFetches;
private final Map<String, PredicateCollector> collectionFilterPredicates = new HashMap<>(); private final Map<String, PredicateCollector> collectionFilterPredicates = new HashMap<>();
private List<Map.Entry<OrderByFragment, TableGroup>> orderByFragments; private List<Map.Entry<OrderByFragment, TableGroup>> orderByFragments;
@ -1692,11 +1693,18 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
} }
} }
sqlQueryPart.setOffsetClauseExpression( visitOffsetExpression( sqmQueryPart.getOffsetExpression() ) ); if ( !containsCollectionFetches || !currentClauseStack.isEmpty() ) {
sqlQueryPart.setFetchClauseExpression( // Strip off the root offset and limit expressions in case the query contains collection fetches to retain
visitFetchExpression( sqmQueryPart.getFetchExpression() ), // the proper cardinality. We could implement pagination for single select statements differently in this
sqmQueryPart.getFetchClauseType() // case by using a subquery e.g. `... where alias in (select subAlias from ... limit ...)`
); // or use window functions e.g. `select ... from (select ..., dense_rank() over(order by ..., id) rn from ...) tmp where tmp.rn between ...`
// but these transformations/translations are non-trivial and can be done later
sqlQueryPart.setOffsetClauseExpression( visitOffsetExpression( sqmQueryPart.getOffsetExpression() ) );
sqlQueryPart.setFetchClauseExpression(
visitFetchExpression( sqmQueryPart.getFetchExpression() ),
sqmQueryPart.getFetchClauseType()
);
}
} }
private TableGroup findTableGroupByPath(NavigablePath navigablePath) { private TableGroup findTableGroupByPath(NavigablePath navigablePath) {
@ -2464,6 +2472,10 @@ public abstract class BaseSqmToSqlAstConverter<T extends Statement> extends Base
final PluralAttributeMapping pluralAttributeMapping = (PluralAttributeMapping) modelPart; final PluralAttributeMapping pluralAttributeMapping = (PluralAttributeMapping) modelPart;
if ( sqmJoin.isFetched() ) {
containsCollectionFetches = true;
}
joinedTableGroupJoin = pluralAttributeMapping.createTableGroupJoin( joinedTableGroupJoin = pluralAttributeMapping.createTableGroupJoin(
sqmJoinNavigablePath, sqmJoinNavigablePath,
ownerTableGroup, ownerTableGroup,

View File

@ -179,10 +179,10 @@ public abstract class SqmQueryPart<T> implements SqmVisitableNode, JpaQueryPart<
sb.append( " rows with ties" ); sb.append( " rows with ties" );
break; break;
case PERCENT_ONLY: case PERCENT_ONLY:
sb.append( " percent only" ); sb.append( " percent rows only" );
break; break;
case PERCENT_WITH_TIES: case PERCENT_WITH_TIES:
sb.append( " percent with ties" ); sb.append( " percent rows with ties" );
break; break;
} }
} }