diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/HQLTemplates.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/HQLTemplates.java index 81857ac07..a6409d8db 100644 --- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/HQLTemplates.java +++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/HQLTemplates.java @@ -116,4 +116,9 @@ public boolean isWithForOn() { public boolean isCaseWithLiterals() { return true; } + + @Override + public boolean isSubQueryModifiersSupported() { + return true; + } } diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLSerializer.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLSerializer.java index 80dbee1c4..a4de7213a 100644 --- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLSerializer.java +++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLSerializer.java @@ -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; @@ -65,6 +66,10 @@ public class JPQLSerializer extends SerializerBase { 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("; @@ -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; } diff --git a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLTemplates.java b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLTemplates.java index 34e7ee4c5..38b43d64a 100644 --- a/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLTemplates.java +++ b/querydsl-libraries/querydsl-jpa/src/main/java/com/querydsl/jpa/JPQLTemplates.java @@ -236,4 +236,8 @@ private String escapeLiteral(String str) { } return builder.toString(); } + + public boolean isSubQueryModifiersSupported() { + return false; + } } diff --git a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateBase.java b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateBase.java index 3c7230162..33cadd4e0 100644 --- a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateBase.java +++ b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/HibernateBase.java @@ -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 allIds = query().from(kitten).orderBy(kitten.id.asc()).select(kitten.id).fetch(); + + List expectedIds = allIds.subList(1, 4); + + List 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 allIds = query().from(kitten).orderBy(kitten.id.asc()).select(kitten.id).fetch(); + + List expectedIds = allIds.subList(2, allIds.size()); + + List 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); + } } diff --git a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLSerializerTest.java b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLSerializerTest.java index a6b4161a8..6e0fd1b7e 100644 --- a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLSerializerTest.java +++ b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLSerializerTest.java @@ -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; @@ -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 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 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 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 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); + } } diff --git a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLTemplatesTest.java b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLTemplatesTest.java index a40308957..e635fc6ff 100644 --- a/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLTemplatesTest.java +++ b/querydsl-libraries/querydsl-jpa/src/test/java/com/querydsl/jpa/JPQLTemplatesTest.java @@ -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(); + } }