diff --git a/modules/database-commons/build.gradle b/modules/database-commons/build.gradle index 8b9c288b9d4..27d1aeeb5e6 100644 --- a/modules/database-commons/build.gradle +++ b/modules/database-commons/build.gradle @@ -2,4 +2,6 @@ description = "Testcontainers :: Database-Commons" dependencies { compile project(':testcontainers') + + testCompile 'org.assertj:assertj-core:3.12.2' } diff --git a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java index eb041708fb6..d30064af28e 100644 --- a/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java +++ b/modules/database-commons/src/main/java/org/testcontainers/ext/ScriptUtils.java @@ -163,10 +163,10 @@ public static void splitSqlScript(String resource, String script, String separat } final boolean inComment = inLineComment || inBlockComment; - if (!inLiteral && !inComment && containsSubstringAtOffset(lowerCaseScriptContent, "BEGIN", i)) { + if (!inLiteral && !inComment && containsKeywordsAtOffset(lowerCaseScriptContent, "BEGIN", i, separator, commentPrefix, blockCommentStartDelimiter)) { compoundStatementDepth++; } - if (!inLiteral && !inComment && containsSubstringAtOffset(lowerCaseScriptContent, "END", i)) { + if (!inLiteral && !inComment && containsKeywordsAtOffset(lowerCaseScriptContent, "END", i, separator, commentPrefix, blockCommentStartDelimiter)) { compoundStatementDepth--; } final boolean inCompoundStatement = compoundStatementDepth != 0; @@ -174,10 +174,7 @@ public static void splitSqlScript(String resource, String script, String separat if (!inLiteral && !inCompoundStatement) { 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(); - } + sb = flushStringBuilder(sb, statements); i += separator.length() - 1; continue; } @@ -199,6 +196,7 @@ else if (script.startsWith(blockCommentStartDelimiter, i)) { int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i); if (indexOfCommentEnd > i) { i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1; + inBlockComment = false; continue; } else { @@ -218,24 +216,57 @@ else if (c == ' ' || c == '\n' || c == '\t' || c == '\r') { } sb.append(c); } - if (StringUtils.isNotEmpty(sb.toString())) { - statements.add(sb.toString()); + flushStringBuilder(sb, statements); + } + + private static StringBuilder flushStringBuilder(StringBuilder sb, List statements) { + if (sb.length() == 0) { + return sb; } + + final String s = sb.toString().trim(); + if (StringUtils.isNotEmpty(s)) { + statements.add(s); + } + + return new StringBuilder(); } + private static boolean isSeperator(char c, String separator, String commentPrefix, + String blockCommentStartDelimiter) { + return c == ' ' || c == '\r' || c == '\n' || c == '\t' || + c == separator.charAt(0) || c == separator.charAt(separator.length() - 1) || + c == commentPrefix.charAt(0) || c == blockCommentStartDelimiter.charAt(0) || + c == blockCommentStartDelimiter.charAt(blockCommentStartDelimiter.length() - 1); + } + private static boolean containsSubstringAtOffset(String lowercaseString, String substring, int offset) { String lowercaseSubstring = substring.toLowerCase(); return lowercaseString.startsWith(lowercaseSubstring, offset); } + private static boolean containsKeywordsAtOffset(String lowercaseString, String keywords, int offset, + String separator, String commentPrefix, + String blockCommentStartDelimiter) { + String lowercaseKeywords = keywords.toLowerCase(); + + boolean backSeperated = (offset == 0) || isSeperator(lowercaseString.charAt(offset - 1), + separator, commentPrefix, blockCommentStartDelimiter); + boolean frontSeperated = (offset >= (lowercaseString.length() - keywords.length())) || + isSeperator(lowercaseString.charAt(offset + keywords.length()), + separator, commentPrefix, blockCommentStartDelimiter); + + return backSeperated && frontSeperated && lowercaseString.startsWith(lowercaseKeywords, offset); + } + private static void checkArgument(boolean expression, String errorMessage) { if (!expression) { throw new IllegalArgumentException(errorMessage); } } - /** + /** * Does the provided SQL script contain the specified delimiter? * @param script the SQL script * @param delim String delimiting each statement - typically a ';' character @@ -356,7 +387,7 @@ public ScriptLoadException(String message, Throwable cause) { } } - private static class ScriptParseException extends RuntimeException { + public static class ScriptParseException extends RuntimeException { public ScriptParseException(String format, String scriptPath) { super(String.format(format, scriptPath)); } diff --git a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java new file mode 100644 index 00000000000..0836592b91e --- /dev/null +++ b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptSplittingTest.java @@ -0,0 +1,294 @@ +package org.testcontainers.ext; + +import org.assertj.core.api.Assertions; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static java.util.Arrays.asList; +import static org.junit.Assert.fail; +import static org.testcontainers.ext.ScriptUtils.*; + +public class ScriptSplittingTest { + + @Test + public void testStringDemarcation() { + String script = "SELECT 'foo `bar`'; SELECT 'foo -- `bar`'; SELECT 'foo /* `bar`';"; + + List expected = asList( + "SELECT 'foo `bar`'", + "SELECT 'foo -- `bar`'", + "SELECT 'foo /* `bar`'" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testIssue1547Case1() { + String script = "create database if not exists ttt;\n" + + "\n" + + "use ttt;\n" + + "\n" + + "create table aaa\n" + + "(\n" + + " id bigint auto_increment primary key,\n" + + " end_time datetime null COMMENT 'end_time',\n" + + " data_status varchar(16) not null\n" + + ") comment 'aaa';\n" + + "\n" + + "create table bbb\n" + + "(\n" + + " id bigint auto_increment primary key\n" + + ") comment 'bbb';"; + + List expected = asList( + "create database if not exists ttt", + "use ttt", + "create table aaa ( id bigint auto_increment primary key, end_time datetime null COMMENT 'end_time', data_status varchar(16) not null ) comment 'aaa'", + "create table bbb ( id bigint auto_increment primary key ) comment 'bbb'" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testIssue1547Case2() { + String script = "CREATE TABLE bar (\n" + + " end_time VARCHAR(255)\n" + + ");\n" + + "CREATE TABLE bar (\n" + + " end_time VARCHAR(255)\n" + + ");"; + + List expected = asList( + "CREATE TABLE bar ( end_time VARCHAR(255) )", + "CREATE TABLE bar ( end_time VARCHAR(255) )" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testUnusualSemicolonPlacement() { + String script = "SELECT 1;;;;;SELECT 2;\n;SELECT 3\n; SELECT 4;\n SELECT 5"; + + List expected = asList( + "SELECT 1", + "SELECT 2", + "SELECT 3", + "SELECT 4", + "SELECT 5" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testCommentedSemicolon() { + String script = "CREATE TABLE bar (\n" + + " foo VARCHAR(255)\n" + + "); \nDROP PROCEDURE IF EXISTS -- ;\n" + + " count_foo"; + + List expected = asList( + "CREATE TABLE bar ( foo VARCHAR(255) )", + "DROP PROCEDURE IF EXISTS count_foo" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testStringEscaping() { + String script = "SELECT \"a /* string literal containing comment characters like -- here\";\n" + + "SELECT \"a 'quoting' \\\"scenario ` involving BEGIN keyword\\\" here\";\n" + + "SELECT * from `bar`;"; + + List expected = asList( + "SELECT \"a /* string literal containing comment characters like -- here\"", + "SELECT \"a 'quoting' \\\"scenario ` involving BEGIN keyword\\\" here\"", + "SELECT * from `bar`" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testBlockCommentExclusion() { + String script = "INSERT INTO bar (foo) /* ; */ VALUES ('hello world');"; + + List expected = asList( + "INSERT INTO bar (foo) VALUES ('hello world')" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testBeginEndKeywordCorrectDetection() { + String script = "INSERT INTO something_end (begin_with_the_token, another_field) /*end*/ VALUES /* end */ (' begin ', `end`)-- begin\n;"; + + List expected = asList( + "INSERT INTO something_end (begin_with_the_token, another_field) VALUES (' begin ', `end`)" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testCommentInStrings() { + String script = "CREATE TABLE bar (foo VARCHAR(255));\n" + + "\n" + + "/* Insert Values */\n" + + "INSERT INTO bar (foo) values ('--1');\n" + + "INSERT INTO bar (foo) values ('--2');\n" + + "INSERT INTO bar (foo) values ('/* something */');\n" + + "/* INSERT INTO bar (foo) values (' */'); -- '*/;\n" + // purposefully broken, to see if it breaks our splitting + "INSERT INTO bar (foo) values ('foo');"; + + List expected = asList( + "CREATE TABLE bar (foo VARCHAR(255))", + "INSERT INTO bar (foo) values ('--1')", + "INSERT INTO bar (foo) values ('--2')", + "INSERT INTO bar (foo) values ('/* something */')", + "'); -- '*/", + "INSERT INTO bar (foo) values ('foo')" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testMultipleBeginEndDetection() { + String script = "CREATE TABLE bar (foo VARCHAR(255));\n" + + "\n" + + "CREATE TABLE gender (gender VARCHAR(255));\n" + + "CREATE TABLE ending (ending VARCHAR(255));\n" + + "CREATE TABLE end2 (end2 VARCHAR(255));\n" + + "CREATE TABLE end_2 (end2 VARCHAR(255));\n" + + "\n" + + "BEGIN\n" + + " INSERT INTO ending values ('ending');\n" + + "END;\n" + + "\n" + + "BEGIN\n" + + " INSERT INTO ending values ('ending');\n" + + "END/*hello*/;\n" + + "\n" + + "BEGIN--Hello\n" + + " INSERT INTO ending values ('ending');\n" + + "END;\n" + + "\n" + + "/*Hello*/BEGIN\n" + + " INSERT INTO ending values ('ending');\n" + + "END;\n" + + "\n" + + "CREATE TABLE foo (bar VARCHAR(255));"; + + List expected = asList( + "CREATE TABLE bar (foo VARCHAR(255))", + "CREATE TABLE gender (gender VARCHAR(255))", + "CREATE TABLE ending (ending VARCHAR(255))", + "CREATE TABLE end2 (end2 VARCHAR(255))", + "CREATE TABLE end_2 (end2 VARCHAR(255))", + "BEGIN\n" + + " INSERT INTO ending values ('ending');\n" + + "END", + "BEGIN\n" + + " INSERT INTO ending values ('ending');\n" + + "END", + "BEGIN--Hello\n" + + " INSERT INTO ending values ('ending');\n" + + "END", + "BEGIN\n" + + " INSERT INTO ending values ('ending');\n" + + "END", + "CREATE TABLE foo (bar VARCHAR(255))" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testProcedureBlock() { + String script = "CREATE PROCEDURE count_foo()\n" + + " BEGIN\n" + + "\n" + + " BEGIN\n" + + " SELECT *\n" + + " FROM bar;\n" + + " SELECT 1\n" + + " FROM dual;\n" + + " END;\n" + + "\n" + + " BEGIN\n" + + " select * from bar;\n" + + " END;\n" + + "\n" + + " -- we can do comments\n" + + "\n" + + " /* including block\n" + + " comments\n" + + " */\n" + + "\n" + + " /* what if BEGIN appears inside a comment? */\n" + + "\n" + + " select \"or what if BEGIN appears inside a literal?\";\n" + + "\n" + + " END /*; */;"; + + List expected = asList( + "CREATE PROCEDURE count_foo() BEGIN\n" + + "\n" + + " BEGIN\n" + + " SELECT *\n" + + " FROM bar;\n" + + " SELECT 1\n" + + " FROM dual;\n" + + " END;\n" + + "\n" + + " BEGIN\n" + + " select * from bar;\n" + + " END;\n" + + "\n" + + " -- we can do comments\n" + + "\n" + + " /* including block\n" + + " comments\n" + + " */\n" + + "\n" + + " /* what if BEGIN appears inside a comment? */\n" + + "\n" + + " select \"or what if BEGIN appears inside a literal?\";\n" + + "\n" + + " END" + ); + + splitAndCompare(script, expected); + } + + @Test + public void testUnclosedBlockComment() { + String script = "SELECT 'foo `bar`'; /*"; + + try { + doSplit(script); + fail("Should have thrown!"); + } catch (ScriptUtils.ScriptParseException expected) { + // ignore expected exception + } + } + + private void splitAndCompare(String script, List expected) { + final List statements = doSplit(script); + Assertions.assertThat(statements).isEqualTo(expected); + } + + private List doSplit(String script) { + final List statements = new ArrayList<>(); + ScriptUtils.splitSqlScript("ignored", script, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_COMMENT_PREFIX, DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements); + return statements; + } +} diff --git a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptUtilsTest.java b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptUtilsTest.java deleted file mode 100644 index c9b1218321b..00000000000 --- a/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptUtilsTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.testcontainers.ext; - -import com.google.common.base.Charsets; -import com.google.common.io.Resources; -import org.junit.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import static org.junit.Assert.assertEquals; - -public class ScriptUtilsTest { - - /* - * Test ScriptUtils script splitting with some ugly/hard-to-split cases - */ - @Test - public void testSplit() throws IOException { - final String script = Resources.toString(Resources.getResource("splittable.sql"), Charsets.UTF_8); - - final List statements = new ArrayList<>(); - ScriptUtils.splitSqlScript("resourcename", script, ";", "--", "/*", "*/", statements); - - assertEquals(7, statements.size()); - assertEquals("SELECT \"a /* string literal containing comment characters like -- here\"", statements.get(2)); - assertEquals("SELECT \"a 'quoting' \\\"scenario ` involving BEGIN keyword\\\" here\"", statements.get(3)); - assertEquals("SELECT * from `bar`", statements.get(4)); - assertEquals("INSERT INTO bar (foo) VALUES ('hello world')", statements.get(6)); - } - - /* - * Test ScriptUtils script splitting with some ugly/hard-to-split cases and linux line endings - */ - @Test - public void testSplitWithWidnwosLineEnding() throws IOException { - final String script = Resources.toString(Resources.getResource("splittable.sql"), Charsets.UTF_8); - final String scriptWithWindowsLineEndings = script.replaceAll("\n", "\r\n"); - final List statements = new ArrayList<>(); - ScriptUtils.splitSqlScript("resourcename", scriptWithWindowsLineEndings, ";", "--", "/*", "*/", statements); - - assertEquals(7, statements.size()); - assertEquals("SELECT \"a /* string literal containing comment characters like -- here\"", statements.get(2)); - assertEquals("SELECT \"a 'quoting' \\\"scenario ` involving BEGIN keyword\\\" here\"", statements.get(3)); - assertEquals("SELECT * from `bar`", statements.get(4)); - assertEquals("INSERT INTO bar (foo) VALUES ('hello world')", statements.get(6)); - } -} diff --git a/modules/database-commons/src/test/resources/splittable.sql b/modules/database-commons/src/test/resources/splittable.sql deleted file mode 100644 index addede19dd7..00000000000 --- a/modules/database-commons/src/test/resources/splittable.sql +++ /dev/null @@ -1,45 +0,0 @@ -CREATE TABLE bar ( - foo VARCHAR(255) -); - -DROP PROCEDURE IF EXISTS -- ; - count_foo; - -SELECT "a /* string literal containing comment characters like -- here"; -SELECT "a 'quoting' \"scenario ` involving BEGIN keyword\" here"; -SELECT * from `bar`; - --- What about a line comment containing imbalanced string delimiters? " - -CREATE PROCEDURE count_foo() - BEGIN - - BEGIN - SELECT * - FROM bar; - SELECT 1 - FROM dual; - END; - - BEGIN - select * from bar; - END; - - -- we can do comments - - /* including block - comments - */ - - /* what if BEGIN appears inside a comment? */ - - select "or what if BEGIN appears inside a literal?"; - - END /*; */; - -/* or a block comment - containing imbalanced string delimiters? - ' " - */ - -INSERT INTO bar (foo) /* ; */ VALUES ('hello world');