Skip to content

Commit bec05ee

Browse files
authored
Add support for subquery modifiers (LIMIT/OFFSET) in Hibernate (#1304)
* Add support for subquery modifiers (LIMIT/OFFSET) in Hibernate This change enables the use of LIMIT and OFFSET modifiers within subqueries when using Hibernate as the JPA provider. This addressing the limitation where modifiers were previously ignored in subquery contexts. **Key changes:** * Add `isSubQueryModifiersSupported()` method to JPQLTemplates - Returns false by default (standard JPQL behavior) - Overridden in HQLTemplates to return true (Hibernate-specific) * Update JPQLSerializer to conditionally serialize subquery modifiers - Add LIMIT and OFFSET constants for serialization - Modify `visit(SubQueryExpression)` to handle modifiers based on template support - Preserve existing behavior for non-supporting providers * Comprehensive test coverage - Unit tests for template support methods across all JPA providers - Serializer tests for both JPQL and HQL templates - Tests for various subquery contexts (WHERE, SELECT, nested subqueries) - Edge cases with combined LIMIT and OFFSET usage **Compatibility:** - Maintains backward compatibility for all existing JPA providers - Only affects Hibernate users who explicitly use subquery modifiers - No breaking changes to existing APIs Fixes subquery modifier limitations in Hibernate environments while preserving standard JPQL compliance for other providers. * Add integration tests for subquery modifiers in HibernateBase - Test subquery with LIMIT and OFFSET in WHERE clause - Test subquery with LIMIT in SELECT clause - Test subquery with OFFSET only - Verify actual functionality - Validate correct result count and data ordering
1 parent 9b9e4fc commit bec05ee

File tree

6 files changed

+344
-1
lines changed

6 files changed

+344
-1
lines changed

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/HQLTemplates.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,9 @@ public boolean isWithForOn() {
116116
public boolean isCaseWithLiterals() {
117117
return true;
118118
}
119+
120+
@Override
121+
public boolean isSubQueryModifiersSupported() {
122+
return true;
123+
}
119124
}

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLSerializer.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import com.querydsl.core.JoinExpression;
1717
import com.querydsl.core.JoinType;
1818
import com.querydsl.core.QueryMetadata;
19+
import com.querydsl.core.QueryModifiers;
1920
import com.querydsl.core.support.SerializerBase;
2021
import com.querydsl.core.types.*;
2122
import com.querydsl.core.types.dsl.Expressions;
@@ -65,6 +66,10 @@ public class JPQLSerializer extends SerializerBase<JPQLSerializer> {
6566

6667
private static final String ORDER_BY = "\norder by ";
6768

69+
private static final String LIMIT = "\nlimit ";
70+
71+
private static final String OFFSET = "\noffset ";
72+
6873
private static final String SELECT = "select ";
6974

7075
private static final String SELECT_COUNT = "select count(";
@@ -412,8 +417,23 @@ protected void serializeConstant(int parameterIndex, String constantLabel) {
412417

413418
@Override
414419
public Void visit(SubQueryExpression<?> query, Void context) {
420+
QueryMetadata metadata = query.getMetadata();
421+
QueryModifiers modifiers = metadata.getModifiers();
422+
415423
append("(");
416-
serialize(query.getMetadata(), false, null);
424+
serialize(metadata, false, null);
425+
426+
if (modifiers != null
427+
&& modifiers.isRestricting()
428+
&& templates.isSubQueryModifiersSupported()) {
429+
if (modifiers.getLimit() != null) {
430+
append(LIMIT).handle(modifiers.getLimit());
431+
}
432+
if (modifiers.getOffset() != null) {
433+
append(OFFSET).handle(modifiers.getOffset());
434+
}
435+
}
436+
417437
append(")");
418438
return null;
419439
}

querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLTemplates.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,4 +236,8 @@ private String escapeLiteral(String str) {
236236
}
237237
return builder.toString();
238238
}
239+
240+
public boolean isSubQueryModifiersSupported() {
241+
return false;
242+
}
239243
}

querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateBase.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,75 @@ public void createQuery4() {
176176
.distinct()
177177
.transform(GroupBy.groupBy(cat.id).as(cat)));
178178
}
179+
180+
@Test
181+
public void subQueryWithLimitAndOffset() {
182+
var kitten = new QCat("kitten");
183+
184+
List<Integer> allIds = query().from(kitten).orderBy(kitten.id.asc()).select(kitten.id).fetch();
185+
186+
List<Integer> expectedIds = allIds.subList(1, 4);
187+
188+
List<Cat> results =
189+
query()
190+
.from(cat)
191+
.where(
192+
cat.id.in(
193+
JPAExpressions.select(kitten.id)
194+
.from(kitten)
195+
.orderBy(kitten.id.asc())
196+
.limit(3)
197+
.offset(1)))
198+
.orderBy(cat.id.asc())
199+
.select(cat)
200+
.fetch();
201+
202+
assertThat(results).hasSize(3);
203+
assertThat(results.stream().map(Cat::getId).toList()).isEqualTo(expectedIds);
204+
}
205+
206+
@Test
207+
public void subQueryWithLimitInSelectClause() {
208+
var firstCat = query().from(cat).orderBy(cat.id.asc()).select(cat).fetchFirst();
209+
var mate = new QCat("mate");
210+
211+
Integer result =
212+
query()
213+
.from(cat)
214+
.where(cat.id.eq(firstCat.getId()))
215+
.select(
216+
JPAExpressions.select(mate.id)
217+
.from(mate)
218+
.where(mate.name.eq(firstCat.getName()))
219+
.orderBy(mate.id.asc())
220+
.limit(1))
221+
.fetchFirst();
222+
223+
assertThat(result).isEqualTo(firstCat.getId());
224+
}
225+
226+
@Test
227+
public void subQueryWithOffsetOnly() {
228+
var kitten = new QCat("kitten");
229+
230+
List<Integer> allIds = query().from(kitten).orderBy(kitten.id.asc()).select(kitten.id).fetch();
231+
232+
List<Integer> expectedIds = allIds.subList(2, allIds.size());
233+
234+
List<Cat> results =
235+
query()
236+
.from(cat)
237+
.where(
238+
cat.id.in(
239+
JPAExpressions.select(kitten.id)
240+
.from(kitten)
241+
.orderBy(kitten.id.asc())
242+
.offset(2)))
243+
.orderBy(cat.id.asc())
244+
.select(cat)
245+
.fetch();
246+
247+
assertThat(results).hasSize(expectedIds.size());
248+
assertThat(results.stream().map(Cat::getId).toList()).isEqualTo(expectedIds);
249+
}
179250
}

querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLSerializerTest.java

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.querydsl.core.types.Expression;
2525
import com.querydsl.core.types.Path;
2626
import com.querydsl.core.types.Predicate;
27+
import com.querydsl.core.types.SubQueryExpression;
2728
import com.querydsl.core.types.dsl.*;
2829
import com.querydsl.jpa.domain.*;
2930
import java.util.Arrays;
@@ -371,4 +372,234 @@ public void inClause_enumCollection() {
371372
Object constant = serializer.getConstants().get(0);
372373
assertThat(constant.toString()).isEqualTo("[BLACK, TABBY]");
373374
}
375+
376+
@Test
377+
public void visit_subQueryExpression_with_limit_jpql() {
378+
var cat = QCat.cat;
379+
var subQuery = JPAExpressions.select(cat.id).from(cat).limit(5);
380+
var serializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
381+
serializer.visit(subQuery, null);
382+
383+
assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat)");
384+
assertThat(serializer.getConstants()).isEmpty();
385+
}
386+
387+
@Test
388+
public void visit_subQueryExpression_with_limit_hql() {
389+
var cat = QCat.cat;
390+
var subQuery = JPAExpressions.select(cat.id).from(cat).limit(5);
391+
var serializer = new JPQLSerializer(HQLTemplates.DEFAULT);
392+
serializer.visit(subQuery, null);
393+
394+
assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat\nlimit ?1)");
395+
assertThat(serializer.getConstants()).hasSize(1);
396+
assertThat(serializer.getConstants().getFirst()).isEqualTo(5L);
397+
}
398+
399+
@Test
400+
public void visit_subQueryExpression_with_offset_jpql() {
401+
var cat = QCat.cat;
402+
var subQuery = JPAExpressions.select(cat.id).from(cat).offset(10);
403+
var serializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
404+
serializer.visit(subQuery, null);
405+
406+
assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat)");
407+
assertThat(serializer.getConstants()).isEmpty();
408+
}
409+
410+
@Test
411+
public void visit_subQueryExpression_with_offset_hql() {
412+
var cat = QCat.cat;
413+
var subQuery = JPAExpressions.select(cat.id).from(cat).offset(10);
414+
var serializer = new JPQLSerializer(HQLTemplates.DEFAULT);
415+
serializer.visit(subQuery, null);
416+
417+
assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat\noffset ?1)");
418+
assertThat(serializer.getConstants()).hasSize(1);
419+
assertThat(serializer.getConstants().getFirst()).isEqualTo(10L);
420+
}
421+
422+
@Test
423+
public void visit_subQueryExpression_with_limit_and_offset_jpql() {
424+
var cat = QCat.cat;
425+
var subQuery = JPAExpressions.select(cat.id).from(cat).limit(5).offset(10);
426+
var serializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
427+
serializer.visit(subQuery, null);
428+
429+
assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat)");
430+
assertThat(serializer.getConstants()).isEmpty();
431+
}
432+
433+
@Test
434+
public void visit_subQueryExpression_with_limit_and_offset_hql() {
435+
var cat = QCat.cat;
436+
var subQuery = JPAExpressions.select(cat.id).from(cat).limit(5).offset(10);
437+
var serializer = new JPQLSerializer(HQLTemplates.DEFAULT);
438+
serializer.visit(subQuery, null);
439+
440+
assertThat(serializer.toString())
441+
.isEqualTo("(select cat.id\nfrom Cat cat\nlimit ?1\noffset ?2)");
442+
assertThat(serializer.getConstants()).hasSize(2);
443+
assertThat(serializer.getConstants().get(0)).isEqualTo(5L);
444+
assertThat(serializer.getConstants().get(1)).isEqualTo(10L);
445+
}
446+
447+
@Test
448+
public void visit_nested_subQueryExpression_with_modifiers_hql() {
449+
var cat = QCat.cat;
450+
var mate = new QCat("mate");
451+
452+
var innerSubQuery = JPAExpressions.select(mate.id).from(mate).limit(3);
453+
454+
var outerSubQuery =
455+
JPAExpressions.select(cat.id).from(cat).where(cat.id.in(innerSubQuery)).limit(5);
456+
457+
var serializer = new JPQLSerializer(HQLTemplates.DEFAULT);
458+
serializer.visit(outerSubQuery, null);
459+
460+
assertThat(serializer.toString())
461+
.isEqualTo(
462+
"""
463+
(select cat.id
464+
from Cat cat
465+
where cat.id in (select mate.id
466+
from Cat mate
467+
limit ?1)
468+
limit ?2)""");
469+
assertThat(serializer.getConstants()).hasSize(2);
470+
assertThat(serializer.getConstants().get(0)).isEqualTo(3L);
471+
assertThat(serializer.getConstants().get(1)).isEqualTo(5L);
472+
}
473+
474+
@Test
475+
public void subquery_in_where_clause_with_modifiers_jpql() {
476+
var cat = QCat.cat;
477+
var kitten = new QCat("kitten");
478+
479+
QueryMetadata mainQuery = new DefaultQueryMetadata();
480+
mainQuery.addJoin(JoinType.DEFAULT, cat);
481+
482+
SubQueryExpression<Integer> subQuery =
483+
JPAExpressions.select(kitten.id)
484+
.from(kitten)
485+
.where(kitten.name.isNotNull())
486+
.limit(5)
487+
.offset(10);
488+
489+
mainQuery.addWhere(cat.id.in(subQuery));
490+
mainQuery.setProjection(cat);
491+
492+
var jpqlSerializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
493+
jpqlSerializer.serialize(mainQuery, false, null);
494+
495+
assertThat(jpqlSerializer.toString())
496+
.isEqualTo(
497+
"""
498+
select cat
499+
from Cat cat
500+
where cat.id in (select kitten.id
501+
from Cat kitten
502+
where kitten.name is not null)""");
503+
assertThat(jpqlSerializer.getConstants()).isEmpty();
504+
}
505+
506+
@Test
507+
public void subquery_in_where_clause_with_modifiers_hql() {
508+
var cat = QCat.cat;
509+
var kitten = new QCat("kitten");
510+
511+
QueryMetadata mainQuery = new DefaultQueryMetadata();
512+
mainQuery.addJoin(JoinType.DEFAULT, cat);
513+
514+
SubQueryExpression<Integer> subQuery =
515+
JPAExpressions.select(kitten.id)
516+
.from(kitten)
517+
.where(kitten.name.isNotNull())
518+
.limit(5)
519+
.offset(10);
520+
521+
mainQuery.addWhere(cat.id.in(subQuery));
522+
mainQuery.setProjection(cat);
523+
524+
var hqlSerializer = new JPQLSerializer(HQLTemplates.DEFAULT);
525+
hqlSerializer.serialize(mainQuery, false, null);
526+
527+
assertThat(hqlSerializer.toString())
528+
.isEqualTo(
529+
"""
530+
select cat
531+
from Cat cat
532+
where cat.id in (select kitten.id
533+
from Cat kitten
534+
where kitten.name is not null
535+
limit ?1
536+
offset ?2)""");
537+
assertThat(hqlSerializer.getConstants()).hasSize(2);
538+
assertThat(hqlSerializer.getConstants().get(0)).isEqualTo(5L);
539+
assertThat(hqlSerializer.getConstants().get(1)).isEqualTo(10L);
540+
}
541+
542+
@Test
543+
public void subquery_in_select_clause_with_modifiers_jpql() {
544+
var cat = QCat.cat;
545+
var mate = new QCat("mate");
546+
547+
QueryMetadata mainQuery = new DefaultQueryMetadata();
548+
mainQuery.addJoin(JoinType.DEFAULT, cat);
549+
550+
SubQueryExpression<Integer> subQuery =
551+
JPAExpressions.select(mate.weight.max())
552+
.from(mate)
553+
.where(mate.name.eq(cat.name))
554+
.limit(1)
555+
.offset(2);
556+
557+
mainQuery.setProjection(subQuery);
558+
559+
var jpqlSerializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
560+
jpqlSerializer.serialize(mainQuery, false, null);
561+
562+
assertThat(jpqlSerializer.toString())
563+
.isEqualTo(
564+
"""
565+
select (select max(mate.weight)
566+
from Cat mate
567+
where mate.name = cat.name)
568+
from Cat cat""");
569+
assertThat(jpqlSerializer.getConstants()).isEmpty();
570+
}
571+
572+
@Test
573+
public void subquery_in_select_clause_with_modifiers_hql() {
574+
var cat = QCat.cat;
575+
var mate = new QCat("mate");
576+
577+
QueryMetadata mainQuery = new DefaultQueryMetadata();
578+
mainQuery.addJoin(JoinType.DEFAULT, cat);
579+
580+
SubQueryExpression<Integer> subQuery =
581+
JPAExpressions.select(mate.weight.max())
582+
.from(mate)
583+
.where(mate.name.eq(cat.name))
584+
.limit(1)
585+
.offset(2);
586+
587+
mainQuery.setProjection(subQuery);
588+
589+
var hqlSerializer = new JPQLSerializer(HQLTemplates.DEFAULT);
590+
hqlSerializer.serialize(mainQuery, false, null);
591+
592+
assertThat(hqlSerializer.toString())
593+
.isEqualTo(
594+
"""
595+
select (select max(mate.weight)
596+
from Cat mate
597+
where mate.name = cat.name
598+
limit ?1
599+
offset ?2)
600+
from Cat cat""");
601+
assertThat(hqlSerializer.getConstants()).hasSize(2);
602+
assertThat(hqlSerializer.getConstants().get(0)).isEqualTo(1L);
603+
assertThat(hqlSerializer.getConstants().get(1)).isEqualTo(2L);
604+
}
374605
}

querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLTemplatesTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,16 @@ public void generic_precedence() {
9595
TemplatesTestUtils.testPrecedence(templates);
9696
}
9797
}
98+
99+
@Test
100+
public void subQueryModifiers_support() {
101+
assertThat(JPQLTemplates.DEFAULT.isSubQueryModifiersSupported()).isFalse();
102+
assertThat(new EclipseLinkTemplates().isSubQueryModifiersSupported()).isFalse();
103+
assertThat(new OpenJPATemplates().isSubQueryModifiersSupported()).isFalse();
104+
assertThat(new DataNucleusTemplates().isSubQueryModifiersSupported()).isFalse();
105+
assertThat(new BatooTemplates().isSubQueryModifiersSupported()).isFalse();
106+
107+
assertThat(HQLTemplates.DEFAULT.isSubQueryModifiersSupported()).isTrue();
108+
assertThat(new HQLTemplates().isSubQueryModifiersSupported()).isTrue();
109+
}
98110
}

0 commit comments

Comments
 (0)