Ignore comments when searching for statement delimiter in ScriptUtils

Prior to this commit, the implementations of
ScriptUtils.containsSqlScriptDelimiters() in spring-jdbc and
spring-r2dbc did not ignore comments when searching for the statement
delimiter within an SQL script. This resulted in subtle bugs if a
comment contained a single single-quote or single double-quote, since
the absence of the closing single-quote or double-quote led the
algorithm to believe that it was still "within a text literal". Similar
issues could arise if a comment contained the sought statement
delimiter but the rest of the script did not contain the sought
statement delimiter. In such cases, the algorithms in ScriptUtils could
erroneously choose an incorrect statement delimiter -- for example,
using the fallback statement delimiter instead of the delimiter
specified by the user.

This commit avoids such bugs by ignoring single-line comments and block
comments when searching for the statement delimiter within an SQL
script.

Closes gh-26911
This commit is contained in:
Sam Brannen 2021-05-13 16:17:51 +02:00
parent fae484855b
commit 569ce840cf
6 changed files with 167 additions and 15 deletions

View File

@ -418,12 +418,44 @@ public abstract class ScriptUtils {
* <p>This method is intended to be used to find the string delimiting each * <p>This method is intended to be used to find the string delimiting each
* SQL statement &mdash; for example, a ';' character. * SQL statement &mdash; for example, a ';' character.
* <p>Any occurrence of the delimiter within the script will be ignored if it * <p>Any occurrence of the delimiter within the script will be ignored if it
* is enclosed within single quotes ({@code '}) or double quotes ({@code "}) * is within a <em>literal</em> block of text enclosed in single quotes
* or if it is escaped with a backslash ({@code \}). * ({@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 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) { 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.
* <p>This method is intended to be used to find the string separating each
* SQL statement &mdash; for example, a ';' character.
* <p>Any occurrence of the separator within the script will be ignored if it
* is within a <em>literal</em> 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 <em>start</em> block comment delimiter
* (typically {@code "/*"})
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter
* (typically <code>"*&#47;"</code>)
* @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 inSingleQuote = false;
boolean inDoubleQuote = false; boolean inDoubleQuote = false;
boolean inEscape = false; boolean inEscape = false;
@ -446,9 +478,33 @@ public abstract class ScriptUtils {
inDoubleQuote = !inDoubleQuote; inDoubleQuote = !inDoubleQuote;
} }
if (!inSingleQuote && !inDoubleQuote) { if (!inSingleQuote && !inDoubleQuote) {
if (script.startsWith(delimiter, i)) { if (script.startsWith(separator, i)) {
return true; 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) { if (separator == null) {
separator = DEFAULT_STATEMENT_SEPARATOR; 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; separator = FALLBACK_STATEMENT_SEPARATOR;
} }

View File

@ -205,9 +205,25 @@ public class ScriptUtilsUnitTests {
"'select 1\n\n select 2' # '\n\n' # true", "'select 1\n\n select 2' # '\n\n' # true",
// semicolon with MySQL style escapes '\\' // semicolon with MySQL style escapes '\\'
"'insert into users(first, last)\nvalues(''a\\\\'', ''b;'')' # ; # false", "'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); assertThat(containsSqlScriptDelimiters(script, delimiter)).isEqualTo(expected);
} }

View File

@ -5,16 +5,19 @@
* x, y, z... * x, y, z...
*/ */
-- This is a single line comment containing single (') and double quotes (").
INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller'); INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller');
-- This is also a comment. -- 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 -- single line comments inside of a multi-line
-- comment block. -- comment block.
Moreover, the block comment end delimiter Moreover, the block comment end delimiter
appears on a line that can potentially also appears on a line that can potentially also
be a single-line comment if we weren't be a single-line comment if we weren't
already inside a multi-line comment run. already inside a multi-line comment run.
And here's a line containing single and double quotes (").
-------------------------------------------*/ -------------------------------------------*/
INSERT INTO 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 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

View File

@ -436,12 +436,44 @@ public abstract class ScriptUtils {
* <p>This method is intended to be used to find the string delimiting each * <p>This method is intended to be used to find the string delimiting each
* SQL statement &mdash; for example, a ';' character. * SQL statement &mdash; for example, a ';' character.
* <p>Any occurrence of the delimiter within the script will be ignored if it * <p>Any occurrence of the delimiter within the script will be ignored if it
* is enclosed within single quotes ({@code '}) or double quotes ({@code "}) * is within a <em>literal</em> block of text enclosed in single quotes
* or if it is escaped with a backslash ({@code \}). * ({@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 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) { 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.
* <p>This method is intended to be used to find the string separating each
* SQL statement &mdash; for example, a ';' character.
* <p>Any occurrence of the separator within the script will be ignored if it
* is within a <em>literal</em> 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 <em>start</em> block comment delimiter
* (typically {@code "/*"})
* @param blockCommentEndDelimiter the <em>end</em> block comment delimiter
* (typically <code>"*&#47;"</code>)
* @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 inSingleQuote = false;
boolean inDoubleQuote = false; boolean inDoubleQuote = false;
boolean inEscape = false; boolean inEscape = false;
@ -464,9 +496,33 @@ public abstract class ScriptUtils {
inDoubleQuote = !inDoubleQuote; inDoubleQuote = !inDoubleQuote;
} }
if (!inSingleQuote && !inDoubleQuote) { if (!inSingleQuote && !inDoubleQuote) {
if (script.startsWith(delimiter, i)) { if (script.startsWith(separator, i)) {
return true; 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);
}
}
} }
} }

View File

@ -207,9 +207,25 @@ public class ScriptUtilsUnitTests {
"'select 1\n\n select 2' # '\n\n' # true", "'select 1\n\n select 2' # '\n\n' # true",
// semicolon with MySQL style escapes '\\' // semicolon with MySQL style escapes '\\'
"'insert into users(first, last)\nvalues(''a\\\\'', ''b;'')' # ; # false", "'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); assertThat(containsSqlScriptDelimiters(script, delimiter)).isEqualTo(expected);
} }

View File

@ -5,16 +5,19 @@
* x, y, z... * x, y, z...
*/ */
-- This is a single line comment containing single (') and double quotes (").
INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller'); INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller');
-- This is also a comment. -- 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 -- single line comments inside of a multi-line
-- comment block. -- comment block.
Moreover, the block comment end delimiter Moreover, the block comment end delimiter
appears on a line that can potentially also appears on a line that can potentially also
be a single-line comment if we weren't be a single-line comment if we weren't
already inside a multi-line comment run. already inside a multi-line comment run.
And here's a line containing single and double quotes (").
-------------------------------------------*/ -------------------------------------------*/
INSERT INTO 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 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