diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index 680213cb09..5c48b99728 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java @@ -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 bindMarkers = getBindMarkers(identifier); - if (bindMarkers == null) { - target.bind(identifier, parameter); - return; - } - if (parameter.getValue() instanceof Collection collection) { - Iterator iterator = collection.iterator(); - Iterator 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 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 bindMarkers = getBindMarkers(identifier); - if (bindMarkers == null) { - target.bind(identifier, parameter); - return; - } - for (BindMarker bindMarker : bindMarkers) { - bindMarker.bind(target, parameter); - } - } - - @Nullable List getBindMarkers(String identifier) { - List parameters = this.parameters.getMarker(identifier); - if (parameters == null) { - return null; - } - List 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> bindMarkers = getBindMarkers(identifier); + if (bindMarkers == null) { + target.bind(identifier, parameter); + return; + } + for (List outer : bindMarkers) { + for (BindMarker bindMarker : outer) { + bindMarker.bind(target, parameter); + } + } } + + private void bind(BindTarget target, String identifier, Parameter parameter) { + List> bindMarkers = getBindMarkers(identifier); + if (bindMarkers == null) { + target.bind(identifier, parameter); + return; + } + + for (List outer : bindMarkers) { + if (parameter.getValue() instanceof Collection collection) { + Iterator 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 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> getBindMarkers(String identifier) { + List parameters = this.parameters.getMarker(identifier); + if (parameters == null) { + return null; + } + List> markers = new ArrayList<>(); + for (NamedParameters.NamedParameter parameter : parameters) { + markers.add(new ArrayList<>(parameter.placeholders)); + } + return markers; + } + } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java index 71569702cc..c66ada2ca1 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java @@ -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. + * + *

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) { diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java index 5fb53c47c1..40f8a71313 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java @@ -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 + * + *

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 diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java index af5a9fd48c..5504cbc01e 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java @@ -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. * *

{@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 + *

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} diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java index 8a4ff087c8..bac723f4c3 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java @@ -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. + * + *

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. + * + *

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 */ diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java index 5f14d937b4..23081c9778 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java @@ -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; } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java index 413c218909..725bcf72a9 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java @@ -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 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 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 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 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 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 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 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(); + } + } + }