Skip to content

Commit 6c8bb03

Browse files
jhyotybeikov
authored andcommitted
HHH-16433 Fix forced follow on locking with order by
1 parent 79d2e20 commit 6c8bb03

File tree

7 files changed

+494
-267
lines changed

7 files changed

+494
-267
lines changed

hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java

Lines changed: 184 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
*/
77
package org.hibernate.community.dialect;
88

9+
import java.util.ArrayList;
910
import java.util.List;
1011

11-
import org.hibernate.LockMode;
1212
import org.hibernate.engine.spi.SessionFactoryImplementor;
1313
import org.hibernate.internal.util.collections.Stack;
14+
import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart;
15+
import org.hibernate.metamodel.mapping.EntityIdentifierMapping;
16+
import org.hibernate.metamodel.mapping.EntityMappingType;
1417
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
1518
import org.hibernate.query.IllegalQueryOperationException;
1619
import org.hibernate.query.sqm.ComparisonOperator;
@@ -24,28 +27,31 @@
2427
import org.hibernate.sql.ast.tree.cte.CteMaterialization;
2528
import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression;
2629
import org.hibernate.sql.ast.tree.expression.ColumnReference;
27-
import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression;
2830
import org.hibernate.sql.ast.tree.expression.Expression;
2931
import org.hibernate.sql.ast.tree.expression.FunctionExpression;
3032
import org.hibernate.sql.ast.tree.expression.Literal;
3133
import org.hibernate.sql.ast.tree.expression.Over;
3234
import org.hibernate.sql.ast.tree.expression.SqlTuple;
3335
import org.hibernate.sql.ast.tree.expression.SqlTupleContainer;
3436
import org.hibernate.sql.ast.tree.expression.Summarization;
37+
import org.hibernate.sql.ast.tree.from.FromClause;
3538
import org.hibernate.sql.ast.tree.from.FunctionTableReference;
36-
import org.hibernate.sql.ast.tree.from.NamedTableReference;
3739
import org.hibernate.sql.ast.tree.from.QueryPartTableReference;
40+
import org.hibernate.sql.ast.tree.from.TableGroup;
3841
import org.hibernate.sql.ast.tree.from.UnionTableGroup;
3942
import org.hibernate.sql.ast.tree.from.ValuesTableReference;
4043
import org.hibernate.sql.ast.tree.insert.InsertSelectStatement;
4144
import org.hibernate.sql.ast.tree.insert.Values;
45+
import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate;
46+
import org.hibernate.sql.ast.tree.predicate.Predicate;
4247
import org.hibernate.sql.ast.tree.select.QueryGroup;
4348
import org.hibernate.sql.ast.tree.select.QueryPart;
4449
import org.hibernate.sql.ast.tree.select.QuerySpec;
4550
import org.hibernate.sql.ast.tree.select.SelectClause;
4651
import org.hibernate.sql.ast.tree.select.SortSpecification;
4752
import org.hibernate.sql.ast.tree.update.Assignment;
4853
import org.hibernate.sql.exec.spi.JdbcOperation;
54+
import org.hibernate.sql.results.internal.SqlSelectionImpl;
4955
import org.hibernate.type.SqlTypes;
5056

5157
/**
@@ -97,12 +103,6 @@ protected LockStrategy determineLockingStrategy(
97103
Boolean followOnLocking) {
98104
LockStrategy strategy = super.determineLockingStrategy( querySpec, forUpdateClause, followOnLocking );
99105
final boolean followOnLockingDisabled = Boolean.FALSE.equals( followOnLocking );
100-
if ( strategy != LockStrategy.FOLLOW_ON && querySpec.hasSortSpecifications() ) {
101-
if ( followOnLockingDisabled ) {
102-
throw new IllegalQueryOperationException( "Locking with ORDER BY is not supported" );
103-
}
104-
strategy = LockStrategy.FOLLOW_ON;
105-
}
106106
// Oracle also doesn't support locks with set operators
107107
// See https://docs.oracle.com/cd/B19306_01/server.102/b14200/statements_10002.htm#i2066346
108108
if ( strategy != LockStrategy.FOLLOW_ON && isPartOfQueryGroup() ) {
@@ -117,29 +117,12 @@ protected LockStrategy determineLockingStrategy(
117117
}
118118
strategy = LockStrategy.FOLLOW_ON;
119119
}
120-
if ( strategy != LockStrategy.FOLLOW_ON && useOffsetFetchClause( querySpec ) && !isRowsOnlyFetchClauseType( querySpec ) ) {
120+
if ( strategy != LockStrategy.FOLLOW_ON && needsLockingWrapper( querySpec ) && !canApplyLockingWrapper( querySpec ) ) {
121121
if ( followOnLockingDisabled ) {
122-
throw new IllegalQueryOperationException( "Locking with FETCH is not supported" );
122+
throw new IllegalQueryOperationException( "Locking with OFFSET/FETCH is not supported" );
123123
}
124124
strategy = LockStrategy.FOLLOW_ON;
125125
}
126-
if ( strategy != LockStrategy.FOLLOW_ON ) {
127-
final boolean hasOffset;
128-
if ( querySpec.isRoot() && hasLimit() && getLimit().getFirstRow() != null ) {
129-
hasOffset = true;
130-
// We must record that the generated SQL depends on the fact that there is an offset
131-
addAppliedParameterBinding( getOffsetParameter(), null );
132-
}
133-
else {
134-
hasOffset = querySpec.getOffsetClauseExpression() != null;
135-
}
136-
if ( hasOffset ) {
137-
if ( followOnLockingDisabled ) {
138-
throw new IllegalQueryOperationException( "Locking with OFFSET is not supported" );
139-
}
140-
strategy = LockStrategy.FOLLOW_ON;
141-
}
142-
}
143126
return strategy;
144127
}
145128

@@ -166,7 +149,7 @@ protected boolean supportsNestedSubqueryCorrelation() {
166149

167150
protected boolean shouldEmulateFetchClause(QueryPart queryPart) {
168151
// Check if current query part is already row numbering to avoid infinite recursion
169-
if (getQueryPartForRowNumbering() == queryPart) {
152+
if ( getQueryPartForRowNumbering() == queryPart ) {
170153
return false;
171154
}
172155
final boolean hasLimit = queryPart.isRoot() && hasLimit() || queryPart.getFetchClauseExpression() != null
@@ -176,77 +159,12 @@ protected boolean shouldEmulateFetchClause(QueryPart queryPart) {
176159
}
177160
// Even if Oracle supports the OFFSET/FETCH clause, there are conditions where we still want to use the ROWNUM pagination
178161
if ( supportsOffsetFetchClause() ) {
179-
// When the query has no sort specifications and offset, we want to use the ROWNUM pagination as that is a special locking case
180-
return !queryPart.hasSortSpecifications() && !hasOffset( queryPart )
181-
// Workaround an Oracle bug, segmentation fault for insert queries with a plain query group and fetch clause
182-
|| queryPart instanceof QueryGroup && getClauseStack().isEmpty() && getStatement() instanceof InsertSelectStatement;
162+
// Workaround an Oracle bug, segmentation fault for insert queries with a plain query group and fetch clause
163+
return queryPart instanceof QueryGroup && getClauseStack().isEmpty() && getStatement() instanceof InsertSelectStatement;
183164
}
184165
return true;
185166
}
186167

187-
@Override
188-
protected FetchClauseType getFetchClauseTypeForRowNumbering(QueryPart queryPart) {
189-
final FetchClauseType fetchClauseType = super.getFetchClauseTypeForRowNumbering( queryPart );
190-
final boolean hasOffset;
191-
if ( queryPart.isRoot() && hasLimit() ) {
192-
hasOffset = getLimit().getFirstRow() != null;
193-
}
194-
else {
195-
hasOffset = queryPart.getOffsetClauseExpression() != null;
196-
}
197-
if ( queryPart instanceof QuerySpec && !hasOffset && fetchClauseType == FetchClauseType.ROWS_ONLY ) {
198-
// We return null here, because in this particular case, we render a special rownum query
199-
// which can be seen in #emulateFetchOffsetWithWindowFunctions
200-
// Note that we also build upon this in #visitOrderBy
201-
return null;
202-
}
203-
return fetchClauseType;
204-
}
205-
206-
@Override
207-
protected void emulateFetchOffsetWithWindowFunctions(
208-
QueryPart queryPart,
209-
Expression offsetExpression,
210-
Expression fetchExpression,
211-
FetchClauseType fetchClauseType,
212-
boolean emulateFetchClause) {
213-
if ( queryPart instanceof QuerySpec && offsetExpression == null && fetchClauseType == FetchClauseType.ROWS_ONLY ) {
214-
// Special case for Oracle to support locking along with simple max results paging
215-
final QuerySpec querySpec = (QuerySpec) queryPart;
216-
withRowNumbering(
217-
querySpec,
218-
true, // we need select aliases to avoid ORA-00918: column ambiguously defined
219-
() -> {
220-
appendSql( "select * from " );
221-
emulateFetchOffsetWithWindowFunctionsVisitQueryPart( querySpec );
222-
appendSql( " where rownum<=" );
223-
final Stack<Clause> clauseStack = getClauseStack();
224-
clauseStack.push( Clause.WHERE );
225-
try {
226-
fetchExpression.accept( this );
227-
228-
// We render the FOR UPDATE clause in the outer query
229-
clauseStack.pop();
230-
clauseStack.push( Clause.FOR_UPDATE );
231-
visitForUpdateClause( querySpec );
232-
}
233-
finally {
234-
clauseStack.pop();
235-
}
236-
}
237-
);
238-
}
239-
else {
240-
super.emulateFetchOffsetWithWindowFunctions(
241-
queryPart,
242-
offsetExpression,
243-
fetchExpression,
244-
fetchClauseType,
245-
emulateFetchClause
246-
);
247-
}
248-
}
249-
250168
@Override
251169
protected void visitOrderBy(List<SortSpecification> sortSpecifications) {
252170
// If we have a query part for row numbering, there is no need to render the order by clause
@@ -262,13 +180,49 @@ protected void visitOrderBy(List<SortSpecification> sortSpecifications) {
262180
final QuerySpec querySpec = (QuerySpec) queryPartForRowNumbering;
263181
if ( querySpec.getOffsetClauseExpression() == null
264182
&& ( !querySpec.isRoot() || getOffsetParameter() == null ) ) {
265-
// When rendering `rownum` for Oracle, we need to render the order by clause still
266-
renderOrderBy( true, sortSpecifications );
183+
// When we enter here, we need to handle the special ROWNUM pagination
184+
if ( hasGroupingOrDistinct( querySpec ) || querySpec.getFromClause().hasJoins() ) {
185+
// When the query spec has joins, a group by, having or distinct clause,
186+
// we just need to render the order by clause, because the query is wrapped
187+
renderOrderBy( true, sortSpecifications );
188+
}
189+
else {
190+
// Otherwise we need to render the ROWNUM pagination predicate in here
191+
final Predicate whereClauseRestrictions = querySpec.getWhereClauseRestrictions();
192+
if ( whereClauseRestrictions != null && !whereClauseRestrictions.isEmpty() ) {
193+
appendSql( " and " );
194+
}
195+
else {
196+
appendSql( " where " );
197+
}
198+
appendSql( "rownum<=" );
199+
final Stack<Clause> clauseStack = getClauseStack();
200+
clauseStack.push( Clause.WHERE );
201+
try {
202+
if ( querySpec.isRoot() && hasLimit() ) {
203+
getLimitParameter().accept( this );
204+
}
205+
else {
206+
querySpec.getFetchClauseExpression().accept( this );
207+
}
208+
}
209+
finally {
210+
clauseStack.pop();
211+
}
212+
renderOrderBy( true, sortSpecifications );
213+
visitForUpdateClause( querySpec );
214+
}
267215
}
268216
}
269217
}
270218
}
271219

220+
private boolean hasGroupingOrDistinct(QuerySpec querySpec) {
221+
return querySpec.getSelectClause().isDistinct()
222+
|| !querySpec.getGroupByClauseExpressions().isEmpty()
223+
|| querySpec.getHavingClauseRestrictions() != null;
224+
}
225+
272226
@Override
273227
protected void visitValuesList(List<Values> valuesList) {
274228
if ( valuesList.size() < 2 ) {
@@ -323,12 +277,142 @@ public void visitQueryGroup(QueryGroup queryGroup) {
323277

324278
@Override
325279
public void visitQuerySpec(QuerySpec querySpec) {
280+
final EntityIdentifierMapping identifierMappingForLockingWrapper = identifierMappingForLockingWrapper( querySpec );
281+
final Expression offsetExpression;
282+
final Expression fetchExpression;
283+
final FetchClauseType fetchClauseType;
284+
if ( querySpec.isRoot() && hasLimit() ) {
285+
prepareLimitOffsetParameters();
286+
offsetExpression = getOffsetParameter();
287+
fetchExpression = getLimitParameter();
288+
fetchClauseType = FetchClauseType.ROWS_ONLY;
289+
}
290+
else {
291+
offsetExpression = querySpec.getOffsetClauseExpression();
292+
fetchExpression = querySpec.getFetchClauseExpression();
293+
fetchClauseType = querySpec.getFetchClauseType();
294+
}
326295
if ( shouldEmulateFetchClause( querySpec ) ) {
327-
emulateFetchOffsetWithWindowFunctions( querySpec, true );
296+
if ( identifierMappingForLockingWrapper == null ) {
297+
emulateFetchOffsetWithWindowFunctions(
298+
querySpec,
299+
offsetExpression,
300+
fetchExpression,
301+
fetchClauseType,
302+
true
303+
);
304+
}
305+
else {
306+
super.visitQuerySpec(
307+
createLockingWrapper(
308+
querySpec,
309+
offsetExpression,
310+
fetchExpression,
311+
fetchClauseType,
312+
identifierMappingForLockingWrapper
313+
)
314+
);
315+
// Render the for update clause for the original query spec, because the locking wrapper is marked as non-root
316+
visitForUpdateClause( querySpec );
317+
}
328318
}
329319
else {
330-
super.visitQuerySpec( querySpec );
320+
if ( identifierMappingForLockingWrapper == null ) {
321+
super.visitQuerySpec( querySpec );
322+
}
323+
else {
324+
super.visitQuerySpec(
325+
createLockingWrapper(
326+
querySpec,
327+
offsetExpression,
328+
fetchExpression,
329+
fetchClauseType,
330+
identifierMappingForLockingWrapper
331+
)
332+
);
333+
// Render the for update clause for the original query spec, because the locking wrapper is marked as non-root
334+
visitForUpdateClause( querySpec );
335+
}
336+
}
337+
}
338+
339+
private QuerySpec createLockingWrapper(
340+
QuerySpec querySpec,
341+
Expression offsetExpression,
342+
Expression fetchExpression,
343+
FetchClauseType fetchClauseType,
344+
EntityIdentifierMapping identifierMappingForLockingWrapper) {
345+
346+
final TableGroup rootTableGroup = querySpec.getFromClause().getRoots().get( 0 );
347+
final List<ColumnReference> idColumnReferences = new ArrayList<>( identifierMappingForLockingWrapper.getJdbcTypeCount() );
348+
identifierMappingForLockingWrapper.forEachSelectable(
349+
0,
350+
(selectionIndex, selectableMapping) -> {
351+
idColumnReferences.add( new ColumnReference( rootTableGroup.getPrimaryTableReference(), selectableMapping ) );
352+
}
353+
);
354+
final Expression idExpression;
355+
if ( identifierMappingForLockingWrapper instanceof EmbeddableValuedModelPart ) {
356+
idExpression = new SqlTuple( idColumnReferences, identifierMappingForLockingWrapper );
357+
}
358+
else {
359+
idExpression = idColumnReferences.get( 0 );
360+
}
361+
final QuerySpec subquery = new QuerySpec( false, 1 );
362+
for ( ColumnReference idColumnReference : idColumnReferences ) {
363+
subquery.getSelectClause().addSqlSelection( new SqlSelectionImpl( 0, -1, idColumnReference ) );
364+
}
365+
subquery.getFromClause().addRoot( rootTableGroup );
366+
subquery.applyPredicate( querySpec.getWhereClauseRestrictions() );
367+
if ( querySpec.hasSortSpecifications() ) {
368+
for ( SortSpecification sortSpecification : querySpec.getSortSpecifications() ) {
369+
subquery.addSortSpecification( sortSpecification );
370+
}
331371
}
372+
subquery.setOffsetClauseExpression( offsetExpression );
373+
subquery.setFetchClauseExpression( fetchExpression, fetchClauseType );
374+
375+
// Mark the query spec as non-root even if it might be the root, to avoid applying the pagination there
376+
final QuerySpec lockingWrapper = new QuerySpec( false, 1 );
377+
lockingWrapper.getFromClause().addRoot( rootTableGroup );
378+
for ( SqlSelection sqlSelection : querySpec.getSelectClause().getSqlSelections() ) {
379+
lockingWrapper.getSelectClause().addSqlSelection( sqlSelection );
380+
}
381+
lockingWrapper.applyPredicate( new InSubQueryPredicate( idExpression, subquery, false ) );
382+
return lockingWrapper;
383+
}
384+
385+
private EntityIdentifierMapping identifierMappingForLockingWrapper(QuerySpec querySpec) {
386+
// We only need a locking wrapper for very simple queries
387+
if ( canApplyLockingWrapper( querySpec )
388+
// There must be the need for locking in this query
389+
&& needsLocking( querySpec )
390+
// The query uses some sort of pagination which makes the wrapper necessary
391+
&& needsLockingWrapper( querySpec )
392+
// The query may not have a group by, having and distinct clause, or use aggregate functions,
393+
// as these features will force the use of follow-on locking
394+
&& querySpec.getGroupByClauseExpressions().isEmpty()
395+
&& querySpec.getHavingClauseRestrictions() == null
396+
&& !querySpec.getSelectClause().isDistinct()
397+
&& !hasAggregateFunctions( querySpec ) ) {
398+
return ( (EntityMappingType) querySpec.getFromClause().getRoots().get( 0 ).getModelPart() ).getIdentifierMapping();
399+
}
400+
return null;
401+
}
402+
403+
private boolean canApplyLockingWrapper(QuerySpec querySpec) {
404+
final FromClause fromClause;
405+
return querySpec.isRoot()
406+
// Must have a single root with no joins for an entity type
407+
&& ( fromClause = querySpec.getFromClause() ).getRoots().size() == 1
408+
&& !fromClause.hasJoins()
409+
&& fromClause.getRoots().get( 0 ).getModelPart() instanceof EntityMappingType;
410+
}
411+
412+
private boolean needsLockingWrapper(QuerySpec querySpec) {
413+
return querySpec.getFetchClauseType() != FetchClauseType.ROWS_ONLY
414+
|| hasOffset( querySpec )
415+
|| hasLimit( querySpec );
332416
}
333417

334418
@Override

0 commit comments

Comments
 (0)