Skip to content

Commit

Permalink
Ensure correct bindings for parameter reuse.
Browse files Browse the repository at this point in the history
We now verify all bindings to ensure that a like-parameter doesn't mix up with plain bindings (e.g. firstname = %:myparam or lastname = :myparam). We also create unique synthetic parameters for positional bindings.

Also, fix Regex to properly detect named, anonymous and positional bind markers.

See #3041
  • Loading branch information
mp911de authored and gregturn committed Jul 21, 2023
1 parent c223481 commit a55480f
Show file tree
Hide file tree
Showing 7 changed files with 392 additions and 144 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import java.util.List;

import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.repository.query.parser.Part.Type;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -151,8 +150,8 @@ public Object prepare(@Nullable Object valueToBind) {
/**
* Check whether the {@code other} binding uses the same bind target.
*
* @param other
* @return
* @param other must not be {@literal null}.
* @return {@code true} if the other binding uses the same parameter to bind to as this one.
*/
public boolean bindsTo(ParameterBinding other) {

Expand All @@ -171,6 +170,17 @@ public boolean bindsTo(ParameterBinding other) {
return false;
}

/**
* Check whether this binding can be bound as the {@code other} binding by checking its type and origin. Subclasses
* may override this method to include other properties for the compatibility check.
*
* @param other
* @return {@code true} if the other binding is compatible with this one.
*/
public boolean isCompatibleWith(ParameterBinding other) {
return other.getClass() == getClass() && other.getOrigin().equals(getOrigin());
}

/**
* Represents a {@link ParameterBinding} in a JPQL query augmented with instructions of how to apply a parameter as an
* {@code IN} parameter.
Expand Down Expand Up @@ -294,6 +304,16 @@ public String toString() {
getType());
}

@Override
public boolean isCompatibleWith(ParameterBinding binding) {

if (super.isCompatibleWith(binding) && binding instanceof LikeParameterBinding other) {
return getType() == other.getType();
}

return false;
}

/**
* Extracts the like {@link Type} from the given JPA like expression.
*
Expand All @@ -319,52 +339,12 @@ static Type getLikeTypeFrom(String expression) {
}
}

static class ParameterImpl<T> implements jakarta.persistence.Parameter<T> {

private final BindingIdentifier identifier;
private final Class<T> parameterType;

/**
* Creates a new {@link ParameterImpl} for the given {@link JpaParameter} and {@link ParameterBinding}.
*
* @param parameter can be {@literal null}.
* @param binding must not be {@literal null}.
* @return a {@link jakarta.persistence.Parameter} object based on the information from the arguments.
*/
static jakarta.persistence.Parameter<?> of(@Nullable JpaParameter parameter, ParameterBinding binding) {

Class<?> type = parameter == null ? Object.class : parameter.getType();

return new ParameterImpl<>(binding.getIdentifier(), type);
}

public ParameterImpl(BindingIdentifier identifier, Class<T> parameterType) {
this.identifier = identifier;
this.parameterType = parameterType;
}

@Nullable
@Override
public String getName() {
return identifier.hasName() ? identifier.getName() : null;
}

@Nullable
@Override
public Integer getPosition() {
return identifier.hasPosition() ? identifier.getPosition() : null;
}

@Override
public Class<T> getParameterType() {
return parameterType;
}

}

/**
* Identifies a binding parameter by name, position or both. Used to bind parameters to a query or to describe a
* {@link MethodInvocationArgument} origin.
*
* @author Mark Paluch
* @since 3.1.2
*/
sealed interface BindingIdentifier permits Named,Indexed,NamedAndIndexed {

Expand Down Expand Up @@ -453,6 +433,11 @@ public boolean hasName() {
public String getName() {
return name();
}

@Override
public String toString() {
return name();
}
}

private record Indexed(int position) implements BindingIdentifier {
Expand All @@ -466,6 +451,11 @@ public boolean hasPosition() {
public int getPosition() {
return position();
}

@Override
public String toString() {
return "[" + position() + "]";
}
}

private record NamedAndIndexed(String name, int position) implements BindingIdentifier {
Expand All @@ -489,10 +479,18 @@ public boolean hasPosition() {
public int getPosition() {
return position();
}

@Override
public String toString() {
return "[" + name() + ", " + position() + "]";
}
}

/**
* Value type hierarchy to describe where a binding parameter comes from, either method call or an expression.
*
* @author Mark Paluch
* @since 3.1.2
*/
sealed interface ParameterOrigin permits Expression,MethodInvocationArgument {

Expand All @@ -508,11 +506,11 @@ static Expression ofExpression(String expression) {

/**
* Creates a {@link MethodInvocationArgument} object for {@code name} and {@code position}. Either the name or the
* position must be given,
* position must be given.
*
* @param name the parameter name from the method invocation, can be {@literal null}.
* @param position the parameter position (1-based) from the method invocation, can be {@literal null}.
* @return {@link MethodInvocationArgument} object for {@code name} and {@code position}
* @return {@link MethodInvocationArgument} object for {@code name} and {@code position}.
*/
static MethodInvocationArgument ofParameter(@Nullable String name, @Nullable Integer position) {

Expand All @@ -528,26 +526,43 @@ static MethodInvocationArgument ofParameter(@Nullable String name, @Nullable Int
return ofParameter(identifier);
}

/**
* Creates a {@link MethodInvocationArgument} object for {@code position}.
*
* @param position the parameter position (1-based) from the method invocation.
* @return {@link MethodInvocationArgument} object for {@code position}.
*/
static MethodInvocationArgument ofParameter(int position) {
return ofParameter(BindingIdentifier.of(position));
}

/**
* Creates a {@link MethodInvocationArgument} using {@link BindingIdentifier}.
*
* @param identifier must not be {@literal null}.
* @return {@link MethodInvocationArgument} for {@link BindingIdentifier}.
*/
static MethodInvocationArgument ofParameter(BindingIdentifier identifier) {

return new MethodInvocationArgument(identifier);
}

/**
* @return {@code true} if the origin is a method argument reference.
*/
boolean isMethodArgument();

/**
* @return {@code true} if the origin is an expression.
*/
boolean isExpression();
}

/**
* Value object capturing the expression of which a binding parameter originates.
*
* @param expression
* @author Mark Paluch
* @since 3.1.2
*/
public record Expression(String expression) implements ParameterOrigin {

Expand All @@ -566,6 +581,8 @@ public boolean isExpression() {
* Value object capturing the method invocation parameter reference.
*
* @param identifier
* @author Mark Paluch
* @since 3.1.2
*/
public record MethodInvocationArgument(BindingIdentifier identifier) implements ParameterOrigin {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
import org.springframework.data.jpa.repository.query.JpaParameters.JpaParameter;
import org.springframework.data.jpa.repository.query.ParameterBinding.BindingIdentifier;
import org.springframework.data.jpa.repository.query.ParameterBinding.MethodInvocationArgument;
import org.springframework.data.jpa.repository.query.ParameterBinding.ParameterImpl;
import org.springframework.data.jpa.repository.query.ParameterMetadataProvider.ParameterMetadata;
import org.springframework.data.jpa.repository.query.QueryParameterSetter.NamedOrIndexedQueryParameterSetter;
import org.springframework.data.repository.query.Parameter;
Expand Down Expand Up @@ -323,4 +322,47 @@ private Object getAndPrepare(JpaParameter parameter, ParameterMetadata<?> metada
}
}

static class ParameterImpl<T> implements jakarta.persistence.Parameter<T> {

private final BindingIdentifier identifier;
private final Class<T> parameterType;

/**
* Creates a new {@link ParameterImpl} for the given {@link JpaParameter} and {@link ParameterBinding}.
*
* @param parameter can be {@literal null}.
* @param binding must not be {@literal null}.
* @return a {@link jakarta.persistence.Parameter} object based on the information from the arguments.
*/
static jakarta.persistence.Parameter<?> of(@Nullable JpaParameter parameter, ParameterBinding binding) {

Class<?> type = parameter == null ? Object.class : parameter.getType();

return new ParameterImpl<>(binding.getIdentifier(), type);
}

public ParameterImpl(BindingIdentifier identifier, Class<T> parameterType) {
this.identifier = identifier;
this.parameterType = parameterType;
}

@Nullable
@Override
public String getName() {
return identifier.hasName() ? identifier.getName() : null;
}

@Nullable
@Override
public Integer getPosition() {
return identifier.hasPosition() ? identifier.getPosition() : null;
}

@Override
public Class<T> getParameterType() {
return parameterType;
}

}

}
Loading

0 comments on commit a55480f

Please sign in to comment.