Skip to content

Commit cd12bd5

Browse files
committed
Polishing.
Refine HQL rendering for duration expressions. Retain whitespace for error message reconstruction, refine error handling. See spring-projects#3757
1 parent 75904a9 commit cd12bd5

File tree

8 files changed

+122
-6
lines changed

8 files changed

+122
-6
lines changed

spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Eql.g4

+1-1
Original file line numberDiff line numberDiff line change
@@ -837,7 +837,7 @@ reserved_word
837837
*/
838838

839839

840-
WS : [ \t\r\n] -> skip ;
840+
WS : [ \t\r\n] -> channel(HIDDEN) ;
841841

842842
// Build up case-insentive tokens
843843

spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Hql.g4

+1-1
Original file line numberDiff line numberDiff line change
@@ -1560,7 +1560,7 @@ identifier
15601560
*/
15611561

15621562

1563-
WS : [ \t\r\n] -> skip ;
1563+
WS : [ \t\r\n] -> channel(HIDDEN);
15641564

15651565
// Build up case-insentive tokens
15661566

spring-data-jpa/src/main/antlr4/org/springframework/data/jpa/repository/query/Jpql.g4

+1-1
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,7 @@ reserved_word
817817
*/
818818

819819

820-
WS : [ \t\r\n] -> skip ;
820+
WS : [ \t\r\n] -> channel(HIDDEN) ;
821821

822822
// Build up case-insentive tokens
823823

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/BadJpqlGrammarErrorListener.java

+49-1
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@
1515
*/
1616
package org.springframework.data.jpa.repository.query;
1717

18+
import java.util.List;
19+
1820
import org.antlr.v4.runtime.BaseErrorListener;
21+
import org.antlr.v4.runtime.CommonToken;
22+
import org.antlr.v4.runtime.InputMismatchException;
23+
import org.antlr.v4.runtime.NoViableAltException;
1924
import org.antlr.v4.runtime.RecognitionException;
2025
import org.antlr.v4.runtime.Recognizer;
2126

27+
import org.springframework.util.ObjectUtils;
28+
2229
/**
2330
* A {@link BaseErrorListener} that will throw a {@link BadJpqlGrammarException} if the query is invalid.
2431
*
@@ -43,7 +50,48 @@ class BadJpqlGrammarErrorListener extends BaseErrorListener {
4350
@Override
4451
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine,
4552
String msg, RecognitionException e) {
46-
throw new BadJpqlGrammarException("Line " + line + ":" + charPositionInLine + " " + msg, grammar, query, null);
53+
throw new BadJpqlGrammarException(formatMessage(offendingSymbol, line, charPositionInLine, msg, e, query), grammar,
54+
query, null);
55+
}
56+
57+
/**
58+
* Rewrite the error message.
59+
*/
60+
private static String formatMessage(Object offendingSymbol, int line, int charPositionInLine, String message,
61+
RecognitionException e, String query) {
62+
63+
String errorText = "At " + line + ":" + charPositionInLine;
64+
65+
if (offendingSymbol instanceof CommonToken ct) {
66+
67+
String token = ct.getText();
68+
if (!ObjectUtils.isEmpty(token)) {
69+
errorText += " and token '" + token + "'";
70+
}
71+
}
72+
errorText += ", ";
73+
74+
if (e instanceof NoViableAltException) {
75+
76+
errorText += message.substring(0, message.indexOf('\''));
77+
if (query.isEmpty()) {
78+
errorText += "'*' (empty query string)";
79+
} else {
80+
81+
List<String> list = query.lines().toList();
82+
String lineText = list.get(line - 1);
83+
String text = lineText.substring(0, charPositionInLine) + "*" + lineText.substring(charPositionInLine);
84+
errorText += "'" + text + "'";
85+
}
86+
87+
} else if (e instanceof InputMismatchException) {
88+
errorText += message.substring(0, message.length() - 1).replace(" expecting {",
89+
", expecting one of the following tokens: ");
90+
} else {
91+
errorText += message;
92+
}
93+
94+
return errorText;
4795
}
4896

4997
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/HqlQueryRenderer.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1752,7 +1752,7 @@ public QueryTokenStream visitFromDurationExpression(HqlParser.FromDurationExpres
17521752

17531753
QueryRendererBuilder builder = QueryRenderer.builder();
17541754

1755-
builder.append(visit(ctx.expression()));
1755+
builder.appendExpression(visit(ctx.expression()));
17561756
builder.append(QueryTokens.expression(ctx.BY()));
17571757
builder.appendExpression(visit(ctx.datetimeField()));
17581758

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.antlr.v4.runtime.Lexer;
2828
import org.antlr.v4.runtime.Parser;
2929
import org.antlr.v4.runtime.ParserRuleContext;
30+
import org.antlr.v4.runtime.RecognitionException;
3031
import org.antlr.v4.runtime.TokenStream;
3132
import org.antlr.v4.runtime.atn.PredictionMode;
3233
import org.antlr.v4.runtime.misc.ParseCancellationException;
@@ -82,7 +83,14 @@ static <P extends Parser> ParserRuleContext parse(String query, Function<CharStr
8283
P parser = getParser(query, lexerFactoryFunction, parserFactoryFunction);
8384

8485
parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
85-
parser.setErrorHandler(new BailErrorStrategy());
86+
parser.setErrorHandler(new BailErrorStrategy() {
87+
@Override
88+
public void reportError(Parser recognizer, RecognitionException e) {
89+
90+
// avoid BadJpqlGrammarException creation in the first pass.
91+
// recover(…) is going to handle cancellation.
92+
}
93+
});
8694

8795
try {
8896

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jpa.repository.query;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import org.junit.jupiter.api.Test;
21+
22+
/**
23+
* Unit tests for {@link BadJpqlGrammarException}.
24+
*
25+
* @author Mark Paluch
26+
*/
27+
class BadJpqlGrammarExceptionUnitTests {
28+
29+
@Test // GH-3757
30+
void shouldContainOriginalText() {
31+
32+
assertThatExceptionOfType(BadJpqlGrammarException.class)
33+
.isThrownBy(() -> JpaQueryEnhancer.HqlQueryParser
34+
.parseQuery("SELECT e FROM Employee e WHERE FOO(x).bar RESPECTING NULLS"))
35+
.withMessageContaining("no viable alternative")
36+
.withMessageContaining("SELECT e FROM Employee e WHERE FOO(x).bar *RESPECTING NULLS")
37+
.withMessageContaining("Bad HQL grammar [SELECT e FROM Employee e WHERE FOO(x).bar RESPECTING NULLS]");
38+
}
39+
40+
@Test // GH-3757
41+
void shouldReportExtraneousInput() {
42+
43+
assertThatExceptionOfType(BadJpqlGrammarException.class)
44+
.isThrownBy(() -> JpaQueryEnhancer.HqlQueryParser.parseQuery("select * from User group by name"))
45+
.withMessageContaining("extraneous input '*'")
46+
.withMessageContaining("Bad HQL grammar [select * from User group by name]");
47+
}
48+
49+
@Test // GH-3757
50+
void shouldReportMismatchedInput() {
51+
52+
assertThatExceptionOfType(BadJpqlGrammarException.class)
53+
.isThrownBy(() -> JpaQueryEnhancer.HqlQueryParser.parseQuery("SELECT AVG(m.price) AS m.avg FROM Magazine m"))
54+
.withMessageContaining("mismatched input '.'").withMessageContaining("expecting one of the following tokens:")
55+
.withMessageContaining("EXCEPT")
56+
.withMessageContaining("Bad HQL grammar [SELECT AVG(m.price) AS m.avg FROM Magazine m]");
57+
}
58+
59+
}

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/HqlQueryRendererTests.java

+1
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,7 @@ void arithmeticDate() {
18911891
assertQuery("SELECT a FROM foo a WHERE (cast(a.createdAt as date) - CURRENT_DATE()) BY day - 2 = 0");
18921892
assertQuery("SELECT a FROM foo a WHERE (cast(a.createdAt as date)) BY day - 2 = 0");
18931893

1894+
assertQuery("SELECT f.start BY DAY - 2 FROM foo f");
18941895
assertQuery("SELECT f.start - 1 minute FROM foo f");
18951896

18961897
assertQuery("SELECT f FROM foo f WHERE (cast(f.start as date) - CURRENT_DATE()) BY day - 2 = 0");

0 commit comments

Comments
 (0)