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:
parent
fae484855b
commit
569ce840cf
|
@ -418,12 +418,44 @@ public abstract class ScriptUtils {
|
|||
* <p>This method is intended to be used to find the string delimiting each
|
||||
* SQL statement — for example, a ';' character.
|
||||
* <p>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 <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 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.
|
||||
* <p>This method is intended to be used to find the string separating each
|
||||
* SQL statement — 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>"*/"</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 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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -436,12 +436,44 @@ public abstract class ScriptUtils {
|
|||
* <p>This method is intended to be used to find the string delimiting each
|
||||
* SQL statement — for example, a ';' character.
|
||||
* <p>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 <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 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.
|
||||
* <p>This method is intended to be used to find the string separating each
|
||||
* SQL statement — 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>"*/"</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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue