Skip to content

Commit 37560ab

Browse files
committed
getExistsQueryString(...) & detectAlias(...) with JSqlParser.
We introduced `JSQLParser/JSqlParser` to the project so SQL can be easily parsed with out any hassle. This commit only focuses on the first two methods inside `JSqlParserQueryUtils` that is the JSqlParser counterpart of `QueryUtils`. Related tickets spring-projects#2409
1 parent 680cc4d commit 37560ab

File tree

3 files changed

+268
-0
lines changed

3 files changed

+268
-0
lines changed

pom.xml

+8
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<postgresql>42.2.19</postgresql>
2828
<springdata.commons>2.7.0-SNAPSHOT</springdata.commons>
2929
<vavr>0.10.3</vavr>
30+
<jsqlparser.version>4.3</jsqlparser.version>
3031

3132
<hibernate.groupId>org.hibernate</hibernate.groupId>
3233

@@ -372,6 +373,13 @@
372373
<scope>test</scope>
373374
</dependency>
374375

376+
<dependency>
377+
<groupId>com.github.jsqlparser</groupId>
378+
<artifactId>jsqlparser</artifactId>
379+
<version>${jsqlparser.version}</version>
380+
<scope>provided</scope>
381+
<optional>true</optional>
382+
</dependency>
375383
</dependencies>
376384

377385
<build>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package org.springframework.data.jpa.repository.query;
2+
3+
import java.util.Collections;
4+
import java.util.List;
5+
import java.util.stream.Collectors;
6+
7+
import org.springframework.data.domain.Sort;
8+
import org.springframework.data.util.Streamable;
9+
import org.springframework.lang.Nullable;
10+
import org.springframework.util.Assert;
11+
import org.springframework.util.CollectionUtils;
12+
import org.springframework.util.StringUtils;
13+
14+
import net.sf.jsqlparser.JSQLParserException;
15+
import net.sf.jsqlparser.expression.Alias;
16+
import net.sf.jsqlparser.expression.Expression;
17+
import net.sf.jsqlparser.expression.Function;
18+
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
19+
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
20+
import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
21+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
22+
import net.sf.jsqlparser.schema.Column;
23+
import net.sf.jsqlparser.schema.Table;
24+
import net.sf.jsqlparser.statement.select.PlainSelect;
25+
import net.sf.jsqlparser.statement.select.Select;
26+
import net.sf.jsqlparser.statement.select.SelectExpressionItem;
27+
import net.sf.jsqlparser.util.SelectUtils;
28+
29+
/**
30+
* Simple utility class to create JPA queries using JSqlParser.
31+
*
32+
* @author Diego Krupitza
33+
*/
34+
public abstract class JSqlParserQueryUtils {
35+
36+
private static final String DEFAULT_TABLE_ALIAS = "x";
37+
38+
private JSqlParserQueryUtils() {
39+
}
40+
41+
/**
42+
* Returns the query string to execute an exists query for the given id attributes.
43+
*
44+
* @param entityName the name of the entity to create the query for, must not be {@literal null}.
45+
* @param countQueryPlaceHolder the placeholder for the count clause, must not be {@literal null}.
46+
* @param idAttributes the id attributes for the entity, must not be {@literal null}.
47+
*/
48+
public static String getExistsQueryString(String entityName, String countQueryPlaceHolder,
49+
Iterable<String> idAttributes) {
50+
51+
final Table tableNameWithAlias = getTableWithAlias(entityName, DEFAULT_TABLE_ALIAS);
52+
Function jSqlCount = getJSqlCount(Collections.singletonList(countQueryPlaceHolder), false);
53+
54+
Select select = SelectUtils.buildSelectFromTableAndSelectItems(tableNameWithAlias,
55+
new SelectExpressionItem(jSqlCount));
56+
57+
PlainSelect selectBody = (PlainSelect) select.getSelectBody();
58+
59+
List<Expression> equalityExpressions = Streamable.of(idAttributes).stream() //
60+
.map(field -> {
61+
Expression tableNameField = new Column().withTable(tableNameWithAlias).withColumnName(field);
62+
Expression inputField = new Column(":" + field);
63+
return new EqualsTo(tableNameField, inputField);
64+
}).collect(Collectors.toList());
65+
66+
if (equalityExpressions.size() > 1) {
67+
AndExpression rootOfWhereClause = concatenateWithAndExpression(equalityExpressions);
68+
selectBody.setWhere(rootOfWhereClause);
69+
} else if (equalityExpressions.size() == 1) {
70+
selectBody.setWhere(equalityExpressions.get(0));
71+
}
72+
73+
return selectBody.toString();
74+
}
75+
76+
/**
77+
* Returns the query string for the given class name.
78+
*
79+
* @param template must not be {@literal null}.
80+
* @param entityName must not be {@literal null}.
81+
* @return the template with placeholders replaced by the {@literal entityName}. Guaranteed to be not {@literal null}.
82+
*/
83+
public static String getQueryString(String template, String entityName) {
84+
Assert.hasText(entityName, "Entity name must not be null or empty!");
85+
return String.format(template, entityName);
86+
}
87+
88+
/**
89+
* Resolves the alias for the entity to be retrieved from the given JPA query. Note that you only provide valid Query
90+
* strings. Things such as <code>from User u</code> will throw an {@link IllegalArgumentException}.
91+
*
92+
* @param query must not be {@literal null}.
93+
* @return Might return {@literal null}.
94+
*/
95+
public static String detectAlias(String query) {
96+
try {
97+
Select selectStatement = (Select) CCJSqlParserUtil.parse(query);
98+
99+
PlainSelect selectBody = (PlainSelect) selectStatement.getSelectBody();
100+
Alias alias = selectBody.getFromItem().getAlias();
101+
return alias == null ? null : alias.getName();
102+
103+
} catch (JSQLParserException e) {
104+
throw new IllegalArgumentException("The query you provided is not a valid SQL Query!", e);
105+
}
106+
}
107+
108+
/**
109+
* Generates a JSqlParser table from an entity name and an optional alias name
110+
*
111+
* @param entityName the name of the table
112+
* @param alias the optional alias. Might be {@literal null} or empty
113+
* @return the newly generated table
114+
*/
115+
private static Table getTableWithAlias(String entityName, String alias) {
116+
Table table = new Table(entityName);
117+
return StringUtils.hasText(alias) ? table.withAlias(new Alias(alias)) : table;
118+
}
119+
120+
/**
121+
* Concatenates a list of expression with <code>AND</code>.
122+
*
123+
* @param expressions the list of expressions to concatenate. Has to be non empty and with size >= 2
124+
* @return the root of the concatenated expression
125+
*/
126+
private static AndExpression concatenateWithAndExpression(List<Expression> expressions) {
127+
128+
if (CollectionUtils.isEmpty(expressions) || expressions.size() == 1) {
129+
throw new IllegalArgumentException(
130+
"The list of expression has to be at least of length 2! Otherwise it is not possible to concatinate with an");
131+
}
132+
133+
AndExpression rootAndExpression = new AndExpression();
134+
AndExpression currentLocation = rootAndExpression;
135+
136+
// traverse the list with looking 1 element ahead
137+
for (int i = 0; i < expressions.size(); i++) {
138+
Expression currentExpression = expressions.get(i);
139+
if (currentLocation.getLeftExpression() == null) {
140+
currentLocation.setLeftExpression(currentExpression);
141+
} else if (currentLocation.getRightExpression() == null && i == expressions.size() - 1) {
142+
currentLocation.setRightExpression(currentExpression);
143+
} else {
144+
AndExpression nextAndExpression = new AndExpression();
145+
nextAndExpression.setLeftExpression(currentExpression);
146+
147+
currentLocation.setRightExpression(nextAndExpression);
148+
currentLocation = (AndExpression) currentLocation.getRightExpression();
149+
}
150+
}
151+
152+
return rootAndExpression;
153+
}
154+
155+
/**
156+
* Generates a count function call, based on the {@code countFields}.
157+
*
158+
* @param countFields the non empty list of fields that are used for counting
159+
* @param distinct if it should be a distinct count
160+
* @return the generated count function call
161+
*/
162+
private static Function getJSqlCount(final List<String> countFields, final boolean distinct) {
163+
List<Expression> countColumns = countFields //
164+
.stream() //
165+
.map(Column::new) //
166+
.collect(Collectors.toList());
167+
168+
ExpressionList countExpression = new ExpressionList(countColumns);
169+
return new Function() //
170+
.withName("count") //
171+
.withParameters(countExpression) //
172+
.withDistinct(distinct);
173+
}
174+
175+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package org.springframework.data.jpa.repository.query;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.Collections;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
9+
10+
/**
11+
* Unit test for {@link JSqlParserQueryUtils}.
12+
*
13+
* @author Diego Krupitza
14+
*/
15+
class JSqlParserQueryUtilsUnitTests {
16+
17+
private static final String QUERY = "select u from User u";
18+
private static final String FQ_QUERY = "select u from org.acme.domain.User$Foo_Bar u";
19+
private static final String INVALID_QUERY = "from User u";
20+
private static final String COUNT_QUERY = "select count(u) from User u";
21+
22+
private static final String QUERY_WITH_AS = "select u from User as u where u.username = ?";
23+
24+
@Test // DATAJPA-1171
25+
void doesNotContainStaticClauseInExistsQuery() {
26+
27+
String existsQueryString = JSqlParserQueryUtils.getExistsQueryString("entity", "x", Collections.singleton("id"));
28+
assertThat(existsQueryString) //
29+
.endsWith("WHERE x.id = :id");
30+
}
31+
32+
@Test
33+
void getQueryStringTest() {
34+
String deleteQueryString = JSqlParserQueryUtils.getQueryString("delete from %s x", "entityName");
35+
assertThat(deleteQueryString).isEqualTo("delete from entityName x");
36+
}
37+
38+
@Test
39+
void detectsAliasCorrectly() {
40+
assertThat(JSqlParserQueryUtils.detectAlias(QUERY)).isEqualTo("u");
41+
assertThat(JSqlParserQueryUtils.detectAlias(COUNT_QUERY)).isEqualTo("u");
42+
assertThat(JSqlParserQueryUtils.detectAlias(QUERY_WITH_AS)).isEqualTo("u");
43+
assertThat(JSqlParserQueryUtils.detectAlias("select u from User u")).isEqualTo("u");
44+
assertThat(JSqlParserQueryUtils.detectAlias("select u from com.acme.User u")).isEqualTo("u");
45+
assertThat(JSqlParserQueryUtils.detectAlias("select u from T05User u")).isEqualTo("u");
46+
}
47+
48+
@Test
49+
void detectAliasThrowsErrorOnInvalidSQL() {
50+
assertThatThrownBy(() -> JSqlParserQueryUtils.detectAlias(INVALID_QUERY))
51+
.isInstanceOf(IllegalArgumentException.class);
52+
53+
assertThatThrownBy(() -> JSqlParserQueryUtils.detectAlias("SELECT FROM USER U"))
54+
.isInstanceOf(IllegalArgumentException.class);
55+
}
56+
57+
@Test
58+
void allowsFullyQualifiedEntityNamesInQuery() {
59+
assertThat(JSqlParserQueryUtils.detectAlias(FQ_QUERY)).isEqualTo("u");
60+
}
61+
62+
@Test // DATAJPA-798
63+
void detectsAliasInQueryContainingLineBreaks() {
64+
assertThat(JSqlParserQueryUtils.detectAlias("select \n u \n from \n User \nu")).isEqualTo("u");
65+
}
66+
67+
@Test // DATAJPA-1506
68+
void detectsAliasWithGroupAndOrderBy() {
69+
70+
assertThat(JSqlParserQueryUtils.detectAlias("select * from User group by name")).isNull();
71+
assertThat(JSqlParserQueryUtils.detectAlias("select * from User order by name")).isNull();
72+
assertThat(JSqlParserQueryUtils.detectAlias("select * from User u group by name")).isEqualTo("u");
73+
assertThat(JSqlParserQueryUtils.detectAlias("select * from User u order by name")).isEqualTo("u");
74+
}
75+
76+
@Test
77+
void detectsAliasWithGroupAndOrderByWithLineBreaks() {
78+
79+
assertThat(JSqlParserQueryUtils.detectAlias("select * from User group\nby name")).isNull();
80+
assertThat(JSqlParserQueryUtils.detectAlias("select * from User order\nby name")).isNull();
81+
assertThat(JSqlParserQueryUtils.detectAlias("select * from User u group\nby name")).isEqualTo("u");
82+
assertThat(JSqlParserQueryUtils.detectAlias("select * from User u order\nby name")).isEqualTo("u");
83+
assertThat(JSqlParserQueryUtils.detectAlias("select * from User\nu\norder \n by name")).isEqualTo("u");
84+
}
85+
}

0 commit comments

Comments
 (0)