diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 index 728d3fe7b..728ef8ca2 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4 @@ -1349,6 +1349,9 @@ CASE : C A S E; CAST : C A S T; CEILING : C E I L I N G; COLLATE : C O L L A T E; +COLUMN : C O L U M N; +CONFLICT : C O N F L I C T; +CONSTRAINT : C O N S T R A I N T; CONTAINS : C O N T A I N S; COUNT : C O U N T; CROSS : C R O S S; @@ -1358,11 +1361,7 @@ CURRENT_DATE : C U R R E N T '_' D A T E; CURRENT_INSTANT : C U R R E N T '_' I N S T A N T; CURRENT_TIME : C U R R E N T '_' T I M E; CURRENT_TIMESTAMP : C U R R E N T '_' T I M E S T A M P; -CONFLICT : C O N F L I C T; -CONSTRAINT : C O N S T R A I N T; -COLUMN : C O L U M N; CYCLE : C Y C L E; -DO : D O; DATE : D A T E; DATETIME : D A T E T I M E ; DAY : D A Y; @@ -1371,6 +1370,7 @@ DELETE : D E L E T E; DEPTH : D E P T H; DESC : D E S C; DISTINCT : D I S T I N C T; +DO : D O; ELEMENT : E L E M E N T; ELEMENTS : E L E M E N T S; ELSE : E L S E; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java index 1bdde97be..87fe53e05 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryRenderer.java @@ -38,6 +38,7 @@ import org.springframework.util.CompositeIterator; * * * @author Mark Paluch + * @author Christoph Strobl */ abstract class QueryRenderer implements QueryTokenStream { @@ -243,7 +244,7 @@ abstract class QueryRenderer implements QueryTokenStream { for (QueryRenderer queryRenderer : nested) { if (lastAppended != null && (lastExpression || queryRenderer.isExpression()) && !builder.isEmpty() - && !lastAppended.endsWith(" ")) { + && (!lastAppended.endsWith(" ") && !lastAppended.endsWith("("))) { builder.append(' '); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java index 4c25ec66f..99547994e 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java @@ -1556,7 +1556,10 @@ class HqlQueryRendererTests { @ValueSource(strings = { "YEAR", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", "NANOSECOND", "EPOCH" }) void durationLiteralsShouldWork(String dtField) { + assertQuery("SELECT ce.id FROM CalendarEvent ce WHERE (ce.endDate - ce.startDate) > 5 %s".formatted(dtField)); + assertQuery("SELECT ce.id FROM CalendarEvent ce WHERE ce.text LIKE :text GROUP BY year(cd.date) HAVING (ce.endDate - ce.startDate) > 5 %s".formatted(dtField)); + assertQuery("SELECT ce.id as id, cd.startDate + 5 %s AS summedDate FROM CalendarEvent ce".formatted(dtField)); } @Test // GH-3025 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java index 482b02db4..40aa7d274 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryTransformerTests.java @@ -1090,7 +1090,7 @@ class HqlQueryTransformerTests { "SELECT t3 FROM Test3 t3 JOIN t3.test2 x WHERE x.id = :test2Id order by t3.testDuplicateColumnName desc"); } - @Test // GH-3269 + @Test // GH-3269, GH-3689 void createsCountQueryUsingAliasCorrectly() { assertCountQuery("select distinct 1 as x from Employee", "select count(distinct 1) from Employee AS __"); @@ -1102,6 +1102,7 @@ class HqlQueryTransformerTests { "select count(distinct a, b, sum(amount), d) from Employee AS __ GROUP BY n"); assertCountQuery("select distinct a, count(b) as c from Employee GROUP BY n", "select count(distinct a, count(b)) from Employee AS __ GROUP BY n"); + assertCountQuery("select distinct substring(e.firstname, 1, position('a' in e.lastname)) as x from from Employee", "select count(distinct substring(e.firstname, 1, position('a' in e.lastname))) from from Employee"); } @Test // GH-3427 diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java index 0dff2c328..80483a05b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlSpecificationTests.java @@ -23,7 +23,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; - import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; /** @@ -35,6 +34,7 @@ import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer * * @author Greg Turnquist * @author Mark Paluch + * @author Christoph Strobl * @since 3.1 */ class HqlSpecificationTests { @@ -335,18 +335,15 @@ class HqlSpecificationTests { """); } - @Test // GH-3689 - void generic() { + @ParameterizedTest // GH-3689 + @ValueSource(strings = { "RESPECT NULLS", "IGNORE NULLS" }) + void generic(String nullHandling) { + // not in the official documentation but supported in the grammar. assertQuery(""" SELECT e FROM Employee e - WHERE FOO(x).bar RESPECT NULLS - """); - - assertQuery(""" - SELECT e FROM Employee e - WHERE FOO(x).bar IGNORE NULLS - """); + WHERE FOO(x).bar %s + """.formatted(nullHandling)); } @Test // GH-3689 @@ -356,6 +353,11 @@ class HqlSpecificationTests { SELECT e FROM Employee e WHERE SIZE(x) > 1 """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE SIZE(e.skills) > 1 + """); } @Test // GH-3689 @@ -384,10 +386,15 @@ class HqlSpecificationTests { SELECT e FROM Employee e WHERE TRUNC(e, 'foo') = TRUNCATE(e, 'bar') """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE TRUNC(e, 'YEAR') = TRUNCATE(LOCAL DATETIME, 'YEAR') + """); } @ParameterizedTest // GH-3689 - @ValueSource(strings = { "YYYY", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", + @ValueSource(strings = { "YEAR", "MONTH", "DAY", "WEEK", "QUARTER", "HOUR", "MINUTE", "SECOND", "NANOSECOND", "NANOSECOND", "EPOCH" }) void trunc(String truncation) { @@ -402,7 +409,17 @@ class HqlSpecificationTests { assertQuery(""" SELECT e FROM Employee e - WHERE FORMAT(x AS 'foo') = FORMAT(x AS 'bar') + WHERE FORMAT(x AS 'yyyy') = FORMAT(e.hiringDate AS 'yyyy') + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE e.hiringDate = format(LOCAL DATETIME as 'yyyy-MM-dd') + """); + + assertQuery(""" + SELECT e FROM Employee e + WHERE e.hiringDate = format(LOCAL_DATE() as 'yyyy-MM-dd') """); } @@ -411,7 +428,7 @@ class HqlSpecificationTests { assertQuery(""" SELECT e FROM Employee e - WHERE COLLATE(x AS foo) = COLLATE(x AS foo.bar) + WHERE COLLATE(x AS ucs_basic) = COLLATE(e.name AS ucs_basic) """); } @@ -424,11 +441,20 @@ class HqlSpecificationTests { assertQuery("select substring(c.number, 1) " + // "from Call c"); + assertQuery("select substring(c.number, 1, position('/0' in c.number)) " + // + "from Call c"); + assertQuery("select substring(c.number FROM 1 FOR 2) " + // "from Call c"); assertQuery("select substring(c.number FROM 1) " + // "from Call c"); + + assertQuery("select substring(c.number FROM 1 FOR position('/0' in c.number)) " + // + "from Call c"); + + assertQuery("select substring(c.number FROM 1) AS shortNumber " + // + "from Call c"); } @Test // GH-3689 @@ -462,6 +488,9 @@ class HqlSpecificationTests { assertQuery("select POSITION(c.number IN 'foo') " + // "from Call c "); + + assertQuery("select POSITION(c.number IN 'foo') + 1 AS pos " + // + "from Call c "); } @Test // GH-3689 @@ -490,6 +519,9 @@ class HqlSpecificationTests { assertQuery("select OFFSET DATETIME, OFFSET_DATETIME() " + // "from Call c "); + + assertQuery("select OFFSET DATETIME AS offsetDatetime, OFFSET_DATETIME() AS offset_datetime " + // + "from Call c "); } @Test // GH-3689 @@ -497,6 +529,8 @@ class HqlSpecificationTests { assertQuery("select CUBE(foo), CUBE(foo, bar) " + // "from Call c "); + + assertQuery("select c.callerId from Call c GROUP BY CUBE(state, province)"); } @Test // GH-3689 @@ -504,6 +538,8 @@ class HqlSpecificationTests { assertQuery("select ROLLUP(foo), ROLLUP(foo, bar) " + // "from Call c "); + + assertQuery("select c.callerId from Call c GROUP BY ROLLUP(state, province)"); } @Test @@ -710,16 +746,13 @@ class HqlSpecificationTests { """); } - @Test // GH-3628 - void functionInvocationWithIsBoolean() { + @ParameterizedTest // GH-3628 + @ValueSource(strings = { "is true", "is not true", "is false", "is not false" }) + void functionInvocationWithIsBoolean(String booleanComparison) { assertQuery(""" - from RoleTmpl where find_in_set(:appId, appIds) is true - """); - - assertQuery(""" - from RoleTmpl where find_in_set(:appId, appIds) is false - """); + from RoleTmpl where find_in_set(:appId, appIds) %s + """.formatted(booleanComparison)); } @Test @@ -1094,20 +1127,36 @@ class HqlSpecificationTests { """); } - @Test // GH-3628 - void distinctFromPredicate() { + @ParameterizedTest // GH-3628 + @ValueSource(strings = { "IS DISTINCT FROM", "IS NOT DISTINCT FROM" }) + void distinctFromPredicate(String distinctFrom) { assertQuery(""" SELECT c FROM Customer c - WHERE c.orders IS DISTINCT FROM c.payments - """); + WHERE c.orders %s c.payments + """.formatted(distinctFrom)); assertQuery(""" SELECT c FROM Customer c - WHERE c.orders IS NOT DISTINCT FROM c.payments - """); + WHERE c.orders %s c.payments + """.formatted(distinctFrom)); + + assertQuery(""" + SELECT c + FROM Customer c + GROUP BY c.lastname + HAVING c.orders %s c.payments + """.formatted(distinctFrom)); + + assertQuery(""" + SELECT c + FROM Customer c + WHERE EXISTS (SELECT c2 + FROM Customer c2 + WHERE c2.orders %s c.orders) + """.formatted(distinctFrom)); } @Test @@ -1576,7 +1625,7 @@ class HqlSpecificationTests { assertQuery("select longest.duration " + // "from Phone p " + // "left join lateral (" + // - " select c.duration as duration " + // + "select c.duration as duration " + // " from p.calls c" + // " order by c.duration desc" + // " limit 1 " + //