Browse Source
We now provide DatabasePopulator and ScriptUtils to run SQL scripts using R2DBC Connections to initialize and clean up databases.pull/1188/head
33 changed files with 2086 additions and 1 deletions
@ -0,0 +1,38 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,99 @@
@@ -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<DatabasePopulator> 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<DatabasePopulator> 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<Void> populate(Connection connection) throws ScriptException { |
||||
|
||||
Assert.notNull(connection, "Connection must not be null!"); |
||||
|
||||
return Flux.fromIterable(this.populators).concatMap(it -> it.populate(connection)).then(); |
||||
} |
||||
} |
||||
@ -0,0 +1,110 @@
@@ -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. |
||||
* <p/> |
||||
* 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); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,42 @@
@@ -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<Void> populate(Connection connection) throws ScriptException; |
||||
} |
||||
@ -0,0 +1,58 @@
@@ -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<Void> 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); |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,280 @@
@@ -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. |
||||
* <ul> |
||||
* <li>Call {@link #addScript} to add a single SQL script location. |
||||
* <li>Call {@link #addScripts} to add multiple SQL script locations. |
||||
* <li>Consult the setter methods in this class for further configuration options. |
||||
* <li>Call {@link #populate} or {@link #execute} to initialize or clean up the database using the configured scripts. |
||||
* </ul> |
||||
* |
||||
* @author Mark Paluch |
||||
* @see DatabasePopulatorUtils |
||||
* @see ScriptUtils |
||||
*/ |
||||
public class ResourceDatabasePopulator implements DatabasePopulator { |
||||
|
||||
List<Resource> 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 <em>empty</em> 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. |
||||
* <p/> |
||||
* 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. |
||||
* <p/> |
||||
* 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. |
||||
* <p/> |
||||
* 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. |
||||
* <p/> |
||||
* 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. |
||||
* <p/> |
||||
* 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. |
||||
* <p/> |
||||
* This is useful for a non-embedded database whose SQL dialect does not support an {@code IF EXISTS} clause in a |
||||
* {@code DROP} statement. |
||||
* <p/> |
||||
* 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. |
||||
* <p/> |
||||
* 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<Void> 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}. |
||||
* <p/> |
||||
* 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<Void> execute(ConnectionFactory connectionFactory) throws ScriptException { |
||||
return DatabasePopulatorUtils.execute(this, connectionFactory); |
||||
} |
||||
} |
||||
@ -0,0 +1,46 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,55 @@
@@ -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 ? "<unknown>" : resource), message); |
||||
} |
||||
} |
||||
@ -0,0 +1,54 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,537 @@
@@ -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. |
||||
* <p/> |
||||
* 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"}. |
||||
* <p/> |
||||
* 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 ^^^"}. |
||||
* <p/> |
||||
* 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 <em>virtual</em> |
||||
* 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: <code>"*/"</code>. |
||||
*/ |
||||
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}. |
||||
* <p/> |
||||
* 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 <em>start</em> and <em>end</em> 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<String> 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}. |
||||
* <p/> |
||||
* 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 <em>start</em> and <em>end</em> 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<String> 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}. |
||||
* <p/> |
||||
* 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 <em>start</em> block comment delimiter. Must not be {@literal null} or empty. |
||||
* @param blockCommentEndDelimiter the <em>end</em> 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<String> 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<String> 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. |
||||
* <p/> |
||||
* Lines <em>beginning</em> 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 <em>end</em> block comment delimiter. |
||||
* @return a {@link Mono} of {@link String} containing the script lines that completes once the resource was loaded. |
||||
*/ |
||||
private static Mono<String> 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. |
||||
* <p/> |
||||
* Lines <em>beginning</em> 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 <em>end</em> 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. |
||||
* <p/> |
||||
* Statement separators and comments will be removed before executing individual statements within the supplied |
||||
* script. |
||||
* <p/> |
||||
* <strong>Warning</strong>: this method does <em>not</em> 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<Void> 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. |
||||
* <p/> |
||||
* Statement separators and comments will be removed before executing individual statements within the supplied |
||||
* script. |
||||
* <p/> |
||||
* <strong>Warning</strong>: this method does <em>not</em> 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<Void> 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. |
||||
* <p/> |
||||
* Statement separators and comments will be removed before executing individual statements within the supplied |
||||
* script. |
||||
* <p/> |
||||
* <strong>Warning</strong>: this method does <em>not</em> 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 <em>start</em> block comment delimiter. |
||||
* @param blockCommentEndDelimiter the <em>end</em> 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<Void> 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<String> script = readScript(resource, dataBufferFactory, commentPrefix, separator, blockCommentEndDelimiter) |
||||
.onErrorMap(IOException.class, ex -> new CannotReadScriptException(resource, ex)); |
||||
|
||||
AtomicInteger statementNumber = new AtomicInteger(); |
||||
|
||||
Flux<Void> 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<String> 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<String> 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<Long> 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(); |
||||
} |
||||
} |
||||
@ -0,0 +1,46 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,6 @@
@@ -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; |
||||
@ -0,0 +1,134 @@
@@ -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(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,112 @@
@@ -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<DatabasePopulator> populators = new LinkedHashSet<>(); |
||||
populators.add(mockedDatabasePopulator1); |
||||
populators.add(mockedDatabasePopulator2); |
||||
|
||||
CompositeDatabasePopulator populator = new CompositeDatabasePopulator(populators); |
||||
populator.populate(mockedConnection).as(StepVerifier::create).verifyComplete(); |
||||
|
||||
verify(mockedDatabasePopulator1, times(1)).populate(mockedConnection); |
||||
verify(mockedDatabasePopulator2, times(1)).populate(mockedConnection); |
||||
} |
||||
} |
||||
@ -0,0 +1,57 @@
@@ -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"); |
||||
} |
||||
} |
||||
@ -0,0 +1,110 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,205 @@
@@ -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<String> statements = new ArrayList<>(); |
||||
ScriptUtils.splitSqlScript(script, ";", statements); |
||||
|
||||
assertThat(statements).hasSize(3).containsSequence(cleanedStatement1, cleanedStatement2, cleanedStatement3); |
||||
} |
||||
|
||||
@Test |
||||
public void splitSqlScriptDelimitedWithNewLine() { |
||||
|
||||
String statement1 = "insert into customer (id, name) values (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')"; |
||||
String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; |
||||
String statement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; |
||||
|
||||
String script = Strings.join(statement1, statement2, statement3).with("\n"); |
||||
|
||||
List<String> statements = new ArrayList<>(); |
||||
ScriptUtils.splitSqlScript(script, "\n", statements); |
||||
|
||||
assertThat(statements).hasSize(3).containsSequence(statement1, statement2, statement3); |
||||
} |
||||
|
||||
@Test |
||||
public void splitSqlScriptDelimitedWithNewLineButDefaultDelimiterSpecified() { |
||||
|
||||
String statement1 = "do something"; |
||||
String statement2 = "do something else"; |
||||
|
||||
char delim = '\n'; |
||||
String script = statement1 + delim + statement2 + delim; |
||||
|
||||
List<String> statements = new ArrayList<>(); |
||||
|
||||
ScriptUtils.splitSqlScript(script, ScriptUtils.DEFAULT_STATEMENT_SEPARATOR, statements); |
||||
|
||||
assertThat(statements).hasSize(1).contains(script.replace('\n', ' ')); |
||||
} |
||||
|
||||
@Test |
||||
public void splitScriptWithSingleQuotesNestedInsideDoubleQuotes() { |
||||
|
||||
String statement1 = "select '1' as \"Dogbert's owner's\" from dual"; |
||||
String statement2 = "select '2' as \"Dilbert's\" from dual"; |
||||
|
||||
char delim = ';'; |
||||
String script = statement1 + delim + statement2 + delim; |
||||
|
||||
List<String> statements = new ArrayList<>(); |
||||
ScriptUtils.splitSqlScript(script, ';', statements); |
||||
|
||||
assertThat(statements).hasSize(2).containsSequence(statement1, statement2); |
||||
} |
||||
|
||||
@Test |
||||
public void readAndSplitScriptWithMultipleNewlinesAsSeparator() { |
||||
|
||||
String script = readScript("db-test-data-multi-newline.sql"); |
||||
List<String> statements = new ArrayList<>(); |
||||
ScriptUtils.splitSqlScript(script, "\n\n", statements); |
||||
|
||||
String statement1 = "insert into users (last_name) values ('Walter')"; |
||||
String statement2 = "insert into users (last_name) values ('Jesse')"; |
||||
|
||||
assertThat(statements.size()).as("wrong number of statements").isEqualTo(2); |
||||
assertThat(statements.get(0)).as("statement 1 not split correctly").isEqualTo(statement1); |
||||
assertThat(statements.get(1)).as("statement 2 not split correctly").isEqualTo(statement2); |
||||
} |
||||
|
||||
@Test |
||||
public void readAndSplitScriptContainingComments() { |
||||
String script = readScript("test-data-with-comments.sql"); |
||||
splitScriptContainingComments(script); |
||||
} |
||||
|
||||
@Test |
||||
public void readAndSplitScriptContainingCommentsWithWindowsLineEnding() { |
||||
String script = readScript("test-data-with-comments.sql").replaceAll("\n", "\r\n"); |
||||
splitScriptContainingComments(script); |
||||
} |
||||
|
||||
private void splitScriptContainingComments(String script) { |
||||
|
||||
List<String> statements = new ArrayList<>(); |
||||
ScriptUtils.splitSqlScript(script, ';', statements); |
||||
|
||||
String statement1 = "insert into customer (id, name) values (1, 'Rod; Johnson'), (2, 'Adrian Collier')"; |
||||
String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; |
||||
String statement3 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)"; |
||||
String statement4 = "INSERT INTO persons( person_id , name) VALUES( 1 , 'Name' )"; |
||||
|
||||
assertThat(statements).hasSize(4).containsSequence(statement1, statement2, statement3, statement4); |
||||
} |
||||
|
||||
@Test |
||||
public void readAndSplitScriptContainingCommentsWithLeadingTabs() { |
||||
|
||||
String script = readScript("test-data-with-comments-and-leading-tabs.sql"); |
||||
List<String> statements = new ArrayList<>(); |
||||
ScriptUtils.splitSqlScript(script, ';', statements); |
||||
|
||||
String statement1 = "insert into customer (id, name) values (1, 'Walter White')"; |
||||
String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2013-06-08', 1)"; |
||||
String statement3 = "insert into orders(id, order_date, customer_id) values (2, '2013-06-08', 1)"; |
||||
|
||||
assertThat(statements).hasSize(3).containsSequence(statement1, statement2, statement3); |
||||
} |
||||
|
||||
@Test |
||||
public void readAndSplitScriptContainingMultiLineComments() { |
||||
|
||||
String script = readScript("test-data-with-multi-line-comments.sql"); |
||||
List<String> statements = new ArrayList<>(); |
||||
ScriptUtils.splitSqlScript(script, ';', statements); |
||||
|
||||
String statement1 = "INSERT INTO users(first_name, last_name) VALUES('Walter', 'White')"; |
||||
String statement2 = "INSERT INTO users(first_name, last_name) VALUES( 'Jesse' , 'Pinkman' )"; |
||||
|
||||
assertThat(statements).hasSize(2).containsSequence(statement1, statement2); |
||||
} |
||||
|
||||
@Test |
||||
public void readAndSplitScriptContainingMultiLineNestedComments() { |
||||
|
||||
String script = readScript("test-data-with-multi-line-nested-comments.sql"); |
||||
List<String> statements = new ArrayList<>(); |
||||
ScriptUtils.splitSqlScript(script, ';', statements); |
||||
|
||||
String statement1 = "INSERT INTO users(first_name, last_name) VALUES('Walter', 'White')"; |
||||
String statement2 = "INSERT INTO users(first_name, last_name) VALUES( 'Jesse' , 'Pinkman' )"; |
||||
|
||||
assertThat(statements).hasSize(2).containsSequence(statement1, statement2); |
||||
} |
||||
|
||||
@Test |
||||
public void containsDelimiters() { |
||||
|
||||
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n select ';'", ";")).isFalse(); |
||||
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1; select 2", ";")).isTrue(); |
||||
|
||||
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1; select '\\n\n';", "\n")).isFalse(); |
||||
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n select 2", "\n")).isTrue(); |
||||
|
||||
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n select 2", "\n\n")).isFalse(); |
||||
assertThat(ScriptUtils.containsSqlScriptDelimiters("select 1\n\n select 2", "\n\n")).isTrue(); |
||||
|
||||
// MySQL style escapes '\\'
|
||||
assertThat( |
||||
ScriptUtils.containsSqlScriptDelimiters("insert into users(first_name, last_name)\nvalues('a\\\\', 'b;')", ";")) |
||||
.isFalse(); |
||||
assertThat(ScriptUtils.containsSqlScriptDelimiters( |
||||
"insert into users(first_name, last_name)\nvalues('Charles', 'd\\'Artagnan'); select 1;", ";")).isTrue(); |
||||
} |
||||
|
||||
private String readScript(String path) { |
||||
EncodedResource resource = new EncodedResource(new ClassPathResource(path, getClass())); |
||||
return ScriptUtils.readScript(resource, new DefaultDataBufferFactory()).block(); |
||||
} |
||||
} |
||||
@ -0,0 +1,5 @@
@@ -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); |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
drop table users if exists; |
||||
|
||||
create table users (last_name varchar(50) not null); |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
insert into users (last_name) values ('Heisenberg')@@ |
||||
insert into users (last_name) values ('Jesse')@@ |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
insert into users (last_name) values ('''Heisenberg'''); |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
INSERT INTO users(first_name, last_name) values('Walter', 'White'); |
||||
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
insert into users (last_name) |
||||
values ('Walter') |
||||
|
||||
insert into users (last_name) |
||||
values ('Jesse') |
||||
@ -0,0 +1,2 @@
@@ -0,0 +1,2 @@
|
||||
insert into users (last_name) values ('Heisenberg'); |
||||
insert into users (last_name) values ('Jesse'); |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
insert into users (last_name) values ('\$Heisenberg\$'); |
||||
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
insert into users (last_name) values ('Heisenberg'); |
||||
@ -0,0 +1,9 @@
@@ -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); |
||||
@ -0,0 +1,16 @@
@@ -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 |
||||
);-- |
||||
@ -0,0 +1,17 @@
@@ -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 |
||||
);-- |
||||
@ -0,0 +1,23 @@
@@ -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 |
||||
);-- |
||||
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
INSERT INTO |
||||
users(first_name, last_name) |
||||
values('Sam', 'Brannen'); |
||||
Loading…
Reference in new issue