diff --git a/pom.xml b/pom.xml index a3be6252ed..d3d1f65c5c 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa-parent - 3.3.0-SNAPSHOT + 3.3.x-3277-SNAPSHOT pom Spring Data JPA Parent diff --git a/spring-data-envers/pom.xml b/spring-data-envers/pom.xml index 470678d048..311a906934 100755 --- a/spring-data-envers/pom.xml +++ b/spring-data-envers/pom.xml @@ -5,12 +5,12 @@ org.springframework.data spring-data-envers - 3.3.0-SNAPSHOT + 3.3.x-3277-SNAPSHOT org.springframework.data spring-data-jpa-parent - 3.3.0-SNAPSHOT + 3.3.x-3277-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa-distribution/pom.xml b/spring-data-jpa-distribution/pom.xml index 6bd074181c..69d44c6176 100644 --- a/spring-data-jpa-distribution/pom.xml +++ b/spring-data-jpa-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-jpa-parent - 3.3.0-SNAPSHOT + 3.3.x-3277-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/pom.xml b/spring-data-jpa/pom.xml index 4a2875c758..25442f5243 100644 --- a/spring-data-jpa/pom.xml +++ b/spring-data-jpa/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-jpa - 3.3.0-SNAPSHOT + 3.3.x-3277-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-jpa-parent - 3.3.0-SNAPSHOT + 3.3.x-3277-SNAPSHOT ../pom.xml diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 index a0aa24491a..9bbf47a86c 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4 @@ -23,6 +23,7 @@ grammar Eql; * * https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/JPQL * * @author Greg Turnquist + * @author Christoph Strobl * @since 3.2 */ } @@ -509,7 +510,7 @@ functions_returning_numerics | LN '(' arithmetic_expression ')' | SIGN '(' arithmetic_expression ')' | SQRT '(' arithmetic_expression ')' - | MOD '(' arithmetic_expression '/' arithmetic_expression ')' + | MOD '(' arithmetic_expression ',' arithmetic_expression ')' | POWER '(' arithmetic_expression ',' arithmetic_expression ')' | ROUND '(' arithmetic_expression ',' arithmetic_expression ')' | SIZE '(' collection_valued_path_expression ')' @@ -894,4 +895,4 @@ INTLITERAL : ('0' .. '9')+ ; LONGLITERAL : ('0' .. '9')+ L; DATELITERAL : '{' D STRINGLITERAL '}'; TIMELITERAL : '{' T STRINGLITERAL '}'; -TIMESTAMPLITERAL : '{' T S STRINGLITERAL '}'; \ No newline at end of file +TIMESTAMPLITERAL : '{' T S STRINGLITERAL '}'; diff --git a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 index 637bac6c34..b80e499ffa 100644 --- a/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 +++ b/spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4 @@ -23,6 +23,7 @@ grammar Jpql; * * @see https://github.com/jakartaee/persistence/blob/master/spec/src/main/asciidoc/ch04-query-language.adoc#bnf * @author Greg Turnquist + * @author Christoph Strobl * @since 3.1 */ } @@ -203,6 +204,7 @@ constructor_item | scalar_expression | aggregate_expression | identification_variable + | literal ; aggregate_expression @@ -619,8 +621,10 @@ constructor_name literal : STRINGLITERAL + | JAVASTRINGLITERAL | INTLITERAL | FLOATLITERAL + | LONGLITERAL | boolean_literal | entity_type_literal ; @@ -650,6 +654,7 @@ escape_character numeric_literal : INTLITERAL | FLOATLITERAL + | LONGLITERAL ; boolean_literal @@ -849,9 +854,10 @@ WHERE : W H E R E; EQUAL : '=' ; NOT_EQUAL : '<>' | '!=' ; - CHARACTER : '\'' (~ ('\'' | '\\')) '\'' ; IDENTIFICATION_VARIABLE : ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '$' | '_') ('a' .. 'z' | 'A' .. 'Z' | '\u0080' .. '\ufffe' | '0' .. '9' | '$' | '_')* ; STRINGLITERAL : '\'' (~ ('\'' | '\\'))* '\'' ; -FLOATLITERAL : ('0' .. '9')* '.' ('0' .. '9')+ (E '0' .. '9')* ; +JAVASTRINGLITERAL : '"' ( ('\\' [btnfr"']) | ~('"'))* '"'; +FLOATLITERAL : ('0' .. '9')* '.' ('0' .. '9')+ (E ('0' .. '9')+)* (F|D)?; INTLITERAL : ('0' .. '9')+ ; +LONGLITERAL : ('0' .. '9')+L ; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java index 07201fd20c..6688d78f01 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/EqlQueryRenderer.java @@ -24,6 +24,7 @@ * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders an EQL query without making any changes. * * @author Greg Turnquist + * @author Christoph Strobl * @since 3.2 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode" }) @@ -1912,7 +1913,8 @@ public List visitFunctions_returning_numerics( tokens.add(new JpaQueryParsingToken(ctx.MOD(), false)); tokens.add(TOKEN_OPEN_PAREN); tokens.addAll(visit(ctx.arithmetic_expression(0))); - tokens.add(new JpaQueryParsingToken("/")); + NOSPACE(tokens); + tokens.add(TOKEN_COMMA); tokens.addAll(visit(ctx.arithmetic_expression(1))); NOSPACE(tokens); tokens.add(TOKEN_CLOSE_PAREN); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index bdd5ddc1ec..46f5335cc5 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java @@ -24,6 +24,7 @@ * An ANTLR {@link org.antlr.v4.runtime.tree.ParseTreeVisitor} that renders a JPQL query without making any changes. * * @author Greg Turnquist + * @author Christoph Strobl * @since 3.1 */ @SuppressWarnings({ "ConstantConditions", "DuplicatedCode" }) @@ -722,6 +723,8 @@ public List visitConstructor_item(JpqlParser.Constructor_i tokens.addAll(visit(ctx.aggregate_expression())); } else if (ctx.identification_variable() != null) { tokens.addAll(visit(ctx.identification_variable())); + } else if (ctx.literal() != null) { + tokens.addAll(visit(ctx.literal())); } return tokens; @@ -2152,10 +2155,14 @@ public List visitLiteral(JpqlParser.LiteralContext ctx) { if (ctx.STRINGLITERAL() != null) { tokens.add(new JpaQueryParsingToken(ctx.STRINGLITERAL())); + } else if (ctx.JAVASTRINGLITERAL() != null) { + tokens.add(new JpaQueryParsingToken(ctx.JAVASTRINGLITERAL())); } else if (ctx.INTLITERAL() != null) { tokens.add(new JpaQueryParsingToken(ctx.INTLITERAL())); } else if (ctx.FLOATLITERAL() != null) { tokens.add(new JpaQueryParsingToken(ctx.FLOATLITERAL())); + } else if(ctx.LONGLITERAL() != null) { + tokens.add(new JpaQueryParsingToken(ctx.LONGLITERAL())); } else if (ctx.boolean_literal() != null) { tokens.addAll(visit(ctx.boolean_literal())); } else if (ctx.entity_type_literal() != null) { @@ -2216,6 +2223,8 @@ public List visitNumeric_literal(JpqlParser.Numeric_litera return List.of(new JpaQueryParsingToken(ctx.INTLITERAL())); } else if (ctx.FLOATLITERAL() != null) { return List.of(new JpaQueryParsingToken(ctx.FLOATLITERAL())); + } else if(ctx.LONGLITERAL() != null) { + return List.of(new JpaQueryParsingToken(ctx.LONGLITERAL())); } else { return List.of(); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java index aacf1e6c50..67cbf22372 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlComplianceTests.java @@ -25,10 +25,13 @@ /** * Tests built around examples of EQL found in the EclipseLink's docs at * https://wiki.eclipse.org/EclipseLink/UserGuide/JPA/Basic_JPA_Development/Querying/JPQL
+ * With the exception of {@literal MOD} which is defined as {@literal MOD(arithmetic_expression , arithmetic_expression)}, + * but shown in tests as {@literal MOD(arithmetic_expression ? arithmetic_expression)}. *
* IMPORTANT: Purely verifies the parser without any transformations. * * @author Greg Turnquist + * @author Christoph Strobl */ class EqlComplianceTests { @@ -214,7 +217,7 @@ void functionsInSelect() { assertQuery("SELECT e.name, CURRENT_TIMESTAMP FROM Employee e"); assertQuery("SELECT LENGTH(e.lastName) FROM Employee e"); assertQuery("SELECT LOWER(e.lastName) FROM Employee e"); - assertQuery("SELECT MOD(e.hoursWorked / 8) FROM Employee e"); + assertQuery("SELECT MOD(e.hoursWorked, 8) FROM Employee e"); assertQuery("SELECT NULLIF(e.salary, 0) FROM Employee e"); assertQuery("SELECT SQRT(o.RESULT) FROM Output o"); assertQuery("SELECT SUBSTRING(e.lastName, 0, 2) FROM Employee e"); @@ -243,7 +246,7 @@ void functionsInWhere() { assertQuery("SELECT e FROM Employee e WHERE CURRENT_TIME > CURRENT_TIMESTAMP"); assertQuery("SELECT e FROM Employee e WHERE LENGTH(e.lastName) > 0"); assertQuery("SELECT e FROM Employee e WHERE LOWER(e.lastName) = 'bilbo'"); - assertQuery("SELECT e FROM Employee e WHERE MOD(e.hoursWorked / 8) > 0"); + assertQuery("SELECT e FROM Employee e WHERE MOD(e.hoursWorked, 8) > 0"); assertQuery("SELECT e FROM Employee e WHERE NULLIF(e.salary, 0) is null"); assertQuery("SELECT e FROM Employee e WHERE SQRT(o.RESULT) > 0.0"); assertQuery("SELECT e FROM Employee e WHERE SUBSTRING(e.lastName, 0, 2) = 'Bilbo'"); @@ -272,7 +275,7 @@ void functionsInOrderBy() { assertQuery("SELECT e FROM Employee e ORDER BY CURRENT_TIMESTAMP"); assertQuery("SELECT e FROM Employee e ORDER BY LENGTH(e.lastName)"); assertQuery("SELECT e FROM Employee e ORDER BY LOWER(e.lastName)"); - assertQuery("SELECT e FROM Employee e ORDER BY MOD(e.hoursWorked / 8)"); + assertQuery("SELECT e FROM Employee e ORDER BY MOD(e.hoursWorked, 8)"); assertQuery("SELECT e FROM Employee e ORDER BY NULLIF(e.salary, 0)"); assertQuery("SELECT e FROM Employee e ORDER BY SQRT(o.RESULT)"); assertQuery("SELECT e FROM Employee e ORDER BY SUBSTRING(e.lastName, 0, 2)"); @@ -301,7 +304,7 @@ void functionsInGroupBy() { assertQuery("SELECT e FROM Employee e GROUP BY CURRENT_TIMESTAMP"); assertQuery("SELECT e FROM Employee e GROUP BY LENGTH(e.lastName)"); assertQuery("SELECT e FROM Employee e GROUP BY LOWER(e.lastName)"); - assertQuery("SELECT e FROM Employee e GROUP BY MOD(e.hoursWorked / 8)"); + assertQuery("SELECT e FROM Employee e GROUP BY MOD(e.hoursWorked, 8)"); assertQuery("SELECT e FROM Employee e GROUP BY NULLIF(e.salary, 0)"); assertQuery("SELECT e FROM Employee e GROUP BY SQRT(o.RESULT)"); assertQuery("SELECT e FROM Employee e GROUP BY SUBSTRING(e.lastName, 0, 2)"); @@ -329,7 +332,7 @@ void functionsInHaving() { assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING CURRENT_TIME > CURRENT_TIMESTAMP"); assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING LENGTH(e.lastName) > 0"); assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING LOWER(e.lastName) = 'bilbo'"); - assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING MOD(e.hoursWorked / 8) > 0"); + assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING MOD(e.hoursWorked, 8) > 0"); assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING NULLIF(e.salary, 0) is null"); assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING SQRT(o.RESULT) > 0.0"); assertQuery("SELECT e FROM Employee e GROUP BY e.salary HAVING SUBSTRING(e.lastName, 0, 2) = 'Bilbo'"); diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java index 5cbcda1a54..639a156bf3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancerUnitTests.java @@ -32,6 +32,7 @@ * @author Mark Paluch * @author Diego Krupitza * @author Geoffrey Deremetz + * @author Christoph Strobl */ public class JSqlParserQueryEnhancerUnitTests extends QueryEnhancerTckTests { @@ -49,6 +50,8 @@ void shouldDeriveJpqlCountQuery(String query, String expected) { assumeThat(query).as("JSQLParser does not support constructor JPQL syntax").doesNotContain(" new "); + assumeThat(query).as("JSQLParser does not support MOD JPQL syntax").doesNotContain("MOD("); + super.shouldDeriveJpqlCountQuery(query, expected); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java new file mode 100644 index 0000000000..f858594973 --- /dev/null +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlComplianceTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jpa.repository.query; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.jpa.repository.query.JpaQueryParsingToken.*; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.junit.jupiter.api.Test; + +/** + * Test to verify compliance of {@link JpqlParser} with standard SQL. Other than {@link JpqlSpecificationTests} tests in + * this class check that the parser follows a lenient approach and does not error on well known concepts like numeric + * suffix. + * + * @author Christoph Strobl + */ +class JpqlComplianceTests { + + private static String parseWithoutChanges(String query) { + + JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(query)); + JpqlParser parser = new JpqlParser(new CommonTokenStream(lexer)); + + parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); + + JpqlParser.StartContext parsedQuery = parser.start(); + + return render(new JpqlQueryRenderer().visit(parsedQuery)); + } + + private void assertQuery(String query) { + + String slimmedDownQuery = reduceWhitespace(query); + assertThat(parseWithoutChanges(slimmedDownQuery)).isEqualTo(slimmedDownQuery); + } + + private String reduceWhitespace(String original) { + + return original // + .replaceAll("[ \\t\\n]{1,}", " ") // + .trim(); + } + + @Test // GH-3277 + void numericLiterals() { + + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234"); + assertQuery("SELECT e FROM Employee e WHERE e.id = 1234L"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14F"); + assertQuery("SELECT s FROM Stat s WHERE s.ratio > 3.14e32D"); + } + + @Test // GH-3308 + void newWithStrings() { + assertQuery("select new com.example.demo.SampleObject(se.id, se.sampleValue, \"java\") from SampleEntity se"); + } + +} diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java index a82d71c21b..3e3465e3d3 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerTckTests.java @@ -164,7 +164,12 @@ static Stream jpqlCountQueries() { Arguments.of( // "select distinct m.genre from Media m where m.user = ?1 order by m.genre asc", // - "select count(distinct m.genre) from Media m where m.user = ?1")); + "select count(distinct m.genre) from Media m where m.user = ?1"), + + Arguments.of( // + "select u from User u where MOD(u.age, 10L) = 2", // + "select count(u) from User u where MOD(u.age, 10L) = 2") + ); } @ParameterizedTest // GH-2511, GH-2773