diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..90ffd65
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,411 @@
+
+
+
+ 4.0.0
+
+ org.springframework.data
+ spring-data-jdbc
+ 1.1.0.r2dbc-SNAPSHOT
+
+ Spring Data JDBC
+ Spring Data module for JDBC repositories.
+ http://projects.spring.io/spring-data-jdbc
+
+
+ org.springframework.data.build
+ spring-data-parent
+ 2.2.0.BUILD-SNAPSHOT
+
+
+
+
+ DATAJDBC
+
+ 2.2.0.BUILD-SNAPSHOT
+ spring.data.jdbc
+ reuseReports
+
+ 3.6.2
+ 0.1.4
+ 2.2.8
+ 3.4.6
+ 1.3.2
+ 5.1.41
+ 42.0.0
+ 2.2.3
+ 1.0.0.BUILD-SNAPSHOT
+ 1.0.0.BUILD-SNAPSHOT
+ 1.7.3
+
+
+
+ 2017
+
+
+
+ schauder
+ Jens Schauder
+ jschauder(at)pivotal.io
+ Pivotal Software, Inc.
+ https://pivotal.io
+
+ Project Lead
+
+ +1
+
+
+ gregturn
+ Greg L. Turnquist
+ gturnquist(at)pivotal.io
+ Pivotal Software, Inc.
+ https://pivotal.io
+
+ Project Contributor
+
+ -6
+
+
+
+
+
+ release
+
+
+
+ org.jfrog.buildinfo
+ artifactory-maven-plugin
+ false
+
+
+
+
+
+
+ no-jacoco
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+ jacoco-initialize
+ none
+
+
+
+
+
+
+
+
+
+ all-dbs
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ mysql-test
+ test
+
+ test
+
+
+
+ **/*IntegrationTests.java
+
+
+ **/*HsqlIntegrationTests.java
+
+
+ mysql
+
+
+
+
+ postgres-test
+ test
+
+ test
+
+
+
+ **/*IntegrationTests.java
+
+
+ **/*HsqlIntegrationTests.java
+
+
+ postgres
+
+
+
+
+ mariadb-test
+ test
+
+ test
+
+
+
+ **/*IntegrationTests.java
+
+
+ **/*HsqlIntegrationTests.java
+
+
+ mariadb
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${project.groupId}
+ spring-data-commons
+ ${springdata.commons}
+
+
+
+ org.springframework
+ spring-tx
+
+
+
+ org.springframework
+ spring-context
+
+
+
+ org.springframework
+ spring-beans
+
+
+
+ org.springframework
+ spring-jdbc
+
+
+
+ org.springframework
+ spring-core
+
+
+
+ io.r2dbc
+ r2dbc-spi
+ ${r2dbc-spi.version}
+ true
+
+
+
+ io.projectreactor
+ reactor-core
+ true
+
+
+
+ org.mybatis
+ mybatis-spring
+ ${mybatis-spring.version}
+ true
+
+
+
+ org.mybatis
+ mybatis
+ ${mybatis.version}
+ true
+
+
+
+ org.hsqldb
+ hsqldb
+ ${hsqldb.version}
+ test
+
+
+
+ org.assertj
+ assertj-core
+ ${assertj-core.version}
+ test
+
+
+
+ io.projectreactor
+ reactor-test
+ test
+
+
+
+ mysql
+ mysql-connector-java
+ ${mysql-connector-java.version}
+ test
+
+
+
+ org.postgresql
+ postgresql
+ ${postgresql.version}
+ test
+
+
+
+ org.mariadb.jdbc
+ mariadb-java-client
+ ${mariadb-java-client.version}
+ test
+
+
+
+ io.r2dbc
+ r2dbc-postgresql
+ ${r2dbc-postgresql.version}
+ test
+
+
+
+ de.schauderhaft.degraph
+ degraph-check
+ ${degraph-check.version}
+ test
+
+
+
+ org.testcontainers
+ mysql
+ ${testcontainers.version}
+ test
+
+
+ org.slf4j
+ jcl-over-slf4j
+
+
+
+
+
+ org.testcontainers
+ postgresql
+ ${testcontainers.version}
+ test
+
+
+
+ org.testcontainers
+ mariadb
+ ${testcontainers.version}
+ test
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.12
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.1.0
+
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ ${jacoco}
+
+ ${jacoco.destfile}
+
+
+
+ jacoco-initialize
+
+ prepare-agent
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ default-test
+
+
+ **/*Tests.java
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+ org.codehaus.mojo
+ wagon-maven-plugin
+
+
+ org.asciidoctor
+ asciidoctor-maven-plugin
+
+
+
+
+
+
+ spring-libs-snapshot
+ https://repo.spring.io/libs-snapshot
+
+
+ nebhale-snapshots
+ https://raw.githubusercontent.com/nebhale/r2dbc/maven/snapshot
+
+ true
+
+
+
+ nebhale-milestones
+ https://raw.githubusercontent.com/nebhale/r2dbc/maven/milestone
+
+ false
+
+
+
+
+
+
+ spring-plugins-snapshot
+ https://repo.spring.io/plugins-snapshot
+
+
+
+
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java
new file mode 100644
index 0000000..663d88f
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import io.r2dbc.spi.ColumnMetadata;
+import io.r2dbc.spi.Row;
+import io.r2dbc.spi.RowMetadata;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.LinkedCaseInsensitiveMap;
+
+/**
+ * {@link RowMapper} implementation that creates a {@link Map} for each row, representing all columns as key-value
+ * pairs: one entry for each column, with the column name as key.
+ *
+ * The {@link Map} implementation to use and the key to use for each column in the column Map can be customized through
+ * overriding {@link #createColumnMap} and {@link #getColumnKey}, respectively.
+ *
+ * Note: By default, {@link ColumnMapRowMapper} will try to build a linked {@link Map} with case-insensitive
+ * keys, to preserve column order as well as allow any casing to be used for column names. This requires Commons
+ * Collections on the classpath (which will be autodetected). Else, the fallback is a standard linked
+ * {@link java.util.HashMap}, which will still preserve column order but requires the application to specify the column
+ * names in the same casing as exposed by the driver.
+ *
+ * @author Mark Paluch
+ */
+public class ColumnMapRowMapper implements BiFunction> {
+
+ public final static ColumnMapRowMapper INSTANCE = new ColumnMapRowMapper();
+
+ @Override
+ public Map apply(Row row, RowMetadata rowMetadata) {
+
+ Collection extends ColumnMetadata> columns = IterableUtils.toCollection(rowMetadata.getColumnMetadatas());
+ int columnCount = columns.size();
+ Map mapOfColValues = createColumnMap(columnCount);
+
+ int index = 0;
+ for (ColumnMetadata column : columns) {
+
+ String key = getColumnKey(column.getName());
+ Object obj = getColumnValue(row, index++);
+ mapOfColValues.put(key, obj);
+ }
+ return mapOfColValues;
+ }
+
+ /**
+ * Create a {@link Map} instance to be used as column map.
+ *
+ * By default, a linked case-insensitive Map will be created.
+ *
+ * @param columnCount the column count, to be used as initial capacity for the Map.
+ * @return the new {@link Map} instance.
+ * @see LinkedCaseInsensitiveMap
+ */
+ protected Map createColumnMap(int columnCount) {
+ return new LinkedCaseInsensitiveMap<>(columnCount);
+ }
+
+ /**
+ * Determine the key to use for the given column in the column {@link Map}.
+ *
+ * @param columnName the column name as returned by the {@link Row}.
+ * @return the column key to use.
+ * @see ColumnMetadata#getName()
+ */
+ protected String getColumnKey(String columnName) {
+ return columnName;
+ }
+
+ /**
+ * Retrieve a R2DBC object value for the specified column.
+ *
+ * The default implementation uses the {@link Row#get(Object, Class)} method.
+ *
+ * @param row is the {@link Row} holding the data.
+ * @param index is the column index.
+ * @return the Object returned.
+ */
+ @Nullable
+ protected Object getColumnValue(Row row, int index) {
+ return row.get(index, Object.class);
+ }
+}
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java
new file mode 100644
index 0000000..5926592
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java
@@ -0,0 +1,451 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.reactivestreams.Publisher;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.jdbc.support.SQLExceptionTranslator;
+
+import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.Row;
+import io.r2dbc.spi.RowMetadata;
+
+/**
+ * A non-blocking, reactive client for performing database calls requests with Reactive Streams back pressure. Provides
+ * a higher level, common API over R2DBC client libraries.
+ *
+ * Use one of the static factory methods {@link #create(ConnectionFactory)} or obtain a {@link DatabaseClient#builder()}
+ * to create an instance.
+ *
+ * @author Mark Paluch
+ */
+public interface DatabaseClient {
+
+ /**
+ * Prepare an SQL call returning a result.
+ */
+ SqlSpec execute();
+
+ /**
+ * Prepare an SQL SELECT call.
+ */
+ SelectFromSpec select();
+
+ /**
+ * Prepare an SQL INSERT call.
+ */
+ InsertIntoSpec insert();
+
+ /**
+ * Return a builder to mutate properties of this database client.
+ */
+ DatabaseClient.Builder mutate();
+
+ // Static, factory methods
+
+ /**
+ * A variant of {@link #create()} that accepts a {@link io.r2dbc.spi.ConnectionFactory}
+ */
+ static DatabaseClient create(ConnectionFactory factory) {
+ return new DefaultDatabaseClientBuilder().connectionFactory(factory).build();
+ }
+
+ /**
+ * Obtain a {@code WebClient} builder.
+ */
+ static DatabaseClient.Builder builder() {
+ return new DefaultDatabaseClientBuilder();
+ }
+
+ /**
+ * A mutable builder for creating a {@link DatabaseClient}.
+ */
+ interface Builder {
+
+ /**
+ * Configures the {@link ConnectionFactory R2DBC connector}.
+ *
+ * @param factory must not be {@literal null}.
+ * @return {@code this} {@link Builder}.
+ */
+ Builder connectionFactory(ConnectionFactory factory);
+
+ /**
+ * Configures a {@link SQLExceptionTranslator}.
+ *
+ * @param exceptionTranslator must not be {@literal null}.
+ * @return {@code this} {@link Builder}.
+ */
+ Builder exceptionTranslator(SQLExceptionTranslator exceptionTranslator);
+
+ /**
+ * Configures a {@link ReactiveDataAccessStrategy}.
+ *
+ * @param accessStrategy must not be {@literal null}.
+ * @return {@code this} {@link Builder}.
+ */
+ Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy);
+
+ /**
+ * Configures a {@link Consumer} to configure this builder.
+ *
+ * @param builderConsumer must not be {@literal null}.
+ * @return {@code this} {@link Builder}.
+ */
+ Builder apply(Consumer builderConsumer);
+
+ /**
+ * Builder the {@link DatabaseClient} instance.
+ */
+ DatabaseClient build();
+ }
+
+ /**
+ * Contract for specifying a SQL call along with options leading to the exchange.
+ */
+ interface SqlSpec {
+
+ /**
+ * Specify a static {@code sql} string to execute.
+ *
+ * @param sql must not be {@literal null} or empty.
+ * @return a new {@link GenericExecuteSpec}.
+ */
+ GenericExecuteSpec sql(String sql);
+
+ /**
+ * Specify a static {@link Supplier SQL supplier} that provides SQL to execute.
+ *
+ * @param sqlSupplier must not be {@literal null}.
+ * @return a new {@link GenericExecuteSpec}.
+ */
+ GenericExecuteSpec sql(Supplier sqlSupplier);
+ }
+
+ /**
+ * Contract for specifying a SQL call along with options leading to the exchange.
+ */
+ interface GenericExecuteSpec extends BindSpec {
+
+ /**
+ * Define the target type the result should be mapped to.
+ * Skip this step if you are anyway fine with the default conversion.
+ *
+ * @param resultType must not be {@literal null}.
+ * @param result type.
+ */
+ TypedExecuteSpec as(Class resultType);
+
+ /**
+ * Perform the SQL call and retrieve the result.
+ */
+ FetchSpec> fetch();
+
+ /**
+ * Perform the SQL request and return a {@link SqlResult}.
+ *
+ * @return a {@code Mono} for the result
+ */
+ Mono>> exchange();
+ }
+
+ /**
+ * Contract for specifying a SQL call along with options leading to the exchange.
+ */
+ interface TypedExecuteSpec extends BindSpec> {
+
+ /**
+ * Define the target type the result should be mapped to.
+ * Skip this step if you are anyway fine with the default conversion.
+ *
+ * @param resultType must not be {@literal null}.
+ * @param result type.
+ */
+ TypedExecuteSpec as(Class resultType);
+
+ /**
+ * Perform the SQL call and retrieve the result.
+ */
+ FetchSpec fetch();
+
+ /**
+ * Perform the SQL request and return a {@link SqlResult}.
+ *
+ * @return a {@code Mono} for the result
+ */
+ Mono> exchange();
+ }
+
+ /**
+ * Contract for specifying {@code SELECT} options leading to the exchange.
+ */
+ interface SelectFromSpec {
+
+ GenericSelectSpec from(String table);
+
+ TypedSelectSpec from(Class table);
+ }
+
+ /**
+ * Contract for specifying {@code SELECT} options leading to the exchange.
+ */
+ interface InsertIntoSpec {
+
+ /**
+ * Specify the target {@literal table} to insert into.
+ *
+ * @param table must not be {@literal null} or empty.
+ * @return
+ */
+ GenericInsertSpec into(String table);
+
+ TypedInsertSpec into(Class table);
+ }
+
+ /**
+ * Contract for specifying {@code SELECT} options leading to the exchange.
+ */
+ interface GenericSelectSpec extends SelectSpec {
+
+ /**
+ * Define the target type the result should be mapped to.
+ * Skip this step if you are anyway fine with the default conversion.
+ *
+ * @param resultType must not be {@literal null}.
+ * @param result type.
+ */
+ TypedSelectSpec as(Class resultType);
+
+ /**
+ * Perform the SQL call and retrieve the result.
+ */
+ FetchSpec> fetch();
+
+ /**
+ * Perform the SQL request and return a {@link SqlResult}.
+ *
+ * @return a {@code Mono} for the result
+ */
+ Mono>> exchange();
+ }
+
+ /**
+ * Contract for specifying {@code SELECT} options leading to the exchange.
+ */
+ interface TypedSelectSpec extends SelectSpec> {
+
+ /**
+ * Define the target type the result should be mapped to.
+ * Skip this step if you are anyway fine with the default conversion.
+ *
+ * @param resultType must not be {@literal null}.
+ * @param result type.
+ */
+ TypedSelectSpec as(Class resultType);
+
+ /**
+ * Configure a result mapping {@link java.util.function.Function}.
+ *
+ * @param mappingFunction must not be {@literal null}.
+ * @param result type.
+ * @return
+ */
+ TypedSelectSpec extract(BiFunction mappingFunction);
+
+ /**
+ * Perform the SQL call and retrieve the result.
+ */
+ FetchSpec fetch();
+
+ /**
+ * Perform the SQL request and return a {@link SqlResult}.
+ *
+ * @return a {@code Mono} for the result
+ */
+ Mono> exchange();
+ }
+
+ /**
+ * Contract for specifying {@code SELECT} options leading to the exchange.
+ */
+ interface SelectSpec> {
+
+ S project(String... selectedFields);
+
+ S where(Object criteriaDefinition);
+
+ S orderBy(Sort sort);
+
+ S page(Pageable page);
+ }
+
+ /**
+ * Contract for specifying {@code INSERT} options leading to the exchange.
+ */
+ interface GenericInsertSpec extends InsertSpec {
+
+ /**
+ * Specify a field and non-{@literal null} value to insert.
+ *
+ * @param field must not be {@literal null} or empty.
+ * @param value must not be {@literal null}
+ */
+ GenericInsertSpec value(String field, Object value);
+
+ /**
+ * Specify a {@literal null} value to insert.
+ *
+ * @param field must not be {@literal null} or empty.
+ */
+ GenericInsertSpec nullValue(String field);
+ }
+
+ /**
+ * Contract for specifying {@code SELECT} options leading the exchange.
+ */
+ interface TypedInsertSpec {
+
+ /**
+ * Insert the given {@code objectToInsert}.
+ *
+ * @param objectToInsert
+ * @return
+ */
+ InsertSpec using(T objectToInsert);
+
+ /**
+ * Use the given {@code tableName} as insert target.
+ *
+ * @param tableName must not be {@literal null} or empty.
+ * @return
+ */
+ TypedInsertSpec table(String tableName);
+
+ /**
+ * Insert the given {@link Publisher} to insert one or more objects.
+ *
+ * @param objectToInsert
+ * @return
+ */
+ InsertSpec using(Publisher objectToInsert);
+ }
+
+ /**
+ * Contract for specifying {@code INSERT} options leading to the exchange.
+ */
+ interface InsertSpec {
+
+ /**
+ * Perform the SQL call.
+ */
+ Mono then();
+
+ /**
+ * Perform the SQL request and return a {@link SqlResult}.
+ *
+ * @return a {@code Mono} for the result
+ */
+ Mono>> exchange();
+ }
+
+ /**
+ * Contract for specifying parameter bindings.
+ */
+ interface BindSpec> {
+
+ /**
+ * Bind a non-{@literal null} value to a parameter identified by its {@code index}.
+ *
+ * @param index
+ * @param value must not be {@literal null}.
+ */
+ S bind(int index, Object value);
+
+ /**
+ * Bind a {@literal null} value to a parameter identified by its {@code index}.
+ *
+ * @param index
+ */
+ S bindNull(int index);
+
+ /**
+ * Bind a non-{@literal null} value to a parameter identified by its {@code name}.
+ *
+ * @param name must not be {@literal null} or empty.
+ * @param value must not be {@literal null}.
+ */
+ S bind(String name, Object value);
+
+ /**
+ * Bind a {@literal null} value to a parameter identified by its {@code name}.
+ *
+ * @param name must not be {@literal null} or empty.
+ */
+ S bindNull(String name);
+
+ /**
+ * Bind a bean according to Java {@link java.beans.BeanInfo Beans} using property names.
+ *
+ * @param bean must not be {@literal null}.
+ */
+ S bind(Object bean);
+ }
+
+ /**
+ * Contract for fetching results.
+ */
+ interface FetchSpec {
+
+ /**
+ * Get exactly zero or one result.
+ *
+ * @return {@link Mono#empty()} if no match found. Never {@literal null}.
+ * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
+ */
+ Mono one();
+
+ /**
+ * Get the first or no result.
+ *
+ * @return {@link Mono#empty()} if no match found. Never {@literal null}.
+ */
+ Mono first();
+
+ /**
+ * Get all matching elements.
+ *
+ * @return never {@literal null}.
+ */
+ Flux all();
+
+ /**
+ * Get the number of updated rows.
+ *
+ * @return {@link Mono} emitting the number of updated rows. Never {@literal null}.
+ */
+ Mono rowsUpdated();
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java
new file mode 100644
index 0000000..de640ca
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java
@@ -0,0 +1,793 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import io.r2dbc.spi.Connection;
+import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.Result;
+import io.r2dbc.spi.Row;
+import io.r2dbc.spi.RowMetadata;
+import io.r2dbc.spi.Statement;
+import lombok.RequiredArgsConstructor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.reactivestreams.Publisher;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.IncorrectResultSizeDataAccessException;
+import org.springframework.dao.UncategorizedDataAccessException;
+import org.springframework.data.jdbc.core.function.connectionfactory.ConnectionProxy;
+import org.springframework.data.util.Pair;
+import org.springframework.jdbc.core.SqlProvider;
+import org.springframework.jdbc.support.SQLExceptionTranslator;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Default implementation of {@link DatabaseClient}.
+ *
+ * @author Mark Paluch
+ */
+class DefaultDatabaseClient implements DatabaseClient {
+
+ /** Logger available to subclasses */
+ protected final Log logger = LogFactory.getLog(getClass());
+
+ private final ConnectionFactory connector;
+
+ private final SQLExceptionTranslator exceptionTranslator;
+
+ private final ReactiveDataAccessStrategy dataAccessStrategy;
+
+ private final DefaultDatabaseClientBuilder builder;
+
+ public DefaultDatabaseClient(ConnectionFactory connector, SQLExceptionTranslator exceptionTranslator,
+ ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) {
+
+ this.connector = connector;
+ this.exceptionTranslator = exceptionTranslator;
+ this.dataAccessStrategy = dataAccessStrategy;
+ this.builder = builder;
+ }
+
+ @Override
+ public Builder mutate() {
+ return builder;
+ }
+
+ @Override
+ public SqlSpec execute() {
+ return new DefaultSqlSpec();
+ }
+
+ @Override
+ public SelectFromSpec select() {
+ throw new UnsupportedOperationException("Implement me");
+ }
+
+ @Override
+ public InsertIntoSpec insert() {
+ return new DefaultInsertIntoSpec();
+ }
+
+ public Flux execute(Function> action) throws DataAccessException {
+
+ Assert.notNull(action, "Callback object must not be null");
+
+ Mono connectionMono = Mono.from(obtainConnectionFactory().create());
+ // Create close-suppressing Connection proxy, also preparing returned Statements.
+
+ return connectionMono.flatMapMany(connection -> {
+
+ Connection connectionToUse = createConnectionProxy(connection);
+
+ // TODO: Release connection
+ return doInConnection(action, connectionToUse);
+
+ }).onErrorMap(SQLException.class, ex -> {
+
+ String sql = getSql(action);
+ return translateException("ConnectionCallback", sql, ex);
+ });
+ }
+
+ /**
+ * Obtain the {@link ConnectionFactory} for actual use.
+ *
+ * @return the ConnectionFactory (never {@code null})
+ * @throws IllegalStateException in case of no DataSource set
+ */
+ protected ConnectionFactory obtainConnectionFactory() {
+ return connector;
+ }
+
+ /**
+ * Create a close-suppressing proxy for the given R2DBC Connection. Called by the {@code execute} method.
+ *
+ * @param con the R2DBC Connection to create a proxy for
+ * @return the Connection proxy
+ */
+ protected Connection createConnectionProxy(Connection con) {
+ return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(),
+ new Class>[] { ConnectionProxy.class }, new CloseSuppressingInvocationHandler(con));
+ }
+
+ /**
+ * Translate the given {@link SQLException} into a generic {@link DataAccessException}.
+ *
+ * @param task readable text describing the task being attempted
+ * @param sql SQL query or update that caused the problem (may be {@code null})
+ * @param ex the offending {@code SQLException}
+ * @return a DataAccessException wrapping the {@code SQLException} (never {@code null})
+ */
+ protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) {
+ DataAccessException dae = exceptionTranslator.translate(task, sql, ex);
+ return (dae != null ? dae : new UncategorizedSQLException(task, sql, ex));
+ }
+
+ static void doBind(Statement statement, Map> byName,
+ Map> byIndex) {
+
+ byIndex.forEach((i, o) -> {
+
+ if (o.isPresent()) {
+ o.ifPresent(v -> statement.bind(i, v));
+ } else {
+ statement.bindNull(i, 0); // TODO: What is type?
+ }
+ });
+
+ byName.forEach((name, o) -> {
+
+ if (o.isPresent()) {
+ o.ifPresent(v -> statement.bind(name, v));
+ } else {
+ statement.bindNull(name, 0); // TODO: What is type?
+ }
+ });
+
+ }
+
+ /**
+ * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SqlSpec} implementation.
+ */
+ private class DefaultSqlSpec implements SqlSpec {
+
+ @Override
+ public GenericExecuteSpec sql(String sql) {
+
+ Assert.hasText(sql, "SQL must not be null or empty!");
+ return sql(() -> sql);
+ }
+
+ @Override
+ public GenericExecuteSpec sql(Supplier sqlSupplier) {
+
+ Assert.notNull(sqlSupplier, "SQL Supplier must not be null!");
+
+ return new DefaultGenericExecuteSpec(sqlSupplier);
+ }
+ }
+
+ /**
+ * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation.
+ */
+ @RequiredArgsConstructor
+ private class GenericExecuteSpecSupport {
+
+ final Map> byIndex;
+ final Map> byName;
+ final Supplier sqlSupplier;
+
+ GenericExecuteSpecSupport(Supplier sqlSupplier) {
+
+ this.byIndex = Collections.emptyMap();
+ this.byName = Collections.emptyMap();
+ this.sqlSupplier = sqlSupplier;
+ }
+
+ protected String getSql() {
+
+ String sql = sqlSupplier.get();
+ Assert.state(sql != null, "SQL supplier returned null!");
+ return sql;
+ }
+
+ Mono> exchange(String sql, BiFunction mappingFunction) {
+
+ return execute(it -> {
+
+ Statement statement = it.createStatement(sql);
+ doBind(statement, byName, byIndex);
+
+ return Flux
+ .just((SqlResult) new DefaultSqlResult<>(sql, Flux.from(statement.add().execute()), mappingFunction));
+ }).next();
+ }
+
+ public GenericExecuteSpecSupport bind(int index, Object value) {
+
+ Map> byIndex = new LinkedHashMap<>(this.byIndex);
+ byIndex.put(index, Optional.of(value));
+
+ return createInstance(byIndex, this.byName, this.sqlSupplier);
+ }
+
+ public GenericExecuteSpecSupport bindNull(int index) {
+
+ Map> byIndex = new LinkedHashMap<>(this.byIndex);
+ byIndex.put(index, Optional.empty());
+
+ return createInstance(byIndex, this.byName, this.sqlSupplier);
+ }
+
+ public GenericExecuteSpecSupport bind(String name, Object value) {
+
+ Assert.hasText(name, "Parameter name must not be null or empty!");
+
+ Map> byName = new LinkedHashMap<>(this.byName);
+ byName.put(name, Optional.of(value));
+
+ return createInstance(this.byIndex, byName, this.sqlSupplier);
+ }
+
+ public GenericExecuteSpecSupport bindNull(String name) {
+
+ Assert.hasText(name, "Parameter name must not be null or empty!");
+
+ Map> byName = new LinkedHashMap<>(this.byName);
+ byName.put(name, Optional.empty());
+
+ return createInstance(this.byIndex, byName, this.sqlSupplier);
+ }
+
+ protected GenericExecuteSpecSupport createInstance(Map> byIndex,
+ Map> byName, Supplier sqlSupplier) {
+ return new GenericExecuteSpecSupport(byIndex, byName, sqlSupplier);
+ }
+
+ public GenericExecuteSpecSupport bind(Object bean) {
+
+ Assert.notNull(bean, "Bean must not be null!");
+
+ throw new UnsupportedOperationException("Implement me!");
+ }
+ }
+
+ /**
+ * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation.
+ */
+ private class DefaultGenericExecuteSpec extends GenericExecuteSpecSupport implements GenericExecuteSpec {
+
+ DefaultGenericExecuteSpec(Map> byIndex, Map> byName,
+ Supplier sqlSupplier) {
+ super(byIndex, byName, sqlSupplier);
+ }
+
+ DefaultGenericExecuteSpec(Supplier sqlSupplier) {
+ super(sqlSupplier);
+ }
+
+ @Override
+ public TypedExecuteSpec as(Class resultType) {
+
+ Assert.notNull(resultType, "Result type must not be null!");
+
+ return new DefaultTypedGenericExecuteSpec<>(this.byIndex, this.byName, this.sqlSupplier, resultType);
+ }
+
+ @Override
+ public FetchSpec> fetch() {
+
+ String sql = getSql();
+ return new DefaultFetchSpec<>(sql, exchange(sql, ColumnMapRowMapper.INSTANCE).flatMapMany(SqlResult::all),
+ exchange(sql, ColumnMapRowMapper.INSTANCE).flatMap(FetchSpec::rowsUpdated));
+ }
+
+ @Override
+ public Mono>> exchange() {
+ return exchange(getSql(), ColumnMapRowMapper.INSTANCE);
+ }
+
+ @Override
+ public DefaultGenericExecuteSpec bind(int index, Object value) {
+ return (DefaultGenericExecuteSpec) super.bind(index, value);
+ }
+
+ @Override
+ public DefaultGenericExecuteSpec bindNull(int index) {
+ return (DefaultGenericExecuteSpec) super.bindNull(index);
+ }
+
+ @Override
+ public DefaultGenericExecuteSpec bind(String name, Object value) {
+ return (DefaultGenericExecuteSpec) super.bind(name, value);
+ }
+
+ @Override
+ public DefaultGenericExecuteSpec bindNull(String name) {
+ return (DefaultGenericExecuteSpec) super.bindNull(name);
+ }
+
+ @Override
+ public DefaultGenericExecuteSpec bind(Object bean) {
+ return (DefaultGenericExecuteSpec) super.bind(bean);
+ }
+
+ @Override
+ protected GenericExecuteSpecSupport createInstance(Map> byIndex,
+ Map> byName, Supplier sqlSupplier) {
+ return new DefaultGenericExecuteSpec(byIndex, byName, sqlSupplier);
+ }
+ }
+
+ /**
+ * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation.
+ */
+ @SuppressWarnings("unchecked")
+ private class DefaultTypedGenericExecuteSpec extends GenericExecuteSpecSupport implements TypedExecuteSpec {
+
+ private final Class typeToRead;
+ private final BiFunction mappingFunction;
+
+ DefaultTypedGenericExecuteSpec(Map> byIndex, Map> byName,
+ Supplier sqlSupplier, Class typeToRead) {
+
+ super(byIndex, byName, sqlSupplier);
+
+ this.typeToRead = typeToRead;
+ this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead);
+ }
+
+ @Override
+ public TypedExecuteSpec as(Class resultType) {
+
+ Assert.notNull(resultType, "Result type must not be null!");
+
+ return new DefaultTypedGenericExecuteSpec<>(this.byIndex, this.byName, this.sqlSupplier, resultType);
+ }
+
+ @Override
+ public FetchSpec fetch() {
+ String sql = getSql();
+ return new DefaultFetchSpec<>(sql, exchange(sql, mappingFunction).flatMapMany(SqlResult::all),
+ exchange(sql, mappingFunction).flatMap(FetchSpec::rowsUpdated));
+ }
+
+ @Override
+ public Mono> exchange() {
+ return exchange(getSql(), mappingFunction);
+ }
+
+ @Override
+ public DefaultTypedGenericExecuteSpec bind(int index, Object value) {
+ return (DefaultTypedGenericExecuteSpec) super.bind(index, value);
+ }
+
+ @Override
+ public DefaultTypedGenericExecuteSpec bindNull(int index) {
+ return (DefaultTypedGenericExecuteSpec) super.bindNull(index);
+ }
+
+ @Override
+ public DefaultTypedGenericExecuteSpec bind(String name, Object value) {
+ return (DefaultTypedGenericExecuteSpec) super.bind(name, value);
+ }
+
+ @Override
+ public DefaultTypedGenericExecuteSpec bindNull(String name) {
+ return (DefaultTypedGenericExecuteSpec) super.bindNull(name);
+ }
+
+ @Override
+ public DefaultTypedGenericExecuteSpec bind(Object bean) {
+ return (DefaultTypedGenericExecuteSpec) super.bind(bean);
+ }
+
+ @Override
+ protected DefaultTypedGenericExecuteSpec createInstance(Map> byIndex,
+ Map> byName, Supplier sqlSupplier) {
+ return new DefaultTypedGenericExecuteSpec<>(byIndex, byName, sqlSupplier, typeToRead);
+ }
+ }
+
+ /**
+ * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.InsertIntoSpec} implementation.
+ */
+ class DefaultInsertIntoSpec implements InsertIntoSpec {
+
+ @Override
+ public GenericInsertSpec into(String table) {
+ return new DefaultGenericInsertSpec(table, Collections.emptyMap());
+ }
+
+ @Override
+ public TypedInsertSpec into(Class table) {
+ return new DefaultTypedInsertSpec<>(table);
+ }
+ }
+
+ /**
+ * Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericInsertSpec}.
+ */
+ @RequiredArgsConstructor
+ class DefaultGenericInsertSpec implements GenericInsertSpec {
+
+ private final String table;
+ private final Map> byName;
+
+ @Override
+ public GenericInsertSpec value(String field, Object value) {
+
+ Assert.notNull(field, "Field must not be null!");
+
+ Map> byName = new LinkedHashMap<>(this.byName);
+ byName.put(field, Optional.of(value));
+
+ return new DefaultGenericInsertSpec(this.table, byName);
+ }
+
+ @Override
+ public GenericInsertSpec nullValue(String field) {
+
+ Assert.notNull(field, "Field must not be null!");
+
+ Map> byName = new LinkedHashMap<>(this.byName);
+ byName.put(field, Optional.empty());
+
+ return new DefaultGenericInsertSpec(this.table, byName);
+ }
+
+ @Override
+ public Mono then() {
+ return exchange().flatMapMany(FetchSpec::all).then();
+ }
+
+ @Override
+ public Mono>> exchange() {
+
+ if (byName.isEmpty()) {
+ throw new IllegalStateException("Insert fields is empty!");
+ }
+
+ StringBuilder builder = new StringBuilder();
+ String fieldNames = byName.keySet().stream().collect(Collectors.joining(","));
+ String placeholders = IntStream.range(0, byName.size()).mapToObj(i -> "$" + (i + 1))
+ .collect(Collectors.joining(","));
+
+ builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(")
+ .append(placeholders).append(")");
+
+ return execute(it -> {
+
+ String sql = builder.toString();
+ Statement statement = it.createStatement(sql);
+
+ AtomicInteger index = new AtomicInteger();
+ for (Optional o : byName.values()) {
+
+ if (o.isPresent()) {
+ o.ifPresent(v -> statement.bind(index.getAndIncrement(), v));
+ } else {
+ statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type?
+ }
+ }
+
+ SqlResult> result = new DefaultSqlResult<>(sql,
+ Flux.from(statement.executeReturningGeneratedKeys()), ColumnMapRowMapper.INSTANCE);
+ return Flux.just(result);
+
+ }).next();
+ }
+ }
+
+ /**
+ * Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.TypedInsertSpec}.
+ */
+ @RequiredArgsConstructor
+ class DefaultTypedInsertSpec implements TypedInsertSpec, InsertSpec {
+
+ private final Class> typeToInsert;
+ private final String table;
+ private final Publisher objectToInsert;
+
+ public DefaultTypedInsertSpec(Class> typeToInsert) {
+
+ this.typeToInsert = typeToInsert;
+ this.table = dataAccessStrategy.getTableName(typeToInsert);
+ this.objectToInsert = Mono.empty();
+ }
+
+ @Override
+ public TypedInsertSpec table(String tableName) {
+
+ Assert.hasText(tableName, "Table name must not be null or empty!");
+
+ return new DefaultTypedInsertSpec<>(typeToInsert, tableName, objectToInsert);
+ }
+
+ @Override
+ public InsertSpec using(T objectToInsert) {
+
+ Assert.notNull(objectToInsert, "Object to insert must not be null!");
+
+ return new DefaultTypedInsertSpec<>(typeToInsert, table, Mono.just(objectToInsert));
+ }
+
+ @Override
+ public InsertSpec using(Publisher objectToInsert) {
+
+ Assert.notNull(objectToInsert, "Publisher to insert must not be null!");
+
+ return new DefaultTypedInsertSpec<>(typeToInsert, table, objectToInsert);
+ }
+
+ @Override
+ public Mono then() {
+ return exchange().flatMapMany(FetchSpec::all).then();
+ }
+
+ @Override
+ public Mono>> exchange() {
+
+ return Mono.from(objectToInsert).flatMap(toInsert -> {
+
+ StringBuilder builder = new StringBuilder();
+
+ List> insertValues = dataAccessStrategy.getInsert(toInsert);
+ String fieldNames = insertValues.stream().map(Pair::getFirst).collect(Collectors.joining(","));
+ String placeholders = IntStream.range(0, insertValues.size()).mapToObj(i -> "$" + (i + 1))
+ .collect(Collectors.joining(","));
+
+ builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(")
+ .append(placeholders).append(")");
+
+ return execute(it -> {
+
+ String sql = builder.toString();
+ Statement statement = it.createStatement(sql);
+
+ AtomicInteger index = new AtomicInteger();
+
+ for (Pair pair : insertValues) {
+
+ if (pair.getSecond() != null) { // TODO: Better type to transport null values.
+ statement.bind(index.getAndIncrement(), pair.getSecond());
+ } else {
+ statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type?
+ }
+ }
+
+ SqlResult> result = new DefaultSqlResult<>(sql,
+ Flux.from(statement.executeReturningGeneratedKeys()), ColumnMapRowMapper.INSTANCE);
+ return Flux.just(result);
+
+ }).next();
+ });
+ }
+ }
+
+ /**
+ * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SqlResult} implementation.
+ */
+ static class DefaultSqlResult implements SqlResult {
+
+ private final String sql;
+ private final Flux result;
+ private final FetchSpec fetchSpec;
+
+ DefaultSqlResult(String sql, Flux result, BiFunction mappingFunction) {
+
+ this.sql = sql;
+ this.result = result;
+ this.fetchSpec = new DefaultFetchSpec<>(sql, result.flatMap(it -> it.map(mappingFunction)),
+ result.flatMap(Result::getRowsUpdated).next());
+ }
+
+ @Override
+ public SqlResult extract(BiFunction mappingFunction) {
+ return new DefaultSqlResult<>(sql, result, mappingFunction);
+ }
+
+ @Override
+ public Mono one() {
+ return fetchSpec.one();
+ }
+
+ @Override
+ public Mono first() {
+ return fetchSpec.first();
+ }
+
+ @Override
+ public Flux all() {
+ return fetchSpec.all();
+ }
+
+ @Override
+ public Mono rowsUpdated() {
+ return fetchSpec.rowsUpdated();
+ }
+ }
+
+ @RequiredArgsConstructor
+ static class DefaultFetchSpec implements FetchSpec {
+
+ private final String sql;
+ private final Flux result;
+ private final Mono updatedRows;
+
+ @Override
+ public Mono one() {
+
+ return all().buffer(2) //
+ .flatMap(it -> {
+
+ if (it.isEmpty()) {
+ return Mono.empty();
+ }
+
+ if (it.size() > 1) {
+ return Mono.error(new IncorrectResultSizeDataAccessException(
+ String.format("Query [%s] returned non unique result.", this.sql), 1));
+ }
+
+ return Mono.just(it.get(0));
+ }).next();
+ }
+
+ @Override
+ public Mono first() {
+ return all().next();
+ }
+
+ @Override
+ public Flux all() {
+ return result;
+ }
+
+ @Override
+ public Mono rowsUpdated() {
+ return updatedRows;
+ }
+ }
+
+ private static Flux doInConnection(Function> action, Connection it) {
+
+ try {
+ return action.apply(it);
+ } catch (RuntimeException e) {
+
+ String sql = getSql(action);
+ return Flux.error(new DefaultDatabaseClient.UncategorizedSQLException("ConnectionCallback", sql, e) {});
+ }
+ }
+
+ /**
+ * Determine SQL from potential provider object.
+ *
+ * @param sqlProvider object that's potentially a SqlProvider
+ * @return the SQL string, or {@code null}
+ * @see SqlProvider
+ */
+ @Nullable
+ private static String getSql(Object sqlProvider) {
+
+ if (sqlProvider instanceof SqlProvider) {
+ return ((SqlProvider) sqlProvider).getSql();
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Invocation handler that suppresses close calls on R2DBC Connections. Also prepares returned Statement
+ * (Prepared/CallbackStatement) objects.
+ *
+ * @see Connection#close()
+ */
+ private class CloseSuppressingInvocationHandler implements InvocationHandler {
+
+ private final Connection target;
+
+ CloseSuppressingInvocationHandler(Connection target) {
+ this.target = target;
+ }
+
+ @Override
+ @Nullable
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ // Invocation on ConnectionProxy interface coming in...
+
+ if (method.getName().equals("equals")) {
+ // Only consider equal when proxies are identical.
+ return (proxy == args[0]);
+ } else if (method.getName().equals("hashCode")) {
+ // Use hashCode of PersistenceManager proxy.
+ return System.identityHashCode(proxy);
+ } else if (method.getName().equals("unwrap")) {
+ if (((Class>) args[0]).isInstance(proxy)) {
+ return proxy;
+ }
+ } else if (method.getName().equals("isWrapperFor")) {
+ if (((Class>) args[0]).isInstance(proxy)) {
+ return true;
+ }
+ } else if (method.getName().equals("close")) {
+ // Handle close method: suppress, not valid.
+ return Mono.error(new UnsupportedOperationException("Close is not supported!"));
+ } else if (method.getName().equals("getTargetConnection")) {
+ // Handle getTargetConnection method: return underlying Connection.
+ return this.target;
+ }
+
+ // Invoke method on target Connection.
+ try {
+ Object retVal = method.invoke(this.target, args);
+
+ return retVal;
+ } catch (InvocationTargetException ex) {
+ throw ex.getTargetException();
+ }
+ }
+ }
+
+ private static class UncategorizedSQLException extends UncategorizedDataAccessException {
+
+ /** SQL that led to the problem */
+ @Nullable private final String sql;
+
+ /**
+ * Constructor for UncategorizedSQLException.
+ *
+ * @param task name of current task
+ * @param sql the offending SQL statement
+ * @param ex the root cause
+ */
+ public UncategorizedSQLException(String task, @Nullable String sql, Exception ex) {
+ super(String.format("%s; uncategorized SQLException%s; %s", task, sql != null ? " for SQL [" + sql + "]" : "",
+ ex.getMessage()), ex);
+ this.sql = sql;
+ }
+
+ /**
+ * Return the SQL that led to the problem (if known).
+ */
+ @Nullable
+ public String getSql() {
+ return this.sql;
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java
new file mode 100644
index 0000000..0830859
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import java.util.function.Consumer;
+
+import org.springframework.data.jdbc.core.function.DatabaseClient.Builder;
+import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
+import org.springframework.jdbc.support.SQLExceptionTranslator;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+import io.r2dbc.spi.ConnectionFactory;
+
+/**
+ * Default implementation of {@link DatabaseClient.Builder}.
+ *
+ * @author Mark Paluch
+ */
+class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
+
+ private @Nullable ConnectionFactory connector;
+ private SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator();
+ private ReactiveDataAccessStrategy accessStrategy = new DefaultReactiveDataAccessStrategy();
+
+ DefaultDatabaseClientBuilder() {}
+
+ DefaultDatabaseClientBuilder(DefaultDatabaseClientBuilder other) {
+
+ Assert.notNull(other, "DefaultDatabaseClientBuilder must not be null!");
+
+ this.connector = other.connector;
+ this.exceptionTranslator = exceptionTranslator;
+ }
+
+ @Override
+ public Builder connectionFactory(ConnectionFactory factory) {
+
+ Assert.notNull(factory, "ConnectionFactory must not be null!");
+
+ this.connector = factory;
+ return this;
+ }
+
+ @Override
+ public Builder exceptionTranslator(SQLExceptionTranslator exceptionTranslator) {
+
+ Assert.notNull(exceptionTranslator, "SQLExceptionTranslator must not be null!");
+
+ this.exceptionTranslator = exceptionTranslator;
+ return this;
+ }
+
+ @Override
+ public Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy) {
+
+ Assert.notNull(accessStrategy, "ReactiveDataAccessStrategy must not be null!");
+
+ this.accessStrategy = accessStrategy;
+ return this;
+ }
+
+ @Override
+ public DatabaseClient build() {
+
+ return new DefaultDatabaseClient(this.connector, exceptionTranslator, accessStrategy,
+ new DefaultDatabaseClientBuilder(this));
+ }
+
+ @Override
+ public DatabaseClient.Builder clone() {
+ return new DefaultDatabaseClientBuilder(this);
+ }
+
+ @Override
+ public DatabaseClient.Builder apply(Consumer builderConsumer) {
+ Assert.notNull(builderConsumer, "BuilderConsumer must not be null");
+
+ builderConsumer.accept(this);
+ return this;
+ }
+}
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java
new file mode 100644
index 0000000..38d101e
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.springframework.data.convert.EntityInstantiators;
+import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
+import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
+import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty;
+import org.springframework.data.mapping.PersistentPropertyAccessor;
+import org.springframework.data.util.Pair;
+import org.springframework.util.ClassUtils;
+
+import io.r2dbc.spi.Row;
+import io.r2dbc.spi.RowMetadata;
+
+/**
+ * @author Mark Paluch
+ */
+public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy {
+
+ private final EntityInstantiators instantiators = new EntityInstantiators();
+ private final JdbcMappingContext mappingContext = new JdbcMappingContext();
+
+ @Override
+ public List> getInsert(Object object) {
+
+ Class> userClass = ClassUtils.getUserClass(object);
+
+ JdbcPersistentEntity> entity = mappingContext.getRequiredPersistentEntity(userClass);
+ PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object);
+
+ List> values = new ArrayList<>();
+
+ for (JdbcPersistentProperty property : entity) {
+
+ Object value = propertyAccessor.getProperty(property);
+
+ if (value == null) {
+ continue;
+ }
+
+ values.add(Pair.of(property.getColumnName(), value));
+ }
+
+ return values;
+ }
+
+ @Override
+ public BiFunction getRowMapper(Class typeToRead) {
+ return new EntityRowMapper((JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(typeToRead),
+ instantiators, mappingContext);
+ }
+
+ @Override
+ public String getTableName(Class> type) {
+ return mappingContext.getRequiredPersistentEntity(type).getTableName();
+ }
+}
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java
new file mode 100644
index 0000000..c78d28c
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+import java.sql.ResultSet;
+import java.util.function.BiFunction;
+
+import org.springframework.core.convert.ConversionService;
+import org.springframework.data.convert.EntityInstantiators;
+import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
+import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
+import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty;
+import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.PersistentProperty;
+import org.springframework.data.mapping.PersistentPropertyAccessor;
+import org.springframework.data.mapping.PreferredConstructor.Parameter;
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
+import org.springframework.data.mapping.model.ParameterValueProvider;
+import org.springframework.util.ClassUtils;
+
+import io.r2dbc.spi.Row;
+import io.r2dbc.spi.RowMetadata;
+
+/**
+ * Maps a {@link io.r2dbc.spi.Row} to an entity of type {@code T}, including entities referenced.
+ *
+ * @author Mark Paluch
+ * @since 1.0
+ */
+public class EntityRowMapper implements BiFunction {
+
+ private final JdbcPersistentEntity entity;
+ private final EntityInstantiators entityInstantiators;
+ private final ConversionService conversions;
+ private final MappingContext, JdbcPersistentProperty> context;
+
+ public EntityRowMapper(JdbcPersistentEntity entity, EntityInstantiators entityInstantiators,
+ JdbcMappingContext context) {
+
+ this.entity = entity;
+ this.entityInstantiators = entityInstantiators;
+ this.conversions = context.getConversions();
+ this.context = context;
+ }
+
+ @Override
+ public T apply(Row row, RowMetadata metadata) {
+
+ T result = createInstance(row, "", entity);
+
+ ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(result),
+ conversions);
+
+ for (JdbcPersistentProperty property : entity) {
+
+ if (property.isCollectionLike()) {
+ throw new UnsupportedOperationException();
+ } else if (property.isMap()) {
+
+ throw new UnsupportedOperationException();
+ } else {
+ propertyAccessor.setProperty(property, readFrom(row, property, ""));
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Read a single value or a complete Entity from the {@link ResultSet} passed as an argument.
+ *
+ * @param row the {@link Row} to extract the value from. Must not be {@code null}.
+ * @param property the {@link JdbcPersistentProperty} for which the value is intended. Must not be {@code null}.
+ * @param prefix to be used for all column names accessed by this method. Must not be {@code null}.
+ * @return the value read from the {@link ResultSet}. May be {@code null}.
+ */
+ private Object readFrom(Row row, JdbcPersistentProperty property, String prefix) {
+
+ try {
+
+ if (property.isEntity()) {
+ return readEntityFrom(row, property);
+ }
+
+ return row.get(prefix + property.getColumnName(), getType(property));
+
+ } catch (Exception o_O) {
+ throw new MappingException(String.format("Could not read property %s from result set!", property), o_O);
+ }
+ }
+
+ private Class> getType(JdbcPersistentProperty property) {
+ return ClassUtils.resolvePrimitiveIfNecessary(property.getActualType());
+ }
+
+ private S readEntityFrom(Row row, PersistentProperty> property) {
+
+ String prefix = property.getName() + "_";
+
+ @SuppressWarnings("unchecked")
+ JdbcPersistentEntity entity = (JdbcPersistentEntity) context
+ .getRequiredPersistentEntity(property.getActualType());
+
+ if (readFrom(row, entity.getRequiredIdProperty(), prefix) == null) {
+ return null;
+ }
+
+ S instance = createInstance(row, prefix, entity);
+
+ PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
+ ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions);
+
+ for (JdbcPersistentProperty p : entity) {
+ propertyAccessor.setProperty(p, readFrom(row, p, prefix));
+ }
+
+ return instance;
+ }
+
+ private S createInstance(Row row, String prefix, JdbcPersistentEntity entity) {
+
+ return entityInstantiators.getInstantiatorFor(entity).createInstance(entity,
+ new RowParameterValueProvider(row, entity, conversions, prefix));
+ }
+
+ @RequiredArgsConstructor
+ private static class RowParameterValueProvider implements ParameterValueProvider {
+
+ @NonNull private final Row resultSet;
+ @NonNull private final JdbcPersistentEntity> entity;
+ @NonNull private final ConversionService conversionService;
+ @NonNull private final String prefix;
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter)
+ */
+ @Override
+ public T getParameterValue(Parameter parameter) {
+
+ String column = prefix + entity.getRequiredPersistentProperty(parameter.getName()).getColumnName();
+
+ try {
+ return conversionService.convert(resultSet.get(column, parameter.getType().getType()),
+ parameter.getType().getType());
+ } catch (Exception o_O) {
+ throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O);
+ }
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java b/src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java
new file mode 100644
index 0000000..8b6e512
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.springframework.util.Assert;
+
+/**
+ * @author Mark Paluch
+ */
+class IterableUtils {
+
+ static Collection toCollection(Iterable iterable) {
+
+ Assert.notNull(iterable, "Iterable must not be null!");
+
+ if (iterable instanceof Collection) {
+ return (Collection) iterable;
+ }
+
+ List result = new ArrayList<>();
+
+ for (T element : iterable) {
+ result.add(element);
+ }
+
+ return result;
+ }
+
+ static List toList(Iterable iterable) {
+
+ Assert.notNull(iterable, "Iterable must not be null!");
+
+ if (iterable instanceof List) {
+ return (List) iterable;
+ }
+
+ List result = new ArrayList<>();
+
+ for (T element : iterable) {
+ result.add(element);
+ }
+
+ return result;
+ }
+}
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java
new file mode 100644
index 0000000..26c9f65
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.springframework.data.util.Pair;
+
+import io.r2dbc.spi.Row;
+import io.r2dbc.spi.RowMetadata;
+
+/**
+ * @author Mark Paluch
+ */
+public interface ReactiveDataAccessStrategy {
+
+ List> getInsert(Object object);
+
+ // TODO: Broaden T to Mono/Flux for reactive relational data access?
+ BiFunction getRowMapper(Class typeToRead);
+
+ String getTableName(Class> type);
+}
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java b/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java
new file mode 100644
index 0000000..70e746f
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import java.util.function.BiFunction;
+
+import org.springframework.data.jdbc.core.function.DatabaseClient.FetchSpec;
+
+import io.r2dbc.spi.Row;
+import io.r2dbc.spi.RowMetadata;
+
+/**
+ * @author Mark Paluch
+ */
+public interface SqlResult extends FetchSpec {
+
+ SqlResult extract(BiFunction mappingFunction);
+}
diff --git a/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java
new file mode 100644
index 0000000..a473df7
--- /dev/null
+++ b/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function.connectionfactory;
+
+import io.r2dbc.spi.Connection;
+
+import java.sql.Wrapper;
+
+/**
+ * Subinterface of {@link Connection} to be implemented by Connection proxies. Allows access to the underlying target
+ * Connection.
+ *
+ * This interface can be checked when there is a need to cast to a native R2DBC {@link Connection}. Alternatively, all
+ * such connections also support JDBC 4.0's {@link Connection#unwrap}.
+ *
+ * @author Mark Paluch
+ */
+public interface ConnectionProxy extends Connection, Wrapper {
+
+ /**
+ * Return the target Connection of this proxy.
+ *
+ * This will typically be the native driver Connection or a wrapper from a connection pool.
+ *
+ * @return the underlying Connection (never {@code null})
+ */
+ Connection getTargetConnection();
+}
diff --git a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java
new file mode 100644
index 0000000..53b4bdc
--- /dev/null
+++ b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import static org.assertj.core.api.Assertions.*;
+
+import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
+import io.r2dbc.postgresql.PostgresqlConnectionFactory;
+import io.r2dbc.spi.ConnectionFactory;
+import lombok.Data;
+import reactor.core.publisher.Hooks;
+import reactor.test.StepVerifier;
+
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
+import org.springframework.data.jdbc.core.function.ExternalDatabase.ProvidedDatabase;
+import org.springframework.data.jdbc.core.mapping.Table;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+/**
+ * Integration tests for {@link DatabaseClient} against PostgreSQL.
+ *
+ * @author Mark Paluch
+ */
+public class DatabaseClientIntegrationTests {
+
+ /**
+ * Local test database at {@code postgres:@localhost:5432/postgres}.
+ */
+ @ClassRule public static final ExternalDatabase database = ProvidedDatabase.builder().hostname("localhost").port(5432)
+ .database("postgres").username("postgres").password("").build();
+
+ private ConnectionFactory connectionFactory;
+
+ private JdbcTemplate jdbc;
+
+ @Before
+ public void before() {
+
+ Hooks.onOperatorDebug();
+
+ connectionFactory = new PostgresqlConnectionFactory(
+ PostgresqlConnectionConfiguration.builder().host(database.getHostname()).database(database.getDatabase())
+ .username(database.getUsername()).password(database.getPassword()).build());
+
+ PGSimpleDataSource dataSource = new PGSimpleDataSource();
+ dataSource.setUser(database.getUsername());
+ dataSource.setPassword(database.getPassword());
+ dataSource.setDatabaseName(database.getDatabase());
+ dataSource.setServerName(database.getHostname());
+ dataSource.setPortNumber(database.getPort());
+
+ String tableToCreate = "CREATE TABLE IF NOT EXISTS legoset (\n"
+ + " id integer CONSTRAINT id PRIMARY KEY,\n" + " name varchar(255) NOT NULL,\n"
+ + " manual integer NULL\n" + ");";
+
+ jdbc = new JdbcTemplate(dataSource);
+ jdbc.execute(tableToCreate);
+ jdbc.execute("DELETE FROM legoset");
+ }
+
+ @Test
+ public void executeInsert() {
+
+ DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
+
+ databaseClient.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
+ .bind(0, 42055) //
+ .bind(1, "SCHAUFELRADBAGGER") //
+ .bindNull("$3") //
+ .fetch().rowsUpdated() //
+ .as(StepVerifier::create) //
+ .expectNext(1) //
+ .verifyComplete();
+
+ assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055);
+ }
+
+ @Test
+ public void executeSelect() {
+
+ jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)");
+
+ DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
+
+ // TODO: Driver/Decode does not support decoding null values?
+ databaseClient.execute().sql("SELECT id, name, manual FROM legoset") //
+ .as(LegoSet.class) //
+ .fetch().all() //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+
+ assertThat(actual.getId()).isEqualTo(42055);
+ assertThat(actual.getName()).isEqualTo("SCHAUFELRADBAGGER");
+ assertThat(actual.getManual()).isEqualTo(12);
+ }).verifyComplete();
+ }
+
+ @Test
+ public void insert() {
+
+ DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
+
+ databaseClient.insert().into("legoset")//
+ .value("id", 42055) //
+ .value("name", "SCHAUFELRADBAGGER") //
+ .nullValue("manual") //
+ .exchange() //
+ .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()).as(StepVerifier::create) //
+ .expectNext(42055).verifyComplete();
+
+ assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055);
+ }
+
+ @Test
+ public void insertTypedObject() {
+
+ LegoSet legoSet = new LegoSet();
+ legoSet.setId(42055);
+ legoSet.setName("SCHAUFELRADBAGGER");
+ legoSet.setManual(12);
+
+ DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
+
+ databaseClient.insert().into(LegoSet.class)//
+ .using(legoSet).exchange() //
+ .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()).as(StepVerifier::create) //
+ .expectNext(42055).verifyComplete();
+
+ assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055);
+ }
+
+ @Data
+ @Table("legoset")
+ static class LegoSet {
+ int id;
+ String name;
+ Integer manual;
+ }
+}
diff --git a/src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java b/src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java
new file mode 100644
index 0000000..c2e8493
--- /dev/null
+++ b/src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2018 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
+ *
+ * http://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.data.jdbc.core.function;
+
+import lombok.Builder;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.AssumptionViolatedException;
+import org.junit.rules.ExternalResource;
+
+/**
+ * {@link ExternalResource} wrapper to encapsulate {@link ProvidedDatabase} and
+ * {@link org.testcontainers.containers.PostgreSQLContainer}.
+ *
+ * @author Mark Paluch
+ */
+public abstract class ExternalDatabase extends ExternalResource {
+
+ /**
+ * @return the post of the database service.
+ */
+ public abstract int getPort();
+
+ /**
+ * @return hostname on which the database service runs.
+ */
+ public abstract String getHostname();
+
+ /**
+ * @return name of the database.
+ */
+ public abstract String getDatabase();
+
+ /**
+ * @return database user name.
+ */
+ public abstract String getUsername();
+
+ @Override
+ protected void before() {
+
+ try (Socket socket = new Socket()) {
+ ;
+ socket.connect(new InetSocketAddress(getHostname(), getPort()), Math.toIntExact(TimeUnit.SECONDS.toMillis(5)));
+
+ } catch (IOException e) {
+ throw new AssumptionViolatedException(
+ String.format("Cannot connect to %s:%d. Skiping tests.", getHostname(), getPort()));
+ }
+ }
+
+ /**
+ * @return password for the database user.
+ */
+ public abstract String getPassword();
+
+ /**
+ * Provided (unmanaged resource) database connection coordinates.
+ */
+ @Builder
+ static class ProvidedDatabase extends ExternalDatabase {
+
+ private final int port;
+ private final String hostname;
+ private final String database;
+ private final String username;
+ private final String password;
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getPort()
+ */
+ @Override
+ public int getPort() {
+ return port;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getHostname()
+ */
+ @Override
+ public String getHostname() {
+ return hostname;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getDatabase()
+ */
+ @Override
+ public String getDatabase() {
+ return database;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getUsername()
+ */
+ @Override
+ public String getUsername() {
+ return username;
+ }
+
+ /* (non-Javadoc)
+ * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getPassword()
+ */
+ @Override
+ public String getPassword() {
+ return password;
+ }
+ }
+}