diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java index 789007521a0..c3e315d0b31 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java +++ b/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; /** - * Generic utility methods for working with SQL scripts. + * Generic utility methods for working with SQL scripts in conjunction with JDBC. * *

Mainly for internal use within the framework. * @@ -50,6 +50,7 @@ import org.springframework.util.StringUtils; * @author Nicolas Debeissat * @author Phillip Webb * @since 4.0.3 + * @see org.springframework.r2dbc.connection.init.ScriptUtils */ public abstract class ScriptUtils { @@ -102,197 +103,207 @@ public abstract class ScriptUtils { /** - * Split an SQL script into separate statements delimited by the provided - * separator character. Each individual statement will be added to the - * provided {@code List}. - *

Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the - * comment prefix; any text beginning with the comment prefix and extending to - * the end of the line will be omitted from the output. Similarly, - * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and - * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the - * start and end block comment delimiters: any text enclosed - * in a block comment will be omitted from the output. In addition, multiple - * adjacent whitespace characters will be collapsed into a single space. - * @param script the SQL script - * @param separator character separating each statement (typically a ';') - * @param statements the list that will contain the individual statements - * @throws ScriptException if an error occurred while splitting the SQL script - * @see #splitSqlScript(String, String, List) - * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List) + * Execute the given SQL script using default settings for statement + * separators, comment delimiters, and exception handling flags. + *

Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

Warning: this method does not release the + * provided {@link Connection}. + * @param connection the 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 splitSqlScript(String script, char separator, List statements) throws ScriptException { - splitSqlScript(script, String.valueOf(separator), statements); + public static void executeSqlScript(Connection connection, Resource resource) throws ScriptException { + executeSqlScript(connection, new EncodedResource(resource)); } /** - * Split an SQL script into separate statements delimited by the provided - * separator string. Each individual statement will be added to the - * provided {@code List}. - *

Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the - * comment prefix; any text beginning with the comment prefix and extending to - * the end of the line will be omitted from the output. Similarly, - * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and - * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the - * start and end block comment delimiters: any text enclosed - * in a block comment will be omitted from the output. In addition, multiple - * adjacent whitespace characters will be collapsed into a single space. - * @param script the SQL script - * @param separator text separating each statement - * (typically a ';' or newline character) - * @param statements the list that will contain the individual statements - * @throws ScriptException if an error occurred while splitting the SQL script - * @see #splitSqlScript(String, char, List) - * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List) + * Execute the given SQL script using default settings for statement + * separators, comment delimiters, and exception handling flags. + *

Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

Warning: this method does not release the + * provided {@link Connection}. + * @param connection the 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 splitSqlScript(String script, String separator, List statements) throws ScriptException { - splitSqlScript(null, script, separator, DEFAULT_COMMENT_PREFIX, DEFAULT_BLOCK_COMMENT_START_DELIMITER, - DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements); + 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); } /** - * Split an SQL script into separate statements delimited by the provided - * separator string. Each individual statement will be added to the provided - * {@code List}. - *

Within the script, the provided {@code commentPrefix} will be honored: - * any text beginning with the comment prefix and extending to the end of the - * line will be omitted from the output. Similarly, the provided - * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter} - * delimiters will be honored: any text enclosed in a block comment will be - * omitted from the output. In addition, multiple adjacent whitespace characters - * will be collapsed into a single space. - * @param resource the resource from which the script was read - * @param script the SQL script - * @param separator text separating each statement - * (typically a ';' or newline character) - * @param commentPrefix the prefix that identifies SQL line comments - * (typically "--") - * @param blockCommentStartDelimiter the start block comment delimiter; - * never {@code null} or empty - * @param blockCommentEndDelimiter the end 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 + * Execute the given SQL script. + *

Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

Warning: this method does not release the + * provided {@link Connection}. + * @param connection the 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 start block comment delimiter + * @param blockCommentEndDelimiter the end 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, - String separator, String commentPrefix, String blockCommentStartDelimiter, - String blockCommentEndDelimiter, List statements) throws ScriptException { + public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError, + boolean ignoreFailedDrops, String commentPrefix, @Nullable String separator, + String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException { - Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty"); - splitSqlScript(resource, script, separator, new String[] { commentPrefix }, - blockCommentStartDelimiter, blockCommentEndDelimiter, statements); + executeSqlScript(connection, resource, continueOnError, ignoreFailedDrops, + new String[] { commentPrefix }, separator, blockCommentStartDelimiter, + blockCommentEndDelimiter); } /** - * Split an SQL script into separate statements delimited by the provided - * separator string. Each individual statement will be added to the provided - * {@code List}. - *

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 start block comment delimiter; - * never {@code null} or empty - * @param blockCommentEndDelimiter the end 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 + * Execute the given SQL script. + *

Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

Warning: this method does not release the + * provided {@link Connection}. + * @param connection the 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 start block comment delimiter + * @param blockCommentEndDelimiter the end 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 splitSqlScript(@Nullable EncodedResource resource, String script, - String separator, String[] commentPrefixes, String blockCommentStartDelimiter, - String blockCommentEndDelimiter, List statements) throws ScriptException { - - Assert.hasText(script, "'script' must not be null or empty"); - Assert.notNull(separator, "'separator' must not be null"); - Assert.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"); + public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError, + boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator, + String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException { - StringBuilder sb = new StringBuilder(); - boolean inSingleQuote = false; - boolean inDoubleQuote = false; - boolean inEscape = false; + try { + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL script from " + resource); + } + long startTime = System.currentTimeMillis(); - for (int i = 0; i < script.length(); i++) { - char c = script.charAt(i); - if (inEscape) { - inEscape = false; - sb.append(c); - continue; + String script; + try { + script = readScript(resource, separator, commentPrefixes, blockCommentEndDelimiter); } - // MySQL style escapes - if (c == '\\') { - inEscape = true; - sb.append(c); - continue; + catch (IOException ex) { + throw new CannotReadScriptException(resource, ex); } - if (!inDoubleQuote && (c == '\'')) { - inSingleQuote = !inSingleQuote; + + if (separator == null) { + separator = DEFAULT_STATEMENT_SEPARATOR; } - else if (!inSingleQuote && (c == '"')) { - inDoubleQuote = !inDoubleQuote; + if (!EOF_STATEMENT_SEPARATOR.equals(separator) && + !containsStatementSeparator(resource, script, separator, commentPrefixes, + blockCommentStartDelimiter, blockCommentEndDelimiter)) { + separator = FALLBACK_STATEMENT_SEPARATOR; } - 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; + + List 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(); + } + } } - else { - // If there's no EOL, we must be at the end of the script, so stop here. - break; + catch (SQLException ex) { + boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop"); + 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 - 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); - } + } + finally { + try { + stmt.close(); } - 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; - } + catch (Throwable ex) { + logger.trace("Could not close JDBC Statement", ex); } } - sb.append(c); - } - if (StringUtils.hasText(sb)) { - statements.add(sb.toString()); + long elapsedTime = System.currentTimeMillis() - startTime; + 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 */ 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())) { 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. *

This method is intended to be used to find the string delimiting each @@ -441,10 +443,185 @@ public abstract class ScriptUtils { * (typically "*/") * @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}. + *

Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the + * comment prefix; any text beginning with the comment prefix and extending to + * the end of the line will be omitted from the output. Similarly, + * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and + * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the + * start and end block comment delimiters: any text enclosed + * in a block comment will be omitted from the output. In addition, multiple + * adjacent whitespace characters will be collapsed into a single space. + * @param script the SQL script + * @param separator character separating each statement (typically a ';') + * @param statements the list that will contain the individual statements + * @throws ScriptException if an error occurred while splitting the SQL script + * @see #splitSqlScript(String, String, List) + * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List) + */ + public static void splitSqlScript(String script, char separator, List statements) throws ScriptException { + splitSqlScript(script, String.valueOf(separator), statements); + } + + /** + * Split an SQL script into separate statements delimited by the provided + * separator string. Each individual statement will be added to the + * provided {@code List}. + *

Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the + * comment prefix; any text beginning with the comment prefix and extending to + * the end of the line will be omitted from the output. Similarly, + * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and + * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the + * start and end block comment delimiters: any text enclosed + * in a block comment will be omitted from the output. In addition, multiple + * adjacent whitespace characters will be collapsed into a single space. + * @param script the SQL script + * @param separator text separating each statement + * (typically a ';' or newline character) + * @param statements the list that will contain the individual statements + * @throws ScriptException if an error occurred while splitting the SQL script + * @see #splitSqlScript(String, char, List) + * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List) + */ + public static void splitSqlScript(String script, String separator, List statements) throws ScriptException { + splitSqlScript(null, script, separator, DEFAULT_COMMENT_PREFIX, DEFAULT_BLOCK_COMMENT_START_DELIMITER, + DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements); + } + + /** + * Split an SQL script into separate statements delimited by the provided + * separator string. Each individual statement will be added to the provided + * {@code List}. + *

Within the script, the provided {@code commentPrefix} will be honored: + * any text beginning with the comment prefix and extending to the end of the + * line will be omitted from the output. Similarly, the provided + * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter} + * delimiters will be honored: any text enclosed in a block comment will be + * omitted from the output. In addition, multiple adjacent whitespace characters + * will be collapsed into a single space. + * @param resource the resource from which the script was read + * @param script the SQL script + * @param separator text separating each statement + * (typically a ';' or newline character) + * @param commentPrefix the prefix that identifies SQL line comments + * (typically "--") + * @param blockCommentStartDelimiter the start block comment delimiter; + * never {@code null} or empty + * @param blockCommentEndDelimiter the end 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 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}. + *

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 start block comment delimiter; + * never {@code null} or empty + * @param blockCommentEndDelimiter the end 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 blockCommentEndDelimiter) throws ScriptException { + String blockCommentEndDelimiter, List statements) throws ScriptException { + + Assert.hasText(script, "'script' must not be null or empty"); + Assert.notNull(separator, "'separator' must not be null"); + Assert.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 inDoubleQuote = false; boolean inEscape = false; @@ -453,11 +630,13 @@ public abstract class ScriptUtils { 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 == '\'')) { @@ -468,7 +647,13 @@ public abstract class ScriptUtils { } if (!inSingleQuote && !inDoubleQuote) { 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)) { // 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); } } - } - } - - return false; - } - - /** - * Execute the given SQL script using default settings for statement - * separators, comment delimiters, and exception handling flags. - *

Statement separators and comments will be removed before executing - * individual statements within the supplied script. - *

Warning: this method does not release the - * provided {@link Connection}. - * @param connection the 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. - *

Statement separators and comments will be removed before executing - * individual statements within the supplied script. - *

Warning: this method does not release the - * provided {@link Connection}. - * @param connection the 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. - *

Statement separators and comments will be removed before executing - * individual statements within the supplied script. - *

Warning: this method does not release the - * provided {@link Connection}. - * @param connection the 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 start block comment delimiter - * @param blockCommentEndDelimiter the end 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. - *

Statement separators and comments will be removed before executing - * individual statements within the supplied script. - *

Warning: this method does not release the - * provided {@link Connection}. - * @param connection the 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 start block comment delimiter - * @param blockCommentEndDelimiter the end 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 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(); - } - } + else if (c == ' ' || c == '\r' || c == '\n' || c == '\t') { + // Avoid multiple adjacent whitespace characters + if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') { + c = ' '; } - catch (SQLException ex) { - boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop"); - if (continueOnError || (dropStatement && ignoreFailedDrops)) { - if (logger.isDebugEnabled()) { - logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, stmtNumber, resource), ex); - } - } - else { - throw new ScriptStatementFailedException(statement, stmtNumber, resource, ex); - } + else { + continue; } } } - finally { - try { - stmt.close(); - } - catch (Throwable ex) { - logger.trace("Could not close JDBC Statement", ex); - } - } + sb.append(c); + } - long elapsedTime = System.currentTimeMillis() - startTime; - if (logger.isDebugEnabled()) { - logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms."); - } + if (StringUtils.hasText(sb)) { + statements.add(sb.toString()); } - 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; } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java index 304f6ec5580..82fdf38ee3e 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java +++ b/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; /** - * Generic utility methods for working with SQL scripts. + * Generic utility methods for working with SQL scripts in conjunction with R2DBC. * *

Mainly for internal use within the framework. * @@ -59,6 +59,7 @@ import org.springframework.util.StringUtils; * @author Phillip Webb * @author Mark Paluch * @since 5.3 + * @see org.springframework.jdbc.datasource.init.ScriptUtils */ public abstract class ScriptUtils { @@ -105,119 +106,171 @@ public abstract class ScriptUtils { /** - * Split an SQL script into separate statements delimited by the provided - * separator string and return a {@code List} containing each individual - * statement. - *

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 start block comment delimiter; - * never {@code null} or empty - * @param blockCommentEndDelimiter the end block comment delimiter; - * never {@code null} or empty - * @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. + *

Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

Warning: this method does not release the + * provided {@link Connection}. + * @param connection the R2DBC connection to use to execute the script; already + * configured and ready to use + * @param resource the resource to load the SQL script from; encoded with the + * current platform's default encoding + * @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 */ - static List splitSqlScript(EncodedResource resource, String script, - String separator, String[] commentPrefixes, String blockCommentStartDelimiter, + public static Mono executeSqlScript(Connection connection, Resource resource) throws ScriptException { + return executeSqlScript(connection, new EncodedResource(resource)); + } + + /** + * Execute the given SQL script using default settings for statement + * separators, comment delimiters, and exception handling flags. + *

Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

Warning: this method does not release the + * provided {@link Connection}. + * @param connection the R2DBC connection to use to execute the script; already + * configured and ready to use + * @param resource the resource (potentially associated with a specific encoding) + * to load the SQL script from + * @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 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. + *

Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

Warning: this method does not release the + * provided {@link Connection}. + * @param connection the R2DBC connection to use to execute the script; already + * configured and ready to use + * @param resource the resource (potentially associated with a specific encoding) + * to load the SQL script from + * @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 start block comment delimiter + * @param blockCommentEndDelimiter the end 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 executeSqlScript(Connection connection, EncodedResource resource, + DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, + String commentPrefix, @Nullable String separator, 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"); + return executeSqlScript(connection, resource, dataBufferFactory, continueOnError, + ignoreFailedDrops, new String[] { commentPrefix }, separator, + blockCommentStartDelimiter, blockCommentEndDelimiter); + } + + /** + * Execute the given SQL script. + *

Statement separators and comments will be removed before executing + * individual statements within the supplied script. + *

Warning: this method does not release the + * provided {@link Connection}. + * @param connection the R2DBC connection to use to execute the script; already + * configured and ready to use + * @param 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 start block comment delimiter + * @param blockCommentEndDelimiter the end 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 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 statements = new ArrayList<>(); - StringBuilder sb = new StringBuilder(); - boolean inSingleQuote = false; - boolean inDoubleQuote = false; - boolean inEscape = false; + long startTime = System.currentTimeMillis(); - 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; + Mono inputScript = readScript(resource, dataBufferFactory, separator) + .onErrorMap(IOException.class, ex -> new CannotReadScriptException(resource, ex)); + + AtomicInteger statementNumber = new AtomicInteger(); + + Flux executeScript = inputScript.flatMapIterable(script -> { + String separatorToUse = separator; + if (separatorToUse == null) { + separatorToUse = DEFAULT_STATEMENT_SEPARATOR; } - 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; - } - } + if (!EOF_STATEMENT_SEPARATOR.equals(separatorToUse) && + !containsStatementSeparator(resource, script, separatorToUse, commentPrefixes, + blockCommentStartDelimiter, blockCommentEndDelimiter)) { + separatorToUse = FALLBACK_STATEMENT_SEPARATOR; } - 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)) { - statements.add(sb.toString()); + if (logger.isDebugEnabled()) { + 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. *

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 - * separators, comment delimiters, and exception handling flags. - *

Statement separators and comments will be removed before executing - * individual statements within the supplied script. - *

Warning: this method does not release the - * provided {@link Connection}. - * @param connection the R2DBC connection to use to execute the script; already - * configured and ready to use - * @param resource the resource to load the SQL script from; encoded with the - * current platform's default encoding - * @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 executeSqlScript(Connection connection, Resource resource) throws ScriptException { - return executeSqlScript(connection, new EncodedResource(resource)); - } - - /** - * Execute the given SQL script using default settings for statement - * separators, comment delimiters, and exception handling flags. - *

Statement separators and comments will be removed before executing - * individual statements within the supplied script. - *

Warning: this method does not release the - * provided {@link Connection}. - * @param connection the R2DBC connection to use to execute the script; already - * configured and ready to use - * @param resource the resource (potentially associated with a specific encoding) - * to load the SQL script from - * @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 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. - *

Statement separators and comments will be removed before executing - * individual statements within the supplied script. - *

Warning: this method does not release the - * provided {@link Connection}. - * @param connection the R2DBC connection to use to execute the script; already - * configured and ready to use - * @param resource the resource (potentially associated with a specific encoding) - * to load the SQL script from - * @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 start block comment delimiter - * @param blockCommentEndDelimiter the end 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 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. - *

Statement separators and comments will be removed before executing - * individual statements within the supplied script. - *

Warning: this method does not release the - * provided {@link Connection}. - * @param connection the R2DBC connection to use to execute the script; already - * configured and ready to use - * @param resource the resource (potentially associated with a specific encoding) - * to load the SQL script from - * @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 start block comment delimiter - * @param blockCommentEndDelimiter the end 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 + * Split an SQL script into separate statements delimited by the provided + * separator string and return a {@code List} containing each individual + * statement. + *

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 start block comment delimiter; + * never {@code null} or empty + * @param blockCommentEndDelimiter the end block comment delimiter; + * never {@code null} or empty + * @return a list of statements + * @throws ScriptException if an error occurred while splitting the SQL script */ - public static Mono executeSqlScript(Connection connection, EncodedResource resource, - DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, - String[] commentPrefixes, @Nullable String separator, String blockCommentStartDelimiter, + static List splitSqlScript(EncodedResource resource, String script, + String separator, String[] commentPrefixes, String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException { - if (logger.isDebugEnabled()) { - logger.debug("Executing SQL script from " + resource); + 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"); - long startTime = System.currentTimeMillis(); - - Mono inputScript = readScript(resource, dataBufferFactory, separator) - .onErrorMap(IOException.class, ex -> new CannotReadScriptException(resource, ex)); - - AtomicInteger statementNumber = new AtomicInteger(); + List statements = new ArrayList<>(); + StringBuilder sb = new StringBuilder(); + boolean inSingleQuote = false; + boolean inDoubleQuote = false; + boolean inEscape = false; - Flux executeScript = inputScript.flatMapIterable(script -> { - String separatorToUse = separator; - if (separatorToUse == null) { - separatorToUse = DEFAULT_STATEMENT_SEPARATOR; + for (int i = 0; i < script.length(); i++) { + char c = script.charAt(i); + if (inEscape) { + inEscape = false; + sb.append(c); + continue; } - if (!EOF_STATEMENT_SEPARATOR.equals(separatorToUse) && - !containsStatementSeparator(resource, script, separatorToUse, commentPrefixes, - blockCommentStartDelimiter, blockCommentEndDelimiter)) { - separatorToUse = FALLBACK_STATEMENT_SEPARATOR; + // MySQL style escapes + if (c == '\\') { + inEscape = true; + sb.append(c); + continue; } - return splitSqlScript(resource, script, separatorToUse, commentPrefixes, - blockCommentStartDelimiter, blockCommentEndDelimiter); - }).concatMap(statement -> { - statementNumber.incrementAndGet(); - return runStatement(statement, connection, resource, continueOnError, ignoreFailedDrops, statementNumber); - }); + 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 (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()) { - executeScript = executeScript.doOnComplete(() -> { - long elapsedTime = System.currentTimeMillis() - startTime; - logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms."); - }); + if (StringUtils.hasText(sb)) { + statements.add(sb.toString()); } - return executeScript.onErrorMap(ex -> !(ex instanceof ScriptException), - ex -> new UncategorizedScriptException( - "Failed to execute database script from resource [" + resource + "]", ex)) - .then(); + return statements; + } + + 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 runStatement(String statement, Connection connection,