diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManager.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManager.java
index e1b92df95..d32e1d2ac 100644
--- a/src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManager.java
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManager.java
@@ -421,7 +421,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
* Prepare the transactional {@link Connection} right after transaction begin.
*
* The default implementation executes a "SET TRANSACTION READ ONLY" statement if the {@link #setEnforceReadOnly
- * "enforceReadOnly"} flag is set to {@code true} and the transaction definition indicates a read-only transaction.
+ * "enforceReadOnly"} flag is set to {@literal true} and the transaction definition indicates a read-only transaction.
*
* The "SET TRANSACTION READ ONLY" is understood by Oracle, MySQL and Postgres and may work with other databases as
* well. If you'd like to adapt this treatment, override this method accordingly.
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/CannotReadScriptException.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/CannotReadScriptException.java
new file mode 100644
index 000000000..5192579c0
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/CannotReadScriptException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import org.springframework.core.io.support.EncodedResource;
+
+/**
+ * Thrown by {@link ScriptUtils} if an SQL script cannot be read.
+ *
+ * @author Mark Paluch
+ */
+public class CannotReadScriptException extends ScriptException {
+
+ private static final long serialVersionUID = 7253084944991764250L;
+
+ /**
+ * Creates a new {@link CannotReadScriptException}.
+ *
+ * @param resource the resource that cannot be read from.
+ * @param cause the underlying cause of the resource access failure.
+ */
+ public CannotReadScriptException(EncodedResource resource, Throwable cause) {
+ super("Cannot read SQL script from " + resource, cause);
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/CompositeDatabasePopulator.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/CompositeDatabasePopulator.java
new file mode 100644
index 000000000..ebbb34a91
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/CompositeDatabasePopulator.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import io.r2dbc.spi.Connection;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import org.springframework.util.Assert;
+
+/**
+ * Composite {@link DatabasePopulator} that delegates to a list of given {@link DatabasePopulator} implementations,
+ * executing all scripts.
+ *
+ * @author Mark Paluch
+ */
+public class CompositeDatabasePopulator implements DatabasePopulator {
+
+ private final List populators = new ArrayList<>(4);
+
+ /**
+ * Creates an empty {@link CompositeDatabasePopulator}.
+ *
+ * @see #setPopulators
+ * @see #addPopulators
+ */
+ public CompositeDatabasePopulator() {}
+
+ /**
+ * Creates a {@link CompositeDatabasePopulator}. with the given populators.
+ *
+ * @param populators one or more populators to delegate to.
+ */
+ public CompositeDatabasePopulator(Collection populators) {
+
+ Assert.notNull(populators, "Collection of DatabasePopulator must not be null!");
+
+ this.populators.addAll(populators);
+ }
+
+ /**
+ * Creates a {@link CompositeDatabasePopulator} with the given populators.
+ *
+ * @param populators one or more populators to delegate to.
+ */
+ public CompositeDatabasePopulator(DatabasePopulator... populators) {
+
+ Assert.notNull(populators, "DatabasePopulators must not be null!");
+
+ this.populators.addAll(Arrays.asList(populators));
+ }
+
+ /**
+ * Specify one or more populators to delegate to.
+ */
+ public void setPopulators(DatabasePopulator... populators) {
+
+ Assert.notNull(populators, "DatabasePopulators must not be null!");
+
+ this.populators.clear();
+ this.populators.addAll(Arrays.asList(populators));
+ }
+
+ /**
+ * Add one or more populators to the list of delegates.
+ */
+ public void addPopulators(DatabasePopulator... populators) {
+
+ Assert.notNull(populators, "DatabasePopulators must not be null!");
+
+ this.populators.addAll(Arrays.asList(populators));
+ }
+
+ @Override
+ public Mono populate(Connection connection) throws ScriptException {
+
+ Assert.notNull(connection, "Connection must not be null!");
+
+ return Flux.fromIterable(this.populators).concatMap(it -> it.populate(connection)).then();
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ConnectionFactoryInitializer.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ConnectionFactoryInitializer.java
new file mode 100644
index 000000000..49ce781ad
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ConnectionFactoryInitializer.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import io.r2dbc.spi.ConnectionFactory;
+
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * Used to {@link #setDatabasePopulator set up} a database during initialization and {@link #setDatabaseCleaner clean
+ * up} a database during destruction.
+ *
+ * @author Mark Paluch
+ * @see DatabasePopulator
+ */
+public class ConnectionFactoryInitializer implements InitializingBean, DisposableBean {
+
+ private @Nullable ConnectionFactory connectionFactory;
+
+ private @Nullable DatabasePopulator databasePopulator;
+
+ private @Nullable DatabasePopulator databaseCleaner;
+
+ private boolean enabled = true;
+
+ /**
+ * The {@link ConnectionFactory} for the database to populate when this component is initialized and to clean up when
+ * this component is shut down.
+ *
+ * This property is mandatory with no default provided.
+ *
+ * @param connectionFactory the R2DBC {@link ConnectionFactory}.
+ */
+ public void setConnectionFactory(ConnectionFactory connectionFactory) {
+ this.connectionFactory = connectionFactory;
+ }
+
+ /**
+ * Set the {@link DatabasePopulator} to execute during the bean initialization phase.
+ *
+ * @param databasePopulator the {@link DatabasePopulator} to use during initialization
+ * @see #setDatabaseCleaner
+ */
+ public void setDatabasePopulator(DatabasePopulator databasePopulator) {
+ this.databasePopulator = databasePopulator;
+ }
+
+ /**
+ * Set the {@link DatabasePopulator} to execute during the bean destruction phase, cleaning up the database and
+ * leaving it in a known state for others.
+ *
+ * @param databaseCleaner the {@link DatabasePopulator} to use during destruction
+ * @see #setDatabasePopulator
+ */
+ public void setDatabaseCleaner(DatabasePopulator databaseCleaner) {
+ this.databaseCleaner = databaseCleaner;
+ }
+
+ /**
+ * Flag to explicitly enable or disable the {@link #setDatabasePopulator database populator} and
+ * {@link #setDatabaseCleaner database cleaner}.
+ *
+ * @param enabled {@literal true} if the database populator and database cleaner should be called on startup and
+ * shutdown, respectively
+ */
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ /**
+ * Use the {@link #setDatabasePopulator database populator} to set up the database.
+ */
+ @Override
+ public void afterPropertiesSet() {
+ execute(this.databasePopulator);
+ }
+
+ /**
+ * Use the {@link #setDatabaseCleaner database cleaner} to clean up the database.
+ */
+ @Override
+ public void destroy() {
+ execute(this.databaseCleaner);
+ }
+
+ private void execute(@Nullable DatabasePopulator populator) {
+
+ Assert.state(this.connectionFactory != null, "ConnectionFactory must be set");
+
+ if (this.enabled && populator != null) {
+ DatabasePopulatorUtils.execute(populator, this.connectionFactory);
+ }
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/DatabasePopulator.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/DatabasePopulator.java
new file mode 100644
index 000000000..b40054ebd
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/DatabasePopulator.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import io.r2dbc.spi.Connection;
+import reactor.core.publisher.Mono;
+
+/**
+ * Strategy used to populate, initialize, or clean up a database.
+ *
+ * @author Mark Paluch
+ * @see ResourceDatabasePopulator
+ * @see DatabasePopulatorUtils
+ * @see ConnectionFactoryInitializer
+ */
+@FunctionalInterface
+public interface DatabasePopulator {
+
+ /**
+ * Populate, initialize, or clean up the database using the provided R2DBC {@link Connection}.
+ *
+ * @param connection the R2DBC connection to use to populate the db; already configured and ready to use, must not be
+ * {@literal null}.
+ * @return {@link Mono} that initiates script execution and is notified upon completion.
+ * @throws ScriptException in all other error cases
+ * @see DatabasePopulatorUtils#execute
+ */
+ Mono populate(Connection connection) throws ScriptException;
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/DatabasePopulatorUtils.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/DatabasePopulatorUtils.java
new file mode 100644
index 000000000..858b1a75e
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/DatabasePopulatorUtils.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import io.r2dbc.spi.Connection;
+import io.r2dbc.spi.ConnectionFactory;
+import reactor.core.publisher.Mono;
+import reactor.util.function.Tuple2;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils;
+import org.springframework.util.Assert;
+
+/**
+ * Utility methods for executing a {@link DatabasePopulator}.
+ *
+ * @author Mark Paluch
+ */
+public abstract class DatabasePopulatorUtils {
+
+ // utility constructor
+ private DatabasePopulatorUtils() {}
+
+ /**
+ * Execute the given {@link DatabasePopulator} against the given {@link io.r2dbc.spi.ConnectionFactory}.
+ *
+ * @param populator the {@link DatabasePopulator} to execute.
+ * @param connectionFactory the {@link ConnectionFactory} to execute against.
+ * @return {@link Mono} that initiates {@link DatabasePopulator#populate(Connection)} and is notified upon completion.
+ */
+ public static Mono execute(DatabasePopulator populator, ConnectionFactory connectionFactory)
+ throws DataAccessException {
+
+ Assert.notNull(populator, "DatabasePopulator must not be null");
+ Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
+
+ return Mono.usingWhen(ConnectionFactoryUtils.getConnection(connectionFactory).map(Tuple2::getT1), //
+ populator::populate, //
+ it -> ConnectionFactoryUtils.releaseConnection(it, connectionFactory), //
+ it -> ConnectionFactoryUtils.releaseConnection(it, connectionFactory))
+ .onErrorMap(ex -> !(ex instanceof ScriptException), ex -> {
+ return new UncategorizedScriptException("Failed to execute database script", ex);
+ });
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ResourceDatabasePopulator.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ResourceDatabasePopulator.java
new file mode 100644
index 000000000..b97eb1c29
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ResourceDatabasePopulator.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import io.r2dbc.spi.Connection;
+import io.r2dbc.spi.ConnectionFactory;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Populates, initializes, or cleans up a database using SQL scripts defined in external resources.
+ *
+ * - Call {@link #addScript} to add a single SQL script location.
+ *
- Call {@link #addScripts} to add multiple SQL script locations.
+ *
- Consult the setter methods in this class for further configuration options.
+ *
- Call {@link #populate} or {@link #execute} to initialize or clean up the database using the configured scripts.
+ *
+ *
+ * @author Mark Paluch
+ * @see DatabasePopulatorUtils
+ * @see ScriptUtils
+ */
+public class ResourceDatabasePopulator implements DatabasePopulator {
+
+ List scripts = new ArrayList<>();
+
+ private @Nullable Charset sqlScriptEncoding;
+
+ private String separator = ScriptUtils.DEFAULT_STATEMENT_SEPARATOR;
+
+ private String commentPrefix = ScriptUtils.DEFAULT_COMMENT_PREFIX;
+
+ private String blockCommentStartDelimiter = ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER;
+
+ private String blockCommentEndDelimiter = ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER;
+
+ private boolean continueOnError = false;
+
+ private boolean ignoreFailedDrops = false;
+
+ private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
+
+ /**
+ * Creates a new {@link ResourceDatabasePopulator} with default settings.
+ */
+ public ResourceDatabasePopulator() {}
+
+ /**
+ * Creates a new {@link ResourceDatabasePopulator} with default settings for the supplied scripts.
+ *
+ * @param scripts the scripts to execute to initialize or clean up the database (never {@literal null})
+ */
+ public ResourceDatabasePopulator(Resource... scripts) {
+ setScripts(scripts);
+ }
+
+ /**
+ * Creates a new {@link ResourceDatabasePopulator} with the supplied values.
+ *
+ * @param continueOnError flag to indicate that all failures in SQL should be logged but not cause a failure
+ * @param ignoreFailedDrops flag to indicate that a failed SQL {@code DROP} statement can be ignored
+ * @param sqlScriptEncoding the encoding for the supplied SQL scripts (may be {@literal null} or empty to
+ * indicate platform encoding)
+ * @param scripts the scripts to execute to initialize or clean up the database, must not be {@literal null}.
+ */
+ public ResourceDatabasePopulator(boolean continueOnError, boolean ignoreFailedDrops,
+ @Nullable String sqlScriptEncoding, Resource... scripts) {
+
+ this.continueOnError = continueOnError;
+ this.ignoreFailedDrops = ignoreFailedDrops;
+ setSqlScriptEncoding(sqlScriptEncoding);
+ setScripts(scripts);
+ }
+
+ /**
+ * Add a script to execute to initialize or clean up the database.
+ *
+ * @param script the path to an SQL script, must not be {@literal null}.
+ */
+ public void addScript(Resource script) {
+ Assert.notNull(script, "Script must not be null");
+ this.scripts.add(script);
+ }
+
+ /**
+ * Add multiple scripts to execute to initialize or clean up the database.
+ *
+ * @param scripts the scripts to execute, must not be {@literal null}.
+ */
+ public void addScripts(Resource... scripts) {
+ assertContentsOfScriptArray(scripts);
+ this.scripts.addAll(Arrays.asList(scripts));
+ }
+
+ /**
+ * Set the scripts to execute to initialize or clean up the database, replacing any previously added scripts.
+ *
+ * @param scripts the scripts to execute, must not be {@literal null}.
+ */
+ public void setScripts(Resource... scripts) {
+ assertContentsOfScriptArray(scripts);
+ // Ensure that the list is modifiable
+ this.scripts = new ArrayList<>(Arrays.asList(scripts));
+ }
+
+ private void assertContentsOfScriptArray(Resource... scripts) {
+ Assert.notNull(scripts, "Scripts array must not be null");
+ Assert.noNullElements(scripts, "Scripts array must not contain null elements");
+ }
+
+ /**
+ * Specify the encoding for the configured SQL scripts, if different from the platform encoding.
+ *
+ * @param sqlScriptEncoding the encoding used in scripts (may be {@literal null} or empty to indicate platform
+ * encoding).
+ * @see #addScript(Resource)
+ */
+ public void setSqlScriptEncoding(@Nullable String sqlScriptEncoding) {
+ setSqlScriptEncoding(StringUtils.hasText(sqlScriptEncoding) ? Charset.forName(sqlScriptEncoding) : null);
+ }
+
+ /**
+ * Specify the encoding for the configured SQL scripts, if different from the platform encoding.
+ *
+ * @param sqlScriptEncoding the encoding used in scripts (may be {@literal null} to indicate platform encoding).
+ * @see #addScript(Resource)
+ */
+ public void setSqlScriptEncoding(@Nullable Charset sqlScriptEncoding) {
+ this.sqlScriptEncoding = sqlScriptEncoding;
+ }
+
+ /**
+ * Specify the statement separator, if a custom one.
+ *
+ * Defaults to {@code ";"} if not specified and falls back to {@code "\n"} as a last resort; may be set to
+ * {@link ScriptUtils#EOF_STATEMENT_SEPARATOR} to signal that each script contains a single statement without a
+ * separator.
+ *
+ * @param separator the script statement separator.
+ */
+ public void setSeparator(String separator) {
+ this.separator = separator;
+ }
+
+ /**
+ * Set the prefix that identifies single-line comments within the SQL scripts.
+ *
+ * Defaults to {@code "--"}.
+ *
+ * @param commentPrefix the prefix for single-line comments
+ */
+ public void setCommentPrefix(String commentPrefix) {
+ this.commentPrefix = commentPrefix;
+ }
+
+ /**
+ * Set the start delimiter that identifies block comments within the SQL scripts.
+ *
+ * Defaults to {@code "/*"}.
+ *
+ * @param blockCommentStartDelimiter the start delimiter for block comments (never {@literal null} or empty).
+ * @see #setBlockCommentEndDelimiter
+ */
+ public void setBlockCommentStartDelimiter(String blockCommentStartDelimiter) {
+
+ Assert.hasText(blockCommentStartDelimiter, "BlockCommentStartDelimiter must not be null or empty");
+
+ this.blockCommentStartDelimiter = blockCommentStartDelimiter;
+ }
+
+ /**
+ * Set the end delimiter that identifies block comments within the SQL scripts.
+ *
+ * Defaults to {@code "*/"}.
+ *
+ * @param blockCommentEndDelimiter the end delimiter for block comments (never {@literal null} or empty)
+ * @see #setBlockCommentStartDelimiter
+ */
+ public void setBlockCommentEndDelimiter(String blockCommentEndDelimiter) {
+
+ Assert.hasText(blockCommentEndDelimiter, "BlockCommentEndDelimiter must not be null or empty");
+
+ this.blockCommentEndDelimiter = blockCommentEndDelimiter;
+ }
+
+ /**
+ * Flag to indicate that all failures in SQL should be logged but not cause a failure.
+ *
+ * Defaults to {@literal false}.
+ *
+ * @param continueOnError {@literal true} if script execution should continue on error.
+ */
+ public void setContinueOnError(boolean continueOnError) {
+ this.continueOnError = continueOnError;
+ }
+
+ /**
+ * Flag to indicate that a failed SQL {@code DROP} statement can be ignored.
+ *
+ * This is useful for a non-embedded database whose SQL dialect does not support an {@code IF EXISTS} clause in a
+ * {@code DROP} statement.
+ *
+ * The default is {@literal false} so that if the populator runs accidentally, it will fail fast if a script starts
+ * with a {@code DROP} statement.
+ *
+ * @param ignoreFailedDrops {@literal true} if failed drop statements should be ignored.
+ */
+ public void setIgnoreFailedDrops(boolean ignoreFailedDrops) {
+ this.ignoreFailedDrops = ignoreFailedDrops;
+ }
+
+ /**
+ * Set the {@link DataBufferFactory} to use for {@link Resource} loading.
+ *
+ * Defaults to {@link DefaultDataBufferFactory}.
+ *
+ * @param dataBufferFactory the {@link DataBufferFactory} to use, must not be {@literal null}.
+ */
+ public void setDataBufferFactory(DataBufferFactory dataBufferFactory) {
+
+ Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null!");
+
+ this.dataBufferFactory = dataBufferFactory;
+ }
+
+ @Override
+ public Mono populate(Connection connection) throws ScriptException {
+
+ Assert.notNull(connection, "Connection must not be null");
+
+ return Flux.fromIterable(this.scripts).concatMap(it -> {
+
+ EncodedResource encodedScript = new EncodedResource(it, this.sqlScriptEncoding);
+
+ return ScriptUtils.executeSqlScript(connection, encodedScript, this.dataBufferFactory, this.continueOnError,
+ this.ignoreFailedDrops, this.commentPrefix, this.separator, this.blockCommentStartDelimiter,
+ this.blockCommentEndDelimiter);
+ }).then();
+ }
+
+ /**
+ * Execute this {@link ResourceDatabasePopulator} against the given {@link ConnectionFactory}.
+ *
+ * Delegates to {@link DatabasePopulatorUtils#execute}.
+ *
+ * @param connectionFactory the {@link ConnectionFactory} to execute against, must not be {@literal null}..
+ * @return {@link Mono} tthat initiates script execution and is notified upon completion.
+ * @throws ScriptException if an error occurs.
+ * @see #populate(Connection)
+ */
+ public Mono execute(ConnectionFactory connectionFactory) throws ScriptException {
+ return DatabasePopulatorUtils.execute(this, connectionFactory);
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptException.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptException.java
new file mode 100644
index 000000000..672740bf0
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptException.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.lang.Nullable;
+
+/**
+ * Root of the hierarchy of data access exceptions that are related to processing of SQL scripts.
+ *
+ * @author Mark Paluch
+ */
+public abstract class ScriptException extends DataAccessException {
+
+ /**
+ * Creates a new {@link ScriptException}.
+ *
+ * @param message the detail message.
+ */
+ public ScriptException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new {@link ScriptException}.
+ *
+ * @param message the detail message.
+ * @param cause the root cause.
+ */
+ public ScriptException(String message, @Nullable Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptParseException.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptParseException.java
new file mode 100644
index 000000000..24128ae7a
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptParseException.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.lang.Nullable;
+
+/**
+ * Thrown by {@link ScriptUtils} if an SQL script cannot be properly parsed.
+ *
+ * @author Mark Paluch
+ */
+public class ScriptParseException extends ScriptException {
+
+ private static final long serialVersionUID = 6130513243627087332L;
+
+ /**
+ * Creates a new {@link ScriptParseException}.
+ *
+ * @param message detailed message.
+ * @param resource the resource from which the SQL script was read.
+ */
+ public ScriptParseException(String message, @Nullable EncodedResource resource) {
+ super(buildMessage(message, resource));
+ }
+
+ /**
+ * Creates a new {@link ScriptParseException}.
+ *
+ * @param message detailed message.
+ * @param resource the resource from which the SQL script was read.
+ * @param cause the underlying cause of the failure.
+ */
+ public ScriptParseException(String message, @Nullable EncodedResource resource, @Nullable Throwable cause) {
+ super(buildMessage(message, resource), cause);
+ }
+
+ private static String buildMessage(String message, @Nullable EncodedResource resource) {
+ return String.format("Failed to parse SQL script from resource [%s]: %s",
+ (resource == null ? "" : resource), message);
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptStatementFailedException.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptStatementFailedException.java
new file mode 100644
index 000000000..02dfdd6b2
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptStatementFailedException.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import org.springframework.core.io.support.EncodedResource;
+
+/**
+ * Thrown by {@link ScriptUtils} if a statement in an SQL script failed when executing it against the target database.
+ *
+ * @author Mark Paluch
+ */
+public class ScriptStatementFailedException extends ScriptException {
+
+ private static final long serialVersionUID = 912676424615782262L;
+
+ /**
+ * Creates a new {@link ScriptStatementFailedException}.
+ *
+ * @param statement the actual SQL statement that failed.
+ * @param statementNumber the statement number in the SQL script (i.e., the n'th statement present in the resource).
+ * @param encodedResource the resource from which the SQL statement was read.
+ * @param cause the underlying cause of the failure.
+ */
+ public ScriptStatementFailedException(String statement, int statementNumber, EncodedResource encodedResource,
+ Throwable cause) {
+ super(buildErrorMessage(statement, statementNumber, encodedResource), cause);
+ }
+
+ /**
+ * Build an error message for an SQL script execution failure, based on the supplied arguments.
+ *
+ * @param statement the actual SQL statement that failed.
+ * @param statementNumber the statement number in the SQL script (i.e., the n'th statement present in the resource).
+ * @param encodedResource the resource from which the SQL statement was read.
+ * @return an error message suitable for an exception's detail message or logging.
+ */
+ public static String buildErrorMessage(String statement, int statementNumber, EncodedResource encodedResource) {
+ return String.format("Failed to execute SQL script statement #%s of %s: %s", statementNumber, encodedResource,
+ statement);
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptUtils.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptUtils.java
new file mode 100644
index 000000000..2b7034e84
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptUtils.java
@@ -0,0 +1,537 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import io.r2dbc.spi.Connection;
+import io.r2dbc.spi.Result;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.reactivestreams.Publisher;
+
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.buffer.DataBufferFactory;
+import org.springframework.core.io.buffer.DataBufferUtils;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * Generic utility methods for working with SQL scripts.
+ *
+ * Mainly for internal use within the framework.
+ *
+ * @author Mark Paluch
+ */
+public abstract class ScriptUtils {
+
+ /**
+ * Default statement separator within SQL scripts: {@code ";"}.
+ */
+ public static final String DEFAULT_STATEMENT_SEPARATOR = ";";
+
+ /**
+ * Fallback statement separator within SQL scripts: {@code "\n"}.
+ *
+ * Used if neither a custom separator nor the {@link #DEFAULT_STATEMENT_SEPARATOR} is present in a given script.
+ */
+ public static final String FALLBACK_STATEMENT_SEPARATOR = "\n";
+
+ /**
+ * End of file (EOF) SQL statement separator: {@code "^^^ END OF SCRIPT ^^^"}.
+ *
+ * This value may be supplied as the {@code separator} to
+ * {@link #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String)} to denote
+ * that an SQL script contains a single statement (potentially spanning multiple lines) with no explicit statement
+ * separator. Note that such a script should not actually contain this value; it is merely a virtual
+ * statement separator.
+ */
+ public static final String EOF_STATEMENT_SEPARATOR = "^^^ END OF SCRIPT ^^^";
+
+ /**
+ * Default prefix for single-line comments within SQL scripts: {@code "--"}.
+ */
+ public static final String DEFAULT_COMMENT_PREFIX = "--";
+
+ /**
+ * Default start delimiter for block comments within SQL scripts: {@code "/*"}.
+ */
+ public static final String DEFAULT_BLOCK_COMMENT_START_DELIMITER = "/*";
+
+ /**
+ * Default end delimiter for block comments within SQL scripts: "*/".
+ */
+ public static final String DEFAULT_BLOCK_COMMENT_END_DELIMITER = "*/";
+
+ private static final Log logger = LogFactory.getLog(ScriptUtils.class);
+
+ // utility constructor
+ private ScriptUtils() {}
+
+ /**
+ * Split an SQL script into separate statements delimited by the provided separator character. Each individual
+ * statement will be added to the provided {@link List}.
+ *
+ * Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the comment prefix; any text beginning with the
+ * comment prefix and extending to the end of the line will be omitted from the output. Similarly,
+ * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as
+ * the start and end block comment delimiters: any text enclosed in a block comment will be omitted
+ * from the output. In addition, multiple adjacent whitespace characters will be collapsed into a single space.
+ *
+ * @param script the SQL script.
+ * @param separator character separating each statement (typically a ';').
+ * @param statements the list that will contain the individual statements .
+ * @throws ScriptException if an error occurred while splitting the SQL script.
+ * @see #splitSqlScript(String, String, List)
+ * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List)
+ */
+ static void splitSqlScript(String script, char separator, List statements) throws ScriptException {
+ splitSqlScript(script, String.valueOf(separator), statements);
+ }
+
+ /**
+ * Split an SQL script into separate statements delimited by the provided separator string. Each individual statement
+ * will be added to the provided {@link List}.
+ *
+ * Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the comment prefix; any text beginning with the
+ * comment prefix and extending to the end of the line will be omitted from the output. Similarly,
+ * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as
+ * the start and end block comment delimiters: any text enclosed in a block comment will be omitted
+ * from the output. In addition, multiple adjacent whitespace characters will be collapsed into a single space.
+ *
+ * @param script the SQL script.
+ * @param separator text separating each statement (typically a ';' or newline character).
+ * @param statements the list that will contain the individual statements.
+ * @throws ScriptException if an error occurred while splitting the SQL script.
+ * @see #splitSqlScript(String, char, List)
+ * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List)
+ */
+ static void splitSqlScript(String script, String separator, List statements) throws ScriptException {
+ splitSqlScript(null, script, separator, DEFAULT_COMMENT_PREFIX, DEFAULT_BLOCK_COMMENT_START_DELIMITER,
+ DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements);
+ }
+
+ /**
+ * Split an SQL script into separate statements delimited by the provided separator string. Each individual statement
+ * will be added to the provided {@link List}.
+ *
+ * Within the script, the provided {@code commentPrefix} will be honored: any text beginning with the comment prefix
+ * and extending to the end of the line will be omitted from the output. Similarly, the provided
+ * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter} delimiters will be honored: any text
+ * enclosed in a block comment will be omitted from the output. In addition, multiple adjacent whitespace characters
+ * will be collapsed into a single space.
+ *
+ * @param resource the resource from which the script was read.
+ * @param script the SQL script.
+ * @param separator text separating each statement (typically a ';' or newline character).
+ * @param commentPrefix the prefix that identifies SQL line comments (typically "--").
+ * @param blockCommentStartDelimiter the start block comment delimiter. Must not be {@literal null} or empty.
+ * @param blockCommentEndDelimiter the end block comment delimiter. Must not be {@literal null} or empty.
+ * @param statements the list that will contain the individual statements.
+ * @throws ScriptException if an error occurred while splitting the SQL script.
+ */
+ private static void splitSqlScript(@Nullable EncodedResource resource, String script, String separator,
+ String commentPrefix, String blockCommentStartDelimiter, String blockCommentEndDelimiter, List statements)
+ throws ScriptException {
+
+ Assert.hasText(script, "'script' must not be null or empty");
+ Assert.notNull(separator, "'separator' must not be null");
+ Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty");
+ Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty");
+ Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty");
+
+ StringBuilder sb = new StringBuilder();
+ boolean inSingleQuote = false;
+ boolean inDoubleQuote = false;
+ boolean inEscape = false;
+
+ for (int i = 0; i < script.length(); i++) {
+ char c = script.charAt(i);
+ if (inEscape) {
+ inEscape = false;
+ sb.append(c);
+ continue;
+ }
+ // MySQL style escapes
+ if (c == '\\') {
+ inEscape = true;
+ sb.append(c);
+ continue;
+ }
+ if (!inDoubleQuote && (c == '\'')) {
+ inSingleQuote = !inSingleQuote;
+ } else if (!inSingleQuote && (c == '"')) {
+ inDoubleQuote = !inDoubleQuote;
+ }
+ if (!inSingleQuote && !inDoubleQuote) {
+ if (script.startsWith(separator, i)) {
+ // We've reached the end of the current statement
+ if (sb.length() > 0) {
+ statements.add(sb.toString());
+ sb = new StringBuilder();
+ }
+ i += separator.length() - 1;
+ continue;
+ } else if (script.startsWith(commentPrefix, i)) {
+ // Skip over any content from the start of the comment to the EOL
+ int indexOfNextNewline = script.indexOf('\n', i);
+ if (indexOfNextNewline > i) {
+ i = indexOfNextNewline;
+ continue;
+ } else {
+ // If there's no EOL, we must be at the end of the script, so stop here.
+ break;
+ }
+ } else if (script.startsWith(blockCommentStartDelimiter, i)) {
+ // Skip over any block comments
+ int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i);
+ if (indexOfCommentEnd > i) {
+ i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1;
+ continue;
+ } else {
+ throw new ScriptParseException("Missing block comment end delimiter: " + blockCommentEndDelimiter,
+ resource);
+ }
+ } else if (c == ' ' || c == '\r' || c == '\n' || c == '\t') {
+ // Avoid multiple adjacent whitespace characters
+ if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
+ c = ' ';
+ } else {
+ continue;
+ }
+ }
+ }
+ sb.append(c);
+ }
+
+ if (StringUtils.hasText(sb)) {
+ statements.add(sb.toString());
+ }
+ }
+
+ /**
+ * Read a script without blocking from the given resource, using "{@code --}" as the comment prefix and "{@code ;}" as
+ * the statement separator, and build a String containing the lines.
+ *
+ * @param resource the {@link EncodedResource} to be read.
+ * @param dataBufferFactory the buffer factory for non-blocking script loading.
+ * @return {@link String} containing the script lines.
+ * @see DefaultDataBufferFactory
+ */
+ public static Mono readScript(EncodedResource resource, DataBufferFactory dataBufferFactory) {
+ return readScript(resource, dataBufferFactory, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR,
+ DEFAULT_BLOCK_COMMENT_END_DELIMITER);
+ }
+
+ /**
+ * Read a script without blocking from the provided resource, using the supplied comment prefix and statement
+ * separator, and build a {@link String} and build a String containing the lines.
+ *
+ * Lines beginning with the comment prefix are excluded from the results; however, line comments anywhere
+ * else — for example, within a statement — will be included in the results.
+ *
+ * @param resource the {@link EncodedResource} containing the script to be processed.
+ * @param commentPrefix the prefix that identifies comments in the SQL script (typically "--").
+ * @param separator the statement separator in the SQL script (typically ";").
+ * @param blockCommentEndDelimiter the end block comment delimiter.
+ * @return a {@link Mono} of {@link String} containing the script lines that completes once the resource was loaded.
+ */
+ private static Mono readScript(EncodedResource resource, DataBufferFactory dataBufferFactory,
+ @Nullable String commentPrefix, @Nullable String separator, @Nullable String blockCommentEndDelimiter) {
+
+ return DataBufferUtils.join(DataBufferUtils.read(resource.getResource(), dataBufferFactory, 8192))
+ .handle((it, sink) -> {
+
+ try (InputStream is = it.asInputStream()) {
+
+ InputStreamReader in = resource.getCharset() != null ? new InputStreamReader(is, resource.getCharset())
+ : new InputStreamReader(is);
+ LineNumberReader lnr = new LineNumberReader(in);
+ String script = readScript(lnr, commentPrefix, separator, blockCommentEndDelimiter);
+
+ sink.next(script);
+ sink.complete();
+ } catch (Exception e) {
+ sink.error(e);
+ } finally {
+ DataBufferUtils.release(it);
+ }
+ });
+ }
+
+ /**
+ * Read a script from the provided {@link LineNumberReader}, using the supplied comment prefix and statement
+ * separator, and build a {@link String} containing the lines.
+ *
+ * Lines beginning with the comment prefix are excluded from the results; however, line comments anywhere
+ * else — for example, within a statement — will be included in the results.
+ *
+ * @param lineNumberReader the {@link LineNumberReader} containing the script to be processed.
+ * @param lineCommentPrefix the prefix that identifies comments in the SQL script (typically "--").
+ * @param separator the statement separator in the SQL script (typically ";").
+ * @param blockCommentEndDelimiter the end block comment delimiter.
+ * @return a {@link String} containing the script lines.
+ * @throws IOException in case of I/O errors
+ */
+ private static String readScript(LineNumberReader lineNumberReader, @Nullable String lineCommentPrefix,
+ @Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException {
+
+ String currentStatement = lineNumberReader.readLine();
+ StringBuilder scriptBuilder = new StringBuilder();
+ while (currentStatement != null) {
+ if ((blockCommentEndDelimiter != null && currentStatement.contains(blockCommentEndDelimiter))
+ || (lineCommentPrefix != null && !currentStatement.startsWith(lineCommentPrefix))) {
+ if (scriptBuilder.length() > 0) {
+ scriptBuilder.append('\n');
+ }
+ scriptBuilder.append(currentStatement);
+ }
+ currentStatement = lineNumberReader.readLine();
+ }
+ appendSeparatorToScriptIfNecessary(scriptBuilder, separator);
+ return scriptBuilder.toString();
+ }
+
+ private static void appendSeparatorToScriptIfNecessary(StringBuilder scriptBuilder, @Nullable String separator) {
+ if (separator == null) {
+ return;
+ }
+ String trimmed = separator.trim();
+ if (trimmed.length() == separator.length()) {
+ return;
+ }
+ // separator ends in whitespace, so we might want to see if the script is trying
+ // to end the same way
+ if (scriptBuilder.lastIndexOf(trimmed) == scriptBuilder.length() - trimmed.length()) {
+ scriptBuilder.append(separator.substring(trimmed.length()));
+ }
+ }
+
+ /**
+ * Does the provided SQL script contain the specified delimiter?
+ *
+ * @param script the SQL script
+ * @param delim the string delimiting each statement - typically a ';' character
+ */
+ static boolean containsSqlScriptDelimiters(String script, String delim) {
+
+ boolean inLiteral = false;
+ boolean inEscape = false;
+
+ for (int i = 0; i < script.length(); i++) {
+ char c = script.charAt(i);
+ if (inEscape) {
+ inEscape = false;
+ continue;
+ }
+ // MySQL style escapes
+ if (c == '\\') {
+ inEscape = true;
+ continue;
+ }
+ if (c == '\'') {
+ inLiteral = !inLiteral;
+ }
+ if (!inLiteral && script.startsWith(delim, i)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Execute the given SQL script using default settings for statement separators, comment delimiters, and exception
+ * handling flags.
+ *
+ * Statement separators and comments will be removed before executing individual statements within the supplied
+ * script.
+ *
+ * Warning: this method does not release the provided {@link Connection}.
+ *
+ * @param connection the R2DBC connection to use to execute the script; already configured and ready to use.
+ * @param resource the resource to load the SQL script from; encoded with the current platform's default encoding.
+ * @return {@link Mono} that initiates script execution and is notified upon completion.
+ * @throws ScriptException if an error occurred while executing the SQL script.
+ * @see #executeSqlScript(Connection, EncodedResource, DataBufferFactory, boolean, boolean, String, String, String,
+ * String)
+ * @see #DEFAULT_STATEMENT_SEPARATOR
+ * @see #DEFAULT_COMMENT_PREFIX
+ * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
+ * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
+ * @see org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils#getConnection
+ * @see org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils#releaseConnection
+ */
+ public static Mono executeSqlScript(Connection connection, Resource resource) throws ScriptException {
+ return executeSqlScript(connection, new EncodedResource(resource));
+ }
+
+ /**
+ * Execute the given SQL script using default settings for statement separators, comment delimiters, and exception
+ * handling flags.
+ *
+ * Statement separators and comments will be removed before executing individual statements within the supplied
+ * script.
+ *
+ * Warning: this method does not release the provided {@link Connection}.
+ *
+ * @param connection the R2DBC connection to use to execute the script; already configured and ready to use.
+ * @param resource the resource (potentially associated with a specific encoding) to load the SQL script from.
+ * @return {@link Mono} that initiates script execution and is notified upon completion.
+ * @throws ScriptException if an error occurred while executing the SQL script.
+ * @see #executeSqlScript(Connection, EncodedResource, DataBufferFactory, boolean, boolean, String, String, String,
+ * String)
+ * @see #DEFAULT_STATEMENT_SEPARATOR
+ * @see #DEFAULT_COMMENT_PREFIX
+ * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
+ * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
+ * @see org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils#getConnection
+ * @see org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils#releaseConnection
+ */
+ public static Mono executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException {
+ return executeSqlScript(connection, resource, new DefaultDataBufferFactory(), false, false, DEFAULT_COMMENT_PREFIX,
+ DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER);
+ }
+
+ /**
+ * Execute the given SQL script.
+ *
+ * Statement separators and comments will be removed before executing individual statements within the supplied
+ * script.
+ *
+ * Warning: this method does not release the provided {@link Connection}.
+ *
+ * @param connection the R2DBC connection to use to execute the script; already configured and ready to use.
+ * @param dataBufferFactory the buffer factory for non-blocking script loading.
+ * @param resource the resource (potentially associated with a specific encoding) to load the SQL script from.
+ * @param continueOnError whether or not to continue without throwing an exception in the event of an error.
+ * @param ignoreFailedDrops whether or not to continue in the event of specifically an error on a {@code DROP}
+ * statement.
+ * @param commentPrefix the prefix that identifies single-line comments in the SQL script (typically "--").
+ * @param separator the script statement separator; defaults to {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified
+ * and falls back to {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort; may be set to
+ * {@value #EOF_STATEMENT_SEPARATOR} to signal that the script contains a single statement without a
+ * separator.
+ * @param blockCommentStartDelimiter the start block comment delimiter.
+ * @param blockCommentEndDelimiter the end block comment delimiter.
+ * @return {@link Mono} that initiates script execution and is notified upon completion.
+ * @throws ScriptException if an error occurred while executing the SQL script.
+ * @see #DEFAULT_STATEMENT_SEPARATOR
+ * @see #FALLBACK_STATEMENT_SEPARATOR
+ * @see #EOF_STATEMENT_SEPARATOR
+ * @see org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils#getConnection
+ * @see org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils#releaseConnection
+ */
+ public static Mono executeSqlScript(Connection connection, EncodedResource resource,
+ DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, String commentPrefix,
+ @Nullable String separator, String blockCommentStartDelimiter, String blockCommentEndDelimiter)
+ throws ScriptException {
+
+ if (logger.isDebugEnabled()) {
+ logger.debug("Executing SQL script from " + resource);
+ }
+
+ long startTime = System.currentTimeMillis();
+
+ Mono script = readScript(resource, dataBufferFactory, commentPrefix, separator, blockCommentEndDelimiter)
+ .onErrorMap(IOException.class, ex -> new CannotReadScriptException(resource, ex));
+
+ AtomicInteger statementNumber = new AtomicInteger();
+
+ Flux executeScript = script.flatMapIterable(it -> {
+ return splitStatements(it, resource, commentPrefix, separator, blockCommentStartDelimiter,
+ blockCommentEndDelimiter);
+ }).concatMap(statement -> {
+
+ statementNumber.incrementAndGet();
+ return runStatement(statement, connection, resource, continueOnError, ignoreFailedDrops, statementNumber);
+ });
+
+ if (logger.isDebugEnabled()) {
+
+ executeScript = executeScript.doOnComplete(() -> {
+
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
+ });
+ }
+
+ return executeScript.onErrorMap(ex -> !(ex instanceof ScriptException),
+ ex -> new UncategorizedScriptException("Failed to execute database script from resource [" + resource + "]",
+ ex))
+ .then();
+ }
+
+ private static List splitStatements(String script, EncodedResource resource, String commentPrefix,
+ @Nullable String separator, String blockCommentStartDelimiter, String blockCommentEndDelimiter) {
+
+ String separatorToUse = separator;
+ if (separatorToUse == null) {
+ separatorToUse = DEFAULT_STATEMENT_SEPARATOR;
+ }
+ if (!EOF_STATEMENT_SEPARATOR.equals(separatorToUse) && !containsSqlScriptDelimiters(script, separatorToUse)) {
+ separatorToUse = FALLBACK_STATEMENT_SEPARATOR;
+ }
+
+ List statements = new ArrayList<>();
+ splitSqlScript(resource, script, separatorToUse, commentPrefix, blockCommentStartDelimiter,
+ blockCommentEndDelimiter, statements);
+
+ return statements;
+ }
+
+ private static Publisher extends Void> runStatement(String statement, Connection connection,
+ EncodedResource resource, boolean continueOnError, boolean ignoreFailedDrops, AtomicInteger statementNumber) {
+
+ Mono execution = Flux.from(connection.createStatement(statement).execute()) //
+ .flatMap(Result::getRowsUpdated) //
+ .collect(Collectors.summingLong(it -> it));
+
+ if (logger.isDebugEnabled()) {
+ execution = execution.doOnNext(rowsAffected -> {
+ logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
+ });
+ }
+
+ return execution.onErrorResume(ex -> {
+
+ boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
+ if (continueOnError || (dropStatement && ignoreFailedDrops)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, statementNumber.get(), resource),
+ ex);
+ }
+ } else {
+ return Mono.error(new ScriptStatementFailedException(statement, statementNumber.get(), resource, ex));
+ }
+
+ return Mono.empty();
+ }).then();
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/UncategorizedScriptException.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/UncategorizedScriptException.java
new file mode 100644
index 000000000..ce09afd01
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/UncategorizedScriptException.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+/**
+ * Thrown when we cannot determine anything more specific than "something went wrong while processing an SQL script":
+ * for example, a {@link io.r2dbc.spi.R2dbcException} from R2DBC that we cannot pinpoint more precisely.
+ *
+ * @author Mark Paluch
+ */
+public class UncategorizedScriptException extends ScriptException {
+
+ private static final long serialVersionUID = -3196706179230349902L;
+
+ /**
+ * Creates a new {@link UncategorizedScriptException}.
+ *
+ * @param message detailed message.
+ */
+ public UncategorizedScriptException(String message) {
+ super(message);
+ }
+
+ /**
+ * Creates a new {@link UncategorizedScriptException}.
+ *
+ * @param message detailed message.
+ * @param cause the root cause.
+ */
+ public UncategorizedScriptException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/package-info.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/package-info.java
new file mode 100644
index 000000000..113c6ba2c
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/init/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Provides extensible support for initializing databases through scripts.
+ */
+@org.springframework.lang.NonNullApi
+@org.springframework.lang.NonNullFields
+package org.springframework.data.r2dbc.connectionfactory.init;
diff --git a/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/AbstractDatabaseInitializationTests.java b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/AbstractDatabaseInitializationTests.java
new file mode 100644
index 000000000..35c49169a
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/AbstractDatabaseInitializationTests.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import io.r2dbc.spi.ConnectionFactory;
+import reactor.test.StepVerifier;
+
+import org.junit.Test;
+
+import org.springframework.core.io.ClassRelativeResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.data.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() {
+ DatabasePopulatorUtils.execute(databasePopulator, 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.execute("select count(0) from users where last_name = :name") //
+ .bind("name", lastName) //
+ .map((row, metadata) -> row.get(0)) //
+ .first() //
+ .map(it -> ((Number) it).intValue()) //
+ .as(StepVerifier::create) //
+ .expectNext(1).as("Did not find user with last name [" + lastName + "].") //
+ .verifyComplete();
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/CompositeDatabasePopulatorTests.java b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/CompositeDatabasePopulatorTests.java
new file mode 100644
index 000000000..1fa6a67d7
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/CompositeDatabasePopulatorTests.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import static org.mockito.Mockito.*;
+
+import io.r2dbc.spi.Connection;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * 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);
+
+ @Before
+ 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 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);
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/H2DatabasePopulatorIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/H2DatabasePopulatorIntegrationTests.java
new file mode 100644
index 000000000..fc388b0f9
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/H2DatabasePopulatorIntegrationTests.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import io.r2dbc.spi.ConnectionFactories;
+import io.r2dbc.spi.ConnectionFactory;
+import reactor.test.StepVerifier;
+
+import java.util.UUID;
+
+import org.junit.Test;
+
+/**
+ * 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");
+
+ DatabasePopulatorUtils.execute(databasePopulator, connectionFactory).as(StepVerifier::create).verifyComplete();
+
+ assertUsersDatabaseCreated(connectionFactory, "White");
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/ResourceDatabasePopulatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/ResourceDatabasePopulatorUnitTests.java
new file mode 100644
index 000000000..d5594572a
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/ResourceDatabasePopulatorUnitTests.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import org.junit.Test;
+
+import org.springframework.core.io.Resource;
+
+/**
+ * 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.size()).isEqualTo(1);
+ }
+
+ @Test
+ public void constructWithMultipleResources() {
+
+ ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(script1, script2);
+ assertThat(databasePopulator.scripts.size()).isEqualTo(2);
+ }
+
+ @Test
+ public void constructWithMultipleResourcesAndThenAddScript() {
+
+ ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(script1, script2);
+ assertThat(databasePopulator.scripts.size()).isEqualTo(2);
+
+ databasePopulator.addScript(script3);
+ assertThat(databasePopulator.scripts.size()).isEqualTo(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.size()).isEqualTo(0);
+
+ databasePopulator.setScripts(script1, script2);
+ assertThat(databasePopulator.scripts.size()).isEqualTo(2);
+
+ databasePopulator.addScript(script3);
+ assertThat(databasePopulator.scripts.size()).isEqualTo(3);
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptUtilsUnitTests.java
new file mode 100644
index 000000000..111c811bd
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/connectionfactory/init/ScriptUtilsUnitTests.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright 2019 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.data.r2dbc.connectionfactory.init;
+
+import static org.assertj.core.api.Assertions.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.assertj.core.util.Strings;
+import org.junit.Test;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.buffer.DefaultDataBufferFactory;
+import org.springframework.core.io.support.EncodedResource;
+
+/**
+ * Unit tests for {@link ScriptUtils}.
+ *
+ * @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 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 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 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 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 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 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 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 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 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();
+ }
+}
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-schema-failed-drop-comments.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-schema-failed-drop-comments.sql
new file mode 100644
index 000000000..8ce888986
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-schema-failed-drop-comments.sql
@@ -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);
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-schema.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-schema.sql
new file mode 100644
index 000000000..4de3841ec
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-schema.sql
@@ -0,0 +1,3 @@
+drop table users if exists;
+
+create table users (last_name varchar(50) not null);
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-endings.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-endings.sql
new file mode 100644
index 000000000..78a82189b
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-endings.sql
@@ -0,0 +1,2 @@
+insert into users (last_name) values ('Heisenberg')@@
+insert into users (last_name) values ('Jesse')@@
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-escaped-literal.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-escaped-literal.sql
new file mode 100644
index 000000000..3ba33ccac
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-escaped-literal.sql
@@ -0,0 +1 @@
+insert into users (last_name) values ('''Heisenberg''');
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-h2.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-h2.sql
new file mode 100644
index 000000000..a62e920a2
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-h2.sql
@@ -0,0 +1 @@
+INSERT INTO users(first_name, last_name) values('Walter', 'White');
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-multi-newline.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-multi-newline.sql
new file mode 100644
index 000000000..6239f6adc
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-multi-newline.sql
@@ -0,0 +1,5 @@
+insert into users (last_name)
+values ('Walter')
+
+insert into users (last_name)
+values ('Jesse')
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-multiple.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-multiple.sql
new file mode 100644
index 000000000..ea185476c
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-multiple.sql
@@ -0,0 +1,2 @@
+insert into users (last_name) values ('Heisenberg');
+insert into users (last_name) values ('Jesse');
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-mysql-escaped-literal.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-mysql-escaped-literal.sql
new file mode 100644
index 000000000..dae44d40e
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data-mysql-escaped-literal.sql
@@ -0,0 +1 @@
+insert into users (last_name) values ('\$Heisenberg\$');
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data.sql
new file mode 100644
index 000000000..85673705f
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/db-test-data.sql
@@ -0,0 +1 @@
+insert into users (last_name) values ('Heisenberg');
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-comments-and-leading-tabs.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-comments-and-leading-tabs.sql
new file mode 100644
index 000000000..ddb67f0cf
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-comments-and-leading-tabs.sql
@@ -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);
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-comments.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-comments.sql
new file mode 100644
index 000000000..82483ca4d
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-comments.sql
@@ -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
+);--
\ No newline at end of file
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-multi-line-comments.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-multi-line-comments.sql
new file mode 100644
index 000000000..8cfa6d438
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-multi-line-comments.sql
@@ -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
+);--
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-multi-line-nested-comments.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-multi-line-nested-comments.sql
new file mode 100644
index 000000000..5a3d3a136
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/test-data-with-multi-line-nested-comments.sql
@@ -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
+);--
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/users-data.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/users-data.sql
new file mode 100644
index 000000000..a6aa78385
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/users-data.sql
@@ -0,0 +1,3 @@
+INSERT INTO
+users(first_name, last_name)
+values('Sam', 'Brannen');
diff --git a/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/users-schema.sql b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/users-schema.sql
new file mode 100644
index 000000000..80ffe23da
--- /dev/null
+++ b/src/test/resources/org/springframework/data/r2dbc/connectionfactory/init/users-schema.sql
@@ -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
+);