DatabaseClient uses SQL Supplier more lazily

This commit modifies the `DefaultDatabaseClient` implementation in order
to ensure lazier usage of the `Supplier<String>` passed to the sql
method (`DatabaseClient#sql(Supplier)`).

Since technically `DatabaseClient` is an interface that could have 3rd
party implementations, the lazyness expectation is only hinted at in the
`DatabaseClient#sql` javadoc.

Possible caveat: some log statements attempt to reflect the now lazily
resolved SQL string. Similarly, some exceptions can capture the SQL that
caused the issue if known. We expect that these always occur after the
execution of the statement has been attempted (see `ResultFunction`).
At this point the SQL string will be accessible and logs and exceptions
should reflect it as before. Keep an eye out for such strings turning
into `null` after this change, which would indicate the opposite.

Closes gh-29367
This commit is contained in:
Simon Baslé
2023-01-25 18:02:10 +01:00
parent 767f59a3ae
commit d72df5ace4
7 changed files with 204 additions and 52 deletions

View File

@@ -17,6 +17,8 @@
package org.springframework.r2dbc.core;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
@@ -63,6 +65,7 @@ import static org.mockito.BDDMockito.when;
* @author Mark Paluch
* @author Ferdinand Jacobs
* @author Jens Schauder
* @author Simon Baslé
*/
@MockitoSettings(strictness = Strictness.LENIENT)
class DefaultDatabaseClientUnitTests {
@@ -385,6 +388,47 @@ class DefaultDatabaseClientUnitTests {
inOrder.verifyNoMoreInteractions();
}
@Test
void sqlSupplierInvocationIsDeferredUntilSubscription() {
// We'll have either 2 or 3 rows, depending on the subscription and the generated SQL
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(
MockColumnMetadata.builder().name("id").javaType(Integer.class).build()).build();
final MockRow row1 = MockRow.builder().metadata(metadata)
.identified("id", Integer.class, 1).build();
final MockRow row2 = MockRow.builder().metadata(metadata)
.identified("id", Integer.class, 2).build();
final MockRow row3 = MockRow.builder().metadata(metadata)
.identified("id", Integer.class, 3).build();
// Set up 2 mock statements
mockStatementFor("SELECT id FROM test WHERE id < '3'", MockResult.builder()
.row(row1, row2).build());
mockStatementFor("SELECT id FROM test WHERE id < '4'", MockResult.builder()
.row(row1, row2, row3).build());
// Create the client
DatabaseClient databaseClient = this.databaseClientBuilder.build();
AtomicInteger invoked = new AtomicInteger();
// Assemble a publisher, but don't subscribe yet
Mono<List<Integer>> operation = databaseClient
.sql(() -> {
int idMax = 2 + invoked.incrementAndGet();
return String.format("SELECT id FROM test WHERE id < '%s'", idMax);
})
.map(r -> r.get("id", Integer.class))
.all()
.collectList();
assertThat(invoked).as("invoked (before subscription)").hasValue(0);
List<Integer> rows = operation.block();
assertThat(invoked).as("invoked (after 1st subscription)").hasValue(1);
assertThat(rows).containsExactly(1, 2);
rows = operation.block();
assertThat(invoked).as("invoked (after 2nd subscription)").hasValue(2);
assertThat(rows).containsExactly(1, 2, 3);
}
private Statement mockStatement() {
return mockStatementFor(null, null);