Add support for R2DBC

This commit introduces support for R2DBC ("Reactive Relational Database
Connectivity") with custom ConnectionFactory implementations, a
functional DatabaseClient for SQL execution, transaction management, a
bind marker abstraction database initialization utilities, and
exception translation.

Closes gh-25065
This commit is contained in:
Mark Paluch
2020-05-13 15:54:25 +02:00
committed by Sam Brannen
parent 7f79a373c3
commit aff601edf1
114 changed files with 13150 additions and 1 deletions

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2019-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection;
import io.r2dbc.spi.R2dbcBadGrammarException;
import io.r2dbc.spi.R2dbcDataIntegrityViolationException;
import io.r2dbc.spi.R2dbcException;
import io.r2dbc.spi.R2dbcNonTransientResourceException;
import io.r2dbc.spi.R2dbcPermissionDeniedException;
import io.r2dbc.spi.R2dbcRollbackException;
import io.r2dbc.spi.R2dbcTimeoutException;
import io.r2dbc.spi.R2dbcTransientResourceException;
import org.junit.jupiter.api.Test;
import org.springframework.dao.ConcurrencyFailureException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.PermissionDeniedDataAccessException;
import org.springframework.dao.QueryTimeoutException;
import org.springframework.dao.TransientDataAccessResourceException;
import org.springframework.r2dbc.BadSqlGrammarException;
import org.springframework.r2dbc.UncategorizedR2dbcException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ConnectionFactoryUtils}.
*
* @author Mark Paluch
*/
public class ConnectionFactoryUtilsUnitTests {
@Test
public void shouldTranslateTransientResourceException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcTransientResourceException(""));
assertThat(exception).isInstanceOf(TransientDataAccessResourceException.class);
}
@Test
public void shouldTranslateRollbackException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcRollbackException());
assertThat(exception).isInstanceOf(ConcurrencyFailureException.class);
}
@Test
public void shouldTranslateTimeoutException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcTimeoutException());
assertThat(exception).isInstanceOf(QueryTimeoutException.class);
}
@Test
public void shouldNotTranslateUnknownExceptions() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new MyTransientExceptions());
assertThat(exception).isInstanceOf(UncategorizedR2dbcException.class);
}
@Test
public void shouldTranslateNonTransientResourceException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcNonTransientResourceException());
assertThat(exception).isInstanceOf(DataAccessResourceFailureException.class);
}
@Test
public void shouldTranslateIntegrityViolationException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcDataIntegrityViolationException());
assertThat(exception).isInstanceOf(DataIntegrityViolationException.class);
}
@Test
public void shouldTranslatePermissionDeniedException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcPermissionDeniedException());
assertThat(exception).isInstanceOf(PermissionDeniedDataAccessException.class);
}
@Test
public void shouldTranslateBadSqlGrammarException() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("", "",
new R2dbcBadGrammarException());
assertThat(exception).isInstanceOf(BadSqlGrammarException.class);
}
@Test
public void messageGeneration() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("TASK",
"SOME-SQL", new R2dbcTransientResourceException("MESSAGE"));
assertThat(exception).isInstanceOf(
TransientDataAccessResourceException.class).hasMessage(
"TASK; SQL [SOME-SQL]; MESSAGE; nested exception is io.r2dbc.spi.R2dbcTransientResourceException: MESSAGE");
}
@Test
public void messageGenerationNullSQL() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("TASK", null,
new R2dbcTransientResourceException("MESSAGE"));
assertThat(exception).isInstanceOf(
TransientDataAccessResourceException.class).hasMessage(
"TASK; MESSAGE; nested exception is io.r2dbc.spi.R2dbcTransientResourceException: MESSAGE");
}
@Test
public void messageGenerationNullMessage() {
Exception exception = ConnectionFactoryUtils.convertR2dbcException("TASK",
"SOME-SQL", new R2dbcTransientResourceException());
assertThat(exception).isInstanceOf(
TransientDataAccessResourceException.class).hasMessage(
"TASK; SQL [SOME-SQL]; null; nested exception is io.r2dbc.spi.R2dbcTransientResourceException");
}
@SuppressWarnings("serial")
private static class MyTransientExceptions extends R2dbcException {
}
}

View File

@@ -0,0 +1,63 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.when;
/**
* Unit tests for {@link DelegatingConnectionFactory}.
*
* @author Mark Paluch
*/
public class DelegatingConnectionFactoryUnitTests {
ConnectionFactory delegate = mock(ConnectionFactory.class);
Connection connectionMock = mock(Connection.class);
DelegatingConnectionFactory connectionFactory = new ExampleConnectionFactory(
delegate);
@Test
public void shouldDelegateGetConnection() {
Mono<Connection> connectionMono = Mono.just(connectionMock);
when(delegate.create()).thenReturn((Mono) connectionMono);
assertThat(connectionFactory.create()).isSameAs(connectionMono);
}
@Test
public void shouldDelegateUnwrapWithoutImplementing() {
assertThat(connectionFactory.unwrap()).isSameAs(delegate);
}
static class ExampleConnectionFactory extends DelegatingConnectionFactory {
ExampleConnectionFactory(ConnectionFactory targetConnectionFactory) {
super(targetConnectionFactory);
}
}
}

View File

@@ -0,0 +1,488 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection;
import java.util.concurrent.atomic.AtomicInteger;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.IsolationLevel;
import io.r2dbc.spi.R2dbcBadGrammarException;
import io.r2dbc.spi.Statement;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.transaction.CannotCreateTransactionException;
import org.springframework.transaction.IllegalTransactionStateException;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.reactive.TransactionSynchronization;
import org.springframework.transaction.reactive.TransactionSynchronizationManager;
import org.springframework.transaction.reactive.TransactionalOperator;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.never;
import static org.mockito.BDDMockito.reset;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.verifyNoMoreInteractions;
import static org.mockito.BDDMockito.when;
/**
* Unit tests for {@link R2dbcTransactionManager}.
*
* @author Mark Paluch
*/
public class R2dbcTransactionManagerUnitTests {
ConnectionFactory connectionFactoryMock = mock(ConnectionFactory.class);
Connection connectionMock = mock(Connection.class);
private R2dbcTransactionManager tm;
@BeforeEach
public void before() {
when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock));
when(connectionMock.beginTransaction()).thenReturn(Mono.empty());
when(connectionMock.close()).thenReturn(Mono.empty());
tm = new R2dbcTransactionManager(connectionFactoryMock);
}
@Test
public void testSimpleTransaction() {
TestTransactionSynchronization sync = new TestTransactionSynchronization(
TransactionSynchronization.STATUS_COMMITTED);
AtomicInteger commits = new AtomicInteger();
when(connectionMock.commitTransaction()).thenReturn(
Mono.fromRunnable(commits::incrementAndGet));
TransactionalOperator operator = TransactionalOperator.create(tm);
ConnectionFactoryUtils.getConnection(connectionFactoryMock)
.flatMap(connection -> TransactionSynchronizationManager.forCurrentTransaction()
.doOnNext(synchronizationManager -> synchronizationManager.registerSynchronization(
sync)))
.as(operator::transactional)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
assertThat(commits).hasValue(1);
verify(connectionMock).isAutoCommit();
verify(connectionMock).beginTransaction();
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
assertThat(sync.beforeCommitCalled).isTrue();
assertThat(sync.afterCommitCalled).isTrue();
assertThat(sync.beforeCompletionCalled).isTrue();
assertThat(sync.afterCompletionCalled).isTrue();
}
@Test
public void testBeginFails() {
reset(connectionFactoryMock);
when(connectionFactoryMock.create()).thenReturn(
Mono.error(new R2dbcBadGrammarException("fail")));
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(
operator::transactional)
.as(StepVerifier::create)
.expectErrorSatisfies(actual -> assertThat(actual).isInstanceOf(
CannotCreateTransactionException.class).hasCauseInstanceOf(
R2dbcBadGrammarException.class))
.verify();
}
@Test
public void appliesIsolationLevel() {
when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
when(connectionMock.getTransactionIsolationLevel()).thenReturn(
IsolationLevel.READ_COMMITTED);
when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(
operator::transactional)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
verify(connectionMock).beginTransaction();
verify(connectionMock).setTransactionIsolationLevel(
IsolationLevel.READ_COMMITTED);
verify(connectionMock).setTransactionIsolationLevel(IsolationLevel.SERIALIZABLE);
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
}
@Test
public void doesNotSetIsolationLevelIfMatch() {
when(connectionMock.getTransactionIsolationLevel()).thenReturn(
IsolationLevel.READ_COMMITTED);
when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(
operator::transactional)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
verify(connectionMock).beginTransaction();
verify(connectionMock, never()).setTransactionIsolationLevel(any());
verify(connectionMock).commitTransaction();
}
@Test
public void doesNotSetAutoCommitDisabled() {
when(connectionMock.isAutoCommit()).thenReturn(false);
when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(
operator::transactional)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
verify(connectionMock).beginTransaction();
verify(connectionMock, never()).setAutoCommit(anyBoolean());
verify(connectionMock).commitTransaction();
}
@Test
public void restoresAutoCommit() {
when(connectionMock.isAutoCommit()).thenReturn(true);
when(connectionMock.setAutoCommit(anyBoolean())).thenReturn(Mono.empty());
when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(
operator::transactional)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
verify(connectionMock).beginTransaction();
verify(connectionMock).setAutoCommit(false);
verify(connectionMock).setAutoCommit(true);
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
}
@Test
public void appliesReadOnly() {
when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty());
Statement statement = mock(Statement.class);
when(connectionMock.createStatement(anyString())).thenReturn(statement);
when(statement.execute()).thenReturn(Mono.empty());
tm.setEnforceReadOnly(true);
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setReadOnly(true);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(
operator::transactional)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
verify(connectionMock).isAutoCommit();
verify(connectionMock).beginTransaction();
verify(connectionMock).createStatement("SET TRANSACTION READ ONLY");
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
}
@Test
public void testCommitFails() {
when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail"))));
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
TransactionalOperator operator = TransactionalOperator.create(tm);
ConnectionFactoryUtils.getConnection(connectionFactoryMock)
.doOnNext(connection -> connection.createStatement("foo")).then()
.as(operator::transactional)
.as(StepVerifier::create)
.verifyError(IllegalTransactionStateException.class);
verify(connectionMock).isAutoCommit();
verify(connectionMock).beginTransaction();
verify(connectionMock).createStatement("foo");
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
}
@Test
public void testRollback() {
AtomicInteger commits = new AtomicInteger();
when(connectionMock.commitTransaction()).thenReturn(
Mono.fromRunnable(commits::incrementAndGet));
AtomicInteger rollbacks = new AtomicInteger();
when(connectionMock.rollbackTransaction()).thenReturn(
Mono.fromRunnable(rollbacks::incrementAndGet));
TransactionalOperator operator = TransactionalOperator.create(tm);
ConnectionFactoryUtils.getConnection(connectionFactoryMock)
.doOnNext(connection -> {
throw new IllegalStateException();
}).as(operator::transactional)
.as(StepVerifier::create)
.verifyError(IllegalStateException.class);
assertThat(commits).hasValue(0);
assertThat(rollbacks).hasValue(1);
verify(connectionMock).isAutoCommit();
verify(connectionMock).beginTransaction();
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
}
@Test
public void testRollbackFails() {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Commit should fail"))), Mono.empty());
TransactionalOperator operator = TransactionalOperator.create(tm);
operator.execute(reactiveTransaction -> {
reactiveTransaction.setRollbackOnly();
return ConnectionFactoryUtils.getConnection(connectionFactoryMock)
.doOnNext(connection -> connection.createStatement("foo")).then();
}).as(StepVerifier::create)
.verifyError(IllegalTransactionStateException.class);
verify(connectionMock).isAutoCommit();
verify(connectionMock).beginTransaction();
verify(connectionMock).createStatement("foo");
verify(connectionMock, never()).commitTransaction();
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
}
@Test
public void testTransactionSetRollbackOnly() {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
TestTransactionSynchronization sync = new TestTransactionSynchronization(
TransactionSynchronization.STATUS_ROLLED_BACK);
TransactionalOperator operator = TransactionalOperator.create(tm);
operator.execute(tx -> {
tx.setRollbackOnly();
assertThat(tx.isNewTransaction()).isTrue();
return TransactionSynchronizationManager.forCurrentTransaction().doOnNext(
synchronizationManager -> {
assertThat(synchronizationManager.hasResource(connectionFactoryMock)).isTrue();
synchronizationManager.registerSynchronization(sync);
}).then();
}).as(StepVerifier::create)
.verifyComplete();
verify(connectionMock).isAutoCommit();
verify(connectionMock).beginTransaction();
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
verifyNoMoreInteractions(connectionMock);
assertThat(sync.beforeCommitCalled).isFalse();
assertThat(sync.afterCommitCalled).isFalse();
assertThat(sync.beforeCompletionCalled).isTrue();
assertThat(sync.afterCompletionCalled).isTrue();
}
@Test
public void testPropagationNeverWithExistingTransaction() {
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
operator.execute(tx1 -> {
assertThat(tx1.isNewTransaction()).isTrue();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER);
return operator.execute(tx2 -> {
fail("Should have thrown IllegalTransactionStateException");
return Mono.empty();
});
}).as(StepVerifier::create)
.verifyError(IllegalTransactionStateException.class);
verify(connectionMock).rollbackTransaction();
verify(connectionMock).close();
}
@Test
public void testPropagationSupportsAndRequiresNew() {
when(connectionMock.commitTransaction()).thenReturn(Mono.empty());
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
TransactionalOperator operator = TransactionalOperator.create(tm, definition);
operator.execute(tx1 -> {
assertThat(tx1.isNewTransaction()).isFalse();
DefaultTransactionDefinition innerDef = new DefaultTransactionDefinition();
innerDef.setPropagationBehavior(
TransactionDefinition.PROPAGATION_REQUIRES_NEW);
TransactionalOperator inner = TransactionalOperator.create(tm, innerDef);
return inner.execute(tx2 -> {
assertThat(tx2.isNewTransaction()).isTrue();
return Mono.empty();
});
}).as(StepVerifier::create)
.verifyComplete();
verify(connectionMock).commitTransaction();
verify(connectionMock).close();
}
private static class TestTransactionSynchronization
implements TransactionSynchronization {
private int status;
public boolean beforeCommitCalled;
public boolean beforeCompletionCalled;
public boolean afterCommitCalled;
public boolean afterCompletionCalled;
public Throwable afterCompletionException;
public TestTransactionSynchronization(int status) {
this.status = status;
}
@Override
public Mono<Void> suspend() {
return Mono.empty();
}
@Override
public Mono<Void> resume() {
return Mono.empty();
}
@Override
public Mono<Void> beforeCommit(boolean readOnly) {
if (this.status != TransactionSynchronization.STATUS_COMMITTED) {
fail("Should never be called");
}
return Mono.fromRunnable(() -> {
assertThat(this.beforeCommitCalled).isFalse();
this.beforeCommitCalled = true;
});
}
@Override
public Mono<Void> beforeCompletion() {
return Mono.fromRunnable(() -> {
assertThat(this.beforeCompletionCalled).isFalse();
this.beforeCompletionCalled = true;
});
}
@Override
public Mono<Void> afterCommit() {
if (this.status != TransactionSynchronization.STATUS_COMMITTED) {
fail("Should never be called");
}
return Mono.fromRunnable(() -> {
assertThat(this.afterCommitCalled).isFalse();
this.afterCommitCalled = true;
});
}
@Override
public Mono<Void> afterCompletion(int status) {
try {
return Mono.fromRunnable(() -> doAfterCompletion(status));
}
catch (Throwable ex) {
this.afterCompletionException = ex;
}
return Mono.empty();
}
protected void doAfterCompletion(int status) {
assertThat(this.afterCompletionCalled).isFalse();
this.afterCompletionCalled = true;
assertThat(status).isEqualTo(this.status);
}
}
}

View File

@@ -0,0 +1,144 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection;
import io.r2dbc.h2.H2Connection;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactoryMetadata;
import io.r2dbc.spi.IsolationLevel;
import io.r2dbc.spi.R2dbcNonTransientResourceException;
import io.r2dbc.spi.Wrapped;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.never;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.when;
/**
* Unit tests for {@link SingleConnectionFactory}.
*
* @author Mark Paluch
*/
public class SingleConnectionFactoryUnitTests {
@Test
public void shouldAllocateSameConnection() {
SingleConnectionFactory factory = new SingleConnectionFactory(
"r2dbc:h2:mem:///foo", false);
Mono<? extends Connection> cf1 = factory.create();
Mono<? extends Connection> cf2 = factory.create();
Connection c1 = cf1.block();
Connection c2 = cf2.block();
assertThat(c1).isSameAs(c2);
factory.destroy();
}
@Test
public void shouldApplyAutoCommit() {
SingleConnectionFactory factory = new SingleConnectionFactory(
"r2dbc:h2:mem:///foo", false);
factory.setAutoCommit(false);
factory.create().as(StepVerifier::create)
.consumeNextWith(actual -> assertThat(actual.isAutoCommit()).isFalse())
.verifyComplete();
factory.setAutoCommit(true);
factory.create().as(StepVerifier::create)
.consumeNextWith(actual -> assertThat(actual.isAutoCommit()).isTrue())
.verifyComplete();
factory.destroy();
}
@Test
public void shouldSuppressClose() {
SingleConnectionFactory factory = new SingleConnectionFactory(
"r2dbc:h2:mem:///foo", true);
Connection connection = factory.create().block();
StepVerifier.create(connection.close()).verifyComplete();
assertThat(connection).isInstanceOf(Wrapped.class);
assertThat(((Wrapped) connection).unwrap()).isInstanceOf(H2Connection.class);
StepVerifier.create(
connection.setTransactionIsolationLevel(IsolationLevel.READ_COMMITTED))
.verifyComplete();
factory.destroy();
}
@Test
public void shouldNotSuppressClose() {
SingleConnectionFactory factory = new SingleConnectionFactory(
"r2dbc:h2:mem:///foo", false);
Connection connection = factory.create().block();
StepVerifier.create(connection.close()).verifyComplete();
StepVerifier.create(connection.setTransactionIsolationLevel(
IsolationLevel.READ_COMMITTED)).verifyError(
R2dbcNonTransientResourceException.class);
factory.destroy();
}
@Test
public void releaseConnectionShouldNotCloseConnection() {
Connection connectionMock = mock(Connection.class);
ConnectionFactoryMetadata metadata = mock(ConnectionFactoryMetadata.class);
SingleConnectionFactory factory = new SingleConnectionFactory(
connectionMock, metadata, true);
Connection connection = factory.create().block();
ConnectionFactoryUtils.releaseConnection(connection, factory)
.as(StepVerifier::create)
.verifyComplete();
verify(connectionMock, never()).close();
}
@Test
public void releaseConnectionShouldCloseUnrelatedConnection() {
Connection connectionMock = mock(Connection.class);
Connection otherConnection = mock(Connection.class);
ConnectionFactoryMetadata metadata = mock(ConnectionFactoryMetadata.class);
when(otherConnection.close()).thenReturn(Mono.empty());
SingleConnectionFactory factory = new SingleConnectionFactory(
connectionMock, metadata, false);
factory.create().as(StepVerifier::create).expectNextCount(1).verifyComplete();
ConnectionFactoryUtils.releaseConnection(otherConnection, factory)
.as(StepVerifier::create)
.verifyComplete();
verify(otherConnection).close();
}
}

View File

@@ -0,0 +1,155 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection;
import java.util.concurrent.atomic.AtomicReference;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Wrapped;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.transaction.reactive.TransactionalOperator;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.times;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.verifyNoInteractions;
import static org.mockito.BDDMockito.when;
/**
* Unit tests for {@link TransactionAwareConnectionFactoryProxy}.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
public class TransactionAwareConnectionFactoryProxyUnitTests {
ConnectionFactory connectionFactoryMock = mock(ConnectionFactory.class);
Connection connectionMock1 = mock(Connection.class);
Connection connectionMock2 = mock(Connection.class);
Connection connectionMock3 = mock(Connection.class);
R2dbcTransactionManager tm;
@BeforeEach
public void before() {
when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock1),
(Mono) Mono.just(connectionMock2), (Mono) Mono.just(connectionMock3));
tm = new R2dbcTransactionManager(connectionFactoryMock);
}
@Test
public void createShouldWrapConnection() {
new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create()
.as(StepVerifier::create)
.consumeNextWith(connection -> assertThat(connection).isInstanceOf(Wrapped.class))
.verifyComplete();
}
@Test
public void unwrapShouldReturnTargetConnection() {
new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create()
.map(Wrapped.class::cast).as(StepVerifier::create)
.consumeNextWith(wrapped -> assertThat(wrapped.unwrap()).isEqualTo(connectionMock1))
.verifyComplete();
}
@Test
public void unwrapShouldReturnTargetConnectionEvenWhenClosed() {
when(connectionMock1.close()).thenReturn(Mono.empty());
new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create()
.map(Connection.class::cast).flatMap(
connection -> Mono.from(connection.close()).then(Mono.just(connection))).as(
StepVerifier::create)
.consumeNextWith(wrapped -> assertThat(((Wrapped<?>) wrapped).unwrap()).isEqualTo(connectionMock1))
.verifyComplete();
}
@Test
public void getTargetConnectionShouldReturnTargetConnection() {
new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create()
.map(Wrapped.class::cast).as(StepVerifier::create)
.consumeNextWith(wrapped -> assertThat(wrapped.unwrap()).isEqualTo(connectionMock1))
.verifyComplete();
}
@Test
public void getMetadataShouldThrowsErrorEvenWhenClosed() {
when(connectionMock1.close()).thenReturn(Mono.empty());
new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create()
.map(Connection.class::cast).flatMap(
connection -> Mono.from(connection.close())
.then(Mono.just(connection))).as(StepVerifier::create)
.consumeNextWith(connection -> assertThatIllegalStateException().isThrownBy(
connection::getMetadata)).verifyComplete();
}
@Test
public void hashCodeShouldReturnProxyHash() {
new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create()
.map(Connection.class::cast).as(StepVerifier::create)
.consumeNextWith(connection -> assertThat(connection.hashCode()).isEqualTo(
System.identityHashCode(connection))).verifyComplete();
}
@Test
public void equalsShouldCompareCorrectly() {
new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create()
.map(Connection.class::cast).as(StepVerifier::create)
.consumeNextWith(connection -> {
assertThat(connection.equals(connection)).isTrue();
assertThat(connection.equals(connectionMock1)).isFalse();
}).verifyComplete();
}
@Test
public void shouldEmitBoundConnection() {
when(connectionMock1.beginTransaction()).thenReturn(Mono.empty());
when(connectionMock1.commitTransaction()).thenReturn(Mono.empty());
when(connectionMock1.close()).thenReturn(Mono.empty());
TransactionalOperator rxtx = TransactionalOperator.create(tm);
AtomicReference<Connection> transactionalConnection = new AtomicReference<>();
TransactionAwareConnectionFactoryProxy proxyCf = new TransactionAwareConnectionFactoryProxy(
connectionFactoryMock);
ConnectionFactoryUtils.getConnection(connectionFactoryMock)
.doOnNext(transactionalConnection::set).flatMap(connection -> proxyCf.create()
.doOnNext(wrappedConnection -> assertThat(((Wrapped<?>) wrappedConnection).unwrap()).isSameAs(connection)))
.as(rxtx::transactional)
.flatMapMany(Connection::close)
.as(StepVerifier::create)
.verifyComplete();
verifyNoInteractions(connectionMock2);
verifyNoInteractions(connectionMock3);
verify(connectionFactoryMock, times(1)).create();
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.init;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
import org.springframework.core.io.ClassRelativeResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.r2dbc.core.DatabaseClient;
/**
* Abstract test support for {@link DatabasePopulator}.
*
* @author Mark Paluch
*/
public abstract class AbstractDatabaseInitializationTests {
ClassRelativeResourceLoader resourceLoader = new ClassRelativeResourceLoader(
getClass());
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
@Test
public void scriptWithSingleLineCommentsAndFailedDrop() {
databasePopulator.addScript(resource("db-schema-failed-drop-comments.sql"));
databasePopulator.addScript(resource("db-test-data.sql"));
databasePopulator.setIgnoreFailedDrops(true);
runPopulator();
assertUsersDatabaseCreated("Heisenberg");
}
private void runPopulator() {
databasePopulator.populate(getConnectionFactory()) //
.as(StepVerifier::create) //
.verifyComplete();
}
@Test
public void scriptWithStandardEscapedLiteral() {
databasePopulator.addScript(defaultSchema());
databasePopulator.addScript(resource("db-test-data-escaped-literal.sql"));
runPopulator();
assertUsersDatabaseCreated("'Heisenberg'");
}
@Test
public void scriptWithMySqlEscapedLiteral() {
databasePopulator.addScript(defaultSchema());
databasePopulator.addScript(resource("db-test-data-mysql-escaped-literal.sql"));
runPopulator();
assertUsersDatabaseCreated("\\$Heisenberg\\$");
}
@Test
public void scriptWithMultipleStatements() {
databasePopulator.addScript(defaultSchema());
databasePopulator.addScript(resource("db-test-data-multiple.sql"));
runPopulator();
assertUsersDatabaseCreated("Heisenberg", "Jesse");
}
@Test
public void scriptWithMultipleStatementsAndLongSeparator() {
databasePopulator.addScript(defaultSchema());
databasePopulator.addScript(resource("db-test-data-endings.sql"));
databasePopulator.setSeparator("@@");
runPopulator();
assertUsersDatabaseCreated("Heisenberg", "Jesse");
}
abstract ConnectionFactory getConnectionFactory();
Resource resource(String path) {
return resourceLoader.getResource(path);
}
Resource defaultSchema() {
return resource("db-schema.sql");
}
Resource usersSchema() {
return resource("users-schema.sql");
}
void assertUsersDatabaseCreated(String... lastNames) {
assertUsersDatabaseCreated(getConnectionFactory(), lastNames);
}
void assertUsersDatabaseCreated(ConnectionFactory connectionFactory,
String... lastNames) {
DatabaseClient client = DatabaseClient.create(connectionFactory);
for (String lastName : lastNames) {
client.sql("select count(0) from users where last_name = :name") //
.bind("name", lastName) //
.map((row, metadata) -> row.get(0)) //
.first() //
.map(number -> ((Number) number).intValue()) //
.as(StepVerifier::create) //
.expectNext(1).as(
"Did not find user with last name [" + lastName + "].") //
.verifyComplete();
}
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.init;
import java.util.LinkedHashSet;
import java.util.Set;
import io.r2dbc.spi.Connection;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.times;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.when;
/**
* Unit tests for {@link CompositeDatabasePopulator}.
*
* @author Mark Paluch
*/
public class CompositeDatabasePopulatorTests {
Connection mockedConnection = mock(Connection.class);
DatabasePopulator mockedDatabasePopulator1 = mock(DatabasePopulator.class);
DatabasePopulator mockedDatabasePopulator2 = mock(DatabasePopulator.class);
@BeforeEach
public void before() {
when(mockedDatabasePopulator1.populate(mockedConnection)).thenReturn(
Mono.empty());
when(mockedDatabasePopulator2.populate(mockedConnection)).thenReturn(
Mono.empty());
}
@Test
public void addPopulators() {
CompositeDatabasePopulator populator = new CompositeDatabasePopulator();
populator.addPopulators(mockedDatabasePopulator1, mockedDatabasePopulator2);
populator.populate(mockedConnection).as(StepVerifier::create).verifyComplete();
verify(mockedDatabasePopulator1, times(1)).populate(mockedConnection);
verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection);
}
@Test
public void setPopulatorsWithMultiple() {
CompositeDatabasePopulator populator = new CompositeDatabasePopulator();
populator.setPopulators(mockedDatabasePopulator1, mockedDatabasePopulator2); // multiple
populator.populate(mockedConnection).as(StepVerifier::create).verifyComplete();
verify(mockedDatabasePopulator1, times(1)).populate(mockedConnection);
verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection);
}
@Test
public void setPopulatorsForOverride() {
CompositeDatabasePopulator populator = new CompositeDatabasePopulator();
populator.setPopulators(mockedDatabasePopulator1);
populator.setPopulators(mockedDatabasePopulator2); // override
populator.populate(mockedConnection).as(StepVerifier::create).verifyComplete();
verify(mockedDatabasePopulator1, times(0)).populate(mockedConnection);
verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection);
}
@Test
public void constructWithVarargs() {
CompositeDatabasePopulator populator = new CompositeDatabasePopulator(
mockedDatabasePopulator1, mockedDatabasePopulator2);
populator.populate(mockedConnection).as(StepVerifier::create).verifyComplete();
verify(mockedDatabasePopulator1, times(1)).populate(mockedConnection);
verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection);
}
@Test
public void constructWithCollection() {
Set<DatabasePopulator> populators = new LinkedHashSet<>();
populators.add(mockedDatabasePopulator1);
populators.add(mockedDatabasePopulator2);
CompositeDatabasePopulator populator = new CompositeDatabasePopulator(populators);
populator.populate(mockedConnection).as(StepVerifier::create).verifyComplete();
verify(mockedDatabasePopulator1, times(1)).populate(mockedConnection);
verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection);
}
}

View File

@@ -0,0 +1,76 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.init;
import java.util.concurrent.atomic.AtomicBoolean;
import io.r2dbc.spi.test.MockConnection;
import io.r2dbc.spi.test.MockConnectionFactory;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.when;
/**
* Unit tests for {@link ConnectionFactoryInitializer}.
*
* @author Mark Paluch
*/
public class ConnectionFactoryInitializerUnitTests {
AtomicBoolean called = new AtomicBoolean();
DatabasePopulator populator = mock(DatabasePopulator.class);
MockConnection connection = MockConnection.builder().build();
MockConnectionFactory connectionFactory = MockConnectionFactory.builder().connection(
connection).build();
@Test
public void shouldInitializeConnectionFactory() {
when(populator.populate(connectionFactory)).thenReturn(
Mono.<Void> empty().doOnSubscribe(subscription -> called.set(true)));
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
initializer.setConnectionFactory(connectionFactory);
initializer.setDatabasePopulator(populator);
initializer.afterPropertiesSet();
assertThat(called).isTrue();
}
@Test
public void shouldCleanConnectionFactory() {
when(populator.populate(connectionFactory)).thenReturn(
Mono.<Void> empty().doOnSubscribe(subscription -> called.set(true)));
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
initializer.setConnectionFactory(connectionFactory);
initializer.setDatabaseCleaner(populator);
initializer.afterPropertiesSet();
initializer.destroy();
assertThat(called).isTrue();
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.init;
import java.util.UUID;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test;
import reactor.test.StepVerifier;
/**
* Integration tests for {@link DatabasePopulator} using H2.
*
* @author Mark Paluch
*/
public class H2DatabasePopulatorIntegrationTests
extends AbstractDatabaseInitializationTests {
UUID databaseName = UUID.randomUUID();
ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:h2:mem:///"
+ databaseName + "?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
@Override
ConnectionFactory getConnectionFactory() {
return this.connectionFactory;
}
@Test
public void shouldRunScript() {
databasePopulator.addScript(usersSchema());
databasePopulator.addScript(resource("db-test-data-h2.sql"));
// Set statement separator to double newline so that ";" is not
// considered a statement separator within the source code of the
// aliased function 'REVERSE'.
databasePopulator.setSeparator("\n\n");
databasePopulator.populate(connectionFactory).as(
StepVerifier::create).verifyComplete();
assertUsersDatabaseCreated(connectionFactory, "White");
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.init;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.Resource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.BDDMockito.mock;
/**
* Unit tests for {@link ResourceDatabasePopulator}.
*
* @author Mark Paluch
*/
public class ResourceDatabasePopulatorUnitTests {
private static final Resource script1 = mock(Resource.class);
private static final Resource script2 = mock(Resource.class);
private static final Resource script3 = mock(Resource.class);
@Test
public void constructWithNullResource() {
assertThatIllegalArgumentException().isThrownBy(
() -> new ResourceDatabasePopulator((Resource) null));
}
@Test
public void constructWithNullResourceArray() {
assertThatIllegalArgumentException().isThrownBy(
() -> new ResourceDatabasePopulator((Resource[]) null));
}
@Test
public void constructWithResource() {
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(
script1);
assertThat(databasePopulator.scripts).hasSize(1);
}
@Test
public void constructWithMultipleResources() {
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(
script1, script2);
assertThat(databasePopulator.scripts).hasSize(2);
}
@Test
public void constructWithMultipleResourcesAndThenAddScript() {
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(
script1, script2);
assertThat(databasePopulator.scripts).hasSize(2);
databasePopulator.addScript(script3);
assertThat(databasePopulator.scripts).hasSize(3);
}
@Test
public void addScriptsWithNullResource() {
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
assertThatIllegalArgumentException().isThrownBy(
() -> databasePopulator.addScripts((Resource) null));
}
@Test
public void addScriptsWithNullResourceArray() {
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
assertThatIllegalArgumentException().isThrownBy(
() -> databasePopulator.addScripts((Resource[]) null));
}
@Test
public void setScriptsWithNullResource() {
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
assertThatIllegalArgumentException().isThrownBy(
() -> databasePopulator.setScripts((Resource) null));
}
@Test
public void setScriptsWithNullResourceArray() {
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
assertThatIllegalArgumentException().isThrownBy(
() -> databasePopulator.setScripts((Resource[]) null));
}
@Test
public void setScriptsAndThenAddScript() {
ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
assertThat(databasePopulator.scripts).isEmpty();
databasePopulator.setScripts(script1, script2);
assertThat(databasePopulator.scripts).hasSize(2);
databasePopulator.addScript(script3);
assertThat(databasePopulator.scripts).hasSize(3);
}
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.init;
import java.util.ArrayList;
import java.util.List;
import org.assertj.core.util.Strings;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.support.EncodedResource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ScriptUtils}.
*
* @author Thomas Risberg
* @author Sam Brannen
* @author Phillip Webb
* @author Chris Baldwin
* @author Nicolas Debeissat
* @author Mark Paluch
*/
public class ScriptUtilsUnitTests {
@Test
public void splitSqlScriptDelimitedWithSemicolon() {
String rawStatement1 = "insert into customer (id, name)\nvalues (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')";
String cleanedStatement1 = "insert into customer (id, name) values (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')";
String rawStatement2 = "insert into orders(id, order_date, customer_id)\nvalues (1, '2008-01-02', 2)";
String cleanedStatement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)";
String rawStatement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)";
String cleanedStatement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)";
String script = Strings.join(rawStatement1, rawStatement2, rawStatement3).with(
";");
List<String> statements = new ArrayList<>();
ScriptUtils.splitSqlScript(script, ";", statements);
assertThat(statements).hasSize(3).containsSequence(cleanedStatement1,
cleanedStatement2, cleanedStatement3);
}
@Test
public void splitSqlScriptDelimitedWithNewLine() {
String statement1 = "insert into customer (id, name) values (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')";
String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)";
String statement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)";
String script = Strings.join(statement1, statement2, statement3).with("\n");
List<String> statements = new ArrayList<>();
ScriptUtils.splitSqlScript(script, "\n", statements);
assertThat(statements).hasSize(3).containsSequence(statement1, statement2,
statement3);
}
@Test
public void splitSqlScriptDelimitedWithNewLineButDefaultDelimiterSpecified() {
String statement1 = "do something";
String statement2 = "do something else";
char delim = '\n';
String script = statement1 + delim + statement2 + delim;
List<String> statements = new ArrayList<>();
ScriptUtils.splitSqlScript(script, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR,
statements);
assertThat(statements).hasSize(1).contains(script.replace('\n', ' '));
}
@Test
public void splitScriptWithSingleQuotesNestedInsideDoubleQuotes() {
String statement1 = "select '1' as \"Dogbert's owner's\" from dual";
String statement2 = "select '2' as \"Dilbert's\" from dual";
char delim = ';';
String script = statement1 + delim + statement2 + delim;
List<String> statements = new ArrayList<>();
ScriptUtils.splitSqlScript(script, ';', statements);
assertThat(statements).hasSize(2).containsSequence(statement1, statement2);
}
@Test
public void readAndSplitScriptWithMultipleNewlinesAsSeparator() {
String script = readScript("db-test-data-multi-newline.sql");
List<String> statements = new ArrayList<>();
ScriptUtils.splitSqlScript(script, "\n\n", statements);
String statement1 = "insert into users (last_name) values ('Walter')";
String statement2 = "insert into users (last_name) values ('Jesse')";
assertThat(statements.size()).as("wrong number of statements").isEqualTo(2);
assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(
statement1);
assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(
statement2);
}
@Test
public void readAndSplitScriptContainingComments() {
String script = readScript("test-data-with-comments.sql");
splitScriptContainingComments(script);
}
@Test
public void readAndSplitScriptContainingCommentsWithWindowsLineEnding() {
String script = readScript("test-data-with-comments.sql").replaceAll("\n",
"\r\n");
splitScriptContainingComments(script);
}
private void splitScriptContainingComments(String script) {
List<String> statements = new ArrayList<>();
ScriptUtils.splitSqlScript(script, ';', statements);
String statement1 = "insert into customer (id, name) values (1, 'Rod; Johnson'), (2, 'Adrian Collier')";
String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)";
String statement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)";
String statement4 = "INSERT INTO persons( person_id , name) VALUES( 1 , 'Name' )";
assertThat(statements).hasSize(4).containsSequence(statement1, statement2,
statement3, statement4);
}
@Test
public void readAndSplitScriptContainingCommentsWithLeadingTabs() {
String script = readScript("test-data-with-comments-and-leading-tabs.sql");
List<String> statements = new ArrayList<>();
ScriptUtils.splitSqlScript(script, ';', statements);
String statement1 = "insert into customer (id, name) values (1, 'Walter White')";
String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2013-06-08', 1)";
String statement3 = "insert into orders(id, order_date, customer_id) values (2, '2013-06-08', 1)";
assertThat(statements).hasSize(3).containsSequence(statement1, statement2,
statement3);
}
@Test
public void readAndSplitScriptContainingMultiLineComments() {
String script = readScript("test-data-with-multi-line-comments.sql");
List<String> statements = new ArrayList<>();
ScriptUtils.splitSqlScript(script, ';', statements);
String statement1 = "INSERT INTO users(first_name, last_name) VALUES('Walter', 'White')";
String statement2 = "INSERT INTO users(first_name, last_name) VALUES( 'Jesse' , 'Pinkman' )";
assertThat(statements).hasSize(2).containsSequence(statement1, statement2);
}
@Test
public void readAndSplitScriptContainingMultiLineNestedComments() {
String script = readScript("test-data-with-multi-line-nested-comments.sql");
List<String> statements = new ArrayList<>();
ScriptUtils.splitSqlScript(script, ';', statements);
String statement1 = "INSERT INTO users(first_name, last_name) VALUES('Walter', 'White')";
String statement2 = "INSERT INTO users(first_name, last_name) VALUES( 'Jesse' , 'Pinkman' )";
assertThat(statements).hasSize(2).containsSequence(statement1, statement2);
}
@Test
public void containsDelimiters() {
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n select ';'",
";")).isFalse();
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1; select 2",
";")).isTrue();
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1; select '\\n\n';",
"\n")).isFalse();
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n select 2",
"\n")).isTrue();
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n select 2",
"\n\n")).isFalse();
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n\n select 2",
"\n\n")).isTrue();
// MySQL style escapes '\\'
assertThat(ScriptUtils.containsSqlScriptDelimiters(
"insert into users(first_name, last_name)\nvalues('a\\\\', 'b;')",
";")).isFalse();
assertThat(ScriptUtils.containsSqlScriptDelimiters(
"insert into users(first_name, last_name)\nvalues('Charles', 'd\\'Artagnan'); select 1;",
";")).isTrue();
}
private String readScript(String path) {
EncodedResource resource = new EncodedResource(
new ClassPathResource(path, getClass()));
return ScriptUtils.readScript(resource, new DefaultDataBufferFactory()).block();
}
}

View File

@@ -0,0 +1,195 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.lookup;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.context.Context;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link AbstractRoutingConnectionFactory}.
*
* @author Mark Paluch
* @author Jens Schauder
*/
@ExtendWith(MockitoExtension.class)
public class AbstractRoutingConnectionFactoryUnitTests {
private static final String ROUTING_KEY = "routingKey";
@Mock
ConnectionFactory defaultConnectionFactory;
@Mock
ConnectionFactory routedConnectionFactory;
DummyRoutingConnectionFactory connectionFactory;
@BeforeEach
public void before() {
connectionFactory = new DummyRoutingConnectionFactory();
connectionFactory.setDefaultTargetConnectionFactory(defaultConnectionFactory);
}
@Test
public void shouldDetermineRoutedFactory() {
connectionFactory.setTargetConnectionFactories(
singletonMap("key", routedConnectionFactory));
connectionFactory.setConnectionFactoryLookup(new MapConnectionFactoryLookup());
connectionFactory.afterPropertiesSet();
connectionFactory.determineTargetConnectionFactory()
.subscriberContext(Context.of(ROUTING_KEY, "key"))
.as(StepVerifier::create)
.expectNext(routedConnectionFactory)
.verifyComplete();
}
@Test
public void shouldFallbackToDefaultConnectionFactory() {
connectionFactory.setTargetConnectionFactories(
singletonMap("key", routedConnectionFactory));
connectionFactory.afterPropertiesSet();
connectionFactory.determineTargetConnectionFactory()
.as(StepVerifier::create)
.expectNext(defaultConnectionFactory)
.verifyComplete();
}
@Test
public void initializationShouldFailUnsupportedLookupKey() {
connectionFactory.setTargetConnectionFactories(singletonMap("key", new Object()));
assertThatThrownBy(() -> connectionFactory.afterPropertiesSet()).isInstanceOf(
IllegalArgumentException.class);
}
@Test
public void initializationShouldFailUnresolvableKey() {
connectionFactory.setTargetConnectionFactories(singletonMap("key", "value"));
connectionFactory.setConnectionFactoryLookup(new MapConnectionFactoryLookup());
assertThatThrownBy(() -> connectionFactory.afterPropertiesSet())
.isInstanceOf(ConnectionFactoryLookupFailureException.class)
.hasMessageContaining(
"No ConnectionFactory with name 'value' registered");
}
@Test
public void unresolvableConnectionFactoryRetrievalShouldFail() {
connectionFactory.setLenientFallback(false);
connectionFactory.setConnectionFactoryLookup(new MapConnectionFactoryLookup());
connectionFactory.setTargetConnectionFactories(
singletonMap("key", routedConnectionFactory));
connectionFactory.afterPropertiesSet();
connectionFactory.determineTargetConnectionFactory()
.subscriberContext(Context.of(ROUTING_KEY, "unknown"))
.as(StepVerifier::create)
.verifyError(IllegalStateException.class);
}
@Test
public void connectionFactoryRetrievalWithUnknownLookupKeyShouldReturnDefaultConnectionFactory() {
connectionFactory.setTargetConnectionFactories(
singletonMap("key", routedConnectionFactory));
connectionFactory.setDefaultTargetConnectionFactory(defaultConnectionFactory);
connectionFactory.afterPropertiesSet();
connectionFactory.determineTargetConnectionFactory()
.subscriberContext(Context.of(ROUTING_KEY, "unknown"))
.as(StepVerifier::create)
.expectNext(defaultConnectionFactory)
.verifyComplete();
}
@Test
public void connectionFactoryRetrievalWithoutLookupKeyShouldReturnDefaultConnectionFactory() {
connectionFactory.setTargetConnectionFactories(
singletonMap("key", routedConnectionFactory));
connectionFactory.setDefaultTargetConnectionFactory(defaultConnectionFactory);
connectionFactory.setLenientFallback(false);
connectionFactory.afterPropertiesSet();
connectionFactory.determineTargetConnectionFactory()
.as(StepVerifier::create)
.expectNext(defaultConnectionFactory)
.verifyComplete();
}
@Test
public void shouldLookupFromMap() {
MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup("lookup-key",
routedConnectionFactory);
connectionFactory.setConnectionFactoryLookup(lookup);
connectionFactory.setTargetConnectionFactories(
singletonMap("my-key", "lookup-key"));
connectionFactory.afterPropertiesSet();
connectionFactory.determineTargetConnectionFactory()
.subscriberContext(Context.of(ROUTING_KEY, "my-key"))
.as(StepVerifier::create)
.expectNext(routedConnectionFactory)
.verifyComplete();
}
@Test
public void shouldAllowModificationsAfterInitialization() {
MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup();
connectionFactory.setConnectionFactoryLookup(lookup);
connectionFactory.setTargetConnectionFactories(lookup.getConnectionFactories());
connectionFactory.afterPropertiesSet();
connectionFactory.determineTargetConnectionFactory()
.subscriberContext(Context.of(ROUTING_KEY, "lookup-key"))
.as(StepVerifier::create)
.expectNext(defaultConnectionFactory)
.verifyComplete();
lookup.addConnectionFactory("lookup-key", routedConnectionFactory);
connectionFactory.afterPropertiesSet();
connectionFactory.determineTargetConnectionFactory()
.subscriberContext(Context.of(ROUTING_KEY, "lookup-key"))
.as(StepVerifier::create)
.expectNext(routedConnectionFactory)
.verifyComplete();
}
static class DummyRoutingConnectionFactory extends AbstractRoutingConnectionFactory {
@Override
protected Mono<Object> determineCurrentLookupKey() {
return Mono.subscriberContext().filter(context -> context.hasKey(ROUTING_KEY))
.map(context -> context.get(ROUTING_KEY));
}
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.lookup;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanNotOfRequiredTypeException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.when;
/**
* Unit tests for {@link BeanFactoryConnectionFactoryLookup}.
*
* @author Mark Paluch
*/
@ExtendWith(MockitoExtension.class)
public class BeanFactoryConnectionFactoryLookupUnitTests {
private static final String CONNECTION_FACTORY_BEAN_NAME = "connectionFactory";
@Mock
BeanFactory beanFactory;
@Test
public void shouldLookupConnectionFactory() {
DummyConnectionFactory expectedConnectionFactory = new DummyConnectionFactory();
when(beanFactory.getBean(CONNECTION_FACTORY_BEAN_NAME,
ConnectionFactory.class)).thenReturn(expectedConnectionFactory);
BeanFactoryConnectionFactoryLookup lookup = new BeanFactoryConnectionFactoryLookup();
lookup.setBeanFactory(beanFactory);
ConnectionFactory connectionFactory = lookup.getConnectionFactory(
CONNECTION_FACTORY_BEAN_NAME);
assertThat(connectionFactory).isNotNull();
assertThat(connectionFactory).isSameAs(expectedConnectionFactory);
}
@Test
public void shouldLookupWhereBeanFactoryYieldsNonConnectionFactoryType() {
BeanFactory beanFactory = mock(BeanFactory.class);
when(beanFactory.getBean(CONNECTION_FACTORY_BEAN_NAME,
ConnectionFactory.class)).thenThrow(
new BeanNotOfRequiredTypeException(CONNECTION_FACTORY_BEAN_NAME,
ConnectionFactory.class, String.class));
BeanFactoryConnectionFactoryLookup lookup = new BeanFactoryConnectionFactoryLookup(
beanFactory);
assertThatExceptionOfType(
ConnectionFactoryLookupFailureException.class).isThrownBy(
() -> lookup.getConnectionFactory(CONNECTION_FACTORY_BEAN_NAME));
}
@Test
public void shouldLookupWhereBeanFactoryHasNotBeenSupplied() {
BeanFactoryConnectionFactoryLookup lookup = new BeanFactoryConnectionFactoryLookup();
assertThatThrownBy(() -> lookup.getConnectionFactory(
CONNECTION_FACTORY_BEAN_NAME)).isInstanceOf(IllegalStateException.class);
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.lookup;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryMetadata;
import org.reactivestreams.Publisher;
/**
* Stub, do-nothing {@link ConnectionFactory} implementation.
* <p>
* All methods throw {@link UnsupportedOperationException}.
*
* @author Mark Paluch
*/
class DummyConnectionFactory implements ConnectionFactory {
@Override
public Publisher<? extends Connection> create() {
throw new UnsupportedOperationException();
}
@Override
public ConnectionFactoryMetadata getMetadata() {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.connection.lookup;
import java.util.HashMap;
import java.util.Map;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Unit tests for {@link MapConnectionFactoryLookup}.
*
* @author Mark Paluch
*/
public class MapConnectionFactoryLookupUnitTests {
private static final String CONNECTION_FACTORY_NAME = "connectionFactory";
@Test
public void getConnectionFactorysReturnsUnmodifiableMap() {
MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup();
Map<String, ConnectionFactory> connectionFactories = lookup.getConnectionFactories();
assertThatThrownBy(() -> connectionFactories.put("",
new DummyConnectionFactory())).isInstanceOf(
UnsupportedOperationException.class);
}
@Test
public void shouldLookupConnectionFactory() {
Map<String, ConnectionFactory> connectionFactories = new HashMap<>();
DummyConnectionFactory expectedConnectionFactory = new DummyConnectionFactory();
connectionFactories.put(CONNECTION_FACTORY_NAME, expectedConnectionFactory);
MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup();
lookup.setConnectionFactories(connectionFactories);
ConnectionFactory connectionFactory = lookup.getConnectionFactory(
CONNECTION_FACTORY_NAME);
assertThat(connectionFactory).isNotNull().isSameAs(expectedConnectionFactory);
}
@Test
public void addingConnectionFactoryPermitsOverride() {
Map<String, ConnectionFactory> connectionFactories = new HashMap<>();
DummyConnectionFactory overriddenConnectionFactory = new DummyConnectionFactory();
DummyConnectionFactory expectedConnectionFactory = new DummyConnectionFactory();
connectionFactories.put(CONNECTION_FACTORY_NAME, overriddenConnectionFactory);
MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup();
lookup.setConnectionFactories(connectionFactories);
lookup.addConnectionFactory(CONNECTION_FACTORY_NAME, expectedConnectionFactory);
ConnectionFactory connectionFactory = lookup.getConnectionFactory(
CONNECTION_FACTORY_NAME);
assertThat(connectionFactory).isNotNull().isSameAs(expectedConnectionFactory);
}
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
public void getConnectionFactoryWhereSuppliedMapHasNonConnectionFactoryTypeUnderSpecifiedKey() {
Map connectionFactories = new HashMap<>();
connectionFactories.put(CONNECTION_FACTORY_NAME, new Object());
MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup(
connectionFactories);
assertThatThrownBy(
() -> lookup.getConnectionFactory(CONNECTION_FACTORY_NAME)).isInstanceOf(
ClassCastException.class);
}
@Test
public void getConnectionFactoryWhereSuppliedMapHasNoEntryForSpecifiedKey() {
MapConnectionFactoryLookup lookup = new MapConnectionFactoryLookup();
assertThatThrownBy(
() -> lookup.getConnectionFactory(CONNECTION_FACTORY_NAME)).isInstanceOf(
ConnectionFactoryLookupFailureException.class);
}
}

View File

@@ -0,0 +1,152 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.dao.DataIntegrityViolationException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link DatabaseClient}.
*
* @author Mark Paluch
* @author Mingyuan Wu
*/
public abstract class AbstractDatabaseClientIntegrationTests {
private ConnectionFactory connectionFactory;
@BeforeEach
public void before() {
connectionFactory = createConnectionFactory();
Mono.from(connectionFactory.create())
.flatMapMany(connection -> Flux.from(connection.createStatement("DROP TABLE legoset").execute())
.flatMap(Result::getRowsUpdated)
.onErrorResume(e -> Mono.empty())
.thenMany(connection.createStatement(getCreateTableStatement()).execute())
.flatMap(Result::getRowsUpdated).thenMany(connection.close())).as(StepVerifier::create)
.verifyComplete();
}
/**
* Creates a {@link ConnectionFactory} to be used in this test.
*
* @return the {@link ConnectionFactory} to be used in this test
*/
protected abstract ConnectionFactory createConnectionFactory();
/**
* Return the the CREATE TABLE statement for table {@code legoset} with the following
* three columns:
* <ul>
* <li>id integer (primary key), not null</li>
* <li>name varchar(255), nullable</li>
* <li>manual integer, nullable</li>
* </ul>
*
* @return the CREATE TABLE statement for table {@code legoset} with three columns.
*/
protected abstract String getCreateTableStatement();
@Test
public void executeInsert() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
databaseClient.sql("INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)")
.bind("id", 42055)
.bind("name", "SCHAUFELRADBAGGER")
.bindNull("manual", Integer.class)
.fetch().rowsUpdated()
.as(StepVerifier::create)
.expectNext(1)
.verifyComplete();
databaseClient.sql("SELECT id FROM legoset")
.map(row -> row.get("id"))
.first()
.as(StepVerifier::create)
.assertNext(actual -> {
assertThat(actual).isInstanceOf(Number.class);
assertThat(((Number) actual).intValue()).isEqualTo(42055);
}).verifyComplete();
}
@Test
public void shouldTranslateDuplicateKeyException() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
executeInsert();
databaseClient.sql(
"INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)")
.bind("id", 42055)
.bind("name", "SCHAUFELRADBAGGER")
.bindNull("manual", Integer.class)
.fetch().rowsUpdated()
.as(StepVerifier::create)
.expectErrorSatisfies(exception -> assertThat(exception)
.isInstanceOf(DataIntegrityViolationException.class)
.hasMessageContaining("execute; SQL [INSERT INTO legoset"))
.verify();
}
@Test
public void executeDeferred() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
databaseClient.sql(() -> "INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)")
.bind("id", 42055)
.bind("name", "SCHAUFELRADBAGGER")
.bindNull("manual", Integer.class)
.fetch().rowsUpdated()
.as(StepVerifier::create)
.expectNext(1)
.verifyComplete();
databaseClient.sql("SELECT id FROM legoset")
.map(row -> row.get("id")).first()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
public void shouldEmitGeneratedKey() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
databaseClient.sql(
"INSERT INTO legoset ( name, manual) VALUES(:name, :manual)")
.bind("name","SCHAUFELRADBAGGER")
.bindNull("manual", Integer.class)
.filter(statement -> statement.returnGeneratedValues("id"))
.map(row -> (Number) row.get("id"))
.first()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}

View File

@@ -0,0 +1,208 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Result;
import org.assertj.core.api.Condition;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.r2dbc.connection.R2dbcTransactionManager;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.reactive.TransactionalOperator;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Abstract base class for transactional integration tests for {@link DatabaseClient}.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
public abstract class AbstractTransactionalDatabaseClientIntegrationTests {
private ConnectionFactory connectionFactory;
AnnotationConfigApplicationContext context;
DatabaseClient databaseClient;
R2dbcTransactionManager transactionManager;
TransactionalOperator rxtx;
@BeforeEach
public void before() {
connectionFactory = createConnectionFactory();
context = new AnnotationConfigApplicationContext();
context.getBeanFactory().registerResolvableDependency(ConnectionFactory.class, connectionFactory);
context.register(Config.class);
context.refresh();
Mono.from(connectionFactory.create())
.flatMapMany(connection -> Flux.from(connection.createStatement("DROP TABLE legoset").execute())
.flatMap(Result::getRowsUpdated)
.onErrorResume(e -> Mono.empty())
.thenMany(connection.createStatement(getCreateTableStatement()).execute())
.flatMap(Result::getRowsUpdated).thenMany(connection.close())).as(StepVerifier::create).verifyComplete();
databaseClient = DatabaseClient.create(connectionFactory);
transactionManager = new R2dbcTransactionManager(connectionFactory);
rxtx = TransactionalOperator.create(transactionManager);
}
@AfterEach
public void tearDown() {
context.close();
}
/**
* Create a {@link ConnectionFactory} to be used in this test.
* @return the {@link ConnectionFactory} to be used in this test.
*/
protected abstract ConnectionFactory createConnectionFactory();
/**
* Return the the CREATE TABLE statement for table {@code legoset} with the following three columns:
* <ul>
* <li>id integer (primary key), not null</li>
* <li>name varchar(255), nullable</li>
* <li>manual integer, nullable</li>
* </ul>
*
* @return the CREATE TABLE statement for table {@code legoset} with three columns.
*/
protected abstract String getCreateTableStatement();
/**
* Get a parameterized {@code INSERT INTO legoset} statement setting id, name, and manual values.
*/
protected String getInsertIntoLegosetStatement() {
return "INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)";
}
@Test
public void executeInsertInTransaction() {
Flux<Integer> integerFlux = databaseClient
.sql(getInsertIntoLegosetStatement())
.bind(0, 42055)
.bind(1, "SCHAUFELRADBAGGER")
.bindNull(2, Integer.class)
.fetch().rowsUpdated().flux().as(rxtx::transactional);
integerFlux.as(StepVerifier::create)
.expectNext(1)
.verifyComplete();
databaseClient
.sql("SELECT id FROM legoset")
.fetch()
.first()
.as(StepVerifier::create)
.assertNext(actual -> assertThat(actual).hasEntrySatisfying("id", numberOf(42055)))
.verifyComplete();
}
@Test
public void shouldRollbackTransaction() {
Mono<Object> integerFlux = databaseClient.sql(getInsertIntoLegosetStatement())
.bind(0, 42055)
.bind(1, "SCHAUFELRADBAGGER")
.bindNull(2, Integer.class)
.fetch().rowsUpdated()
.then(Mono.error(new IllegalStateException("failed")))
.as(rxtx::transactional);
integerFlux.as(StepVerifier::create)
.expectError(IllegalStateException.class)
.verify();
databaseClient
.sql("SELECT id FROM legoset")
.fetch()
.first()
.as(StepVerifier::create)
.verifyComplete();
}
@Test
public void shouldRollbackTransactionUsingTransactionalOperator() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
TransactionalOperator transactionalOperator = TransactionalOperator
.create(new R2dbcTransactionManager(connectionFactory), new DefaultTransactionDefinition());
Flux<Integer> integerFlux = databaseClient.sql(getInsertIntoLegosetStatement())
.bind(0, 42055)
.bind(1, "SCHAUFELRADBAGGER")
.bindNull(2, Integer.class)
.fetch().rowsUpdated()
.thenMany(Mono.fromSupplier(() -> {
throw new IllegalStateException("failed");
}));
integerFlux.as(transactionalOperator::transactional)
.as(StepVerifier::create)
.expectError(IllegalStateException.class)
.verify();
databaseClient
.sql("SELECT id FROM legoset")
.fetch()
.first()
.as(StepVerifier::create)
.verifyComplete();
}
private Condition<? super Object> numberOf(int expected) {
return new Condition<>(object -> object instanceof Number &&
((Number) object).intValue() == expected, "Number %d", expected);
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Autowired GenericApplicationContext context;
@Bean
ReactiveTransactionManager txMgr(ConnectionFactory connectionFactory) {
return new R2dbcTransactionManager(connectionFactory);
}
@Bean
TransactionalOperator transactionalOperator(ReactiveTransactionManager transactionManager) {
return TransactionalOperator.create(transactionManager);
}
}
}

View File

@@ -0,0 +1,435 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core;
import java.util.Arrays;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Result;
import io.r2dbc.spi.Statement;
import io.r2dbc.spi.test.MockColumnMetadata;
import io.r2dbc.spi.test.MockResult;
import io.r2dbc.spi.test.MockRow;
import io.r2dbc.spi.test.MockRowMetadata;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscription;
import reactor.core.CoreSubscriber;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.lang.Nullable;
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
import org.springframework.r2dbc.core.binding.BindTarget;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.doReturn;
import static org.mockito.BDDMockito.inOrder;
import static org.mockito.BDDMockito.mock;
import static org.mockito.BDDMockito.times;
import static org.mockito.BDDMockito.verify;
import static org.mockito.BDDMockito.verifyNoInteractions;
import static org.mockito.BDDMockito.verifyNoMoreInteractions;
import static org.mockito.BDDMockito.when;
/**
* Unit tests for {@link DefaultDatabaseClient}.
*
* @author Mark Paluch
* @author Ferdinand Jacobs
* @author Jens Schauder
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class DefaultDatabaseClientUnitTests {
@Mock
Connection connection;
private DatabaseClient.Builder databaseClientBuilder;
@BeforeEach
public void before() {
ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
when(connectionFactory.create()).thenReturn((Publisher) Mono.just(connection));
when(connection.close()).thenReturn(Mono.empty());
databaseClientBuilder = DatabaseClient.builder().connectionFactory(
connectionFactory).bindMarkers(BindMarkersFactory.indexed("$", 1));
}
@Test
public void shouldCloseConnectionOnlyOnce() {
DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) databaseClientBuilder.build();
Flux<Object> flux = databaseClient.inConnectionMany(connection -> Flux.empty());
flux.subscribe(new CoreSubscriber<Object>() {
Subscription subscription;
@Override
public void onSubscribe(Subscription s) {
s.request(1);
subscription = s;
}
@Override
public void onNext(Object o) {
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
subscription.cancel();
}
});
verify(connection, times(1)).close();
}
@Test
public void executeShouldBindNullValues() {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.namedParameters(false).build();
databaseClient.sql("SELECT * FROM table WHERE key = $1").bindNull(0,
String.class).then().as(StepVerifier::create).verifyComplete();
verify(statement).bindNull(0, String.class);
databaseClient.sql("SELECT * FROM table WHERE key = $1").bindNull("$1",
String.class).then().as(StepVerifier::create).verifyComplete();
verify(statement).bindNull("$1", String.class);
}
@Test
public void executeShouldBindSettableValues() {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.namedParameters(false).build();
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind(0,
Parameter.empty(String.class)).then().as(
StepVerifier::create).verifyComplete();
verify(statement).bindNull(0, String.class);
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind("$1",
Parameter.empty(String.class)).then().as(
StepVerifier::create).verifyComplete();
verify(statement).bindNull("$1", String.class);
}
@Test
public void executeShouldBindNamedNullValues() {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM table WHERE key = :key").bindNull("key",
String.class).then().as(StepVerifier::create).verifyComplete();
verify(statement).bindNull(0, String.class);
}
@Test
public void executeShouldBindNamedValuesFromIndexes() {
Statement statement = mockStatementFor(
"SELECT id, name, manual FROM legoset WHERE name IN ($1, $2, $3)");
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql(
"SELECT id, name, manual FROM legoset WHERE name IN (:name)").bind(0,
Arrays.asList("unknown", "dunno", "other")).then().as(
StepVerifier::create).verifyComplete();
verify(statement).bind(0, "unknown");
verify(statement).bind(1, "dunno");
verify(statement).bind(2, "other");
verify(statement).execute();
verifyNoMoreInteractions(statement);
}
@Test
public void executeShouldBindValues() {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind(0,
Parameter.from("foo")).then().as(StepVerifier::create).verifyComplete();
verify(statement).bind(0, "foo");
databaseClient.sql("SELECT * FROM table WHERE key = $1").bind("$1",
"foo").then().as(StepVerifier::create).verifyComplete();
verify(statement).bind("$1", "foo");
}
@Test
public void executeShouldBindNamedValuesByIndex() {
Statement statement = mockStatementFor("SELECT * FROM table WHERE key = $1");
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM table WHERE key = :key").bind("key",
"foo").then().as(StepVerifier::create).verifyComplete();
verify(statement).bind(0, "foo");
}
@Test
public void rowsUpdatedShouldEmitSingleValue() {
Result result = mock(Result.class);
when(result.getRowsUpdated()).thenReturn(Mono.empty(), Mono.just(2),
Flux.just(1, 2, 3));
mockStatementFor("DROP TABLE tab;", result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("DROP TABLE tab;").fetch().rowsUpdated().as(
StepVerifier::create).expectNextCount(1).verifyComplete();
databaseClient.sql("DROP TABLE tab;").fetch().rowsUpdated().as(
StepVerifier::create).expectNextCount(1).verifyComplete();
databaseClient.sql("DROP TABLE tab;").fetch().rowsUpdated().as(
StepVerifier::create).expectNextCount(1).verifyComplete();
}
@Test
public void selectShouldEmitFirstValue() {
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(
MockColumnMetadata.builder().name("name").build()).build();
MockResult.Builder resultBuilder = MockResult.builder().rowMetadata(metadata);
MockResult result = resultBuilder.row(MockRow.builder().identified(0, Object.class, "Walter").build())
.row(MockRow.builder().identified(0, Object.class, "White").build()).build();
mockStatementFor("SELECT * FROM person", result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM person").map(row -> row.get(0))
.first()
.as(StepVerifier::create)
.expectNext("Walter")
.verifyComplete();
}
@Test
public void selectShouldEmitAllValues() {
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(
MockColumnMetadata.builder().name("name").build()).build();
MockResult.Builder resultBuilder = MockResult.builder().rowMetadata(metadata);
MockResult result = resultBuilder.row(MockRow.builder().identified(0, Object.class, "Walter").build())
.row(MockRow.builder().identified(0, Object.class, "White").build()).build();
mockStatementFor("SELECT * FROM person", result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM person").map(row -> row.get(0))
.all()
.as(StepVerifier::create)
.expectNext("Walter")
.expectNext("White")
.verifyComplete();
}
@Test
public void selectOneShouldFailWithException() {
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(
MockColumnMetadata.builder().name("name").build()).build();
MockResult.Builder resultBuilder = MockResult.builder().rowMetadata(metadata);
MockResult result = resultBuilder.row(MockRow.builder().identified(0, Object.class, "Walter").build())
.row(MockRow.builder().identified(0, Object.class, "White").build()).build();
mockStatementFor("SELECT * FROM person", result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT * FROM person").map(row -> row.get(0))
.one()
.as(StepVerifier::create)
.verifyError(IncorrectResultSizeDataAccessException.class);
}
@Test
public void shouldApplyExecuteFunction() {
Statement statement = mockStatement();
MockResult result = mockSingleColumnResult(
MockRow.builder().identified(0, Object.class, "Walter"));
DatabaseClient databaseClient = databaseClientBuilder.executeFunction(
stmnt -> Mono.just(result)).build();
databaseClient.sql("SELECT").fetch().all().as(
StepVerifier::create).expectNextCount(1).verifyComplete();
verifyNoInteractions(statement);
}
@Test
public void shouldApplyPreparedOperation() {
MockResult result = mockSingleColumnResult(
MockRow.builder().identified(0, Object.class, "Walter"));
Statement statement = mockStatementFor("SELECT * FROM person", result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql(new PreparedOperation<String>() {
@Override
public String toQuery() {
return "SELECT * FROM person";
}
@Override
public String getSource() {
return "SELECT";
}
@Override
public void bindTo(BindTarget target) {
target.bind("index", "value");
}
}).fetch().all().as(
StepVerifier::create).expectNextCount(1).verifyComplete();
verify(statement).bind("index", "value");
}
@Test
public void shouldApplyStatementFilterFunctions() {
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(
MockColumnMetadata.builder().name("name").build()).build();
MockResult result = MockResult.builder().rowMetadata(metadata).build();
Statement statement = mockStatement(result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT").filter(
(s, next) -> next.execute(s.returnGeneratedValues("foo"))).filter(
(s, next) -> next.execute(
s.returnGeneratedValues("bar"))).fetch().all().as(
StepVerifier::create).verifyComplete();
InOrder inOrder = inOrder(statement);
inOrder.verify(statement).returnGeneratedValues("foo");
inOrder.verify(statement).returnGeneratedValues("bar");
inOrder.verify(statement).execute();
inOrder.verifyNoMoreInteractions();
}
@Test
public void shouldApplySimpleStatementFilterFunctions() {
MockResult result = mockSingleColumnEmptyResult();
Statement statement = mockStatement(result);
DatabaseClient databaseClient = databaseClientBuilder.build();
databaseClient.sql("SELECT").filter(
s -> s.returnGeneratedValues("foo")).filter(
s -> s.returnGeneratedValues("bar")).fetch().all().as(
StepVerifier::create).verifyComplete();
InOrder inOrder = inOrder(statement);
inOrder.verify(statement).returnGeneratedValues("foo");
inOrder.verify(statement).returnGeneratedValues("bar");
inOrder.verify(statement).execute();
inOrder.verifyNoMoreInteractions();
}
private Statement mockStatement() {
return mockStatementFor(null, null);
}
private Statement mockStatement(Result result) {
return mockStatementFor(null, result);
}
private Statement mockStatementFor(String sql) {
return mockStatementFor(sql, null);
}
private Statement mockStatementFor(@Nullable String sql, @Nullable Result result) {
Statement statement = mock(Statement.class);
when(connection.createStatement(sql == null ? anyString() : eq(sql))).thenReturn(
statement);
when(statement.returnGeneratedValues(anyString())).thenReturn(statement);
when(statement.returnGeneratedValues()).thenReturn(statement);
doReturn(result == null ? Mono.empty() : Flux.just(result)).when(
statement).execute();
return statement;
}
private MockResult mockSingleColumnEmptyResult() {
return mockSingleColumnResult(null);
}
/**
* Mocks a {@link Result} with a single column "name" and a single row if a non null
* row is provided.
*/
private MockResult mockSingleColumnResult(@Nullable MockRow.Builder row) {
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(
MockColumnMetadata.builder().name("name").build()).build();
MockResult.Builder resultBuilder = MockResult.builder().rowMetadata(metadata);
if (row != null) {
resultBuilder = resultBuilder.row(row.build());
}
return resultBuilder.build();
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core;
import io.r2dbc.h2.H2ConnectionFactory;
import io.r2dbc.spi.ConnectionFactory;
/**
* Integration tests for {@link DatabaseClient} against H2.
*
* @author Mark Paluch
*/
public class H2DatabaseClientIntegrationTests
extends AbstractDatabaseClientIntegrationTests {
public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" //
+ " id serial CONSTRAINT id PRIMARY KEY,\n" //
+ " version integer NULL,\n" //
+ " name varchar(255) NOT NULL,\n" //
+ " manual integer NULL\n" //
+ ");";
@Override
protected ConnectionFactory createConnectionFactory() {
return H2ConnectionFactory.inMemory("r2dbc-test");
}
@Override
protected String getCreateTableStatement() {
return CREATE_TABLE_LEGOSET;
}
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core;
import io.r2dbc.h2.H2ConnectionFactory;
import io.r2dbc.spi.ConnectionFactory;
/**
* Integration tests for {@link DatabaseClient} against H2.
*
* @author Mark Paluch
*/
public class H2TransactionalDatabaseClientIntegrationTests
extends AbstractTransactionalDatabaseClientIntegrationTests {
public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" //
+ " id integer CONSTRAINT id PRIMARY KEY,\n" //
+ " version integer NULL,\n" //
+ " name varchar(255) NOT NULL,\n" //
+ " manual integer NULL\n" //
+ ");";
@Override
protected ConnectionFactory createConnectionFactory() {
return H2ConnectionFactory.inMemory("r2dbc-transactional");
}
@Override
protected String getCreateTableStatement() {
return CREATE_TABLE_LEGOSET;
}
}

View File

@@ -0,0 +1,462 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
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;
import static org.mockito.Mockito.verify;
/**
* Unit tests for {@link NamedParameterUtils}.
*
* @author Mark Paluch
* @author Jens Schauder
*/
public class NamedParameterUtilsUnitTests {
private final BindMarkersFactory BIND_MARKERS = BindMarkersFactory.indexed("$", 1);
@Test
public void shouldParseSql() {
String sql = "xxx :a yyyy :b :c :a zzzzz";
ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(psql.getParameterNames()).containsExactly("a", "b", "c", "a");
assertThat(psql.getTotalParameterCount()).isEqualTo(4);
assertThat(psql.getNamedParameterCount()).isEqualTo(3);
String sql2 = "xxx &a yyyy ? zzzzz";
ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2);
assertThat(psql2.getParameterNames()).containsExactly("a");
assertThat(psql2.getTotalParameterCount()).isEqualTo(1);
assertThat(psql2.getNamedParameterCount()).isEqualTo(1);
String sql3 = "xxx &ä+:ö" + '\t' + ":ü%10 yyyy ? zzzzz";
ParsedSql psql3 = NamedParameterUtils.parseSqlStatement(sql3);
assertThat(psql3.getParameterNames()).containsExactly("ä", "ö", "ü");
}
@Test
public void substituteNamedParameters() {
MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
namedParams.addValue("a", "a").addValue("b", "b").addValue("c", "c");
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
"xxx :a :b :c", BIND_MARKERS, namedParams);
assertThat(operation.toQuery()).isEqualTo("xxx $1 $2 $3");
PreparedOperation<?> operation2 = NamedParameterUtils.substituteNamedParameters(
"xxx :a :b :c", BindMarkersFactory.named("@", "P", 8), namedParams);
assertThat(operation2.toQuery()).isEqualTo("xxx @P0a @P1b @P2c");
}
@Test
public void substituteObjectArray() {
MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
namedParams.addValue("a", Arrays.asList(new Object[] { "Walter", "Heisenberg" },
new Object[] { "Walt Jr.", "Flynn" }));
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
"xxx :a", BIND_MARKERS, namedParams);
assertThat(operation.toQuery()).isEqualTo("xxx ($1, $2), ($3, $4)");
}
@Test
public void shouldBindObjectArray() {
MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
namedParams.addValue("a", Arrays.asList(new Object[] { "Walter", "Heisenberg" },
new Object[] { "Walt Jr.", "Flynn" }));
BindTarget bindTarget = mock(BindTarget.class);
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
"xxx :a", BIND_MARKERS, namedParams);
operation.bindTo(bindTarget);
verify(bindTarget).bind(0, "Walter");
verify(bindTarget).bind(1, "Heisenberg");
verify(bindTarget).bind(2, "Walt Jr.");
verify(bindTarget).bind(3, "Flynn");
}
@Test
public void parseSqlContainingComments() {
String sql1 = "/*+ HINT */ xxx /* comment ? */ :a yyyy :b :c :a zzzzz -- :xx XX\n";
ParsedSql psql1 = NamedParameterUtils.parseSqlStatement(sql1);
assertThat(expand(psql1)).isEqualTo(
"/*+ HINT */ xxx /* comment ? */ $1 yyyy $2 $3 $1 zzzzz -- :xx XX\n");
MapBindParameterSource paramMap = new MapBindParameterSource(new HashMap<>());
paramMap.addValue("a", "a");
paramMap.addValue("b", "b");
paramMap.addValue("c", "c");
String sql2 = "/*+ HINT */ xxx /* comment ? */ :a yyyy :b :c :a zzzzz -- :xx XX";
ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2);
assertThat(expand(psql2)).isEqualTo(
"/*+ HINT */ xxx /* comment ? */ $1 yyyy $2 $3 $1 zzzzz -- :xx XX");
}
@Test
public void parseSqlStatementWithPostgresCasting() {
String expectedSql = "select 'first name' from artists where id = $1 and birth_date=$2::timestamp";
String sql = "select 'first name' from artists where id = :id and birth_date=:birthDate::timestamp";
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
parsedSql, BIND_MARKERS, new MapBindParameterSource());
assertThat(operation.toQuery()).isEqualTo(expectedSql);
}
@Test
public void parseSqlStatementWithPostgresContainedOperator() {
String expectedSql = "select 'first name' from artists where info->'stat'->'albums' = ?? $1 and '[\"1\",\"2\",\"3\"]'::jsonb ?? '4'";
String sql = "select 'first name' from artists where info->'stat'->'albums' = ?? :album and '[\"1\",\"2\",\"3\"]'::jsonb ?? '4'";
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1);
assertThat(expand(parsedSql)).isEqualTo(expectedSql);
}
@Test
public void parseSqlStatementWithPostgresAnyArrayStringsExistsOperator() {
String expectedSql = "select '[\"3\", \"11\"]'::jsonb ?| '{1,3,11,12,17}'::text[]";
String sql = "select '[\"3\", \"11\"]'::jsonb ?| '{1,3,11,12,17}'::text[]";
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(parsedSql.getTotalParameterCount()).isEqualTo(0);
assertThat(expand(parsedSql)).isEqualTo(expectedSql);
}
@Test
public void parseSqlStatementWithPostgresAllArrayStringsExistsOperator() {
String expectedSql = "select '[\"3\", \"11\"]'::jsonb ?& '{1,3,11,12,17}'::text[] AND $1 = 'Back in Black'";
String sql = "select '[\"3\", \"11\"]'::jsonb ?& '{1,3,11,12,17}'::text[] AND :album = 'Back in Black'";
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1);
assertThat(expand(parsedSql)).isEqualTo(expectedSql);
}
@Test
public void parseSqlStatementWithEscapedColon() {
String expectedSql = "select '0\\:0' as a, foo from bar where baz < DATE($1 23:59:59) and baz = $2";
String sql = "select '0\\:0' as a, foo from bar where baz < DATE(:p1 23\\:59\\:59) and baz = :p2";
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(parsedSql.getParameterNames()).containsExactly("p1", "p2");
assertThat(expand(parsedSql)).isEqualTo(expectedSql);
}
@Test
public void parseSqlStatementWithBracketDelimitedParameterNames() {
String expectedSql = "select foo from bar where baz = b$1$2z";
String sql = "select foo from bar where baz = b:{p1}:{p2}z";
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(parsedSql.getParameterNames()).containsExactly("p1", "p2");
assertThat(expand(parsedSql)).isEqualTo(expectedSql);
}
@Test
public void parseSqlStatementWithEmptyBracketsOrBracketsInQuotes() {
String expectedSql = "select foo from bar where baz = b:{}z";
String sql = "select foo from bar where baz = b:{}z";
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(parsedSql.getParameterNames()).isEmpty();
assertThat(expand(parsedSql)).isEqualTo(expectedSql);
String expectedSql2 = "select foo from bar where baz = 'b:{p1}z'";
String sql2 = "select foo from bar where baz = 'b:{p1}z'";
ParsedSql parsedSql2 = NamedParameterUtils.parseSqlStatement(sql2);
assertThat(parsedSql2.getParameterNames()).isEmpty();
assertThat(expand(parsedSql2)).isEqualTo(expectedSql2);
}
@Test
public void parseSqlStatementWithSingleLetterInBrackets() {
String expectedSql = "select foo from bar where baz = b$1z";
String sql = "select foo from bar where baz = b:{p}z";
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(parsedSql.getParameterNames()).containsExactly("p");
assertThat(expand(parsedSql)).isEqualTo(expectedSql);
}
@Test
public void parseSqlStatementWithLogicalAnd() {
String expectedSql = "xxx & yyyy";
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(expectedSql);
assertThat(expand(parsedSql)).isEqualTo(expectedSql);
}
@Test
public void substituteNamedParametersWithLogicalAnd() {
String expectedSql = "xxx & yyyy";
assertThat(expand(expectedSql)).isEqualTo(expectedSql);
}
@Test
public void variableAssignmentOperator() {
String expectedSql = "x := 1";
assertThat(expand(expectedSql)).isEqualTo(expectedSql);
}
@Test
public void parseSqlStatementWithQuotedSingleQuote() {
String sql = "SELECT ':foo'':doo', :xxx FROM DUAL";
ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(psql.getTotalParameterCount()).isEqualTo(1);
assertThat(psql.getParameterNames()).containsExactly("xxx");
}
@Test
public void parseSqlStatementWithQuotesAndCommentBefore() {
String sql = "SELECT /*:doo*/':foo', :xxx FROM DUAL";
ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
assertThat(psql.getTotalParameterCount()).isEqualTo(1);
assertThat(psql.getParameterNames()).containsExactly("xxx");
}
@Test
public void parseSqlStatementWithQuotesAndCommentAfter() {
String sql2 = "SELECT ':foo'/*:doo*/, :xxx FROM DUAL";
ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2);
assertThat(psql2.getTotalParameterCount()).isEqualTo(1);
assertThat(psql2.getParameterNames()).containsExactly("xxx");
}
@Test
public void shouldAllowParsingMultipleUseOfParameter() {
String sql = "SELECT * FROM person where name = :id or lastname = :id";
ParsedSql parsed = NamedParameterUtils.parseSqlStatement(sql);
assertThat(parsed.getTotalParameterCount()).isEqualTo(2);
assertThat(parsed.getNamedParameterCount()).isEqualTo(1);
assertThat(parsed.getParameterNames()).containsExactly("id", "id");
}
@Test
public void multipleEqualParameterReferencesBindsValueOnce() {
String sql = "SELECT * FROM person where name = :id or lastname = :id";
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(
Collections.singletonMap("id", Parameter.from("foo"))));
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name = $0 or lastname = $0");
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
assertThat(index).isEqualTo(0);
assertThat(value).isEqualTo("foo");
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
}
@Test
public void multipleEqualCollectionParameterReferencesBindsValueOnce() {
String sql = "SELECT * FROM person where name IN (:ids) or lastname IN (:ids)";
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
MultiValueMap<Integer, Object> bindings = new LinkedMultiValueMap<>();
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(Collections.singletonMap("ids",
Parameter.from(Arrays.asList("foo", "bar", "baz")))));
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name IN ($0, $1, $2) or lastname IN ($0, $1, $2)");
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
assertThat(index).isIn(0, 1, 2);
assertThat(value).isIn("foo", "bar", "baz");
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();
}
});
assertThat(bindings).containsEntry(0, Collections.singletonList("foo")) //
.containsEntry(1, Collections.singletonList("bar")) //
.containsEntry(2, Collections.singletonList("baz"));
}
@Test
public void multipleEqualParameterReferencesForAnonymousMarkersBindsValueMultipleTimes() {
String sql = "SELECT * FROM person where name = :id or lastname = :id";
BindMarkersFactory factory = BindMarkersFactory.anonymous("?");
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(
Collections.singletonMap("id", Parameter.from("foo"))));
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name = ? or lastname = ?");
Map<Integer, Object> bindValues = new LinkedHashMap<>();
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
bindValues.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(bindValues).hasSize(2).containsEntry(0, "foo").containsEntry(1, "foo");
}
@Test
public void multipleEqualParameterReferencesBindsNullOnce() {
String sql = "SELECT * FROM person where name = :id or lastname = :id";
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(
Collections.singletonMap("id", Parameter.empty(String.class))));
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name = $0 or lastname = $0");
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
assertThat(index).isEqualTo(0);
assertThat(type).isEqualTo(String.class);
}
});
}
private String expand(ParsedSql sql) {
return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS,
new MapBindParameterSource()).toQuery();
}
private String expand(String sql) {
return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS,
new MapBindParameterSource()).toQuery();
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core.binding;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Unit tests for {@link AnonymousBindMarkers}.
*
* @author Mark Paluch
*/
class AnonymousBindMarkersUnitTests {
@Test
public void shouldCreateNewBindMarkers() {
BindMarkersFactory factory = BindMarkersFactory.anonymous("?");
BindMarkers bindMarkers1 = factory.create();
BindMarkers bindMarkers2 = factory.create();
assertThat(bindMarkers1.next().getPlaceholder()).isEqualTo("?");
assertThat(bindMarkers2.next().getPlaceholder()).isEqualTo("?");
}
@Test
public void shouldBindByIndex() {
BindTarget bindTarget = mock(BindTarget.class);
BindMarkers bindMarkers = BindMarkersFactory.anonymous("?").create();
BindMarker first = bindMarkers.next();
BindMarker second = bindMarkers.next();
second.bind(bindTarget, "foo");
first.bindNull(bindTarget, Object.class);
verify(bindTarget).bindNull(0, Object.class);
verify(bindTarget).bind(1, "foo");
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core.binding;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Unit tests for {@link Bindings}.
*
* @author Mark Paluch
*/
class BindingsUnitTests {
BindMarkersFactory markersFactory = BindMarkersFactory.indexed("$", 1);
BindTarget bindTarget = mock(BindTarget.class);
@Test
void shouldCreateBindings() {
MutableBindings bindings = new MutableBindings(markersFactory.create());
bindings.bind(bindings.nextMarker(), "foo");
bindings.bindNull(bindings.nextMarker(), String.class);
assertThat(bindings).hasSize(2);
}
@Test
void shouldApplyValueBinding() {
MutableBindings bindings = new MutableBindings(markersFactory.create());
bindings.bind(bindings.nextMarker(), "foo");
bindings.apply(bindTarget);
verify(bindTarget).bind(0, "foo");
}
@Test
void shouldApplySimpleValueBinding() {
MutableBindings bindings = new MutableBindings(markersFactory.create());
BindMarker marker = bindings.bind("foo");
bindings.apply(bindTarget);
assertThat(marker.getPlaceholder()).isEqualTo("$1");
verify(bindTarget).bind(0, "foo");
}
@Test
void shouldApplyNullBinding() {
MutableBindings bindings = new MutableBindings(markersFactory.create());
bindings.bindNull(bindings.nextMarker(), String.class);
bindings.apply(bindTarget);
verify(bindTarget).bindNull(0, String.class);
}
@Test
void shouldApplySimpleNullBinding() {
MutableBindings bindings = new MutableBindings(markersFactory.create());
BindMarker marker = bindings.bindNull(String.class);
bindings.apply(bindTarget);
assertThat(marker.getPlaceholder()).isEqualTo("$1");
verify(bindTarget).bindNull(0, String.class);
}
@Test
void shouldConsumeBindings() {
MutableBindings bindings = new MutableBindings(markersFactory.create());
bindings.bind(bindings.nextMarker(), "foo");
bindings.bindNull(bindings.nextMarker(), String.class);
AtomicInteger counter = new AtomicInteger();
bindings.forEach(binding -> {
if (binding.hasValue()) {
counter.incrementAndGet();
assertThat(binding.getValue()).isEqualTo("foo");
assertThat(binding.getBindMarker().getPlaceholder()).isEqualTo("$1");
}
if (binding.isNull()) {
counter.incrementAndGet();
assertThat(((Bindings.NullBinding) binding).getValueType()).isEqualTo(String.class);
assertThat(binding.getBindMarker().getPlaceholder()).isEqualTo("$2");
}
});
assertThat(counter).hasValue(2);
}
@Test
void shouldMergeBindings() {
BindMarkers markers = markersFactory.create();
BindMarker shared = markers.next();
BindMarker leftMarker = markers.next();
List<Bindings.Binding> left = new ArrayList<>();
left.add(new Bindings.NullBinding(shared, String.class));
left.add(new Bindings.ValueBinding(leftMarker, "left"));
BindMarker rightMarker = markers.next();
List<Bindings.Binding> right = new ArrayList<>();
left.add(new Bindings.ValueBinding(shared, "override"));
left.add(new Bindings.ValueBinding(rightMarker, "right"));
Bindings merged = Bindings.merge(new Bindings(left), new Bindings(right));
assertThat(merged).hasSize(3);
merged.apply(bindTarget);
verify(bindTarget).bind(0, "override");
verify(bindTarget).bind(1, "left");
verify(bindTarget).bind(2, "right");
}
}

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core.binding;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Unit tests for {@link IndexedBindMarkers}.
*
* @author Mark Paluch
*/
class IndexedBindMarkersUnitTests {
@Test
void shouldCreateNewBindMarkers() {
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
BindMarkers bindMarkers1 = factory.create();
BindMarkers bindMarkers2 = factory.create();
assertThat(bindMarkers1.next().getPlaceholder()).isEqualTo("$0");
assertThat(bindMarkers2.next().getPlaceholder()).isEqualTo("$0");
}
@Test
void shouldCreateNewBindMarkersWithOffset() {
BindTarget bindTarget = mock(BindTarget.class);
BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 1).create();
BindMarker first = bindMarkers.next();
first.bind(bindTarget, "foo");
BindMarker second = bindMarkers.next();
second.bind(bindTarget, "bar");
assertThat(first.getPlaceholder()).isEqualTo("$1");
assertThat(second.getPlaceholder()).isEqualTo("$2");
verify(bindTarget).bind(0, "foo");
verify(bindTarget).bind(1, "bar");
}
@Test
void nextShouldIncrementBindMarker() {
String[] prefixes = { "$", "?" };
for (String prefix : prefixes) {
BindMarkers bindMarkers = BindMarkersFactory.indexed(prefix, 0).create();
BindMarker marker1 = bindMarkers.next();
BindMarker marker2 = bindMarkers.next();
assertThat(marker1.getPlaceholder()).isEqualTo(prefix + "0");
assertThat(marker2.getPlaceholder()).isEqualTo(prefix + "1");
}
}
@Test
void bindValueShouldBindByIndex() {
BindTarget bindTarget = mock(BindTarget.class);
BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 0).create();
bindMarkers.next().bind(bindTarget, "foo");
bindMarkers.next().bind(bindTarget, "bar");
verify(bindTarget).bind(0, "foo");
verify(bindTarget).bind(1, "bar");
}
@Test
void bindNullShouldBindByIndex() {
BindTarget bindTarget = mock(BindTarget.class);
BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 0).create();
bindMarkers.next(); // ignore
bindMarkers.next().bindNull(bindTarget, Integer.class);
verify(bindTarget).bindNull(1, Integer.class);
}
}

View File

@@ -0,0 +1,115 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core.binding;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Unit tests for {@link NamedBindMarkers}.
*
* @author Mark Paluch
*/
class NamedBindMarkersUnitTests {
@Test
void shouldCreateNewBindMarkers() {
BindMarkersFactory factory = BindMarkersFactory.named("@", "p", 32);
BindMarkers bindMarkers1 = factory.create();
BindMarkers bindMarkers2 = factory.create();
assertThat(bindMarkers1.next().getPlaceholder()).isEqualTo("@p0");
assertThat(bindMarkers2.next().getPlaceholder()).isEqualTo("@p0");
}
@ParameterizedTest
@ValueSource(strings = { "$", "?" })
void nextShouldIncrementBindMarker(String prefix) {
BindMarkers bindMarkers = BindMarkersFactory.named(prefix, "p", 32).create();
BindMarker marker1 = bindMarkers.next();
BindMarker marker2 = bindMarkers.next();
assertThat(marker1.getPlaceholder()).isEqualTo(prefix + "p0");
assertThat(marker2.getPlaceholder()).isEqualTo(prefix + "p1");
}
@Test
void nextShouldConsiderNameHint() {
BindMarkers bindMarkers = BindMarkersFactory.named("@", "x", 32).create();
BindMarker marker1 = bindMarkers.next("foo1bar");
BindMarker marker2 = bindMarkers.next();
assertThat(marker1.getPlaceholder()).isEqualTo("@x0foo1bar");
assertThat(marker2.getPlaceholder()).isEqualTo("@x1");
}
@Test
void nextShouldConsiderFilteredNameHint() {
BindMarkers bindMarkers = BindMarkersFactory.named("@", "p", 32,
s -> s.chars().filter(Character::isAlphabetic).collect(StringBuilder::new,
StringBuilder::appendCodePoint, StringBuilder::append).toString()).create();
BindMarker marker1 = bindMarkers.next("foo1.bar?");
BindMarker marker2 = bindMarkers.next();
assertThat(marker1.getPlaceholder()).isEqualTo("@p0foobar");
assertThat(marker2.getPlaceholder()).isEqualTo("@p1");
}
@Test
void nextShouldConsiderNameLimit() {
BindMarkers bindMarkers = BindMarkersFactory.named("@", "p", 10).create();
BindMarker marker1 = bindMarkers.next("123456789");
assertThat(marker1.getPlaceholder()).isEqualTo("@p012345678");
}
@Test
void bindValueShouldBindByName() {
BindTarget bindTarget = mock(BindTarget.class);
BindMarkers bindMarkers = BindMarkersFactory.named("@", "p", 32).create();
bindMarkers.next().bind(bindTarget, "foo");
bindMarkers.next().bind(bindTarget, "bar");
verify(bindTarget).bind("p0", "foo");
verify(bindTarget).bind("p1", "bar");
}
@Test
void bindNullShouldBindByName() {
BindTarget bindTarget = mock(BindTarget.class);
BindMarkers bindMarkers = BindMarkersFactory.named("@", "p", 32).create();
bindMarkers.next(); // ignore
bindMarkers.next().bindNull(bindTarget, Integer.class);
verify(bindTarget).bindNull("p1", Integer.class);
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test
import reactor.core.publisher.Mono
/**
* Unit tests for [DatabaseClient] extensions.
*
* @author Sebastien Deleuze
* @author Jonas Bark
* @author Mark Paluch
*/
class DatabaseClientExtensionsTests {
@Test
fun bindByIndexShouldBindValue() {
val spec = mockk<DatabaseClient.GenericExecuteSpec>()
every { spec.bind(eq(0), any()) } returns spec
runBlocking {
spec.bind<String>(0, "foo")
}
verify {
spec.bind(0, Parameter.fromOrEmpty("foo", String::class.java))
}
}
@Test
fun bindByIndexShouldBindNull() {
val spec = mockk<DatabaseClient.GenericExecuteSpec>()
every { spec.bind(eq(0), any()) } returns spec
runBlocking {
spec.bind<String>(0, null)
}
verify {
spec.bind(0, Parameter.empty(String::class.java))
}
}
@Test
fun bindByNameShouldBindValue() {
val spec = mockk<DatabaseClient.GenericExecuteSpec>()
every { spec.bind(eq("field"), any()) } returns spec
runBlocking {
spec.bind<String>("field", "foo")
}
verify {
spec.bind("field", Parameter.fromOrEmpty("foo", String::class.java))
}
}
@Test
fun bindByNameShouldBindNull() {
val spec = mockk<DatabaseClient.GenericExecuteSpec>()
every { spec.bind(eq("field"), any()) } returns spec
runBlocking {
spec.bind<String>("field", null)
}
verify {
spec.bind("field", Parameter.empty(String::class.java))
}
}
@Test
fun genericExecuteSpecAwait() {
val spec = mockk<DatabaseClient.GenericExecuteSpec>()
every { spec.then() } returns Mono.empty()
runBlocking {
spec.await()
}
verify {
spec.then()
}
}
}

View File

@@ -0,0 +1,166 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType
import org.junit.jupiter.api.Test
import org.springframework.dao.EmptyResultDataAccessException
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
/**
* Unit tests for [RowsFetchSpec] extensions.
*
* @author Sebastien Deleuze
* @author Mark Paluch
*/
class RowsFetchSpecExtensionsTests {
@Test
fun awaitOneWithValue() {
val spec = mockk<RowsFetchSpec<String>>()
every { spec.one() } returns Mono.just("foo")
runBlocking {
assertThat(spec.awaitOne()).isEqualTo("foo")
}
verify {
spec.one()
}
}
@Test
fun awaitOneWithNull() {
val spec = mockk<RowsFetchSpec<String>>()
every { spec.one() } returns Mono.empty()
assertThatExceptionOfType(EmptyResultDataAccessException::class.java).isThrownBy {
runBlocking { spec.awaitOne() }
}
verify {
spec.one()
}
}
@Test
fun awaitOneOrNullWithValue() {
val spec = mockk<RowsFetchSpec<String>>()
every { spec.one() } returns Mono.just("foo")
runBlocking {
assertThat(spec.awaitOneOrNull()).isEqualTo("foo")
}
verify {
spec.one()
}
}
@Test
fun awaitOneOrNullWithNull() {
val spec = mockk<RowsFetchSpec<String>>()
every { spec.one() } returns Mono.empty()
runBlocking {
assertThat(spec.awaitOneOrNull()).isNull()
}
verify {
spec.one()
}
}
@Test
fun awaitFirstWithValue() {
val spec = mockk<RowsFetchSpec<String>>()
every { spec.first() } returns Mono.just("foo")
runBlocking {
assertThat(spec.awaitFirst()).isEqualTo("foo")
}
verify {
spec.first()
}
}
@Test
fun awaitFirstWithNull() {
val spec = mockk<RowsFetchSpec<String>>()
every { spec.first() } returns Mono.empty()
assertThatExceptionOfType(EmptyResultDataAccessException::class.java).isThrownBy {
runBlocking { spec.awaitFirst() }
}
verify {
spec.first()
}
}
@Test
fun awaitFirstOrNullWithValue() {
val spec = mockk<RowsFetchSpec<String>>()
every { spec.first() } returns Mono.just("foo")
runBlocking {
assertThat(spec.awaitFirstOrNull()).isEqualTo("foo")
}
verify {
spec.first()
}
}
@Test
fun awaitFirstOrNullWithNull() {
val spec = mockk<RowsFetchSpec<String>>()
every { spec.first() } returns Mono.empty()
runBlocking {
assertThat(spec.awaitFirstOrNull()).isNull()
}
verify {
spec.first()
}
}
@Test
@ExperimentalCoroutinesApi
fun allAsFlow() {
val spec = mockk<RowsFetchSpec<String>>()
every { spec.all() } returns Flux.just("foo", "bar", "baz")
runBlocking {
assertThat(spec.flow().toList()).contains("foo", "bar", "baz")
}
verify {
spec.all()
}
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2002-2020 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.r2dbc.core
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import reactor.core.publisher.Mono
/**
* Unit tests for [UpdatedRowsFetchSpec] extensions.
*
* @author Fred Montariol
*/
class UpdatedRowsFetchSpecExtensionsTests {
@Test
fun awaitRowsUpdatedWithValue() {
val spec = mockk<UpdatedRowsFetchSpec>()
every { spec.rowsUpdated() } returns Mono.just(42)
runBlocking {
assertThat(spec.awaitRowsUpdated()).isEqualTo(42)
}
verify {
spec.rowsUpdated()
}
}
}

View File

@@ -0,0 +1,5 @@
-- Failed DROP can be ignored if necessary
drop table users;
-- Create the test table
create table users (last_name varchar(50) not null);

View File

@@ -0,0 +1,3 @@
drop table users if exists;
create table users (last_name varchar(50) not null);

View File

@@ -0,0 +1,2 @@
insert into users (last_name) values ('Heisenberg')@@
insert into users (last_name) values ('Jesse')@@

View File

@@ -0,0 +1 @@
insert into users (last_name) values ('''Heisenberg''');

View File

@@ -0,0 +1 @@
INSERT INTO users(first_name, last_name) values('Walter', 'White');

View File

@@ -0,0 +1,5 @@
insert into users (last_name)
values ('Walter')
insert into users (last_name)
values ('Jesse')

View File

@@ -0,0 +1,2 @@
insert into users (last_name) values ('Heisenberg');
insert into users (last_name) values ('Jesse');

View File

@@ -0,0 +1 @@
insert into users (last_name) values ('\$Heisenberg\$');

View File

@@ -0,0 +1 @@
insert into users (last_name) values ('Heisenberg');

View File

@@ -0,0 +1,9 @@
-- The next comment line starts with a tab.
-- x, y, z...
insert into customer (id, name)
values (1, 'Walter White');
-- This is also a comment with a leading tab.
insert into orders(id, order_date, customer_id) values (1, '2013-06-08', 1);
-- This is also a comment with a leading tab, a space, and a tab.
insert into orders(id, order_date, customer_id) values (2, '2013-06-08', 1);

View File

@@ -0,0 +1,16 @@
-- The next comment line has no text after the '--' prefix.
--
-- The next comment line starts with a space.
-- x, y, z...
insert into customer (id, name)
values (1, 'Rod; Johnson'), (2, 'Adrian Collier');
-- This is also a comment.
insert into orders(id, order_date, customer_id)
values (1, '2008-01-02', 2);
insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2);
INSERT INTO persons( person_id--
, name)
VALUES( 1 -- person_id
, 'Name' --name
);--

View File

@@ -0,0 +1,17 @@
/* This is a multi line comment
* The next comment line has no text
* The next comment line starts with a space.
* x, y, z...
*/
INSERT INTO users(first_name, last_name) VALUES('Walter', 'White');
-- This is also a comment.
/*
* Let's add another comment
* that covers multiple lines
*/INSERT INTO
users(first_name, last_name)
VALUES( 'Jesse' -- first_name
, 'Pinkman' -- last_name
);--

View File

@@ -0,0 +1,23 @@
/* This is a multi line comment
* The next comment line has no text
* The next comment line starts with a space.
* x, y, z...
*/
INSERT INTO users(first_name, last_name) VALUES('Walter', 'White');
-- This is also a comment.
/*-------------------------------------------
-- A fancy multi-line comments that puts
-- single line comments inside of a multi-line
-- comment block.
Moreover, the block comment end delimiter
appears on a line that can potentially also
be a single-line comment if we weren't
already inside a multi-line comment run.
-------------------------------------------*/
INSERT INTO
users(first_name, last_name) -- This is a single line comment containing the block-end-comment sequence here */ but it's still a single-line comment
VALUES( 'Jesse' -- first_name
, 'Pinkman' -- last_name
);--

View File

@@ -0,0 +1,3 @@
INSERT INTO
users(first_name, last_name)
values('Sam', 'Brannen');

View File

@@ -0,0 +1,7 @@
DROP TABLE users IF EXISTS;
CREATE TABLE users (
id INTEGER NOT NULL IDENTITY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL
);