diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java index 124df50346..f9e9fb76f6 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilder.java @@ -44,6 +44,7 @@ * A Domain-Specific Language to build JPQL queries using Java code. * * @author Mark Paluch + * @author Choi Wang Gyu */ @SuppressWarnings("JavadocDeclaration") public final class JpqlQueryBuilder { @@ -1422,8 +1423,14 @@ record InPredicate(Expression path, String operator, Expression predicate) imple @Override public String render(RenderContext context) { - // TODO: should we rather wrap it with nested or check if its a nested predicate before we call render - return "%s %s (%s)".formatted(path.render(context), operator, predicate.render(context)); + String predicateStr = predicate.render(context); + + // Avoid double parentheses if predicate string already starts and ends with parentheses + if (predicateStr.startsWith("(") && predicateStr.endsWith(")")) { + return "%s %s %s".formatted(path.render(context), operator, predicateStr); + } + + return "%s %s (%s)".formatted(path.render(context), operator, predicateStr); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java index 46952dee71..376b116cf0 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryBuilderUnitTests.java @@ -34,6 +34,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Choi Wang Gyu */ class JpqlQueryBuilderUnitTests { @@ -136,6 +137,31 @@ void predicateRendering() { assertThat(where.neq(expression("'AT'")).render(context)).isEqualTo("o.country != 'AT'"); } + @Test // GH-3961 - Nested predicate parentheses handling + void inPredicateWithNestedExpression() { + + Entity entity = JpqlQueryBuilder.entity(Order.class); + WhereStep where = JpqlQueryBuilder.where(JpqlQueryBuilder.path(entity, "country")); + RenderContext context = ctx(entity); + + // Test regular IN predicate with parentheses + assertThat(where.in(expression("'AT', 'DE'")).render(context)).isEqualTo("o.country IN ('AT', 'DE')"); + + // Test IN predicate with already parenthesized expression - should avoid double parentheses + Expression parenthesizedExpression = expression("('AT', 'DE')"); + assertThat(where.in(parenthesizedExpression).render(context)) + .isEqualTo("o.country IN ('AT', 'DE')"); + + // Test NOT IN predicate with already parenthesized expression + assertThat(where.notIn(parenthesizedExpression).render(context)) + .isEqualTo("o.country NOT IN ('AT', 'DE')"); + + // Test IN with subquery (already parenthesized) + Expression subqueryExpression = expression("(SELECT c.code FROM Country c WHERE c.active = true)"); + assertThat(where.in(subqueryExpression).render(context)) + .isEqualTo("o.country IN (SELECT c.code FROM Country c WHERE c.active = true)"); + } + @Test // GH-3588 void selectRendering() {