Skip to content

Commit 2dec9cb

Browse files
committed
Refine count-query derivation parameter post-processing.
We've now expanded parameter post-processing for derived count queries to consider binding types (in, like) and to correctly retain invocation parameter redirects instead of assuming an exact mapping of parameter positions in the final query to the actual invocation argument names/indices. Closes #3784
1 parent 0187162 commit 2dec9cb

File tree

3 files changed

+123
-2
lines changed

3 files changed

+123
-2
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ public Object prepare(@Nullable Object valueToBind) {
156156
*/
157157
public boolean bindsTo(ParameterBinding other) {
158158

159+
if (getIdentifier().equals(other.getIdentifier())) {
160+
return true;
161+
}
162+
159163
if (identifier.hasName() && other.identifier.hasName()) {
160164
if (identifier.getName().equals(other.identifier.getName())) {
161165
return true;
@@ -503,6 +507,16 @@ static Expression ofExpression(ValueExpression expression) {
503507
return new Expression(expression);
504508
}
505509

510+
/**
511+
* Creates a {@link MethodInvocationArgument} object for {@code name}
512+
*
513+
* @param name the parameter name from the method invocation.
514+
* @return {@link MethodInvocationArgument} object for {@code name}.
515+
*/
516+
static MethodInvocationArgument ofParameter(String name) {
517+
return ofParameter(name, null);
518+
}
519+
506520
/**
507521
* Creates a {@link MethodInvocationArgument} object for {@code name} and {@code position}. Either the name or the
508522
* position must be given.

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.function.BiFunction;
2626
import java.util.function.Consumer;
2727
import java.util.function.Function;
28+
import java.util.function.Predicate;
2829
import java.util.regex.Matcher;
2930
import java.util.regex.Pattern;
3031

@@ -143,8 +144,12 @@ public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) {
143144

144145
for (ParameterBinding binding : bindings) {
145146

146-
if (binding.getOrigin().isExpression() && derivedBindings.removeIf(
147-
it -> !it.getOrigin().isExpression() && it.getIdentifier().equals(binding.getIdentifier()))) {
147+
Predicate<ParameterBinding> identifier = binding::bindsTo;
148+
Predicate<ParameterBinding> notCompatible = Predicate.not(binding::isCompatibleWith);
149+
150+
// replace incompatible bindings
151+
if ( derivedBindings.removeIf(
152+
it -> identifier.test(it) && notCompatible.test(it))) {
148153
derivedBindings.add(binding);
149154
}
150155
}

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

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,66 @@ void rewritesNamedLikeToUniqueParametersIfNecessary() {
161161
assertThat(((MethodInvocationArgument) parameterBinding.getOrigin()).identifier().getName()).isEqualTo("firstname");
162162
}
163163

164+
@Test // GH-3784
165+
void rewritesNamedLikeToUniqueParametersRetainingCountQuery() {
166+
167+
DeclaredQuery query = new StringQuery(
168+
"select u from User u where u.firstname like %:firstname or u.firstname like :firstname% or u.firstname = :firstname",
169+
false).deriveCountQuery(null);
170+
171+
assertThat(query.getQueryString()) //
172+
.isEqualTo(
173+
"select count(u) from User u where u.firstname like :firstname or u.firstname like :firstname_1 or u.firstname = :firstname_2");
174+
175+
List<ParameterBinding> bindings = query.getParameterBindings();
176+
assertThat(bindings).hasSize(3);
177+
178+
LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
179+
assertThat(binding).isNotNull();
180+
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter("firstname"));
181+
assertThat(binding.getName()).isEqualTo("firstname");
182+
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
183+
184+
binding = (LikeParameterBinding) bindings.get(1);
185+
assertThat(binding).isNotNull();
186+
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter("firstname"));
187+
assertThat(binding.getName()).isEqualTo("firstname_1");
188+
assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH);
189+
190+
ParameterBinding parameterBinding = bindings.get(2);
191+
assertThat(parameterBinding).isNotNull();
192+
assertThat(parameterBinding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter("firstname"));
193+
assertThat(parameterBinding.getName()).isEqualTo("firstname_2");
194+
assertThat(((MethodInvocationArgument) parameterBinding.getOrigin()).identifier().getName()).isEqualTo("firstname");
195+
}
196+
197+
@Test // GH-3784
198+
void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() {
199+
200+
DeclaredQuery query = new StringQuery(
201+
"select u from User u where u.firstname like %:#{firstname} or u.firstname like :#{firstname}%", false)
202+
.deriveCountQuery(null);
203+
204+
assertThat(query.getQueryString()) //
205+
.isEqualTo(
206+
"select count(u) from User u where u.firstname like :__$synthetic$__1 or u.firstname like :__$synthetic$__2");
207+
208+
List<ParameterBinding> bindings = query.getParameterBindings();
209+
assertThat(bindings).hasSize(2);
210+
211+
LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
212+
assertThat(binding).isNotNull();
213+
assertThat(binding.getOrigin().isExpression()).isTrue();
214+
assertThat(binding.getName()).isEqualTo("__$synthetic$__1");
215+
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
216+
217+
binding = (LikeParameterBinding) bindings.get(1);
218+
assertThat(binding).isNotNull();
219+
assertThat(binding.getOrigin().isExpression()).isTrue();
220+
assertThat(binding.getName()).isEqualTo("__$synthetic$__2");
221+
assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH);
222+
}
223+
164224
@Test // GH-3041
165225
void rewritesPositionalLikeToUniqueParametersIfNecessary() {
166226

@@ -290,6 +350,48 @@ void detectsMultipleNamedInParameterBindings() {
290350
assertNamedBinding(ParameterBinding.class, "bar", bindings.get(2));
291351
}
292352

353+
@Test // GH-3784
354+
void deriveCountQueryWithNamedInRetainsOrigin() {
355+
356+
String queryString = "select u from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins)";
357+
DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null);
358+
359+
assertThat(query.getQueryString())
360+
.isEqualTo("select count(u) from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins_1)");
361+
362+
List<ParameterBinding> bindings = query.getParameterBindings();
363+
assertThat(bindings).hasSize(2);
364+
365+
assertNamedBinding(ParameterBinding.class, "logins", bindings.get(0));
366+
assertThat((MethodInvocationArgument) bindings.get(0).getOrigin()).extracting(MethodInvocationArgument::identifier)
367+
.extracting(BindingIdentifier::getName).isEqualTo("logins");
368+
369+
assertNamedBinding(InParameterBinding.class, "logins_1", bindings.get(1));
370+
assertThat((MethodInvocationArgument) bindings.get(1).getOrigin()).extracting(MethodInvocationArgument::identifier)
371+
.extracting(BindingIdentifier::getName).isEqualTo("logins");
372+
}
373+
374+
@Test // GH-3784
375+
void deriveCountQueryWithPositionalInRetainsOrigin() {
376+
377+
String queryString = "select u from User u where (?1) IS NULL OR LOWER(u.login) IN (?1)";
378+
DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null);
379+
380+
assertThat(query.getQueryString())
381+
.isEqualTo("select count(u) from User u where (?1) IS NULL OR LOWER(u.login) IN (?2)");
382+
383+
List<ParameterBinding> bindings = query.getParameterBindings();
384+
assertThat(bindings).hasSize(2);
385+
386+
assertPositionalBinding(ParameterBinding.class, 1, bindings.get(0));
387+
assertThat((MethodInvocationArgument) bindings.get(0).getOrigin()).extracting(MethodInvocationArgument::identifier)
388+
.extracting(BindingIdentifier::getPosition).isEqualTo(1);
389+
390+
assertPositionalBinding(InParameterBinding.class, 2, bindings.get(1));
391+
assertThat((MethodInvocationArgument) bindings.get(1).getOrigin()).extracting(MethodInvocationArgument::identifier)
392+
.extracting(BindingIdentifier::getPosition).isEqualTo(1);
393+
}
394+
293395
@Test // DATAJPA-461
294396
void detectsPositionalInParameterBindings() {
295397

0 commit comments

Comments
 (0)