Skip to content

Add support for subquery modifiers (LIMIT/OFFSET) in Hibernate #1304

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

Merged
merged 5 commits into from
Aug 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,9 @@ public boolean isWithForOn() {
public boolean isCaseWithLiterals() {
return true;
}

@Override
public boolean isSubQueryModifiersSupported() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import com.querydsl.core.JoinExpression;
import com.querydsl.core.JoinType;
import com.querydsl.core.QueryMetadata;
import com.querydsl.core.QueryModifiers;
import com.querydsl.core.support.SerializerBase;
import com.querydsl.core.types.*;
import com.querydsl.core.types.dsl.Expressions;
Expand Down Expand Up @@ -65,6 +66,10 @@ public class JPQLSerializer extends SerializerBase<JPQLSerializer> {

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

private static final String LIMIT = "\nlimit ";

private static final String OFFSET = "\noffset ";

private static final String SELECT = "select ";

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

@Override
public Void visit(SubQueryExpression<?> query, Void context) {
QueryMetadata metadata = query.getMetadata();
QueryModifiers modifiers = metadata.getModifiers();

append("(");
serialize(query.getMetadata(), false, null);
serialize(metadata, false, null);

if (modifiers != null
&& modifiers.isRestricting()
&& templates.isSubQueryModifiersSupported()) {
if (modifiers.getLimit() != null) {
append(LIMIT).handle(modifiers.getLimit());
}
if (modifiers.getOffset() != null) {
append(OFFSET).handle(modifiers.getOffset());
}
}

append(")");
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,8 @@ private String escapeLiteral(String str) {
}
return builder.toString();
}

public boolean isSubQueryModifiersSupported() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,75 @@ public void createQuery4() {
.distinct()
.transform(GroupBy.groupBy(cat.id).as(cat)));
}

@Test
public void subQueryWithLimitAndOffset() {
var kitten = new QCat("kitten");

List<Integer> allIds = query().from(kitten).orderBy(kitten.id.asc()).select(kitten.id).fetch();

List<Integer> expectedIds = allIds.subList(1, 4);

List<Cat> results =
query()
.from(cat)
.where(
cat.id.in(
JPAExpressions.select(kitten.id)
.from(kitten)
.orderBy(kitten.id.asc())
.limit(3)
.offset(1)))
.orderBy(cat.id.asc())
.select(cat)
.fetch();

assertThat(results).hasSize(3);
assertThat(results.stream().map(Cat::getId).toList()).isEqualTo(expectedIds);
}

@Test
public void subQueryWithLimitInSelectClause() {
var firstCat = query().from(cat).orderBy(cat.id.asc()).select(cat).fetchFirst();
var mate = new QCat("mate");

Integer result =
query()
.from(cat)
.where(cat.id.eq(firstCat.getId()))
.select(
JPAExpressions.select(mate.id)
.from(mate)
.where(mate.name.eq(firstCat.getName()))
.orderBy(mate.id.asc())
.limit(1))
.fetchFirst();

assertThat(result).isEqualTo(firstCat.getId());
}

@Test
public void subQueryWithOffsetOnly() {
var kitten = new QCat("kitten");

List<Integer> allIds = query().from(kitten).orderBy(kitten.id.asc()).select(kitten.id).fetch();

List<Integer> expectedIds = allIds.subList(2, allIds.size());

List<Cat> results =
query()
.from(cat)
.where(
cat.id.in(
JPAExpressions.select(kitten.id)
.from(kitten)
.orderBy(kitten.id.asc())
.offset(2)))
.orderBy(cat.id.asc())
.select(cat)
.fetch();

assertThat(results).hasSize(expectedIds.size());
assertThat(results.stream().map(Cat::getId).toList()).isEqualTo(expectedIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.querydsl.core.types.Expression;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.Predicate;
import com.querydsl.core.types.SubQueryExpression;
import com.querydsl.core.types.dsl.*;
import com.querydsl.jpa.domain.*;
import java.util.Arrays;
Expand Down Expand Up @@ -371,4 +372,234 @@ public void inClause_enumCollection() {
Object constant = serializer.getConstants().get(0);
assertThat(constant.toString()).isEqualTo("[BLACK, TABBY]");
}

@Test
public void visit_subQueryExpression_with_limit_jpql() {
var cat = QCat.cat;
var subQuery = JPAExpressions.select(cat.id).from(cat).limit(5);
var serializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
serializer.visit(subQuery, null);

assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat)");
assertThat(serializer.getConstants()).isEmpty();
}

@Test
public void visit_subQueryExpression_with_limit_hql() {
var cat = QCat.cat;
var subQuery = JPAExpressions.select(cat.id).from(cat).limit(5);
var serializer = new JPQLSerializer(HQLTemplates.DEFAULT);
serializer.visit(subQuery, null);

assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat\nlimit ?1)");
assertThat(serializer.getConstants()).hasSize(1);
assertThat(serializer.getConstants().getFirst()).isEqualTo(5L);
}

@Test
public void visit_subQueryExpression_with_offset_jpql() {
var cat = QCat.cat;
var subQuery = JPAExpressions.select(cat.id).from(cat).offset(10);
var serializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
serializer.visit(subQuery, null);

assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat)");
assertThat(serializer.getConstants()).isEmpty();
}

@Test
public void visit_subQueryExpression_with_offset_hql() {
var cat = QCat.cat;
var subQuery = JPAExpressions.select(cat.id).from(cat).offset(10);
var serializer = new JPQLSerializer(HQLTemplates.DEFAULT);
serializer.visit(subQuery, null);

assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat\noffset ?1)");
assertThat(serializer.getConstants()).hasSize(1);
assertThat(serializer.getConstants().getFirst()).isEqualTo(10L);
}

@Test
public void visit_subQueryExpression_with_limit_and_offset_jpql() {
var cat = QCat.cat;
var subQuery = JPAExpressions.select(cat.id).from(cat).limit(5).offset(10);
var serializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
serializer.visit(subQuery, null);

assertThat(serializer.toString()).isEqualTo("(select cat.id\nfrom Cat cat)");
assertThat(serializer.getConstants()).isEmpty();
}

@Test
public void visit_subQueryExpression_with_limit_and_offset_hql() {
var cat = QCat.cat;
var subQuery = JPAExpressions.select(cat.id).from(cat).limit(5).offset(10);
var serializer = new JPQLSerializer(HQLTemplates.DEFAULT);
serializer.visit(subQuery, null);

assertThat(serializer.toString())
.isEqualTo("(select cat.id\nfrom Cat cat\nlimit ?1\noffset ?2)");
assertThat(serializer.getConstants()).hasSize(2);
assertThat(serializer.getConstants().get(0)).isEqualTo(5L);
assertThat(serializer.getConstants().get(1)).isEqualTo(10L);
}

@Test
public void visit_nested_subQueryExpression_with_modifiers_hql() {
var cat = QCat.cat;
var mate = new QCat("mate");

var innerSubQuery = JPAExpressions.select(mate.id).from(mate).limit(3);

var outerSubQuery =
JPAExpressions.select(cat.id).from(cat).where(cat.id.in(innerSubQuery)).limit(5);

var serializer = new JPQLSerializer(HQLTemplates.DEFAULT);
serializer.visit(outerSubQuery, null);

assertThat(serializer.toString())
.isEqualTo(
"""
(select cat.id
from Cat cat
where cat.id in (select mate.id
from Cat mate
limit ?1)
limit ?2)""");
assertThat(serializer.getConstants()).hasSize(2);
assertThat(serializer.getConstants().get(0)).isEqualTo(3L);
assertThat(serializer.getConstants().get(1)).isEqualTo(5L);
}

@Test
public void subquery_in_where_clause_with_modifiers_jpql() {
var cat = QCat.cat;
var kitten = new QCat("kitten");

QueryMetadata mainQuery = new DefaultQueryMetadata();
mainQuery.addJoin(JoinType.DEFAULT, cat);

SubQueryExpression<Integer> subQuery =
JPAExpressions.select(kitten.id)
.from(kitten)
.where(kitten.name.isNotNull())
.limit(5)
.offset(10);

mainQuery.addWhere(cat.id.in(subQuery));
mainQuery.setProjection(cat);

var jpqlSerializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
jpqlSerializer.serialize(mainQuery, false, null);

assertThat(jpqlSerializer.toString())
.isEqualTo(
"""
select cat
from Cat cat
where cat.id in (select kitten.id
from Cat kitten
where kitten.name is not null)""");
assertThat(jpqlSerializer.getConstants()).isEmpty();
}

@Test
public void subquery_in_where_clause_with_modifiers_hql() {
var cat = QCat.cat;
var kitten = new QCat("kitten");

QueryMetadata mainQuery = new DefaultQueryMetadata();
mainQuery.addJoin(JoinType.DEFAULT, cat);

SubQueryExpression<Integer> subQuery =
JPAExpressions.select(kitten.id)
.from(kitten)
.where(kitten.name.isNotNull())
.limit(5)
.offset(10);

mainQuery.addWhere(cat.id.in(subQuery));
mainQuery.setProjection(cat);

var hqlSerializer = new JPQLSerializer(HQLTemplates.DEFAULT);
hqlSerializer.serialize(mainQuery, false, null);

assertThat(hqlSerializer.toString())
.isEqualTo(
"""
select cat
from Cat cat
where cat.id in (select kitten.id
from Cat kitten
where kitten.name is not null
limit ?1
offset ?2)""");
assertThat(hqlSerializer.getConstants()).hasSize(2);
assertThat(hqlSerializer.getConstants().get(0)).isEqualTo(5L);
assertThat(hqlSerializer.getConstants().get(1)).isEqualTo(10L);
}

@Test
public void subquery_in_select_clause_with_modifiers_jpql() {
var cat = QCat.cat;
var mate = new QCat("mate");

QueryMetadata mainQuery = new DefaultQueryMetadata();
mainQuery.addJoin(JoinType.DEFAULT, cat);

SubQueryExpression<Integer> subQuery =
JPAExpressions.select(mate.weight.max())
.from(mate)
.where(mate.name.eq(cat.name))
.limit(1)
.offset(2);

mainQuery.setProjection(subQuery);

var jpqlSerializer = new JPQLSerializer(JPQLTemplates.DEFAULT);
jpqlSerializer.serialize(mainQuery, false, null);

assertThat(jpqlSerializer.toString())
.isEqualTo(
"""
select (select max(mate.weight)
from Cat mate
where mate.name = cat.name)
from Cat cat""");
assertThat(jpqlSerializer.getConstants()).isEmpty();
}

@Test
public void subquery_in_select_clause_with_modifiers_hql() {
var cat = QCat.cat;
var mate = new QCat("mate");

QueryMetadata mainQuery = new DefaultQueryMetadata();
mainQuery.addJoin(JoinType.DEFAULT, cat);

SubQueryExpression<Integer> subQuery =
JPAExpressions.select(mate.weight.max())
.from(mate)
.where(mate.name.eq(cat.name))
.limit(1)
.offset(2);

mainQuery.setProjection(subQuery);

var hqlSerializer = new JPQLSerializer(HQLTemplates.DEFAULT);
hqlSerializer.serialize(mainQuery, false, null);

assertThat(hqlSerializer.toString())
.isEqualTo(
"""
select (select max(mate.weight)
from Cat mate
where mate.name = cat.name
limit ?1
offset ?2)
from Cat cat""");
assertThat(hqlSerializer.getConstants()).hasSize(2);
assertThat(hqlSerializer.getConstants().get(0)).isEqualTo(1L);
assertThat(hqlSerializer.getConstants().get(1)).isEqualTo(2L);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,16 @@ public void generic_precedence() {
TemplatesTestUtils.testPrecedence(templates);
}
}

@Test
public void subQueryModifiers_support() {
assertThat(JPQLTemplates.DEFAULT.isSubQueryModifiersSupported()).isFalse();
assertThat(new EclipseLinkTemplates().isSubQueryModifiersSupported()).isFalse();
assertThat(new OpenJPATemplates().isSubQueryModifiersSupported()).isFalse();
assertThat(new DataNucleusTemplates().isSubQueryModifiersSupported()).isFalse();
assertThat(new BatooTemplates().isSubQueryModifiersSupported()).isFalse();

assertThat(HQLTemplates.DEFAULT.isSubQueryModifiersSupported()).isTrue();
assertThat(new HQLTemplates().isSubQueryModifiersSupported()).isTrue();
}
}