Skip to content

Commit b83fb68

Browse files
committed
Fix double parentheses in IN predicate rendering
Prevent duplicate parentheses when rendering IN/NOT IN predicates with expressions that already contain parentheses. Added logic to check if predicate string starts and ends with parentheses before wrapping with additional parentheses. This change improves JPQL query readability by avoiding patterns like "field IN (('value1', 'value2'))" and ensures proper syntax for subqueries and already-parenthesized expressions. Added comprehensive unit tests to verify the fix handles various scenarios including regular expressions, pre-parenthesized expressions, and subquery expressions. Signed-off-by: Choi Wang Gyu <dhkdrb897@gmail.com>
1 parent ae2ff8f commit b83fb68

File tree

2 files changed

+35
-2
lines changed

2 files changed

+35
-2
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
* A Domain-Specific Language to build JPQL queries using Java code.
4545
*
4646
* @author Mark Paluch
47+
* @author Choi Wang Gyu
4748
*/
4849
@SuppressWarnings("JavadocDeclaration")
4950
public final class JpqlQueryBuilder {
@@ -1422,8 +1423,14 @@ record InPredicate(Expression path, String operator, Expression predicate) imple
14221423
@Override
14231424
public String render(RenderContext context) {
14241425

1425-
// TODO: should we rather wrap it with nested or check if its a nested predicate before we call render
1426-
return "%s %s (%s)".formatted(path.render(context), operator, predicate.render(context));
1426+
String predicateStr = predicate.render(context);
1427+
1428+
// Avoid double parentheses if predicate string already starts and ends with parentheses
1429+
if (predicateStr.startsWith("(") && predicateStr.endsWith(")")) {
1430+
return "%s %s %s".formatted(path.render(context), operator, predicateStr);
1431+
}
1432+
1433+
return "%s %s (%s)".formatted(path.render(context), operator, predicateStr);
14271434
}
14281435

14291436
@Override

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
*
3535
* @author Christoph Strobl
3636
* @author Mark Paluch
37+
* @author Choi Wang Gyu
3738
*/
3839
class JpqlQueryBuilderUnitTests {
3940

@@ -136,6 +137,31 @@ void predicateRendering() {
136137
assertThat(where.neq(expression("'AT'")).render(context)).isEqualTo("o.country != 'AT'");
137138
}
138139

140+
@Test // GH-3961 - Nested predicate parentheses handling
141+
void inPredicateWithNestedExpression() {
142+
143+
Entity entity = JpqlQueryBuilder.entity(Order.class);
144+
WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country"));
145+
RenderContext context = ctx(entity);
146+
147+
// Test regular IN predicate with parentheses
148+
assertThat(where.in(expression("'AT', 'DE'")).render(context)).isEqualTo("o.country IN ('AT', 'DE')");
149+
150+
// Test IN predicate with already parenthesized expression - should avoid double parentheses
151+
Expression parenthesizedExpression = expression("('AT', 'DE')");
152+
assertThat(where.in(parenthesizedExpression).render(context))
153+
.isEqualTo("o.country IN ('AT', 'DE')");
154+
155+
// Test NOT IN predicate with already parenthesized expression
156+
assertThat(where.notIn(parenthesizedExpression).render(context))
157+
.isEqualTo("o.country NOT IN ('AT', 'DE')");
158+
159+
// Test IN with subquery (already parenthesized)
160+
Expression subqueryExpression = expression("(SELECT c.code FROM Country c WHERE c.active = true)");
161+
assertThat(where.in(subqueryExpression).render(context))
162+
.isEqualTo("o.country IN (SELECT c.code FROM Country c WHERE c.active = true)");
163+
}
164+
139165
@Test // GH-3588
140166
void selectRendering() {
141167

0 commit comments

Comments
 (0)