Fix regression for newline separators in SQL scripts

Changes made in conjunction with SPR-9531, introduced a regression with
regard to support for using a single newline character as the statement
separator within SQL scripts. Investigation of the cause of this issue
resulted in the discovery of another, similar issue: support for
multiple newlines as a statement separator has been broken for years
but has gone unnoticed until now.

The reason that both of these issues have gone unnoticed is a result of
the fact that the test suite only executes SQL script integration tests
against HSQL DB, and HSQL does not care if two statements occur on the
same line; whereas, the H2 database will throw an exception if multiple
statements are included on the same line when executing an update.

This commit addresses both of these issues and provides further
enhancements to Spring's SQL script support as follows.

 - ScriptUtils now properly checks if the supplied script contains the
   custom statement separator or default separator before falling back
   to the 'fallback' separator (i.e., newline).

 - Introduced FALLBACK_STATEMENT_SEPARATOR constant in ScriptUtils.

 - ScriptUtils.readScript() no longer omits empty lines from the input
   file since a statement separator string may in fact be composed of
   multiple newline characters.

 - Introduced overloaded variants of splitSqlScript() and
   executeSqlScript() in ScriptUtils with smaller argument lists for
   common use cases.

 - Extracted AbstractDatabasePopulatorTests from DatabasePopulatorTests
   and introduced concrete HsqlDatabasePopulatorTests and
   H2DatabasePopulatorTests subclasses for testing against HSQL and H2.

 - Split ScriptUtilsTests into ScriptUtilsUnitTests and
   ScriptUtilsIntegrationTests for faster builds.

Issue: SPR-11560
This commit is contained in:
Sam Brannen
2014-03-16 16:41:08 +01:00
parent e7b8a657b4
commit bb67cd4657
7 changed files with 359 additions and 149 deletions

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.jdbc.datasource.init;
import org.junit.After;
import org.junit.Before;
import org.springframework.core.io.ClassRelativeResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.transaction.support.TransactionSynchronizationManager;
/**
* Abstract base class for integration tests involving database initialization.
*
* @author Sam Brannen
* @since 4.0.3
*/
public abstract class AbstractDatabaseInitializationTests {
private final ClassRelativeResourceLoader resourceLoader = new ClassRelativeResourceLoader(getClass());
protected EmbeddedDatabase db;
protected JdbcTemplate jdbcTemplate;
@Before
public void setUp() {
db = new EmbeddedDatabaseBuilder().setType(getEmbeddedDatabaseType()).build();
jdbcTemplate = new JdbcTemplate(db);
}
@After
public void shutDown() {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.clear();
TransactionSynchronizationManager.unbindResource(db);
}
db.shutdown();
}
protected abstract EmbeddedDatabaseType getEmbeddedDatabaseType();
protected Resource resource(String path) {
return resourceLoader.getResource(path);
}
}

View File

@@ -19,14 +19,9 @@ package org.springframework.jdbc.datasource.init;
import java.sql.Connection;
import java.sql.SQLException;
import org.junit.After;
import org.junit.Test;
import org.springframework.core.io.ClassRelativeResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import static org.hamcrest.Matchers.*;
@@ -34,57 +29,16 @@ import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Abstract base class for integration tests for {@link ResourceDatabasePopulator}.
*
* @author Dave Syer
* @author Sam Brannen
* @author Oliver Gierke
*/
public class DatabasePopulatorTests {
private final EmbeddedDatabase db = new EmbeddedDatabaseBuilder().build();
public abstract class AbstractDatabasePopulatorTests extends AbstractDatabaseInitializationTests {
private final ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
private final ClassRelativeResourceLoader resourceLoader = new ClassRelativeResourceLoader(getClass());
private final JdbcTemplate jdbcTemplate = new JdbcTemplate(db);
private void assertTestDatabaseCreated() {
assertTestDatabaseCreated("Keith");
}
private void assertTestDatabaseCreated(String name) {
assertEquals(name, jdbcTemplate.queryForObject("select NAME from T_TEST", String.class));
}
private void assertUsersDatabaseCreated(String... lastNames) {
for (String lastName : lastNames) {
assertThat("Did not find user with last name [" + lastName + "].",
jdbcTemplate.queryForObject("select count(0) from users where last_name = ?", Integer.class, lastName),
equalTo(1));
}
}
private Resource resource(String path) {
return resourceLoader.getResource(path);
}
private Resource defaultSchema() {
return resource("db-schema.sql");
}
private Resource usersSchema() {
return resource("users-schema.sql");
}
@After
public void shutDown() {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.clear();
TransactionSynchronizationManager.unbindResource(db);
}
db.shutdown();
}
@Test
public void buildWithCommentsAndFailedDrop() throws Exception {
@@ -226,4 +180,28 @@ public class DatabasePopulatorTests {
DatabasePopulatorUtils.execute(databasePopulator, db);
}
private void assertTestDatabaseCreated() {
assertTestDatabaseCreated("Keith");
}
private void assertTestDatabaseCreated(String name) {
assertEquals(name, jdbcTemplate.queryForObject("select NAME from T_TEST", String.class));
}
private void assertUsersDatabaseCreated(String... lastNames) {
for (String lastName : lastNames) {
assertThat("Did not find user with last name [" + lastName + "].",
jdbcTemplate.queryForObject("select count(0) from users where last_name = ?", Integer.class, lastName),
equalTo(1));
}
}
private Resource defaultSchema() {
return resource("db-schema.sql");
}
private Resource usersSchema() {
return resource("users-schema.sql");
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.jdbc.datasource.init;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
/**
* @author Sam Brannen
* @since 4.0.3
*/
public class H2DatabasePopulatorTests extends AbstractDatabasePopulatorTests {
protected EmbeddedDatabaseType getEmbeddedDatabaseType() {
return EmbeddedDatabaseType.H2;
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.jdbc.datasource.init;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
/**
* @author Sam Brannen
* @since 4.0.3
*/
public class HsqlDatabasePopulatorTests extends AbstractDatabasePopulatorTests {
protected EmbeddedDatabaseType getEmbeddedDatabaseType() {
return EmbeddedDatabaseType.HSQL;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2002-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.jdbc.datasource.init;
import java.sql.SQLException;
import org.junit.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
/**
* Integration tests for {@link ScriptUtils}.
*
* @author Sam Brannen
* @see ScriptUtilsUnitTests
* @since 4.0.3
*/
public class ScriptUtilsIntegrationTests extends AbstractDatabaseInitializationTests {
protected EmbeddedDatabaseType getEmbeddedDatabaseType() {
return EmbeddedDatabaseType.HSQL;
}
@Test
public void executeSqlScript() throws SQLException {
ScriptUtils.executeSqlScript(db.getConnection(), resource("users-schema.sql"));
ScriptUtils.executeSqlScript(db.getConnection(), resource("test-data-with-multi-line-comments.sql"));
assertUsersDatabaseCreated("Hoeller", "Brannen");
}
private void assertUsersDatabaseCreated(String... lastNames) {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(db);
for (String lastName : lastNames) {
assertThat("Did not find user with last name [" + lastName + "].",
jdbcTemplate.queryForObject("select count(0) from users where last_name = ?", Integer.class, lastName),
equalTo(1));
}
}
}

View File

@@ -16,44 +16,27 @@
package org.springframework.jdbc.datasource.init;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import org.junit.After;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.springframework.jdbc.datasource.init.ScriptUtils.*;
/**
* Unit and integration tests for {@link ScriptUtils}.
* Unit tests for {@link ScriptUtils}.
*
* @author Thomas Risberg
* @author Sam Brannen
* @author Phillip Webb
* @author Chris Baldwin
* @see ScriptUtilsIntegrationTests
* @since 4.0.3
*/
public class ScriptUtilsTests {
private final EmbeddedDatabase db = new EmbeddedDatabaseBuilder().build();
@After
public void shutDown() {
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.clear();
TransactionSynchronizationManager.unbindResource(db);
}
db.shutdown();
}
public class ScriptUtilsUnitTests {
@Test
public void splitSqlScriptDelimitedWithSemicolon() {
@@ -66,7 +49,7 @@ public class ScriptUtilsTests {
char delim = ';';
String script = rawStatement1 + delim + rawStatement2 + delim + rawStatement3 + delim;
List<String> statements = new ArrayList<String>();
ScriptUtils.splitSqlScript(script, delim, statements);
splitSqlScript(script, delim, statements);
assertEquals("wrong number of statements", 3, statements.size());
assertEquals("statement 1 not split correctly", cleanedStatement1, statements.get(0));
assertEquals("statement 2 not split correctly", cleanedStatement2, statements.get(1));
@@ -81,7 +64,7 @@ public class ScriptUtilsTests {
char delim = '\n';
String script = statement1 + delim + statement2 + delim + statement3 + delim;
List<String> statements = new ArrayList<String>();
ScriptUtils.splitSqlScript(script, delim, statements);
splitSqlScript(script, delim, statements);
assertEquals("wrong number of statements", 3, statements.size());
assertEquals("statement 1 not split correctly", statement1, statements.get(0));
assertEquals("statement 2 not split correctly", statement2, statements.get(1));
@@ -89,14 +72,40 @@ public class ScriptUtilsTests {
}
@Test
public void readAndSplitScriptContainingComments() throws Exception {
EncodedResource resource = new EncodedResource(new ClassPathResource("test-data-with-comments.sql", getClass()));
String script = ScriptUtils.readScript(resource);
char delim = ';';
public void splitSqlScriptDelimitedWithNewLineButDefaultDelimiterSpecified() {
String statement1 = "do something";
String statement2 = "do something else";
char delim = '\n';
String script = statement1 + delim + statement2 + delim;
List<String> statements = new ArrayList<String>();
ScriptUtils.splitSqlScript(script, delim, statements);
splitSqlScript(script, DEFAULT_STATEMENT_SEPARATOR, statements);
assertEquals("wrong number of statements", 1, statements.size());
assertEquals("script should have been 'stripped' but not actually 'split'", script.replace('\n', ' '),
statements.get(0));
}
/**
* See <a href="https://jira.spring.io/browse/SPR-11560">SPR-11560</a>
*/
@Test
public void readAndSplitScriptWithMultipleNewlinesAsSeparator() throws Exception {
String script = readScript("db-test-data-multi-newline.sql");
List<String> statements = new ArrayList<String>();
splitSqlScript(script, "\n\n", statements);
String statement1 = "insert into T_TEST (NAME) values ('Keith')";
String statement2 = "insert into T_TEST (NAME) values ('Dave')";
assertEquals("wrong number of statements", 2, statements.size());
assertEquals("statement 1 not split correctly", statement1, statements.get(0));
assertEquals("statement 2 not split correctly", statement2, statements.get(1));
}
@Test
public void readAndSplitScriptContainingComments() throws Exception {
String script = readScript("test-data-with-comments.sql");
List<String> statements = new ArrayList<String>();
splitSqlScript(script, ';', 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)";
@@ -112,18 +121,13 @@ public class ScriptUtilsTests {
}
/**
* See <a href="https://jira.springsource.org/browse/SPR-10330">SPR-10330</a>
* See <a href="https://jira.spring.io/browse/SPR-10330">SPR-10330</a>
*/
@Test
public void readAndSplitScriptContainingCommentsWithLeadingTabs() throws Exception {
EncodedResource resource = new EncodedResource(new ClassPathResource(
"test-data-with-comments-and-leading-tabs.sql", getClass()));
String script = ScriptUtils.readScript(resource);
char delim = ';';
String script = readScript("test-data-with-comments-and-leading-tabs.sql");
List<String> statements = new ArrayList<String>();
ScriptUtils.splitSqlScript(script, delim, statements);
splitSqlScript(script, ';', statements);
String statement1 = "insert into customer (id, name) values (1, 'Sam Brannen')";
String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2013-06-08', 1)";
@@ -136,18 +140,13 @@ public class ScriptUtilsTests {
}
/**
* See <a href="https://jira.springsource.org/browse/SPR-9531">SPR-9531</a>
* See <a href="https://jira.spring.io/browse/SPR-9531">SPR-9531</a>
*/
@Test
public void readAndSplitScriptContainingMuliLineComments() throws Exception {
EncodedResource resource = new EncodedResource(new ClassPathResource("test-data-with-multi-line-comments.sql",
getClass()));
String script = ScriptUtils.readScript(resource);
char delim = ';';
String script = readScript("test-data-with-multi-line-comments.sql");
List<String> statements = new ArrayList<String>();
ScriptUtils.splitSqlScript(script, delim, statements);
splitSqlScript(script, ';', statements);
String statement1 = "INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller')";
String statement2 = "INSERT INTO users(first_name, last_name) VALUES( 'Sam' , 'Brannen' )";
@@ -159,38 +158,15 @@ public class ScriptUtilsTests {
@Test
public void containsDelimiters() {
assertTrue("test with ';' is wrong", !ScriptUtils.containsSqlScriptDelimiters("select 1\n select ';'", ";"));
assertTrue("test with delimiter ; is wrong", ScriptUtils.containsSqlScriptDelimiters("select 1; select 2", ";"));
assertTrue("test with '\\n' is wrong",
!ScriptUtils.containsSqlScriptDelimiters("select 1; select '\\n\n';", "\n"));
assertTrue("test with delimiter \\n is wrong",
ScriptUtils.containsSqlScriptDelimiters("select 1\n select 2", "\n"));
assertTrue("test with ';' is wrong", !containsSqlScriptDelimiters("select 1\n select ';'", ";"));
assertTrue("test with delimiter ; is wrong", containsSqlScriptDelimiters("select 1; select 2", ";"));
assertTrue("test with '\\n' is wrong", !containsSqlScriptDelimiters("select 1; select '\\n\n';", "\n"));
assertTrue("test with delimiter \\n is wrong", containsSqlScriptDelimiters("select 1\n select 2", "\n"));
}
@Test
public void executeSqlScript() throws SQLException {
EncodedResource schemaResource = new EncodedResource(new ClassPathResource("users-schema.sql", getClass()));
EncodedResource commentResource = new EncodedResource(new ClassPathResource(
"test-data-with-multi-line-comments.sql", getClass()));
Connection connection = db.getConnection();
ScriptUtils.executeSqlScript(connection, schemaResource, false, false, ScriptUtils.DEFAULT_COMMENT_PREFIX,
ScriptUtils.DEFAULT_STATEMENT_SEPARATOR, ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER,
ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER);
ScriptUtils.executeSqlScript(connection, commentResource, false, false, ScriptUtils.DEFAULT_COMMENT_PREFIX,
ScriptUtils.DEFAULT_STATEMENT_SEPARATOR, ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER,
ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER);
assertUsersDatabaseCreated("Hoeller", "Brannen");
}
private void assertUsersDatabaseCreated(String... lastNames) {
final JdbcTemplate jdbcTemplate = new JdbcTemplate(db);
for (String lastName : lastNames) {
assertThat("Did not find user with last name [" + lastName + "].",
jdbcTemplate.queryForObject("select count(0) from users where last_name = ?", Integer.class, lastName),
equalTo(1));
}
private String readScript(String path) throws Exception {
EncodedResource resource = new EncodedResource(new ClassPathResource(path, getClass()));
return ScriptUtils.readScript(resource);
}
}