diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java index 09cb56950..0c675029b 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryEnhancer.java @@ -20,6 +20,7 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; +import org.antlr.v4.runtime.BailErrorStrategy; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; @@ -28,7 +29,9 @@ import org.antlr.v4.runtime.Parser; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.TokenStream; import org.antlr.v4.runtime.atn.PredictionMode; +import org.antlr.v4.runtime.misc.ParseCancellationException; import org.antlr.v4.runtime.tree.ParseTreeVisitor; + import org.springframework.data.domain.Sort; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -65,9 +68,35 @@ class JpaQueryEnhancer implements QueryEnhancer { this.projection = tokens.isEmpty() ? "" : new QueryRenderer.TokenRenderer(tokens).render(); } + /** + * Parse the query and return the parser context (AST). This method attempts parsing the query using + * {@link PredictionMode#SLL} first to attempt a fast-path parse without using the context. If that fails, it retries + * using {@link PredictionMode#LL} which is much slower, however it allows for contextual ambiguity resolution. + */ static

ParserRuleContext parse(String query, Function lexerFactoryFunction, Function parserFactoryFunction, Function parseFunction) { + P parser = getParser(query, lexerFactoryFunction, parserFactoryFunction); + + parser.getInterpreter().setPredictionMode(PredictionMode.SLL); + parser.setErrorHandler(new BailErrorStrategy()); + + try { + + return parseFunction.apply(parser); + } catch (BadJpqlGrammarException | ParseCancellationException e) { + + parser = getParser(query, lexerFactoryFunction, parserFactoryFunction); + // fall back to LL(*)-based parsing + parser.getInterpreter().setPredictionMode(PredictionMode.LL); + + return parseFunction.apply(parser); + } + } + + private static

P getParser(String query, Function lexerFactoryFunction, + Function parserFactoryFunction) { + Lexer lexer = lexerFactoryFunction.apply(CharStreams.fromString(query)); P parser = parserFactoryFunction.apply(new CommonTokenStream(lexer)); @@ -79,11 +108,11 @@ class JpaQueryEnhancer implements QueryEnhancer { configureParser(query, grammar.toUpperCase(), lexer, parser); - return parseFunction.apply(parser); + return parser; } /** - * Apply common configuration (SLL prediction for performance, our own error listeners). + * Apply common configuration. * * @param query * @param lexer @@ -96,8 +125,6 @@ class JpaQueryEnhancer implements QueryEnhancer { lexer.removeErrorListeners(); lexer.addErrorListener(errorListener); - parser.getInterpreter().setPredictionMode(PredictionMode.SLL); - parser.removeErrorListeners(); parser.addErrorListener(errorListener); } @@ -142,6 +169,13 @@ class JpaQueryEnhancer implements QueryEnhancer { return EqlQueryParser.parseQuery(query.getQueryString()); } + /** + * @return the parser context (AST) representing the parsed query. + */ + ParserRuleContext getContext() { + return context; + } + /** * Checks if the select clause has a new constructor instantiation in the JPA query. * 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 1336b9f3a..ca9eb8ded 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 @@ -17,9 +17,8 @@ package org.springframework.data.jpa.repository.query; import static org.assertj.core.api.Assertions.*; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Test; + import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; /** @@ -40,14 +39,9 @@ class EqlComplianceTests { */ private static String parseWithoutChanges(String query) { - EqlLexer lexer = new EqlLexer(CharStreams.fromString(query)); - EqlParser parser = new EqlParser(new CommonTokenStream(lexer)); + JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery(query); - parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); - - EqlParser.StartContext parsedQuery = parser.start(); - - return TokenRenderer.render(new EqlQueryRenderer().visit(parsedQuery)); + return TokenRenderer.render(new EqlQueryRenderer().visit(parser.getContext())); } private void assertQuery(String query) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java index b86fc5bda..70ecb1fe5 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlQueryRendererTests.java @@ -19,14 +19,13 @@ import static org.assertj.core.api.Assertions.*; import java.util.stream.Stream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; + import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; /** @@ -47,14 +46,9 @@ class EqlQueryRendererTests { */ private static String parseWithoutChanges(String query) { - EqlLexer lexer = new EqlLexer(CharStreams.fromString(query)); - EqlParser parser = new EqlParser(new CommonTokenStream(lexer)); + JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery(query); - parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); - - EqlParser.StartContext parsedQuery = parser.start(); - - return TokenRenderer.render(new EqlQueryRenderer().visit(parsedQuery)); + return TokenRenderer.render(new EqlQueryRenderer().visit(parser.getContext())); } static Stream reservedWords() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java index 5bd15b196..bff45ec75 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/EqlSpecificationTests.java @@ -17,10 +17,9 @@ package org.springframework.data.jpa.repository.query; import static org.assertj.core.api.Assertions.*; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; + import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; /** @@ -37,14 +36,9 @@ class EqlSpecificationTests { private static String parseWithoutChanges(String query) { - EqlLexer lexer = new EqlLexer(CharStreams.fromString(query)); - EqlParser parser = new EqlParser(new CommonTokenStream(lexer)); + JpaQueryEnhancer.EqlQueryParser parser = JpaQueryEnhancer.EqlQueryParser.parseQuery(query); - parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); - - EqlParser.StartContext parsedQuery = parser.start(); - - return TokenRenderer.render(new EqlQueryRenderer().visit(parsedQuery)); + return TokenRenderer.render(new EqlQueryRenderer().visit(parser.getContext())); } private void assertQuery(String query) { 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 725e09950..4de4c2b9d 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 @@ -19,8 +19,6 @@ import static org.assertj.core.api.Assertions.*; import java.util.stream.Stream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -50,14 +48,9 @@ class HqlQueryRendererTests { */ private static String parseWithoutChanges(String query) { - HqlLexer lexer = new HqlLexer(CharStreams.fromString(query)); - HqlParser parser = new HqlParser(new CommonTokenStream(lexer)); + JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery(query); - parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); - - HqlParser.StartContext parsedQuery = parser.start(); - - QueryTokenStream tokens = new HqlQueryRenderer().visit(parsedQuery); + QueryTokenStream tokens = new HqlQueryRenderer().visit(parser.getContext()); return QueryRenderer.from(tokens).render(); } @@ -1891,6 +1884,22 @@ class HqlQueryRendererTests { """); } + @Test // GH-3757 + void arithmeticDate() { + + assertQuery("SELECT a FROM foo a WHERE (cast(a.createdAt as date) - CURRENT_DATE()) BY day - 2 = 0"); + assertQuery("SELECT a FROM foo a WHERE (cast(a.createdAt as date) - CURRENT_DATE()) BY day - 2 = 0"); + assertQuery("SELECT a FROM foo a WHERE (cast(a.createdAt as date)) BY day - 2 = 0"); + + assertQuery("SELECT f.start - 1 minute FROM foo f"); + + assertQuery("SELECT f FROM foo f WHERE (cast(f.start as date) - CURRENT_DATE()) BY day - 2 = 0"); + assertQuery("SELECT 1 week - 1 day FROM foo f"); + assertQuery("SELECT f.birthday - local date day FROM foo f"); + assertQuery("SELECT local datetime - f.birthday FROM foo f"); + assertQuery("SELECT (1 year) by day FROM foo f"); + } + @ParameterizedTest // GH-3342 @ValueSource( strings = { "select 1 from User", "select -1 from User", "select +1 from User", "select +1 * -100 from User", 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 4214b9980..be05e3fce 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 @@ -17,12 +17,11 @@ package org.springframework.data.jpa.repository.query; import static org.assertj.core.api.Assertions.*; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; 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; /** @@ -43,14 +42,9 @@ class HqlSpecificationTests { private static String parseWithoutChanges(String query) { - HqlLexer lexer = new HqlLexer(CharStreams.fromString(query)); - HqlParser parser = new HqlParser(new CommonTokenStream(lexer)); + JpaQueryEnhancer.HqlQueryParser parser = JpaQueryEnhancer.HqlQueryParser.parseQuery(query); - parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); - - HqlParser.StartContext parsedQuery = parser.start(); - - return TokenRenderer.render(new HqlQueryRenderer().visit(parsedQuery)); + return TokenRenderer.render(new HqlQueryRenderer().visit(parser.getContext())); } private void assertQuery(String query) { @@ -490,7 +484,7 @@ class HqlSpecificationTests { "from Call c "); assertQuery("select POSITION(c.number IN 'foo') + 1 AS pos " + // - "from Call c "); + "from Call c "); } @Test // GH-3689 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 index 1c412fcf8..81722f9b9 100644 --- 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 @@ -17,8 +17,6 @@ package org.springframework.data.jpa.repository.query; import static org.assertj.core.api.Assertions.*; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Test; /** @@ -32,14 +30,9 @@ class JpqlComplianceTests { private static String parseWithoutChanges(String query) { - JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(query)); - JpqlParser parser = new JpqlParser(new CommonTokenStream(lexer)); + JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery(query); - parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); - - JpqlParser.StartContext parsedQuery = parser.start(); - - return QueryRenderer.render(new JpqlQueryRenderer().visit(parsedQuery)); + return QueryRenderer.render(new JpqlQueryRenderer().visit(parser.getContext())); } private void assertQuery(String query) { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java index 4446a5195..1047d4cc1 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlQueryRendererTests.java @@ -19,14 +19,13 @@ import static org.assertj.core.api.Assertions.*; import java.util.stream.Stream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; + import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; /** @@ -48,14 +47,9 @@ class JpqlQueryRendererTests { */ private static String parseWithoutChanges(String query) { - JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(query)); - JpqlParser parser = new JpqlParser(new CommonTokenStream(lexer)); + JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery(query); - parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); - - JpqlParser.StartContext parsedQuery = parser.start(); - - return TokenRenderer.render(new JpqlQueryRenderer().visit(parsedQuery)); + return TokenRenderer.render(new JpqlQueryRenderer().visit(parser.getContext())); } static Stream reservedWords() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java index b3c7ea1a8..289e52245 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/JpqlSpecificationTests.java @@ -17,10 +17,9 @@ package org.springframework.data.jpa.repository.query; import static org.assertj.core.api.Assertions.*; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; + import org.springframework.data.jpa.repository.query.QueryRenderer.TokenRenderer; /** @@ -41,14 +40,9 @@ class JpqlSpecificationTests { */ private static String parseWithoutChanges(String query) { - JpqlLexer lexer = new JpqlLexer(CharStreams.fromString(query)); - JpqlParser parser = new JpqlParser(new CommonTokenStream(lexer)); + JpaQueryEnhancer.JpqlQueryParser parser = JpaQueryEnhancer.JpqlQueryParser.parseQuery(query); - parser.addErrorListener(new BadJpqlGrammarErrorListener(query)); - - JpqlParser.StartContext parsedQuery = parser.start(); - - return TokenRenderer.render(new JpqlQueryRenderer().visit(parsedQuery)); + return TokenRenderer.render(new JpqlQueryRenderer().visit(parser.getContext())); } private void assertQuery(String query) {