Skip to content

Commit

Permalink
Refine count-query derivation parameter post-processing.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mp911de committed Feb 24, 2025
1 parent 0187162 commit 2dec9cb
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ public Object prepare(@Nullable Object valueToBind) {
*/
public boolean bindsTo(ParameterBinding other) {

if (getIdentifier().equals(other.getIdentifier())) {
return true;
}

if (identifier.hasName() && other.identifier.hasName()) {
if (identifier.getName().equals(other.identifier.getName())) {
return true;
Expand Down Expand Up @@ -503,6 +507,16 @@ static Expression ofExpression(ValueExpression expression) {
return new Expression(expression);
}

/**
* Creates a {@link MethodInvocationArgument} object for {@code name}
*
* @param name the parameter name from the method invocation.
* @return {@link MethodInvocationArgument} object for {@code name}.
*/
static MethodInvocationArgument ofParameter(String name) {
return ofParameter(name, null);
}

/**
* Creates a {@link MethodInvocationArgument} object for {@code name} and {@code position}. Either the name or the
* position must be given.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

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

for (ParameterBinding binding : bindings) {

if (binding.getOrigin().isExpression() && derivedBindings.removeIf(
it -> !it.getOrigin().isExpression() && it.getIdentifier().equals(binding.getIdentifier()))) {
Predicate<ParameterBinding> identifier = binding::bindsTo;
Predicate<ParameterBinding> notCompatible = Predicate.not(binding::isCompatibleWith);

// replace incompatible bindings
if ( derivedBindings.removeIf(
it -> identifier.test(it) && notCompatible.test(it))) {
derivedBindings.add(binding);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,66 @@ void rewritesNamedLikeToUniqueParametersIfNecessary() {
assertThat(((MethodInvocationArgument) parameterBinding.getOrigin()).identifier().getName()).isEqualTo("firstname");
}

@Test // GH-3784
void rewritesNamedLikeToUniqueParametersRetainingCountQuery() {

DeclaredQuery query = new StringQuery(
"select u from User u where u.firstname like %:firstname or u.firstname like :firstname% or u.firstname = :firstname",
false).deriveCountQuery(null);

assertThat(query.getQueryString()) //
.isEqualTo(
"select count(u) from User u where u.firstname like :firstname or u.firstname like :firstname_1 or u.firstname = :firstname_2");

List<ParameterBinding> bindings = query.getParameterBindings();
assertThat(bindings).hasSize(3);

LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
assertThat(binding).isNotNull();
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter("firstname"));
assertThat(binding.getName()).isEqualTo("firstname");
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);

binding = (LikeParameterBinding) bindings.get(1);
assertThat(binding).isNotNull();
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter("firstname"));
assertThat(binding.getName()).isEqualTo("firstname_1");
assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH);

ParameterBinding parameterBinding = bindings.get(2);
assertThat(parameterBinding).isNotNull();
assertThat(parameterBinding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter("firstname"));
assertThat(parameterBinding.getName()).isEqualTo("firstname_2");
assertThat(((MethodInvocationArgument) parameterBinding.getOrigin()).identifier().getName()).isEqualTo("firstname");
}

@Test // GH-3784
void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() {

DeclaredQuery query = new StringQuery(
"select u from User u where u.firstname like %:#{firstname} or u.firstname like :#{firstname}%", false)
.deriveCountQuery(null);

assertThat(query.getQueryString()) //
.isEqualTo(
"select count(u) from User u where u.firstname like :__$synthetic$__1 or u.firstname like :__$synthetic$__2");

List<ParameterBinding> bindings = query.getParameterBindings();
assertThat(bindings).hasSize(2);

LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
assertThat(binding).isNotNull();
assertThat(binding.getOrigin().isExpression()).isTrue();
assertThat(binding.getName()).isEqualTo("__$synthetic$__1");
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);

binding = (LikeParameterBinding) bindings.get(1);
assertThat(binding).isNotNull();
assertThat(binding.getOrigin().isExpression()).isTrue();
assertThat(binding.getName()).isEqualTo("__$synthetic$__2");
assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH);
}

@Test // GH-3041
void rewritesPositionalLikeToUniqueParametersIfNecessary() {

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

@Test // GH-3784
void deriveCountQueryWithNamedInRetainsOrigin() {

String queryString = "select u from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins)";
DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null);

assertThat(query.getQueryString())
.isEqualTo("select count(u) from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins_1)");

List<ParameterBinding> bindings = query.getParameterBindings();
assertThat(bindings).hasSize(2);

assertNamedBinding(ParameterBinding.class, "logins", bindings.get(0));
assertThat((MethodInvocationArgument) bindings.get(0).getOrigin()).extracting(MethodInvocationArgument::identifier)
.extracting(BindingIdentifier::getName).isEqualTo("logins");

assertNamedBinding(InParameterBinding.class, "logins_1", bindings.get(1));
assertThat((MethodInvocationArgument) bindings.get(1).getOrigin()).extracting(MethodInvocationArgument::identifier)
.extracting(BindingIdentifier::getName).isEqualTo("logins");
}

@Test // GH-3784
void deriveCountQueryWithPositionalInRetainsOrigin() {

String queryString = "select u from User u where (?1) IS NULL OR LOWER(u.login) IN (?1)";
DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null);

assertThat(query.getQueryString())
.isEqualTo("select count(u) from User u where (?1) IS NULL OR LOWER(u.login) IN (?2)");

List<ParameterBinding> bindings = query.getParameterBindings();
assertThat(bindings).hasSize(2);

assertPositionalBinding(ParameterBinding.class, 1, bindings.get(0));
assertThat((MethodInvocationArgument) bindings.get(0).getOrigin()).extracting(MethodInvocationArgument::identifier)
.extracting(BindingIdentifier::getPosition).isEqualTo(1);

assertPositionalBinding(InParameterBinding.class, 2, bindings.get(1));
assertThat((MethodInvocationArgument) bindings.get(1).getOrigin()).extracting(MethodInvocationArgument::identifier)
.extracting(BindingIdentifier::getPosition).isEqualTo(1);
}

@Test // DATAJPA-461
void detectsPositionalInParameterBindings() {

Expand Down

0 comments on commit 2dec9cb

Please sign in to comment.