Merge branch '6.2.x'

This commit is contained in:
Sam Brannen 2025-05-09 14:32:13 +02:00
commit 4f38c5bdfa
7 changed files with 161 additions and 206 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -49,6 +49,7 @@ import org.springframework.util.Assert;
* @author Juergen Hoeller
* @author Mark Paluch
* @author Anton Naydenov
* @author Sam Brannen
* @since 5.3
*/
abstract class NamedParameterUtils {
@ -512,69 +513,17 @@ abstract class NamedParameterUtils {
private final BindParameterSource parameterSource;
ExpandedQuery(String expandedSql, NamedParameters parameters, BindParameterSource parameterSource) {
this.expandedSql = expandedSql;
this.parameters = parameters;
this.parameterSource = parameterSource;
}
@SuppressWarnings({"rawtypes", "unchecked"})
public void bind(BindTarget target, String identifier, Parameter parameter) {
List<BindMarker> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
target.bind(identifier, parameter);
return;
}
if (parameter.getValue() instanceof Collection collection) {
Iterator<Object> iterator = collection.iterator();
Iterator<BindMarker> markers = bindMarkers.iterator();
while (iterator.hasNext()) {
Object valueToBind = iterator.next();
if (valueToBind instanceof Object[] objects) {
for (Object object : objects) {
bind(target, markers, object);
}
}
else {
bind(target, markers, valueToBind);
}
}
}
else {
for (BindMarker bindMarker : bindMarkers) {
bindMarker.bind(target, parameter);
}
}
}
private void bind(BindTarget target, Iterator<BindMarker> markers, Object valueToBind) {
Assert.isTrue(markers.hasNext(), () -> String.format(
"No bind marker for value [%s] in SQL [%s]. Check that the query was expanded using the same arguments.",
valueToBind, toQuery()));
markers.next().bind(target, valueToBind);
}
public void bindNull(BindTarget target, String identifier, Parameter parameter) {
List<BindMarker> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
target.bind(identifier, parameter);
return;
}
for (BindMarker bindMarker : bindMarkers) {
bindMarker.bind(target, parameter);
}
}
@Nullable List<BindMarker> getBindMarkers(String identifier) {
List<NamedParameters.NamedParameter> parameters = this.parameters.getMarker(identifier);
if (parameters == null) {
return null;
}
List<BindMarker> markers = new ArrayList<>();
for (NamedParameters.NamedParameter parameter : parameters) {
markers.addAll(parameter.placeholders);
}
return markers;
@Override
public String toQuery() {
return this.expandedSql;
}
@Override
@ -595,10 +544,67 @@ abstract class NamedParameterUtils {
}
}
@Override
public String toQuery() {
return this.expandedSql;
private void bindNull(BindTarget target, String identifier, Parameter parameter) {
List<List<BindMarker>> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
target.bind(identifier, parameter);
return;
}
for (List<BindMarker> outer : bindMarkers) {
for (BindMarker bindMarker : outer) {
bindMarker.bind(target, parameter);
}
}
}
private void bind(BindTarget target, String identifier, Parameter parameter) {
List<List<BindMarker>> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
target.bind(identifier, parameter);
return;
}
for (List<BindMarker> outer : bindMarkers) {
if (parameter.getValue() instanceof Collection<?> collection) {
Iterator<BindMarker> markers = outer.iterator();
for (Object valueToBind : collection) {
if (valueToBind instanceof Object[] objects) {
for (Object object : objects) {
bind(target, markers, object);
}
}
else {
bind(target, markers, valueToBind);
}
}
}
else {
for (BindMarker bindMarker : outer) {
bindMarker.bind(target, parameter);
}
}
}
}
private void bind(BindTarget target, Iterator<BindMarker> markers, Object valueToBind) {
Assert.isTrue(markers.hasNext(), () -> String.format(
"No bind marker for value [%s] in SQL [%s]. Check that the query was expanded using the same arguments.",
valueToBind, toQuery()));
markers.next().bind(target, valueToBind);
}
private @Nullable List<List<BindMarker>> getBindMarkers(String identifier) {
List<NamedParameters.NamedParameter> parameters = this.parameters.getMarker(identifier);
if (parameters == null) {
return null;
}
List<List<BindMarker>> markers = new ArrayList<>();
for (NamedParameters.NamedParameter parameter : parameters) {
markers.add(new ArrayList<>(parameter.placeholders));
}
return markers;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,8 +19,9 @@ package org.springframework.r2dbc.core.binding;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* Anonymous, index-based bind marker using a static placeholder.
* Instances are bound by the ordinal position ordered by the appearance of
* Anonymous, index-based bind markers that use a static placeholder.
*
* <p>Instances are bound by the ordinal position ordered by the appearance of
* the placeholder. This implementation creates indexed bind markers using
* an anonymous placeholder that correlates with an index.
*
@ -46,7 +47,7 @@ class AnonymousBindMarkers implements BindMarkers {
/**
* Create a new {@link AnonymousBindMarkers} instance given {@code placeholder}.
* Create a new {@link AnonymousBindMarkers} instance for the given {@code placeholder}.
* @param placeholder parameter bind marker
*/
AnonymousBindMarkers(String placeholder) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,7 +20,8 @@ import io.r2dbc.spi.Statement;
/**
* A bind marker represents a single bindable parameter within a query.
* Bind markers are dialect-specific and provide a
*
* <p>Bind markers are dialect-specific and provide a
* {@link #getPlaceholder() placeholder} that is used in the actual query.
*
* @author Mark Paluch
@ -37,7 +38,8 @@ public interface BindMarker {
String getPlaceholder();
/**
* Bind the given {@code value} to the {@link Statement} using the underlying binding strategy.
* Bind the given {@code value} to the {@link Statement} using the underlying
* binding strategy.
* @param bindTarget the target to bind the value to
* @param value the actual value (must not be {@code null};
* use {@link #bindNull(BindTarget, Class)} for {@code null} values)
@ -46,7 +48,8 @@ public interface BindMarker {
void bind(BindTarget bindTarget, Object value);
/**
* Bind a {@code null} value to the {@link Statement} using the underlying binding strategy.
* Bind a {@code null} value to the {@link Statement} using the underlying
* binding strategy.
* @param bindTarget the target to bind the value to
* @param valueType the value type (must not be {@code null})
* @see Statement#bindNull

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,10 +20,10 @@ package org.springframework.r2dbc.core.binding;
* Bind markers represent placeholders in SQL queries for substitution
* for an actual parameter. Using bind markers allows creating safe queries
* so query strings are not required to contain escaped values but rather
* the driver encodes parameter in the appropriate representation.
* the driver encodes the parameter in the appropriate representation.
*
* <p>{@link BindMarkers} is stateful and can be only used for a single binding
* pass of one or more parameters. It maintains bind indexes/bind parameter names.
* pass of one or more parameters. It maintains bind indexes or bind parameter names.
*
* @author Mark Paluch
* @since 5.3
@ -41,7 +41,7 @@ public interface BindMarkers {
/**
* Create a new {@link BindMarker} that accepts a {@code hint}.
* Implementations are allowed to consider/ignore/filter
* <p>Implementations are allowed to consider/ignore/filter
* the name hint to create more expressive bind markers.
* @param hint an optional name hint that can be used as part of the bind marker
* @return a new {@link BindMarker}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -30,8 +30,8 @@ import org.springframework.util.LinkedCaseInsensitiveMap;
/**
* Resolves a {@link BindMarkersFactory} from a {@link ConnectionFactory} using
* {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's
* {@link SpringFactoriesLoader spring.factories} to determine available extensions.
* a {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's
* {@link SpringFactoriesLoader spring.factories} file to determine available extensions.
*
* @author Mark Paluch
* @since 5.3
@ -45,8 +45,8 @@ public final class BindMarkersFactoryResolver {
/**
* Retrieve a {@link BindMarkersFactory} by inspecting {@link ConnectionFactory}
* and its metadata.
* Retrieve a {@link BindMarkersFactory} by inspecting the supplied
* {@link ConnectionFactory} and its metadata.
* @param connectionFactory the connection factory to inspect
* @return the resolved {@link BindMarkersFactory}
* @throws NoBindMarkersFactoryException if no {@link BindMarkersFactory} can be resolved
@ -69,18 +69,21 @@ public final class BindMarkersFactoryResolver {
/**
* SPI to extend Spring's default R2DBC BindMarkersFactory discovery mechanism.
* Implementations of this interface are discovered through Spring's
* SPI to extend Spring's default R2DBC {@link BindMarkersFactory} discovery
* mechanism.
*
* <p>Implementations of this interface are discovered through Spring's
* {@link SpringFactoriesLoader} mechanism.
*
* @see SpringFactoriesLoader
*/
@FunctionalInterface
public interface BindMarkerFactoryProvider {
/**
* Return a {@link BindMarkersFactory} for a {@link ConnectionFactory}.
* @param connectionFactory the connection factory to be used with the {@link BindMarkersFactory}
* @return the {@link BindMarkersFactory} if the {@link BindMarkerFactoryProvider}
* Return a {@link BindMarkersFactory} for the given {@link ConnectionFactory}.
* @param connectionFactory the connection factory to be used with the {@code BindMarkersFactory}
* @return the {@code BindMarkersFactory} if this {@code BindMarkerFactoryProvider}
* can provide a bind marker factory object, otherwise {@code null}
*/
@Nullable BindMarkersFactory getBindMarkers(ConnectionFactory connectionFactory);
@ -88,7 +91,7 @@ public final class BindMarkersFactoryResolver {
/**
* Exception thrown when {@link BindMarkersFactoryResolver} cannot resolve a
* Exception thrown when a {@link BindMarkersFactoryResolver} cannot resolve a
* {@link BindMarkersFactory}.
*/
@SuppressWarnings("serial")
@ -105,8 +108,11 @@ public final class BindMarkersFactoryResolver {
/**
* Built-in bind maker factories. Used typically as last {@link BindMarkerFactoryProvider}
* when other providers register with a higher precedence.
* Built-in bind marker factories.
*
* <p>Typically used as the last {@link BindMarkerFactoryProvider} when other
* providers are registered with a higher precedence.
*
* @see org.springframework.core.Ordered
* @see org.springframework.core.annotation.AnnotationAwareOrderComparator
*/

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,7 +19,7 @@ package org.springframework.r2dbc.core.binding;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* Index-based bind marker. This implementation creates indexed bind
* Index-based bind markers. This implementation creates indexed bind
* markers using a numeric index and an optional prefix for bind markers
* to be represented within the query string.
*
@ -43,14 +43,15 @@ class IndexedBindMarkers implements BindMarkers {
/**
* Create a new {@link IndexedBindMarker} instance given {@code prefix} and {@code beginWith}.
* @param prefix bind parameter prefix
* @param beginWith the first index to use
* Create a new {@link IndexedBindMarker} instance for the given {@code prefix}
* and {@code beginWith} value.
* @param prefix the bind parameter prefix
* @param beginIndex the first index to use
*/
IndexedBindMarkers(String prefix, int beginWith) {
IndexedBindMarkers(String prefix, int beginIndex) {
this.counter = 0;
this.prefix = prefix;
this.offset = beginWith;
this.offset = beginIndex;
}

View File

@ -21,15 +21,12 @@ import java.util.List;
import java.util.Map;
import io.r2dbc.spi.Parameters;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
import org.springframework.r2dbc.core.binding.BindTarget;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@ -320,34 +317,17 @@ class NamedParameterUtilsTests {
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name = $1 or lastname = $1");
Map<Integer, Object> bindings = new HashMap<>();
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
bindings.put(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
operation.bindTo(trackingBindTarget);
assertThat(bindings)
assertThat(trackingBindTarget.bindings)
.hasSize(1)
.containsEntry(0, Parameters.in("foo"));
}
@Test
void multipleEqualCollectionParameterReferencesForIndexedMarkersBindsValueOnce() {
void multipleEqualCollectionParameterReferencesForIndexedMarkersBindsValuesOnce() {
String sql = "SELECT * FROM person where name IN (:ids) or lastname IN (:ids)";
MapBindParameterSource source = new MapBindParameterSource(Map.of("ids",
@ -357,71 +337,39 @@ class NamedParameterUtilsTests {
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name IN ($1, $2, $3) or lastname IN ($1, $2, $3)");
MultiValueMap<Integer, Object> bindings = new LinkedMultiValueMap<>();
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
bindings.add(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
operation.bindTo(trackingBindTarget);
assertThat(bindings)
assertThat(trackingBindTarget.bindings)
.hasSize(3)
.containsEntry(0, List.of("foo"))
.containsEntry(1, List.of("bar"))
.containsEntry(2, List.of("baz"));
.containsEntry(0, "foo")
.containsEntry(1, "bar")
.containsEntry(2, "baz");
}
@Test // gh-34768
@Disabled("Disabled until gh-34768 is addressed")
void multipleEqualCollectionParameterReferencesForAnonymousMarkersBindsValueTwice() {
void multipleEqualCollectionParameterReferencesForAnonymousMarkersBindsValuesTwice() {
String sql = "SELECT * FROM fund_info WHERE fund_code IN (:fundCodes) OR fund_code IN (:fundCodes)";
MapBindParameterSource source = new MapBindParameterSource(Map.of("fundCodes", Parameters.in(List.of("foo"))));
MapBindParameterSource source = new MapBindParameterSource(Map.of("fundCodes", Parameters.in(List.of("foo", "bar", "baz"))));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, ANONYMOUS_MARKERS, source);
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM fund_info WHERE fund_code IN (?) OR fund_code IN (?)");
.isEqualTo("SELECT * FROM fund_info WHERE fund_code IN (?, ?, ?) OR fund_code IN (?, ?, ?)");
Map<Integer, Object> bindings = new HashMap<>();
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {}
operation.bindTo(trackingBindTarget);
@Override
public void bind(int index, Object value) {
bindings.put(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
assertThat(bindings)
.hasSize(2)
assertThat(trackingBindTarget.bindings)
.hasSize(6)
.containsEntry(0, "foo")
.containsEntry(1, "foo");
.containsEntry(1, "bar")
.containsEntry(2, "baz")
.containsEntry(3, "foo")
.containsEntry(4, "bar")
.containsEntry(5, "baz");
}
@Test
@ -434,28 +382,11 @@ class NamedParameterUtilsTests {
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name = ? or lastname = ?");
Map<Integer, Object> bindings = new HashMap<>();
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
bindings.put(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
operation.bindTo(trackingBindTarget);
assertThat(bindings)
assertThat(trackingBindTarget.bindings)
.hasSize(2)
.containsEntry(0, Parameters.in("foo"))
.containsEntry(1, Parameters.in("foo"));
@ -471,28 +402,11 @@ class NamedParameterUtilsTests {
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name = $1 or lastname = $1");
Map<Integer, Object> bindings = new HashMap<>();
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
bindings.put(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
operation.bindTo(trackingBindTarget);
assertThat(bindings)
assertThat(trackingBindTarget.bindings)
.hasSize(1)
.containsEntry(0, Parameters.in(String.class));
}
@ -508,4 +422,28 @@ class NamedParameterUtilsTests {
new MapBindParameterSource()).toQuery();
}
private static class TrackingBindTarget implements BindTarget {
final Map<Integer, Object> bindings = new HashMap<>();
@Override
public void bind(String identifier, Object value) {}
@Override
public void bind(int index, Object value) {
this.bindings.put(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
}
}