diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java index 48cdd1873..b55e4e2ac 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JSqlParserQueryEnhancer.java @@ -18,27 +18,6 @@ package org.springframework.data.jpa.repository.query; import static org.springframework.data.jpa.repository.query.JSqlParserUtils.*; import static org.springframework.data.jpa.repository.query.QueryUtils.*; -import net.sf.jsqlparser.JSQLParserException; -import net.sf.jsqlparser.expression.Alias; -import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.Function; -import net.sf.jsqlparser.parser.CCJSqlParserUtil; -import net.sf.jsqlparser.schema.Column; -import net.sf.jsqlparser.statement.Statement; -import net.sf.jsqlparser.statement.delete.Delete; -import net.sf.jsqlparser.statement.merge.Merge; -import net.sf.jsqlparser.statement.insert.Insert; -import net.sf.jsqlparser.statement.select.OrderByElement; -import net.sf.jsqlparser.statement.select.PlainSelect; -import net.sf.jsqlparser.statement.select.Select; -import net.sf.jsqlparser.statement.select.SelectBody; -import net.sf.jsqlparser.statement.select.SelectExpressionItem; -import net.sf.jsqlparser.statement.select.SelectItem; -import net.sf.jsqlparser.statement.select.SetOperationList; -import net.sf.jsqlparser.statement.select.WithItem; -import net.sf.jsqlparser.statement.update.Update; -import net.sf.jsqlparser.statement.values.ValuesStatement; - import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -53,6 +32,27 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.merge.Merge; +import net.sf.jsqlparser.statement.select.OrderByElement; +import net.sf.jsqlparser.statement.select.PlainSelect; +import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.select.SelectBody; +import net.sf.jsqlparser.statement.select.SelectExpressionItem; +import net.sf.jsqlparser.statement.select.SelectItem; +import net.sf.jsqlparser.statement.select.SetOperationList; +import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.statement.values.ValuesStatement; + /** * The implementation of {@link QueryEnhancer} using JSqlParser. * @@ -304,24 +304,28 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { @Nullable private String detectAlias(String query) { - if (this.parsedType != ParsedType.SELECT) { - return null; + if (ParsedType.MERGE.equals(this.parsedType)) { + Merge mergeStatement = parseSelectStatement(query, Merge.class); + return detectAlias(mergeStatement); + + } else if (ParsedType.SELECT.equals(this.parsedType)) { + Select selectStatement = parseSelectStatement(query); + + /* + For all the other types ({@link ValuesStatement} and {@link SetOperationList}) it does not make sense to provide + alias since: + * ValuesStatement has no alias + * SetOperation can have multiple alias for each operation item + */ + if (!(selectStatement.getSelectBody() instanceof PlainSelect)) { + return null; + } + + PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody(); + return detectAlias(selectBody); } - Select selectStatement = parseSelectStatement(query); - - /* - For all the other types ({@link ValuesStatement} and {@link SetOperationList}) it does not make sense to provide - alias since: - * ValuesStatement has no alias - * SetOperation can have multiple alias for each operation item - */ - if (!(selectStatement.getSelectBody() instanceof PlainSelect)) { - return null; - } - - PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody(); - return detectAlias(selectBody); + return null; } /** @@ -332,7 +336,7 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { * @return Might return {@literal null}. */ @Nullable - private static String detectAlias(PlainSelect selectBody) { + private String detectAlias(PlainSelect selectBody) { if (selectBody.getFromItem() == null) { return null; @@ -342,6 +346,18 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { return alias == null ? null : alias.getName(); } + /** + * Resolves the alias for the given {@link Merge} statement. + * + * @param mergeStatement must not be {@literal null}. + * @return Might return {@literal null}. + */ + @Nullable + private String detectAlias(Merge mergeStatement) { + Alias alias = mergeStatement.getUsingAlias(); + return alias == null ? null : alias.getName(); + } + @Override public String createCountQueryFor(@Nullable String countProjection) { @@ -449,15 +465,25 @@ public class JSqlParserQueryEnhancer implements QueryEnhancer { * @param query the query to parse * @return the parsed query */ - private static Select parseSelectStatement(String query) { + private T parseSelectStatement(String query, Class classOfT) { try { - return (Select) CCJSqlParserUtil.parse(query); + return classOfT.cast(CCJSqlParserUtil.parse(query)); } catch (JSQLParserException e) { throw new IllegalArgumentException("The query you provided is not a valid SQL Query", e); } } + /** + * Parses a query string with JSqlParser. + * + * @param query the query to parse + * @return the parsed query + */ + private Select parseSelectStatement(String query) { + return parseSelectStatement(query, Select.class); + } + /** * Checks whether a given projection only contains a single column definition (aka without functions, etc.) * diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java index 8aa76ffc8..febfbd947 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryEnhancerUnitTests.java @@ -921,14 +921,17 @@ class QueryEnhancerUnitTests { assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); } - @Test // GH-2641 - void mergeStatementWorksWithJSqlParser() { - String query = "merge into a using (select id, value from b) query on (a.id = query.id) when matched then update set a.value = value"; + @ParameterizedTest // GH-2641 + @MethodSource("mergeStatementWorksWithJSqlParserSource") + void mergeStatementWorksWithJSqlParser(String query, String alias) { StringQuery stringQuery = new StringQuery(query, true); QueryEnhancer queryEnhancer = QueryEnhancerFactory.forQuery(stringQuery); + assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); + assertThat(QueryUtils.detectAlias(query)).isNull(); + assertThat(queryEnhancer.getJoinAliases()).isEmpty(); - assertThat(queryEnhancer.detectAlias()).isNull(); + assertThat(queryEnhancer.detectAlias()).isEqualTo(alias); assertThat(queryEnhancer.getProjection()).isEmpty(); assertThat(queryEnhancer.hasConstructorExpression()).isFalse(); } @@ -940,6 +943,15 @@ class QueryEnhancerUnitTests { ); } + public static Stream mergeStatementWorksWithJSqlParserSource() { + return Stream.of(Arguments.of( + "merge into a using (select id, value from b) query on (a.id = query.id) when matched then update set a.value = value", + "query"), + Arguments.of( + "merge into a using (select id2, value from b) on (id = id2) when matched then update set a.value = value", + null)); + } + public static Stream detectsJoinAliasesCorrectlySource() { return Stream.of( //