From b83fb680b464bb43b59fc1b914ff9dc2abb81894 Mon Sep 17 00:00:00 2001 From: Choi Wang Gyu Date: Mon, 4 Aug 2025 22:07:54 +0900 Subject: [PATCH] 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 --- .../repository/query/JpqlQueryBuilder.java | 11 ++++++-- .../query/JpqlQueryBuilderUnitTests.java | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) 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() {