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 fe1e502a6..b80e499ff 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 @@ -204,6 +204,7 @@ constructor_item | scalar_expression | aggregate_expression | identification_variable + | literal ; aggregate_expression @@ -620,6 +621,7 @@ constructor_name literal : STRINGLITERAL + | JAVASTRINGLITERAL | INTLITERAL | FLOATLITERAL | LONGLITERAL @@ -852,10 +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/JpqlQueryRenderer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpqlQueryRenderer.java index c07811bd7..aae392469 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 @@ -722,6 +722,8 @@ class JpqlQueryRenderer extends JpqlBaseVisitor> { 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,6 +2154,8 @@ class JpqlQueryRenderer extends JpqlBaseVisitor> { 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) { 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 000000000..f85859497 --- /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 a82d71c21..3e3465e3d 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 @@ abstract class QueryEnhancerTckTests { 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