Add multi-prefix comment support for SQL scripts

Update `ResourceDatabasePopulator` and `ScriptUtils` so that more than
one comment prefix can be used when processing SQL scripts. This
feature is particularly useful when dealing with scripts provided by
Quartz since they often use a mix `--` and `#`.

Closes gh-23289
This commit is contained in:
Phillip Webb 2019-07-14 10:33:42 +01:00 committed by Sam Brannen
parent 1ff29b0f6b
commit 17914fc44b
4 changed files with 173 additions and 16 deletions

View File

@ -60,7 +60,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
private String separator = ScriptUtils.DEFAULT_STATEMENT_SEPARATOR;
private String commentPrefix = ScriptUtils.DEFAULT_COMMENT_PREFIX;
private String[] commentPrefixes = ScriptUtils.DEFAULT_COMMENT_PREFIXES;
private String blockCommentStartDelimiter = ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER;
@ -171,9 +171,22 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
* Set the prefix that identifies single-line comments within the SQL scripts.
* <p>Defaults to {@code "--"}.
* @param commentPrefix the prefix for single-line comments
* @see #setCommentPrefixes(String...)
*/
public void setCommentPrefix(String commentPrefix) {
this.commentPrefix = commentPrefix;
Assert.hasText(commentPrefix, "CommentPrefix must not be null or empty");
this.commentPrefixes = new String[] { commentPrefix };
}
/**
* Set the prefixes that identify single-line comments within the SQL scripts.
* <p>Defaults to {@code "--"}.
* @param commentPrefixes the prefixes for single-line comments
* @since 5.2
*/
public void setCommentPrefixes(String... commentPrefixes) {
Assert.notNull(commentPrefixes, "CommentPrefixes must not be null");
this.commentPrefixes = commentPrefixes;
}
/**
@ -236,7 +249,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
for (Resource script : this.scripts) {
EncodedResource encodedScript = new EncodedResource(script, this.sqlScriptEncoding);
ScriptUtils.executeSqlScript(connection, encodedScript, this.continueOnError, this.ignoreFailedDrops,
this.commentPrefix, this.separator, this.blockCommentStartDelimiter, this.blockCommentEndDelimiter);
this.commentPrefixes, this.separator, this.blockCommentStartDelimiter, this.blockCommentEndDelimiter);
}
}

View File

@ -80,6 +80,12 @@ public abstract class ScriptUtils {
*/
public static final String DEFAULT_COMMENT_PREFIX = "--";
/**
* Default prefixes for single-line comments within SQL scripts: {@code ["--"]}.
* @since 5.2
*/
public static final String[] DEFAULT_COMMENT_PREFIXES = { DEFAULT_COMMENT_PREFIX };
/**
* Default start delimiter for block comments within SQL scripts: {@code "/*"}.
*/
@ -170,9 +176,46 @@ public abstract class ScriptUtils {
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 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 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 blockCommentEndDelimiter, List<String> statements) throws ScriptException {
Assert.hasText(script, "'script' must not be null or empty");
Assert.notNull(separator, "'separator' must not be null");
Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty");
Assert.notNull(commentPrefixes, "'commentPrefixes' must not be null");
for (int i = 0; i < commentPrefixes.length; i++) {
Assert.hasText(commentPrefixes[i], "'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");
@ -210,7 +253,7 @@ public abstract class ScriptUtils {
i += separator.length() - 1;
continue;
}
else if (script.startsWith(commentPrefix, i)) {
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) {
@ -260,7 +303,7 @@ public abstract class ScriptUtils {
* @throws IOException in case of I/O errors
*/
static String readScript(EncodedResource resource) throws IOException {
return readScript(resource, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_END_DELIMITER);
return readScript(resource, DEFAULT_COMMENT_PREFIXES, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_END_DELIMITER);
}
/**
@ -271,19 +314,19 @@ public abstract class ScriptUtils {
* a statement &mdash; will be included in the results.
* @param resource the {@code EncodedResource} containing the script
* to be processed
* @param commentPrefix the prefix that identifies comments in the SQL script
* @param commentPrefixes the prefix that identifies comments in the SQL script
* (typically "--")
* @param separator the statement separator in the SQL script (typically ";")
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter
* @return a {@code String} containing the script lines
* @throws IOException in case of I/O errors
*/
private static String readScript(EncodedResource resource, @Nullable String commentPrefix,
private static String readScript(EncodedResource resource, @Nullable String[] commentPrefixes,
@Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException {
LineNumberReader lnr = new LineNumberReader(resource.getReader());
try {
return readScript(lnr, commentPrefix, separator, blockCommentEndDelimiter);
return readScript(lnr, commentPrefixes, separator, blockCommentEndDelimiter);
}
finally {
lnr.close();
@ -309,11 +352,35 @@ public abstract class ScriptUtils {
public static String readScript(LineNumberReader lineNumberReader, @Nullable String lineCommentPrefix,
@Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException {
String[] lineCommentPrefixes = (lineCommentPrefix != null) ? new String[] { lineCommentPrefix } : null;
return readScript(lineNumberReader, lineCommentPrefixes, separator, blockCommentEndDelimiter);
}
/**
* Read a script from the provided {@code LineNumberReader}, using the supplied
* comment prefix and statement separator, and build a {@code String} containing
* the lines.
* <p>Lines <em>beginning</em> with the comment prefix are excluded from the
* results; however, line comments anywhere else &mdash; for example, within
* a statement &mdash; will be included in the results.
* @param lineNumberReader the {@code LineNumberReader} containing the script
* to be processed
* @param lineCommentPrefixes the prefixes that identify comments in the SQL script
* (typically "--")
* @param separator the statement separator in the SQL script (typically ";")
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter
* @return a {@code String} containing the script lines
* @throws IOException in case of I/O errors
* @since 5.2
*/
public static String readScript(LineNumberReader lineNumberReader, @Nullable String[] lineCommentPrefixes,
@Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException {
String currentStatement = lineNumberReader.readLine();
StringBuilder scriptBuilder = new StringBuilder();
while (currentStatement != null) {
if ((blockCommentEndDelimiter != null && currentStatement.contains(blockCommentEndDelimiter)) ||
(lineCommentPrefix != null && !currentStatement.startsWith(lineCommentPrefix))) {
(lineCommentPrefixes != null && !startsWithAny(currentStatement, lineCommentPrefixes, 0))) {
if (scriptBuilder.length() > 0) {
scriptBuilder.append('\n');
}
@ -340,6 +407,15 @@ public abstract class ScriptUtils {
}
}
private static boolean startsWithAny(String script, String[] prefixes, int toffset) {
for (String prefix : prefixes) {
if (script.startsWith(prefix, toffset)) {
return true;
}
}
return false;
}
/**
* Does the provided SQL script contain the specified delimiter?
* @param script the SQL script
@ -454,6 +530,46 @@ public abstract class ScriptUtils {
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);
@ -462,7 +578,7 @@ public abstract class ScriptUtils {
String script;
try {
script = readScript(resource, commentPrefix, separator, blockCommentEndDelimiter);
script = readScript(resource, commentPrefixes, separator, blockCommentEndDelimiter);
}
catch (IOException ex) {
throw new CannotReadScriptException(resource, ex);
@ -476,7 +592,7 @@ public abstract class ScriptUtils {
}
List<String> statements = new ArrayList<>();
splitSqlScript(resource, script, separator, commentPrefix, blockCommentStartDelimiter,
splitSqlScript(resource, script, separator, commentPrefixes, blockCommentStartDelimiter,
blockCommentEndDelimiter, statements);
int stmtNumber = 0;

View File

@ -25,6 +25,9 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER;
import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER;
import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_COMMENT_PREFIXES;
import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_STATEMENT_SEPARATOR;
import static org.springframework.jdbc.datasource.init.ScriptUtils.containsSqlScriptDelimiters;
import static org.springframework.jdbc.datasource.init.ScriptUtils.splitSqlScript;
@ -117,18 +120,25 @@ public class ScriptUtilsUnitTests {
@Test
public void readAndSplitScriptContainingComments() throws Exception {
String script = readScript("test-data-with-comments.sql");
splitScriptContainingComments(script);
splitScriptContainingComments(script, DEFAULT_COMMENT_PREFIXES);
}
@Test
public void readAndSplitScriptContainingCommentsWithWindowsLineEnding() throws Exception {
String script = readScript("test-data-with-comments.sql").replaceAll("\n", "\r\n");
splitScriptContainingComments(script);
splitScriptContainingComments(script, DEFAULT_COMMENT_PREFIXES);
}
private void splitScriptContainingComments(String script) throws Exception {
@Test
public void readAndSplitScriptContainingCommentsWithMultiplePrefixes() throws Exception {
String script = readScript("test-data-with-multi-prefix-comments.sql");
splitScriptContainingComments(script, "--", "#", "^");
}
private void splitScriptContainingComments(String script, String... commentPrefixes) throws Exception {
List<String> statements = new ArrayList<>();
splitSqlScript(script, ';', statements);
splitSqlScript(null, script, ";", commentPrefixes, DEFAULT_BLOCK_COMMENT_START_DELIMITER,
DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements);
String statement1 = "insert into customer (id, name) values (1, 'Rod; Johnson'), (2, 'Adrian Collier')";
String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)";

View File

@ -0,0 +1,18 @@
-- The next comment line has no text after the '--' prefix.
--
-- The next comment line starts with a space.
-- x, y, z...
insert into customer (id, name)
values (1, 'Rod; Johnson'), (2, 'Adrian Collier');
-- This is also a comment.
insert into orders(id, order_date, customer_id)
values (1, '2008-01-02', 2);
# A comment with a different prefix
insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2);
INSERT INTO persons( person_id--
, name)
^ A comment with yet another different prefix
VALUES( 1 -- person_id
, 'Name' --name
);--