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