-
Notifications
You must be signed in to change notification settings - Fork 7
Limit/Offset/Fetch MQL translation #94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
…a field path, and its testing case
# Conflicts: # src/integrationTest/java/com/mongodb/hibernate/query/select/AbstractSelectionQueryIntegrationTests.java # src/integrationTest/java/com/mongodb/hibernate/query/select/Book.java # src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
…rtingSelectQueryIntegrationTests.java Co-authored-by: Viacheslav Babanin <[email protected]>
…tMqlTranslator.java Co-authored-by: Viacheslav Babanin <[email protected]>
…-68-new # Conflicts: # src/integrationTest/java/com/mongodb/hibernate/query/select/SortingSelectQueryIntegrationTests.java
var stages = new ArrayList<AstStage>(2); | ||
final Expression skipExpression; | ||
final Expression limitExpression; | ||
if (queryPart.isRoot() && limit != null && !limit.isEmpty()) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
QueryOptions
's Limit
only applies for top-level query (or not in subquery whose isRoot()
would return false). However, given subquery is not supported for now, it is hard to cover this logic in testing code.
.bind( | ||
statement, | ||
parameterValueAccess.apply( | ||
executionContext.getQueryOptions().getLimit()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the magic to achieve Limit
cache. Firstly a JdbcParameter
was created, then a special ParameterBinder
is created here to use the executeContext
environment variable which shares the same QueryOptions
so we can get the different Limit
value at runtime to populate the static JDBC parameter placeholder.
getAffectedTableNames(), | ||
0, | ||
Integer.MAX_VALUE, | ||
Collections.emptyMap(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the first two values are dummy or default values, meaning rowsToSkip
and maxRows
are not set. Why?
The two values are only meant for those rare dialects who doesn't support SQL (native) level spec and pure JDBC API (ResultSet#absolute(int)
and PreparedStatement#setMaxRows(int)
) usage is the only vialbe solution.
To furhter investigate, let us analyze the only usptream of the above JdbcSelect
object returned from the translator method, i.e. DeferedResultSetAccess
(https://github.com/hibernate/hibernate-orm/blob/main/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java).
Particularly the following method is the main logic driving everything:
private void executeQuery() {
final LogicalConnectionImplementor logicalConnection = getPersistenceContext().getJdbcCoordinator().getLogicalConnection();
final SharedSessionContractImplementor session = executionContext.getSession();
try {
LOG.tracef( "Executing query to retrieve ResultSet : %s", finalSql );
// prepare the query
preparedStatement = statementCreator.createStatement( executionContext, finalSql );
bindParameters( preparedStatement );
final SessionEventListenerManager eventListenerManager = session
.getEventListenerManager();
long executeStartNanos = 0;
if ( sqlStatementLogger.getLogSlowQuery() > 0 ) {
executeStartNanos = System.nanoTime();
}
final EventManager eventManager = session.getEventManager();
final HibernateMonitoringEvent jdbcPreparedStatementExecutionEvent = eventManager.beginJdbcPreparedStatementExecutionEvent();
try {
eventListenerManager.jdbcExecuteStatementStart();
resultSet = wrapResultSet( preparedStatement.executeQuery() );
}
finally {
eventManager.completeJdbcPreparedStatementExecutionEvent( jdbcPreparedStatementExecutionEvent, finalSql );
eventListenerManager.jdbcExecuteStatementEnd();
sqlStatementLogger.logSlowQuery( finalSql, executeStartNanos, context() );
}
skipRows( resultSet );
logicalConnection.getResourceRegistry().register( resultSet, preparedStatement );
}
catch (SQLException e) {
try {
release();
}
catch (RuntimeException e2) {
e.addSuppressed( e2 );
}
throw session.getJdbcServices().getSqlExceptionHelper().convert(
e,
"JDBC exception executing SQL [" + finalSql + "]"
);
}
}
Notice there is a skipRows
statement above right after ResultSet
has been returned. Below is its logic:
protected void skipRows(ResultSet resultSet) throws SQLException {
// For dialects that don't support an offset clause
final int rowsToSkip;
if ( !jdbcSelect.usesLimitParameters() && limit != null && limit.getFirstRow() != null && !limitHandler.supportsLimitOffset() ) {
rowsToSkip = limit.getFirstRow();
}
else {
rowsToSkip = jdbcSelect.getRowsToSkip();
}
if ( rowsToSkip != 0 ) {
try {
resultSet.absolute( rowsToSkip );
}
catch (SQLException ex) {
// This could happen with the jTDS driver which throws an exception on non-scrollable result sets
// To avoid throwing a wrong exception in case this was some other error, check if we can advance to next
try {
resultSet.next();
}
catch (SQLException ex2) {
throw ex;
}
// Traverse to the actual row
for (int i = 1; i < rowsToSkip && resultSet.next(); i++) {}
}
}
}
the jdbcSelect
corresponds to what we have returned from SelectMqlTranslator
and we set its rowsToSkip
to 0 above.
Note that the !jdbcSelect.usesLimitParameters() && limit != null && limit.getFirstRow() != null
would be always false for when limit
is not empty, we should have created LimitParameters already, so rowsToSkip
would be evaulated to zero for that is the value we returned, thus ending up with skipping the ResultSet#absolute(int)
in the first place.
How about the second value or maxRows
which we set to Integer.MAX_VALUE
above?
Below is the relevant method in the same class:
protected void bindParameters(PreparedStatement preparedStatement) throws SQLException {
final QueryOptions queryOptions = executionContext.getQueryOptions();
// set options
if ( queryOptions != null ) {
if ( queryOptions.getFetchSize() != null ) {
preparedStatement.setFetchSize( queryOptions.getFetchSize() );
}
if ( queryOptions.getTimeout() != null ) {
preparedStatement.setQueryTimeout( queryOptions.getTimeout() );
}
}
// bind parameters
// todo : validate that all query parameters were bound?
int paramBindingPosition = 1;
paramBindingPosition += limitHandler.bindLimitParametersAtStartOfQuery( limit, preparedStatement, paramBindingPosition );
for ( JdbcParameterBinder parameterBinder : jdbcSelect.getParameterBinders() ) {
parameterBinder.bindParameterValue(
preparedStatement,
paramBindingPosition++,
jdbcParameterBindings,
executionContext
);
}
paramBindingPosition += limitHandler.bindLimitParametersAtEndOfQuery( limit, preparedStatement, paramBindingPosition );
if ( !jdbcSelect.usesLimitParameters() && limit != null && limit.getMaxRows() != null ) {
limitHandler.setMaxRows( limit, preparedStatement );
}
else {
final int maxRows = jdbcSelect.getMaxRows();
if ( maxRows != Integer.MAX_VALUE ) {
preparedStatement.setMaxRows( maxRows );
}
}
}
Again, !jdbcSelect.usesLimitParameters() && limit != null && limit.getMaxRows() != null
is always false, so we end up within the following logic branch below:
final int maxRows = jdbcSelect.getMaxRows();
if ( maxRows != Integer.MAX_VALUE ) {
preparedStatement.setMaxRows( maxRows );
}
again, given we provided the default value or Integer.MAX_VALUE
, so we would end up skipping the invocation of PreparedStatement#setMaxRows(int)
.
Overall, given we don't need to tap into JDBC API's special methods as last resort, we have to set both fields above to default values to skip them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The third value above is appliedParameters
which will impact query plan cache logic (if some parameter has been applied or value has been inserted into the translated SQL, we can't reuse it for future query unless the applied value is indentical). Given we are not applying any parameter, we simply return empty map above.
7794f6a
to
658e0b6
Compare
658e0b6
to
5e4a6fd
Compare
3302a31
to
5e8cd0a
Compare
…-70-new # Conflicts: # src/integrationTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java
…ier cross-reference with AbstractSqlAstTranslator
…tchClauseIntegrationTests
@Test | ||
void testHqlLimitClauseOnly() { | ||
assertSelectionQuery( | ||
"from Book order by id LIMIT :limit", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Test | |
void testHqlLimitClauseOnly() { | |
assertSelectionQuery( | |
"from Book order by id LIMIT :limit", | |
@ParameterizedTest | |
@ValueSource(strings = { | |
"FETCH FIRST :limit ROWS ONLY", | |
"FETCH NEXT :limit ROWS ONLY", | |
}) | |
void testHqlFetchClauseOnly(final String fetchClause) { | |
assertSelectionQuery( | |
"from Book order by id " + fetchClause, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is testHqlFetchClauseOnly
method already, so I guess we can enrich it as above without touching testHqlLimitClauseOnly
.
src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
Show resolved
Hide resolved
src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
Outdated
Show resolved
Hide resolved
...tionTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java
Outdated
Show resolved
Hide resolved
} | ||
|
||
@Test | ||
void testCacheInvalidatedDueToQueryOptionsAdded() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For other reviewers:
Hibernate's ConcreteSqmSelectQueryPlan
checks if QueryOptions
have changed when reusing a cached query plan (see ConcreteSqmSelectQueryPlan.java#L428-L433, more logic is in isCompatibleWith
method)
When either firstRow
or maxRows
transitions to
or from
null, Hibernate rebuilds the JdbcOperationQuerySelect
to reflect potential changes in the raw SQL query, such as updated LIMIT/OFFSET
clauses.
...tionTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java
Show resolved
Hide resolved
...tionTest/java/com/mongodb/hibernate/query/select/LimitOffsetFetchClauseIntegrationTests.java
Outdated
Show resolved
Hide resolved
src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java
Outdated
Show resolved
Hide resolved
…qlTranslator.java Co-authored-by: Viacheslav Babanin <[email protected]>
# Conflicts: # src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This PR introduces support for translating Hibernate HQL limit/offset/fetch clauses into MongoDB query components while reconciling the differences between HQL and QueryOptions–based configurations. The changes include adding new AST stage implementations and tests for skip and limit stages, integrating QueryOptions limit parameters into the query plan cache, and updating related translator and visitor code.
Reviewed Changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.
Show a summary per file
File | Description |
---|---|
src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStageTests.java | Adds tests for rendering the skip stage |
src/test/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStageTests.java | Adds tests for rendering the limit stage |
src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstSkipStage.java | New implementation of the $skip stage |
src/main/java/com/mongodb/hibernate/internal/translate/mongoast/command/aggregate/AstLimitStage.java | New implementation of the $limit stage |
src/main/java/com/mongodb/hibernate/internal/translate/SelectMqlTranslator.java | Updated to incorporate QueryOptions into the query plan cache handling |
src/main/java/com/mongodb/hibernate/internal/translate/AstVisitorValueDescriptor.java | Added a descriptor for skip/limit stages |
src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java | Enhanced to generate the correct pipeline stages for limit/offset and added JDBC parameter support |
Comments suppressed due to low confidence (3)
src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java:910
- Update documentation/comments in checkQueryOptionsSupportability to reflect the new support for QueryOptions limit, as the unsupported exception for non-empty limits has been removed.
if (queryOptions.getLimit() != null && !queryOptions.getLimit().isEmpty()) {
src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java:410
- Add an inline comment here to clarify why QueryOptions–derived offset/limit handling is only applied for root query parts and how these parameters are used in query plan caching.
if (queryPart.isRoot() && queryOptionsLimit != null && !queryOptionsLimit.isEmpty()) {
src/main/java/com/mongodb/hibernate/internal/translate/AbstractMqlTranslator.java:1007
- Include JavaDoc for the OffsetJdbcParameter and LimitJdbcParameter inner classes to explain their role in binding QueryOptions limit values for query plan caching.
private static class OffsetJdbcParameter extends AbstractJdbcParameter {
limitExpression = queryPart.getFetchClauseExpression(); | ||
} | ||
if (skipExpression != null) { | ||
var skipValue = acceptAndYield(skipExpression, FIELD_VALUE); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that the visitParameter
and visitUnparsedNumericLiteral
methods yield FIELD_VALUE
s, including the values of skip
/limit
, the FIELD_VALUE
name of the descriptor does not seem right anymore. It's really just a value at this point, not necessarily a value of a document field, not necessarily a value of a parameter. Let's rename FIELD_VALUE
-> VALUE
?
import org.junit.jupiter.params.provider.ValueSource; | ||
|
||
@DomainModel(annotatedClasses = Book.class) | ||
class LimitOffsetFetchClauseIntegrationTests extends AbstractSelectionQueryIntegrationTests { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't seem to have tests where limit / offset / fetch first/last are literals. Let's add them?
if (queryOptions.getLimit() != null) { | ||
queryOptionsLimit = queryOptions.getLimit().makeCopy(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Why do we call
makeCopy
here? Is it because Hibernate ORM may mutate thisLimit
after this point in execution? - Just writing
queryOptionsLimit = queryOptions.getLimit()
would have been nicer (and that's what I did in the changes proposed in other comments), but if that can't be done, let's explicitly use the same value we check fornull
:
if (queryOptions.getLimit() != null) { | |
queryOptionsLimit = queryOptions.getLimit().makeCopy(); | |
} | |
var limit = queryOptions.getLimit(); | |
if (limit != null) { | |
queryOptionsLimit = limit.makeCopy(); | |
} |
private List<AstStage> createSkipLimitStages(QuerySpec querySpec) { | ||
return astVisitorValueHolder.execute(SKIP_LIMIT_STAGES, () -> visitOffsetFetchClause(querySpec)); | ||
} | ||
|
||
@Override | ||
public void visitOffsetFetchClause(QueryPart queryPart) { | ||
var skipLimitStages = createSkipLimitStages(queryPart); | ||
astVisitorValueHolder.yield(SKIP_LIMIT_STAGES, skipLimitStages); | ||
} | ||
|
||
private List<AstStage> createSkipLimitStages(QueryPart queryPart) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AstVisitorValueHolder.execute
calling createSkipLimitStages
directly, immediately rose a flag for me. The sole reason behind astVisitorValueHolder
is that we
- have to call the
visit*
methods indirectly by callingStatement
/SqlAstNode.accept
, allowing Hibernate ORM to decide whichvisit*
methods are to be called; - we want those
visit*
methods to yield results that we could access, despite those methods, as well asStatement
/SqlAstNode.accept
, beingvoid
.
The situation with AbstractMqlTranslator.visitOffsetFetchClause
is different:
- There is no
Statement
/SqlAstNode.accept
method implementation that calls this method, which means we don't need to utilizeAstVisitorValueHolder
. - The only code in Hibernate ORM 6.6 that calls
visitOffsetFetchClause
isorg.hibernate.sql.ast.spi.AbstractSqlAstTranslator
, and we are not using it, which means that Habernate ORM never calls theAbstractMqlTranslator.visitOffsetFetchClause
method.
Thus, I propose us to
- remove
AstVisitorValueDescriptor.SKIP_LIMIT_STAGES
- call
createSkipLimitStages
directly - implement
AbstractMqlTranslator.visitOffsetFetchClause
by callingMongoAssertions.fail
.
The proposed change is here: stIncMale@9778069.
if (queryOptions.getFetchSize() != null) { | ||
throw new FeatureNotSupportedException("TODO-HIBERNATE-54 https://jira.mongodb.org/browse/HIBERNATE-54"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why was this check removed? https://jira.mongodb.org/browse/HIBERNATE-54 is still in the backlog, and the current PR does not seem to address the fetch size.
var stages = new ArrayList<AstStage>(3); | ||
|
||
createMatchStage(querySpec).ifPresent(stages::add); | ||
createSortStage(querySpec).ifPresent(stages::add); | ||
|
||
stages.addAll(createSkipLimitStages(querySpec)); | ||
stages.add(createProjectStage(querySpec.getSelectClause())); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stages
is no longer guaranteed to have 3 elements, its size varies from 3 to 5. As far as I remember, your are trying to preallocate arrays for the sake of alleged performance improvements, which I believe is immeasurable for this project, even if theoretically not impossible. However, incorrectly predicting the size here results in an additional allocation and copy in a situation when more than 3 stages are added, ruling out even the theoretical possiblity of any improvement.
I think, we should replace new ArrayList<AstStage>(3)
with new ArrayList<AstStage>()
.
if (queryOptionsLimit.getFirstRow() != null) { | ||
queryOptionsOffsetParameter = new OffsetJdbcParameter(basicIntegerType); | ||
} | ||
if (queryOptionsLimit.getMaxRows() != null) { | ||
queryOptionsLimitParameter = new LimitJdbcParameter(basicIntegerType); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We check queryOptionsLimit.getFirstRow
/getMaxRows
, but we don't use them when instantiating OffsetJdbcParameter
/LimitJdbcParameter
. The reason behind this is unobvious enough to warrant an explanatory comment. My proposed comment is here: stIncMale@a5927b3.
// The following parameters are provided for query plan cache purposes. | ||
// Not setting them could result in reusing the wrong query plan and subsequently the wrong MQL. | ||
queryOptionsOffsetParameter, | ||
queryOptionsLimitParameter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The result of translating selectStatement
(an AST provided to us by Hibernate ORM) is a combination of:
aggregateCommand
getParameterBinders()
getAffectedTableNames()
queryOptionsOffsetParameter
queryOptionsLimitParameter
However, only aggregateCommand
is returned from acceptAndYield((Statement) selectStatement, COLLECTION_AGGREGATE)
. We've seen how the number of parts of the combined result grew from just selectStatement
to what we have now, and it is not impossible that it will continue growing as the product evolves. But even if it won't, we should return the full result from acceptAndYield((Statement) selectStatement, COLLECTION_AGGREGATE)
, instead of returning a part of it, and having to call getters or access fields to get the rest of the result. I propose the following change: stIncMale@fec21e7.
@Nullable Limit queryOptionsLimit; | ||
|
||
@Nullable JdbcParameter queryOptionsOffsetParameter; | ||
|
||
@Nullable JdbcParameter queryOptionsLimitParameter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These three instance fields are related to each other, and given that the set of AbstractMqlTranslator
s instance fields keeps growing, it is useful to group related fields together when possible. Furthermore, when I started grouping them in a new object, I discovered that we don't even have to have the queryOptionsOffsetParameter
/queryOptionsLimitParameter
fields. The proposed change is in stIncMale@fec21e7 (it's part of the change I mentioned in another comment).
@Test | ||
void testCacheInvalidatedDueToQueryOptionsAdded() { | ||
getSessionFactoryScope().inTransaction(session -> { | ||
setQueryOptionsAndQuery(session, null, null, format(expectedMqlTemplate, "", "")); | ||
var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); | ||
|
||
assertThat(initialSelectTranslatingCount).isPositive(); | ||
|
||
setQueryOptionsAndQuery(session, 1, null, format(expectedMqlTemplate, "{\"$skip\": 1},", "")); | ||
assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) | ||
.isEqualTo(initialSelectTranslatingCount + 1); | ||
|
||
setQueryOptionsAndQuery( | ||
session, 1, 5, format(expectedMqlTemplate, "{\"$skip\": 1},", "{\"$limit\": 5},")); | ||
assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) | ||
.isEqualTo(initialSelectTranslatingCount + 2); | ||
}); | ||
} | ||
|
||
@Test | ||
void testCacheInvalidatedDueToQueryOptionsRemoved() { | ||
getSessionFactoryScope().inTransaction(session -> { | ||
setQueryOptionsAndQuery(session, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); | ||
|
||
var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); | ||
|
||
assertThat(initialSelectTranslatingCount).isPositive(); | ||
|
||
setQueryOptionsAndQuery(session, null, null, format(expectedMqlTemplate, "", "")); | ||
|
||
assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) | ||
.isEqualTo(initialSelectTranslatingCount + 1); | ||
}); | ||
} | ||
|
||
@Test | ||
void testCacheInvalidatedDueToQueryOptionsChanged() { | ||
getSessionFactoryScope().inTransaction(session -> { | ||
setQueryOptionsAndQuery(session, 10, null, format(expectedMqlTemplate, "{\"$skip\": 10},", "")); | ||
|
||
var initialSelectTranslatingCount = translatingCacheTestingDialect.getSelectTranslatingCounter(); | ||
|
||
assertThat(initialSelectTranslatingCount).isPositive(); | ||
|
||
setQueryOptionsAndQuery(session, null, 20, format(expectedMqlTemplate, "", "{\"$limit\": 20},")); | ||
|
||
assertThat(translatingCacheTestingDialect.getSelectTranslatingCounter()) | ||
.isEqualTo(initialSelectTranslatingCount + 1); | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think these three tests are consistent with each other:
testCacheInvalidatedDueToQueryOptionsAdded
has threesetQueryOptionsAndQuery
invocations, testing
1.1. thenull
-to-number transition forfirstResult
1.2. thenull
-to-number transition formaxResults
testCacheInvalidatedDueToQueryOptionsRemoved
is supposed to do a similar thing, but instead has only twosetQueryOptionsAndQuery
invocations, testing
2.1 the number-to-null
transition forfirstResult
2.2. and missing the number-to-null
transition formaxResults
testCacheInvalidatedDueToQueryOptionsChanged
has twosetQueryOptionsAndQuery
invocations, testing
3.1. the number-to-null
transition forfirstResult
together with thenull
-to-number transition formaxResults
3.2. and missing thenull
-to-number transition forfirstResult
together with the number-to-null
transition formaxResults
My thoughts:
testCacheInvalidatedDueToQueryOptionsRemoved
needs to also test the number-to-null
transition formaxResults
testCacheInvalidatedDueToQueryOptionsChanged
duplicates what is already tested bytestCacheInvalidatedDueToQueryOptionsAdded
andtestCacheInvalidatedDueToQueryOptionsRemoved
individually forfirstResult
andmaxResults
.- Furthermore, its name is quite confusing. The two previous tests are named
...OptionsAdded
,...OptionsRemoved
, but this one is named...OptionsChanged
, as if neither option were removed or added, which is not true: one of them is removed together with the other one being added.
- Furthermore, its name is quite confusing. The two previous tests are named
My proposed change is here: stIncMale@47f9c70.
https://jira.mongodb.org/browse/HIBERNATE-70
Hibernate user guide see https://docs.jboss.org/hibernate/orm/6.6/userguide/html_single/Hibernate_User_Guide.html#hql-limit-offset
Relevant HQL BNF grammar is at https://github.com/hibernate/hibernate-orm/blob/main/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4#L180:
and the three clauses grammar is at https://github.com/hibernate/hibernate-orm/blob/main/hibernate-core/src/main/antlr/org/hibernate/grammars/hql/HqlParser.g4#L591:
A natural followup of the sorting ticket at https://jira.mongodb.org/browse/HIBERNATE-68 for as seen above, this ticket requires
orderByClause
as precondition.So basically
fetchClause
is the verbose version oflimitClause
and they can't be used at the same time. WhenfetchClause
is used, it should show up afteroffsetClause
.What is really complex is Hibernate supports two ways of limit/offset spec. The above is for HQL we are familar with, but there is another way to pass similar config through
QueryOptions
. For instance:The above
setFirstRow()
andsetMaxRows
will end up with the aLimit
field (contianing bothfirstRow
andmaxRows
fields mirroring the above query setter usage) inQueryOptions
.The important tech detail the config passed through
QueryOptions
would overshallow the corresponding HQL counterparts.Why does Hibernate provide two ways of the seemingly simple limit/offset configs? It boils down to some special SQL dialects:
ResultSet#absolute(int)
,PreparedStatement#setMaxRows(int)
)For that reason, Hibernate has dialect customized
LimitHandler
to go about the above special logic.MongoDB dialect has friendly support by
$skip
and$limit
aggregate stages; however, we still need to support the above two ways (and their precedence). That is the gist of this PR.One important tech complexity is Hibernate tries its best to tap into query plan cache to avoid unnecessary duplicated SQL translation. If only HQL way is used, there is nothing new. But Hibernate also to ensure the following two
QueryOptions
usages would end up with query plan cache hit (so the second query would reuse the same SQL translatied by the first one):Hibernate has to include the
Limit
info inQueryOptions
in its cache logic. The trick is to make the abovefirstRow
Limit end up with aJdbcParameter
. However, there is nontrivial convoluted logic to figure out completely.In this PR, I tried to reuse existing default logic in
AbstractSqlAstTranslator
to achieve the goal of incoporatingQueryOptions
'sLimit
into cache management with good integration testing cases to prove. See further details in relevant code-specific comments.