Browse Source

Logically reorganize methods in ScriptUtils

See gh-26947
pull/26952/head
Sam Brannen 5 years ago
parent
commit
4c642ccdcd
  1. 757
      spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java
  2. 533
      spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java

757
spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java

@ -35,7 +35,7 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
* Generic utility methods for working with SQL scripts. * Generic utility methods for working with SQL scripts in conjunction with JDBC.
* *
* <p>Mainly for internal use within the framework. * <p>Mainly for internal use within the framework.
* *
@ -50,6 +50,7 @@ import org.springframework.util.StringUtils;
* @author Nicolas Debeissat * @author Nicolas Debeissat
* @author Phillip Webb * @author Phillip Webb
* @since 4.0.3 * @since 4.0.3
* @see org.springframework.r2dbc.connection.init.ScriptUtils
*/ */
public abstract class ScriptUtils { public abstract class ScriptUtils {
@ -102,197 +103,207 @@ public abstract class ScriptUtils {
/** /**
* Split an SQL script into separate statements delimited by the provided * Execute the given SQL script using default settings for statement
* separator character. Each individual statement will be added to the * separators, comment delimiters, and exception handling flags.
* provided {@code List}. * <p>Statement separators and comments will be removed before executing
* <p>Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the * individual statements within the supplied script.
* comment prefix; any text beginning with the comment prefix and extending to * <p><strong>Warning</strong>: this method does <em>not</em> release the
* the end of the line will be omitted from the output. Similarly, * provided {@link Connection}.
* {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and * @param connection the JDBC connection to use to execute the script; already
* {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the * configured and ready to use
* <em>start</em> and <em>end</em> block comment delimiters: any text enclosed * @param resource the resource to load the SQL script from; encoded with the
* in a block comment will be omitted from the output. In addition, multiple * current platform's default encoding
* adjacent whitespace characters will be collapsed into a single space. * @throws ScriptException if an error occurred while executing the SQL script
* @param script the SQL script * @see #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String)
* @param separator character separating each statement (typically a ';') * @see #DEFAULT_STATEMENT_SEPARATOR
* @param statements the list that will contain the individual statements * @see #DEFAULT_COMMENT_PREFIX
* @throws ScriptException if an error occurred while splitting the SQL script * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
* @see #splitSqlScript(String, String, List) * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
* @see #splitSqlScript(EncodedResource, String, String, String, String, String, List) * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
* @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/ */
public static void splitSqlScript(String script, char separator, List<String> statements) throws ScriptException { public static void executeSqlScript(Connection connection, Resource resource) throws ScriptException {
splitSqlScript(script, String.valueOf(separator), statements); executeSqlScript(connection, new EncodedResource(resource));
} }
/** /**
* Split an SQL script into separate statements delimited by the provided * Execute the given SQL script using default settings for statement
* separator string. Each individual statement will be added to the * separators, comment delimiters, and exception handling flags.
* provided {@code List}. * <p>Statement separators and comments will be removed before executing
* <p>Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the * individual statements within the supplied script.
* comment prefix; any text beginning with the comment prefix and extending to * <p><strong>Warning</strong>: this method does <em>not</em> release the
* the end of the line will be omitted from the output. Similarly, * provided {@link Connection}.
* {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and * @param connection the JDBC connection to use to execute the script; already
* {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the * configured and ready to use
* <em>start</em> and <em>end</em> block comment delimiters: any text enclosed * @param resource the resource (potentially associated with a specific encoding)
* in a block comment will be omitted from the output. In addition, multiple * to load the SQL script from
* adjacent whitespace characters will be collapsed into a single space. * @throws ScriptException if an error occurred while executing the SQL script
* @param script the SQL script * @see #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String)
* @param separator text separating each statement * @see #DEFAULT_STATEMENT_SEPARATOR
* (typically a ';' or newline character) * @see #DEFAULT_COMMENT_PREFIX
* @param statements the list that will contain the individual statements * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
* @throws ScriptException if an error occurred while splitting the SQL script * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
* @see #splitSqlScript(String, char, List) * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
* @see #splitSqlScript(EncodedResource, String, String, String, String, String, List) * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/ */
public static void splitSqlScript(String script, String separator, List<String> statements) throws ScriptException { public static void executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException {
splitSqlScript(null, script, separator, DEFAULT_COMMENT_PREFIX, DEFAULT_BLOCK_COMMENT_START_DELIMITER, executeSqlScript(connection, resource, false, false, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR,
DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements); DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER);
} }
/** /**
* Split an SQL script into separate statements delimited by the provided * Execute the given SQL script.
* separator string. Each individual statement will be added to the provided * <p>Statement separators and comments will be removed before executing
* {@code List}. * individual statements within the supplied script.
* <p>Within the script, the provided {@code commentPrefix} will be honored: * <p><strong>Warning</strong>: this method does <em>not</em> release the
* any text beginning with the comment prefix and extending to the end of the * provided {@link Connection}.
* line will be omitted from the output. Similarly, the provided * @param connection the JDBC connection to use to execute the script; already
* {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter} * configured and ready to use
* delimiters will be honored: any text enclosed in a block comment will be * @param resource the resource (potentially associated with a specific encoding)
* omitted from the output. In addition, multiple adjacent whitespace characters * to load the SQL script from
* will be collapsed into a single space. * @param continueOnError whether or not to continue without throwing an exception
* @param resource the resource from which the script was read * in the event of an error
* @param script the SQL script * @param ignoreFailedDrops whether or not to continue in the event of specifically
* @param separator text separating each statement * an error on a {@code DROP} statement
* (typically a ';' or newline character) * @param commentPrefix the prefix that identifies single-line comments in the
* @param commentPrefix the prefix that identifies SQL line comments * SQL script (typically "--")
* (typically "--") * @param separator the script statement separator; defaults to
* @param blockCommentStartDelimiter the <em>start</em> block comment delimiter; * {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and falls back to
* never {@code null} or empty * {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort; may be set to
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter; * {@value #EOF_STATEMENT_SEPARATOR} to signal that the script contains a
* never {@code null} or empty * single statement without a separator
* @param statements the list that will contain the individual statements * @param blockCommentStartDelimiter the <em>start</em> block comment delimiter
* @throws ScriptException if an error occurred while splitting the SQL script * @param blockCommentEndDelimiter the <em>end</em> block comment delimiter
* @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.jdbc.datasource.DataSourceUtils#getConnection
* @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/ */
public static void splitSqlScript(@Nullable EncodedResource resource, String script, public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError,
String separator, String commentPrefix, String blockCommentStartDelimiter, boolean ignoreFailedDrops, String commentPrefix, @Nullable String separator,
String blockCommentEndDelimiter, List<String> statements) throws ScriptException { String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty"); executeSqlScript(connection, resource, continueOnError, ignoreFailedDrops,
splitSqlScript(resource, script, separator, new String[] { commentPrefix }, new String[] { commentPrefix }, separator, blockCommentStartDelimiter,
blockCommentStartDelimiter, blockCommentEndDelimiter, statements); blockCommentEndDelimiter);
} }
/** /**
* Split an SQL script into separate statements delimited by the provided * Execute the given SQL script.
* separator string. Each individual statement will be added to the provided * <p>Statement separators and comments will be removed before executing
* {@code List}. * individual statements within the supplied script.
* <p>Within the script, the provided {@code commentPrefixes} will be honored: * <p><strong>Warning</strong>: this method does <em>not</em> release the
* any text beginning with one of the comment prefixes and extending to the * provided {@link Connection}.
* end of the line will be omitted from the output. Similarly, the provided * @param connection the JDBC connection to use to execute the script; already
* {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter} * configured and ready to use
* delimiters will be honored: any text enclosed in a block comment will be * @param resource the resource (potentially associated with a specific encoding)
* omitted from the output. In addition, multiple adjacent whitespace characters * to load the SQL script from
* will be collapsed into a single space. * @param continueOnError whether or not to continue without throwing an exception
* @param resource the resource from which the script was read * in the event of an error
* @param script the SQL script * @param ignoreFailedDrops whether or not to continue in the event of specifically
* @param separator text separating each statement * an error on a {@code DROP} statement
* (typically a ';' or newline character) * @param commentPrefixes the prefixes that identify single-line comments in the
* @param commentPrefixes the prefixes that identify SQL line comments * SQL script (typically "--")
* (typically "--") * @param separator the script statement separator; defaults to
* @param blockCommentStartDelimiter the <em>start</em> block comment delimiter; * {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and falls back to
* never {@code null} or empty * {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort; may be set to
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter; * {@value #EOF_STATEMENT_SEPARATOR} to signal that the script contains a
* never {@code null} or empty * single statement without a separator
* @param statements the list that will contain the individual statements * @param blockCommentStartDelimiter the <em>start</em> block comment delimiter
* @throws ScriptException if an error occurred while splitting the SQL script * @param blockCommentEndDelimiter the <em>end</em> block comment delimiter
* @throws ScriptException if an error occurred while executing the SQL script
* @since 5.2 * @since 5.2
* @see #DEFAULT_STATEMENT_SEPARATOR
* @see #FALLBACK_STATEMENT_SEPARATOR
* @see #EOF_STATEMENT_SEPARATOR
* @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
* @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/ */
public static void splitSqlScript(@Nullable EncodedResource resource, String script, public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError,
String separator, String[] commentPrefixes, String blockCommentStartDelimiter, boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator,
String blockCommentEndDelimiter, List<String> statements) throws ScriptException { String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
Assert.hasText(script, "'script' must not be null or empty");
Assert.notNull(separator, "'separator' must not be null");
Assert.notEmpty(commentPrefixes, "'commentPrefixes' must not be null or empty");
for (String commentPrefix : commentPrefixes) {
Assert.hasText(commentPrefix, "'commentPrefixes' must not contain null or empty elements");
}
Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty");
Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty");
StringBuilder sb = new StringBuilder(); try {
boolean inSingleQuote = false; if (logger.isDebugEnabled()) {
boolean inDoubleQuote = false; logger.debug("Executing SQL script from " + resource);
boolean inEscape = false; }
long startTime = System.currentTimeMillis();
for (int i = 0; i < script.length(); i++) { String script;
char c = script.charAt(i); try {
if (inEscape) { script = readScript(resource, separator, commentPrefixes, blockCommentEndDelimiter);
inEscape = false;
sb.append(c);
continue;
} }
// MySQL style escapes catch (IOException ex) {
if (c == '\\') { throw new CannotReadScriptException(resource, ex);
inEscape = true;
sb.append(c);
continue;
} }
if (!inDoubleQuote && (c == '\'')) {
inSingleQuote = !inSingleQuote; if (separator == null) {
separator = DEFAULT_STATEMENT_SEPARATOR;
} }
else if (!inSingleQuote && (c == '"')) { if (!EOF_STATEMENT_SEPARATOR.equals(separator) &&
inDoubleQuote = !inDoubleQuote; !containsStatementSeparator(resource, script, separator, commentPrefixes,
blockCommentStartDelimiter, blockCommentEndDelimiter)) {
separator = FALLBACK_STATEMENT_SEPARATOR;
} }
if (!inSingleQuote && !inDoubleQuote) {
if (script.startsWith(separator, i)) { List<String> statements = new ArrayList<>();
// We've reached the end of the current statement splitSqlScript(resource, script, separator, commentPrefixes, blockCommentStartDelimiter,
if (sb.length() > 0) { blockCommentEndDelimiter, statements);
statements.add(sb.toString());
sb = new StringBuilder(); int stmtNumber = 0;
} Statement stmt = connection.createStatement();
i += separator.length() - 1; try {
continue; for (String statement : statements) {
} stmtNumber++;
else if (startsWithAny(script, commentPrefixes, i)) { try {
// Skip over any content from the start of the comment to the EOL stmt.execute(statement);
int indexOfNextNewline = script.indexOf('\n', i); int rowsAffected = stmt.getUpdateCount();
if (indexOfNextNewline > i) { if (logger.isDebugEnabled()) {
i = indexOfNextNewline; logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
continue; SQLWarning warningToLog = stmt.getWarnings();
while (warningToLog != null) {
logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
"', error code '" + warningToLog.getErrorCode() +
"', message [" + warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
}
}
} }
else { catch (SQLException ex) {
// If there's no EOL, we must be at the end of the script, so stop here. boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
break; if (continueOnError || (dropStatement && ignoreFailedDrops)) {
if (logger.isDebugEnabled()) {
logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, stmtNumber, resource), ex);
}
}
else {
throw new ScriptStatementFailedException(statement, stmtNumber, resource, ex);
}
} }
} }
else if (script.startsWith(blockCommentStartDelimiter, i)) { }
// Skip over any block comments finally {
int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i); try {
if (indexOfCommentEnd > i) { stmt.close();
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') { catch (Throwable ex) {
// Avoid multiple adjacent whitespace characters logger.trace("Could not close JDBC Statement", ex);
if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
c = ' ';
}
else {
continue;
}
} }
} }
sb.append(c);
}
if (StringUtils.hasText(sb)) { long elapsedTime = System.currentTimeMillis() - startTime;
statements.add(sb.toString()); if (logger.isDebugEnabled()) {
logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
}
}
catch (Exception ex) {
if (ex instanceof ScriptException) {
throw (ScriptException) ex;
}
throw new UncategorizedScriptException(
"Failed to execute database script from resource [" + resource + "]", ex);
} }
} }
@ -312,7 +323,7 @@ public abstract class ScriptUtils {
* @throws IOException in case of I/O errors * @throws IOException in case of I/O errors
*/ */
static String readScript(EncodedResource resource, @Nullable String separator, static String readScript(EncodedResource resource, @Nullable String separator,
@Nullable String[] commentPrefixes, @Nullable String blockCommentEndDelimiter) throws IOException { String[] commentPrefixes, String blockCommentEndDelimiter) throws IOException {
try (LineNumberReader lnr = new LineNumberReader(resource.getReader())) { try (LineNumberReader lnr = new LineNumberReader(resource.getReader())) {
return readScript(lnr, commentPrefixes, separator, blockCommentEndDelimiter); return readScript(lnr, commentPrefixes, separator, blockCommentEndDelimiter);
@ -393,15 +404,6 @@ public abstract class ScriptUtils {
} }
} }
private static boolean startsWithAny(String script, String[] prefixes, int offset) {
for (String prefix : prefixes) {
if (script.startsWith(prefix, offset)) {
return true;
}
}
return false;
}
/** /**
* Determine if the provided SQL script contains the specified delimiter. * Determine if the provided SQL script contains the specified delimiter.
* <p>This method is intended to be used to find the string delimiting each * <p>This method is intended to be used to find the string delimiting each
@ -441,10 +443,185 @@ public abstract class ScriptUtils {
* (typically <code>"*&#47;"</code>) * (typically <code>"*&#47;"</code>)
* @since 5.2.16 * @since 5.2.16
*/ */
private static boolean containsStatementSeparator(@Nullable EncodedResource resource, String script, private static boolean containsStatementSeparator(@Nullable EncodedResource resource, String script,
String separator, String[] commentPrefixes, String blockCommentStartDelimiter,
String blockCommentEndDelimiter) throws ScriptException {
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;
continue;
}
// MySQL style escapes
if (c == '\\') {
inEscape = true;
continue;
}
if (!inDoubleQuote && (c == '\'')) {
inSingleQuote = !inSingleQuote;
}
else if (!inSingleQuote && (c == '"')) {
inDoubleQuote = !inDoubleQuote;
}
if (!inSingleQuote && !inDoubleQuote) {
if (script.startsWith(separator, i)) {
return true;
}
else if (startsWithAny(script, commentPrefixes, 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);
}
}
}
}
return false;
}
/**
* Split an SQL script into separate statements delimited by the provided
* separator character. Each individual statement will be added to the
* provided {@code 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)
*/
public 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 {@code 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)
*/
public 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
* {@code 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;
* never {@code null} or empty
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter;
* never {@code null} or empty
* @param statements the list that will contain the individual statements
* @throws ScriptException if an error occurred while splitting the SQL script
*/
public static void splitSqlScript(@Nullable EncodedResource resource, String script,
String separator, String commentPrefix, String blockCommentStartDelimiter,
String blockCommentEndDelimiter, List<String> statements) throws ScriptException {
Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty");
splitSqlScript(resource, script, separator, new String[] { commentPrefix },
blockCommentStartDelimiter, blockCommentEndDelimiter, statements);
}
/**
* Split an SQL script into separate statements delimited by the provided
* separator string. Each individual statement will be added to the provided
* {@code List}.
* <p>Within the script, the provided {@code commentPrefixes} will be honored:
* any text beginning with one of the comment prefixes 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 commentPrefixes the prefixes that identify SQL line comments
* (typically "--")
* @param blockCommentStartDelimiter the <em>start</em> block comment delimiter;
* never {@code null} or empty
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter;
* never {@code null} or empty
* @param statements the list that will contain the individual statements
* @throws ScriptException if an error occurred while splitting the SQL script
* @since 5.2
*/
public static void splitSqlScript(@Nullable EncodedResource resource, String script,
String separator, String[] commentPrefixes, String blockCommentStartDelimiter, String separator, String[] commentPrefixes, String blockCommentStartDelimiter,
String blockCommentEndDelimiter) throws ScriptException { 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.notEmpty(commentPrefixes, "'commentPrefixes' must not be null or empty");
for (String commentPrefix : commentPrefixes) {
Assert.hasText(commentPrefix, "'commentPrefixes' must not contain null or empty elements");
}
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 inSingleQuote = false;
boolean inDoubleQuote = false; boolean inDoubleQuote = false;
boolean inEscape = false; boolean inEscape = false;
@ -453,11 +630,13 @@ public abstract class ScriptUtils {
char c = script.charAt(i); char c = script.charAt(i);
if (inEscape) { if (inEscape) {
inEscape = false; inEscape = false;
sb.append(c);
continue; continue;
} }
// MySQL style escapes // MySQL style escapes
if (c == '\\') { if (c == '\\') {
inEscape = true; inEscape = true;
sb.append(c);
continue; continue;
} }
if (!inDoubleQuote && (c == '\'')) { if (!inDoubleQuote && (c == '\'')) {
@ -468,7 +647,13 @@ public abstract class ScriptUtils {
} }
if (!inSingleQuote && !inDoubleQuote) { if (!inSingleQuote && !inDoubleQuote) {
if (script.startsWith(separator, i)) { if (script.startsWith(separator, i)) {
return true; // 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 (startsWithAny(script, commentPrefixes, i)) { else if (startsWithAny(script, commentPrefixes, i)) {
// Skip over any content from the start of the comment to the EOL // Skip over any content from the start of the comment to the EOL
@ -494,215 +679,31 @@ public abstract class ScriptUtils {
"Missing block comment end delimiter: " + blockCommentEndDelimiter, resource); "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) != ' ') {
return false; c = ' ';
}
/**
* 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 JDBC 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
* @throws ScriptException if an error occurred while executing the SQL script
* @see #executeSqlScript(Connection, EncodedResource, 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.jdbc.datasource.DataSourceUtils#getConnection
* @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/
public static void executeSqlScript(Connection connection, Resource resource) throws ScriptException {
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 JDBC 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
* @throws ScriptException if an error occurred while executing the SQL script
* @see #executeSqlScript(Connection, EncodedResource, 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.jdbc.datasource.DataSourceUtils#getConnection
* @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/
public static void executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException {
executeSqlScript(connection, resource, 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 JDBC 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
* @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
* @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.jdbc.datasource.DataSourceUtils#getConnection
* @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/
public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError,
boolean ignoreFailedDrops, String commentPrefix, @Nullable String separator,
String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
executeSqlScript(connection, resource, continueOnError, ignoreFailedDrops,
new String[] { commentPrefix }, separator, blockCommentStartDelimiter,
blockCommentEndDelimiter);
}
/**
* 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 JDBC 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
* @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 commentPrefixes the prefixes that identify 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
* @throws ScriptException if an error occurred while executing the SQL script
* @since 5.2
* @see #DEFAULT_STATEMENT_SEPARATOR
* @see #FALLBACK_STATEMENT_SEPARATOR
* @see #EOF_STATEMENT_SEPARATOR
* @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
* @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/
public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError,
boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator,
String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
try {
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL script from " + resource);
}
long startTime = System.currentTimeMillis();
String script;
try {
script = readScript(resource, separator, commentPrefixes, blockCommentEndDelimiter);
}
catch (IOException ex) {
throw new CannotReadScriptException(resource, ex);
}
if (separator == null) {
separator = DEFAULT_STATEMENT_SEPARATOR;
}
if (!EOF_STATEMENT_SEPARATOR.equals(separator) &&
!containsStatementSeparator(resource, script, separator, commentPrefixes,
blockCommentStartDelimiter, blockCommentEndDelimiter)) {
separator = FALLBACK_STATEMENT_SEPARATOR;
}
List<String> statements = new ArrayList<>();
splitSqlScript(resource, script, separator, commentPrefixes, blockCommentStartDelimiter,
blockCommentEndDelimiter, statements);
int stmtNumber = 0;
Statement stmt = connection.createStatement();
try {
for (String statement : statements) {
stmtNumber++;
try {
stmt.execute(statement);
int rowsAffected = stmt.getUpdateCount();
if (logger.isDebugEnabled()) {
logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
SQLWarning warningToLog = stmt.getWarnings();
while (warningToLog != null) {
logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
"', error code '" + warningToLog.getErrorCode() +
"', message [" + warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
}
}
} }
catch (SQLException ex) { else {
boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop"); continue;
if (continueOnError || (dropStatement && ignoreFailedDrops)) {
if (logger.isDebugEnabled()) {
logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, stmtNumber, resource), ex);
}
}
else {
throw new ScriptStatementFailedException(statement, stmtNumber, resource, ex);
}
} }
} }
} }
finally { sb.append(c);
try { }
stmt.close();
}
catch (Throwable ex) {
logger.trace("Could not close JDBC Statement", ex);
}
}
long elapsedTime = System.currentTimeMillis() - startTime; if (StringUtils.hasText(sb)) {
if (logger.isDebugEnabled()) { statements.add(sb.toString());
logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
}
} }
catch (Exception ex) { }
if (ex instanceof ScriptException) {
throw (ScriptException) ex; private static boolean startsWithAny(String script, String[] prefixes, int offset) {
for (String prefix : prefixes) {
if (script.startsWith(prefix, offset)) {
return true;
} }
throw new UncategorizedScriptException(
"Failed to execute database script from resource [" + resource + "]", ex);
} }
return false;
} }
} }

533
spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java

@ -43,7 +43,7 @@ import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
* Generic utility methods for working with SQL scripts. * Generic utility methods for working with SQL scripts in conjunction with R2DBC.
* *
* <p>Mainly for internal use within the framework. * <p>Mainly for internal use within the framework.
* *
@ -59,6 +59,7 @@ import org.springframework.util.StringUtils;
* @author Phillip Webb * @author Phillip Webb
* @author Mark Paluch * @author Mark Paluch
* @since 5.3 * @since 5.3
* @see org.springframework.jdbc.datasource.init.ScriptUtils
*/ */
public abstract class ScriptUtils { public abstract class ScriptUtils {
@ -105,119 +106,171 @@ public abstract class ScriptUtils {
/** /**
* Split an SQL script into separate statements delimited by the provided * Execute the given SQL script using default settings for statement
* separator string and return a {@code List} containing each individual * separators, comment delimiters, and exception handling flags.
* statement. * <p>Statement separators and comments will be removed before executing
* <p>Within the script, the provided {@code commentPrefixes} will be honored: * individual statements within the supplied script.
* any text beginning with one of the comment prefixes and extending to the * <p><strong>Warning</strong>: this method does <em>not</em> release the
* end of the line will be omitted from the output. Similarly, the provided * provided {@link Connection}.
* {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter} * @param connection the R2DBC connection to use to execute the script; already
* delimiters will be honored: any text enclosed in a block comment will be * configured and ready to use
* omitted from the output. In addition, multiple adjacent whitespace characters * @param resource the resource to load the SQL script from; encoded with the
* will be collapsed into a single space. * current platform's default encoding
* @param resource the resource from which the script was read * @throws ScriptException if an error occurred while executing the SQL script
* @param script the SQL script * @see #executeSqlScript(Connection, EncodedResource, DataBufferFactory, boolean, boolean, String[], String, String, String)
* @param separator text separating each statement * @see #DEFAULT_STATEMENT_SEPARATOR
* (typically a ';' or newline character) * @see #DEFAULT_COMMENT_PREFIXES
* @param commentPrefixes the prefixes that identify SQL line comments * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
* (typically "--") * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
* @param blockCommentStartDelimiter the <em>start</em> block comment delimiter; * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection
* never {@code null} or empty * @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter;
* never {@code null} or empty
* @return a list of statements
* @throws ScriptException if an error occurred while splitting the SQL script
*/ */
static List<String> splitSqlScript(EncodedResource resource, String script, public static Mono<Void> executeSqlScript(Connection connection, Resource resource) throws ScriptException {
String separator, String[] commentPrefixes, String blockCommentStartDelimiter, 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
* @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_PREFIXES
* @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
* @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection
*/
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException {
return executeSqlScript(connection, resource, DefaultDataBufferFactory.sharedInstance, false, false,
DEFAULT_COMMENT_PREFIXES, 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 resource the resource (potentially associated with a specific encoding)
* to load the SQL script from
* @param dataBufferFactory the factory to create data buffers with
* @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
* @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.r2dbc.connection.ConnectionFactoryUtils#getConnection
* @see org.springframework.r2dbc.connection.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 { String blockCommentEndDelimiter) throws ScriptException {
Assert.hasText(script, "'script' must not be null or empty"); return executeSqlScript(connection, resource, dataBufferFactory, continueOnError,
Assert.notNull(separator, "'separator' must not be null"); ignoreFailedDrops, new String[] { commentPrefix }, separator,
Assert.notEmpty(commentPrefixes, "'commentPrefixes' must not be null or empty"); blockCommentStartDelimiter, blockCommentEndDelimiter);
for (String commentPrefix : commentPrefixes) { }
Assert.hasText(commentPrefix, "'commentPrefixes' must not contain null or empty elements");
/**
* 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 resource the resource (potentially associated with a specific encoding)
* to load the SQL script from
* @param dataBufferFactory the factory to create data buffers with
* @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 commentPrefixes the prefixes that identify 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
* @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.r2dbc.connection.ConnectionFactoryUtils#getConnection
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection
*/
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource,
DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops,
String[] commentPrefixes, @Nullable String separator, String blockCommentStartDelimiter,
String blockCommentEndDelimiter) throws ScriptException {
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL script from " + resource);
} }
Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty");
Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty");
List<String> statements = new ArrayList<>(); long startTime = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
boolean inSingleQuote = false;
boolean inDoubleQuote = false;
boolean inEscape = false;
for (int i = 0; i < script.length(); i++) { Mono<String> inputScript = readScript(resource, dataBufferFactory, separator)
char c = script.charAt(i); .onErrorMap(IOException.class, ex -> new CannotReadScriptException(resource, ex));
if (inEscape) {
inEscape = false; AtomicInteger statementNumber = new AtomicInteger();
sb.append(c);
continue; Flux<Void> executeScript = inputScript.flatMapIterable(script -> {
} String separatorToUse = separator;
// MySQL style escapes if (separatorToUse == null) {
if (c == '\\') { separatorToUse = DEFAULT_STATEMENT_SEPARATOR;
inEscape = true;
sb.append(c);
continue;
}
if (!inDoubleQuote && (c == '\'')) {
inSingleQuote = !inSingleQuote;
}
else if (!inSingleQuote && (c == '"')) {
inDoubleQuote = !inDoubleQuote;
} }
if (!inSingleQuote && !inDoubleQuote) { if (!EOF_STATEMENT_SEPARATOR.equals(separatorToUse) &&
if (script.startsWith(separator, i)) { !containsStatementSeparator(resource, script, separatorToUse, commentPrefixes,
// We've reached the end of the current statement blockCommentStartDelimiter, blockCommentEndDelimiter)) {
if (sb.length() > 0) { separatorToUse = FALLBACK_STATEMENT_SEPARATOR;
statements.add(sb.toString());
sb = new StringBuilder();
}
i += separator.length() - 1;
continue;
}
else if (startsWithAny(script, commentPrefixes, 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); return splitSqlScript(resource, script, separatorToUse, commentPrefixes,
} blockCommentStartDelimiter, blockCommentEndDelimiter);
}).concatMap(statement -> {
statementNumber.incrementAndGet();
return runStatement(statement, connection, resource, continueOnError, ignoreFailedDrops, statementNumber);
});
if (StringUtils.hasText(sb)) { if (logger.isDebugEnabled()) {
statements.add(sb.toString()); executeScript = executeScript.doOnComplete(() -> {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
});
} }
return statements; return executeScript.onErrorMap(ex -> !(ex instanceof ScriptException),
ex -> new UncategorizedScriptException(
"Failed to execute database script from resource [" + resource + "]", ex))
.then();
} }
/** /**
@ -290,15 +343,6 @@ public abstract class ScriptUtils {
} }
} }
private static boolean startsWithAny(String script, String[] prefixes, int offset) {
for (String prefix : prefixes) {
if (script.startsWith(prefix, offset)) {
return true;
}
}
return false;
}
/** /**
* Determine if the provided SQL script contains the specified statement separator. * Determine if the provided SQL script contains the specified statement separator.
* <p>This method is intended to be used to find the string separating each * <p>This method is intended to be used to find the string separating each
@ -379,171 +423,128 @@ public abstract class ScriptUtils {
} }
/** /**
* Execute the given SQL script using default settings for statement * Split an SQL script into separate statements delimited by the provided
* separators, comment delimiters, and exception handling flags. * separator string and return a {@code List} containing each individual
* <p>Statement separators and comments will be removed before executing * statement.
* individual statements within the supplied script. * <p>Within the script, the provided {@code commentPrefixes} will be honored:
* <p><strong>Warning</strong>: this method does <em>not</em> release the * any text beginning with one of the comment prefixes and extending to the
* provided {@link Connection}. * end of the line will be omitted from the output. Similarly, the provided
* @param connection the R2DBC connection to use to execute the script; already * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter}
* configured and ready to use * delimiters will be honored: any text enclosed in a block comment will be
* @param resource the resource to load the SQL script from; encoded with the * omitted from the output. In addition, multiple adjacent whitespace characters
* current platform's default encoding * will be collapsed into a single space.
* @throws ScriptException if an error occurred while executing the SQL script * @param resource the resource from which the script was read
* @see #executeSqlScript(Connection, EncodedResource, DataBufferFactory, boolean, boolean, String[], String, String, String) * @param script the SQL script
* @see #DEFAULT_STATEMENT_SEPARATOR * @param separator text separating each statement
* @see #DEFAULT_COMMENT_PREFIXES * (typically a ';' or newline character)
* @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER * @param commentPrefixes the prefixes that identify SQL line comments
* @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER * (typically "--")
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection * @param blockCommentStartDelimiter the <em>start</em> block comment delimiter;
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection * never {@code null} or empty
*/ * @param blockCommentEndDelimiter the <em>end</em> block comment delimiter;
public static Mono<Void> executeSqlScript(Connection connection, Resource resource) throws ScriptException { * never {@code null} or empty
return executeSqlScript(connection, new EncodedResource(resource)); * @return a list of statements
} * @throws ScriptException if an error occurred while splitting the SQL script
/**
* 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
* @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_PREFIXES
* @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
* @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection
*/
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException {
return executeSqlScript(connection, resource, DefaultDataBufferFactory.sharedInstance, false, false,
DEFAULT_COMMENT_PREFIXES, 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 resource the resource (potentially associated with a specific encoding)
* to load the SQL script from
* @param dataBufferFactory the factory to create data buffers with
* @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
* @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.r2dbc.connection.ConnectionFactoryUtils#getConnection
* @see org.springframework.r2dbc.connection.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 {
return executeSqlScript(connection, resource, dataBufferFactory, continueOnError,
ignoreFailedDrops, new String[] { commentPrefix }, separator,
blockCommentStartDelimiter, blockCommentEndDelimiter);
}
/**
* 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 resource the resource (potentially associated with a specific encoding)
* to load the SQL script from
* @param dataBufferFactory the factory to create data buffers with
* @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 commentPrefixes the prefixes that identify 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
* @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.r2dbc.connection.ConnectionFactoryUtils#getConnection
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection
*/ */
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource, static List<String> splitSqlScript(EncodedResource resource, String script,
DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, String separator, String[] commentPrefixes, String blockCommentStartDelimiter,
String[] commentPrefixes, @Nullable String separator, String blockCommentStartDelimiter,
String blockCommentEndDelimiter) throws ScriptException { String blockCommentEndDelimiter) throws ScriptException {
if (logger.isDebugEnabled()) { Assert.hasText(script, "'script' must not be null or empty");
logger.debug("Executing SQL script from " + resource); Assert.notNull(separator, "'separator' must not be null");
Assert.notEmpty(commentPrefixes, "'commentPrefixes' must not be null or empty");
for (String commentPrefix : commentPrefixes) {
Assert.hasText(commentPrefix, "'commentPrefixes' must not contain null or empty elements");
} }
Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty");
Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty");
long startTime = System.currentTimeMillis(); List<String> statements = new ArrayList<>();
StringBuilder sb = new StringBuilder();
Mono<String> inputScript = readScript(resource, dataBufferFactory, separator) boolean inSingleQuote = false;
.onErrorMap(IOException.class, ex -> new CannotReadScriptException(resource, ex)); boolean inDoubleQuote = false;
boolean inEscape = false;
AtomicInteger statementNumber = new AtomicInteger();
Flux<Void> executeScript = inputScript.flatMapIterable(script -> { for (int i = 0; i < script.length(); i++) {
String separatorToUse = separator; char c = script.charAt(i);
if (separatorToUse == null) { if (inEscape) {
separatorToUse = DEFAULT_STATEMENT_SEPARATOR; inEscape = false;
sb.append(c);
continue;
} }
if (!EOF_STATEMENT_SEPARATOR.equals(separatorToUse) && // MySQL style escapes
!containsStatementSeparator(resource, script, separatorToUse, commentPrefixes, if (c == '\\') {
blockCommentStartDelimiter, blockCommentEndDelimiter)) { inEscape = true;
separatorToUse = FALLBACK_STATEMENT_SEPARATOR; sb.append(c);
continue;
} }
return splitSqlScript(resource, script, separatorToUse, commentPrefixes, if (!inDoubleQuote && (c == '\'')) {
blockCommentStartDelimiter, blockCommentEndDelimiter); inSingleQuote = !inSingleQuote;
}).concatMap(statement -> { }
statementNumber.incrementAndGet(); else if (!inSingleQuote && (c == '"')) {
return runStatement(statement, connection, resource, continueOnError, ignoreFailedDrops, statementNumber); 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 (startsWithAny(script, commentPrefixes, 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 (logger.isDebugEnabled()) { if (StringUtils.hasText(sb)) {
executeScript = executeScript.doOnComplete(() -> { statements.add(sb.toString());
long elapsedTime = System.currentTimeMillis() - startTime;
logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
});
} }
return executeScript.onErrorMap(ex -> !(ex instanceof ScriptException), return statements;
ex -> new UncategorizedScriptException( }
"Failed to execute database script from resource [" + resource + "]", ex))
.then(); private static boolean startsWithAny(String script, String[] prefixes, int offset) {
for (String prefix : prefixes) {
if (script.startsWith(prefix, offset)) {
return true;
}
}
return false;
} }
private static Publisher<? extends Void> runStatement(String statement, Connection connection, private static Publisher<? extends Void> runStatement(String statement, Connection connection,

Loading…
Cancel
Save