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 271eff0210..6397c45d48 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 @@ -418,12 +418,44 @@ public abstract class ScriptUtils { *

This method is intended to be used to find the string delimiting each * SQL statement — for example, a ';' character. *

Any occurrence of the delimiter within the script will be ignored if it - * is enclosed within single quotes ({@code '}) or double quotes ({@code "}) - * or if it is escaped with a backslash ({@code \}). + * is within a literal block of text enclosed in single quotes + * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash + * ({@code \}), or if it is within a single-line comment or block comment. * @param script the SQL script to search within - * @param delimiter the delimiter to search for + * @param delimiter the statement delimiter to search for + * @see #DEFAULT_COMMENT_PREFIXES + * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER + * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER */ public static boolean containsSqlScriptDelimiters(String script, String delimiter) { + return containsStatementSeparator(null, script, delimiter, DEFAULT_COMMENT_PREFIXES, + DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER); + } + + /** + * Determine if the provided SQL script contains the specified statement separator. + *

This method is intended to be used to find the string separating each + * SQL statement — for example, a ';' character. + *

Any occurrence of the separator within the script will be ignored if it + * is within a literal block of text enclosed in single quotes + * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash + * ({@code \}), or if it is within a single-line comment or block comment. + * @param resource the resource from which the script was read, or {@code null} + * if unknown + * @param script the SQL script to search within + * @param separator the statement separator to search for + * @param commentPrefixes the prefixes that identify single-line comments + * (typically {@code "--"}) + * @param blockCommentStartDelimiter the start block comment delimiter + * (typically {@code "/*"}) + * @param blockCommentEndDelimiter the end block comment delimiter + * (typically "*/") + * @since 5.2.16 + */ + 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; @@ -446,9 +478,33 @@ public abstract class ScriptUtils { inDoubleQuote = !inDoubleQuote; } if (!inSingleQuote && !inDoubleQuote) { - if (script.startsWith(delimiter, i)) { + 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); + } + } } } @@ -595,7 +651,9 @@ public abstract class ScriptUtils { if (separator == null) { separator = DEFAULT_STATEMENT_SEPARATOR; } - if (!EOF_STATEMENT_SEPARATOR.equals(separator) && !containsSqlScriptDelimiters(script, separator)) { + if (!EOF_STATEMENT_SEPARATOR.equals(separator) && + !containsStatementSeparator(resource, script, separator, commentPrefixes, + blockCommentStartDelimiter, blockCommentEndDelimiter)) { separator = FALLBACK_STATEMENT_SEPARATOR; } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java index 25c2e7f13a..8b07ce617c 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java @@ -205,9 +205,25 @@ public class ScriptUtilsUnitTests { "'select 1\n\n select 2' # '\n\n' # true", // semicolon with MySQL style escapes '\\' "'insert into users(first, last)\nvalues(''a\\\\'', ''b;'')' # ; # false", - "'insert into users(first, last)\nvalues(''Charles'', ''d\\''Artagnan''); select 1' # ; # true" + "'insert into users(first, last)\nvalues(''Charles'', ''d\\''Artagnan''); select 1' # ; # true", + // semicolon inside comments + "'-- a;b;c\ninsert into colors(color_num) values(42);' # ; # true", + "'/* a;b;c */\ninsert into colors(color_num) values(42);' # ; # true", + "'-- a;b;c\ninsert into colors(color_num) values(42)' # ; # false", + "'/* a;b;c */\ninsert into colors(color_num) values(42)' # ; # false", + // single quotes inside comments + "'-- What\\''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true", + "'-- What''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true", + "'/* What\\''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true", + "'/* What''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true", + // double quotes inside comments + "'-- double \" quotes\ninsert into colors(color_num) values(42);' # ; # true", + "'-- double \\\" quotes\ninsert into colors(color_num) values(42);' # ; # true", + "'/* double \" quotes */\ninsert into colors(color_num) values(42);' # ; # true", + "'/* double \\\" quotes */\ninsert into colors(color_num) values(42);' # ; # true" }) - public void containsDelimiter(String script, String delimiter, boolean expected) { + public void containsStatementSeparator(String script, String delimiter, boolean expected) { + // Indirectly tests ScriptUtils.containsStatementSeparator(EncodedResource, String, String, String[], String, String). assertThat(containsSqlScriptDelimiters(script, delimiter)).isEqualTo(expected); } diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql index 7faa91c250..5df377ccad 100644 --- a/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql @@ -5,16 +5,19 @@ * x, y, z... */ +-- This is a single line comment containing single (') and double quotes ("). INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller'); -- This is also a comment. /*------------------------------------------- --- A fancy multi-line comments that puts +-- A fancy multi-line comment that puts -- single line comments inside of a multi-line -- comment block. Moreover, the block comment end delimiter appears on a line that can potentially also be a single-line comment if we weren't already inside a multi-line comment run. + +And here's a line containing single and double quotes ("). -------------------------------------------*/ INSERT INTO users(first_name, last_name) -- This is a single line comment containing the block-end-comment sequence here */ but it's still a single-line comment 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 6cd2b6a490..6dddcc1632 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 @@ -436,12 +436,44 @@ public abstract class ScriptUtils { *

This method is intended to be used to find the string delimiting each * SQL statement — for example, a ';' character. *

Any occurrence of the delimiter within the script will be ignored if it - * is enclosed within single quotes ({@code '}) or double quotes ({@code "}) - * or if it is escaped with a backslash ({@code \}). + * is within a literal block of text enclosed in single quotes + * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash + * ({@code \}), or if it is within a single-line comment or block comment. * @param script the SQL script to search within - * @param delimiter the delimiter to search for + * @param delimiter the statement delimiter to search for + * @see #DEFAULT_COMMENT_PREFIXES + * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER + * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER */ public static boolean containsSqlScriptDelimiters(String script, String delimiter) { + return containsStatementSeparator(null, script, delimiter, DEFAULT_COMMENT_PREFIXES, + DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER); + } + + /** + * Determine if the provided SQL script contains the specified statement separator. + *

This method is intended to be used to find the string separating each + * SQL statement — for example, a ';' character. + *

Any occurrence of the separator within the script will be ignored if it + * is within a literal block of text enclosed in single quotes + * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash + * ({@code \}), or if it is within a single-line comment or block comment. + * @param resource the resource from which the script was read, or {@code null} + * if unknown + * @param script the SQL script to search within + * @param separator the statement separator to search for + * @param commentPrefixes the prefixes that identify single-line comments + * (typically {@code "--"}) + * @param blockCommentStartDelimiter the start block comment delimiter + * (typically {@code "/*"}) + * @param blockCommentEndDelimiter the end block comment delimiter + * (typically "*/") + * @since 5.2.16 + */ + 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; @@ -464,9 +496,33 @@ public abstract class ScriptUtils { inDoubleQuote = !inDoubleQuote; } if (!inSingleQuote && !inDoubleQuote) { - if (script.startsWith(delimiter, i)) { + 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); + } + } } } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java index e98c5dafc0..334afe6e3a 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ScriptUtilsUnitTests.java @@ -207,9 +207,25 @@ public class ScriptUtilsUnitTests { "'select 1\n\n select 2' # '\n\n' # true", // semicolon with MySQL style escapes '\\' "'insert into users(first, last)\nvalues(''a\\\\'', ''b;'')' # ; # false", - "'insert into users(first, last)\nvalues(''Charles'', ''d\\''Artagnan''); select 1' # ; # true" + "'insert into users(first, last)\nvalues(''Charles'', ''d\\''Artagnan''); select 1' # ; # true", + // semicolon inside comments + "'-- a;b;c\ninsert into colors(color_num) values(42);' # ; # true", + "'/* a;b;c */\ninsert into colors(color_num) values(42);' # ; # true", + "'-- a;b;c\ninsert into colors(color_num) values(42)' # ; # false", + "'/* a;b;c */\ninsert into colors(color_num) values(42)' # ; # false", + // single quotes inside comments + "'-- What\\''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true", + "'-- What''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true", + "'/* What\\''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true", + "'/* What''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true", + // double quotes inside comments + "'-- double \" quotes\ninsert into colors(color_num) values(42);' # ; # true", + "'-- double \\\" quotes\ninsert into colors(color_num) values(42);' # ; # true", + "'/* double \" quotes */\ninsert into colors(color_num) values(42);' # ; # true", + "'/* double \\\" quotes */\ninsert into colors(color_num) values(42);' # ; # true" }) - public void containsDelimiter(String script, String delimiter, boolean expected) { + public void containsStatementSeparator(String script, String delimiter, boolean expected) { + // Indirectly tests ScriptUtils.containsStatementSeparator(EncodedResource, String, String, String[], String, String). assertThat(containsSqlScriptDelimiters(script, delimiter)).isEqualTo(expected); } diff --git a/spring-r2dbc/src/test/resources/org/springframework/r2dbc/connection/init/test-data-with-multi-line-nested-comments.sql b/spring-r2dbc/src/test/resources/org/springframework/r2dbc/connection/init/test-data-with-multi-line-nested-comments.sql index 7faa91c250..5df377ccad 100644 --- a/spring-r2dbc/src/test/resources/org/springframework/r2dbc/connection/init/test-data-with-multi-line-nested-comments.sql +++ b/spring-r2dbc/src/test/resources/org/springframework/r2dbc/connection/init/test-data-with-multi-line-nested-comments.sql @@ -5,16 +5,19 @@ * x, y, z... */ +-- This is a single line comment containing single (') and double quotes ("). INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller'); -- This is also a comment. /*------------------------------------------- --- A fancy multi-line comments that puts +-- A fancy multi-line comment that puts -- single line comments inside of a multi-line -- comment block. Moreover, the block comment end delimiter appears on a line that can potentially also be a single-line comment if we weren't already inside a multi-line comment run. + +And here's a line containing single and double quotes ("). -------------------------------------------*/ INSERT INTO users(first_name, last_name) -- This is a single line comment containing the block-end-comment sequence here */ but it's still a single-line comment