Browse Source

Support multiple result sets in ScriptUtils.executeSqlScript()

Prior to this commit, ScriptUtils.executeSqlScript() treated every
statement within the script as if it were a single insert/update/delete
statement. This disregarded the fact that the execution of a JDBC
Statement can result in multiple individual statements, some of which
result in a ResultSet and others that result in an update count.

For example, when executing a stored procedure on Sybase, ScriptUtils
did not execute all statements within the stored procedure.

To address that, this commit revises the implementation of
ScriptUtils.executeSqlScript() so that it handles multiple results and
differentiates between result sets and update counts.

Closes gh-35248
pull/35405/head
Sam Brannen 4 months ago
parent
commit
37b076be51
  1. 39
      spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java
  2. 10
      spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java
  3. 2
      spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertIntegrationTests.java
  4. 69
      spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsIntegrationTests.java
  5. 2
      spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/users-schema-with-custom-schema.sql
  6. 2
      spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/users-schema.sql

39
spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java

@ -258,18 +258,29 @@ public abstract class ScriptUtils { @@ -258,18 +258,29 @@ public abstract class ScriptUtils {
for (String statement : statements) {
stmtNumber++;
try {
stmt.execute(statement);
int rowsAffected = stmt.getUpdateCount();
boolean hasResultSet = stmt.execute(statement);
int updateCount = -1;
if (logger.isDebugEnabled()) {
logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
SQLWarning warningToLog = stmt.getWarnings();
while (warningToLog != null) {
logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
"', error code '" + warningToLog.getErrorCode() +
"', message [" + warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
logSqlWarnings(stmt);
}
do {
if (hasResultSet) {
// We invoke getResultSet() to ensure the JDBC driver processes
// it, but we intentionally ignore the returned ResultSet since
// we cannot do anything meaningful with it here.
stmt.getResultSet();
if (logger.isDebugEnabled()) {
logger.debug("ResultSet returned for SQL: " + statement);
}
}
else {
updateCount = stmt.getUpdateCount();
if (updateCount >= 0 && logger.isDebugEnabled()) {
logger.debug(updateCount + " returned as update count for SQL: " + statement);
}
}
hasResultSet = stmt.getMoreResults();
} while (hasResultSet || updateCount != -1);
}
catch (SQLException ex) {
boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
@ -307,6 +318,16 @@ public abstract class ScriptUtils { @@ -307,6 +318,16 @@ public abstract class ScriptUtils {
}
}
private static void logSqlWarnings(Statement stmt) throws SQLException {
SQLWarning warningToLog = stmt.getWarnings();
while (warningToLog != null) {
logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
"', error code '" + warningToLog.getErrorCode() +
"', message [" + warningToLog.getMessage() + "]");
warningToLog = warningToLog.getNextWarning();
}
}
/**
* Read a script from the provided resource, using the supplied comment prefixes
* and statement separator, and build a {@code String} containing the lines.

10
spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java

@ -74,7 +74,7 @@ class JdbcClientIntegrationTests { @@ -74,7 +74,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeys() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@ -92,7 +92,7 @@ class JdbcClientIntegrationTests { @@ -92,7 +92,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeysAndKeyColumnNames() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@ -110,7 +110,7 @@ class JdbcClientIntegrationTests { @@ -110,7 +110,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeysUsingNamedParameters() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@ -129,7 +129,7 @@ class JdbcClientIntegrationTests { @@ -129,7 +129,7 @@ class JdbcClientIntegrationTests {
@Test
void updateWithGeneratedKeysAndKeyColumnNamesUsingNamedParameters() {
int expectedId = 2;
int expectedId = 1;
String firstName = "Jane";
String lastName = "Smith";
@ -217,7 +217,7 @@ class JdbcClientIntegrationTests { @@ -217,7 +217,7 @@ class JdbcClientIntegrationTests {
private static void assertResults(List<User> users) {
assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith"));
assertThat(users).containsExactly(new User(1, "John", "John"), new User(2, "John", "Smith"));
}
record Name(String name) {}

2
spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertIntegrationTests.java

@ -323,7 +323,7 @@ class SimpleJdbcInsertIntegrationTests { @@ -323,7 +323,7 @@ class SimpleJdbcInsertIntegrationTests {
protected void insertJaneSmith(SimpleJdbcInsert insert) {
Number id = insert.executeAndReturnKey(Map.of("first_name", "Jane", "last_name", "Smith"));
assertThat(id.intValue()).isEqualTo(2);
assertThat(id.intValue()).isEqualTo(1);
assertNumRows(2);
}

69
spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsIntegrationTests.java

@ -16,13 +16,24 @@ @@ -16,13 +16,24 @@
package org.springframework.jdbc.datasource.init;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.EnumSource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.jdbc.core.DataClassRowMapper;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScript;
/**
@ -32,16 +43,22 @@ import static org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScr @@ -32,16 +43,22 @@ import static org.springframework.jdbc.datasource.init.ScriptUtils.executeSqlScr
* @since 4.0.3
* @see ScriptUtilsTests
*/
@ParameterizedClass
@EnumSource(EmbeddedDatabaseType.class)
class ScriptUtilsIntegrationTests extends AbstractDatabaseInitializationTests {
@Parameter
EmbeddedDatabaseType databaseType;
@Override
protected EmbeddedDatabaseType getEmbeddedDatabaseType() {
return EmbeddedDatabaseType.HSQL;
return this.databaseType;
}
@BeforeEach
void setUpSchema() throws SQLException {
executeSqlScript(db.getConnection(), usersSchema());
executeSqlScript(db.getConnection(), encodedResource(usersSchema()), false, true, "--", null, "/*", "*/");
}
@Test
@ -59,4 +76,52 @@ class ScriptUtilsIntegrationTests extends AbstractDatabaseInitializationTests { @@ -59,4 +76,52 @@ class ScriptUtilsIntegrationTests extends AbstractDatabaseInitializationTests {
assertUsersDatabaseCreated("Hoeller", "Brannen");
}
@Test
@SuppressWarnings("unchecked")
void statementWithMultipleResultSets() throws SQLException {
// Derby does not support multiple statements/ResultSets within a single Statement.
assumeThat(this.databaseType).isNotSameAs(EmbeddedDatabaseType.DERBY);
EncodedResource resource = encodedResource(resource("users-data.sql"));
executeSqlScript(db.getConnection(), resource, false, true, "--", null, "/*", "*/");
assertUsersInDatabase(user("Sam", "Brannen"));
resource = encodedResource(inlineResource("""
SELECT last_name FROM users WHERE id = 0;
UPDATE users SET first_name = 'Jane' WHERE id = 0;
UPDATE users SET last_name = 'Smith' WHERE id = 0;
SELECT last_name FROM users WHERE id = 0;
GO
"""));
String separator = "GO\n";
executeSqlScript(db.getConnection(), resource, false, true, "--", separator, "/*", "*/");
assertUsersInDatabase(user("Jane", "Smith"));
}
private void assertUsersInDatabase(User... expectedUsers) {
List<User> users = jdbcTemplate.query("SELECT * FROM users WHERE id = 0",
new DataClassRowMapper<>(User.class));
assertThat(users).containsExactly(expectedUsers);
}
private static EncodedResource encodedResource(Resource resource) {
return new EncodedResource(resource);
}
private static Resource inlineResource(String sql) {
byte[] bytes = sql.getBytes(StandardCharsets.UTF_8);
return new ByteArrayResource(bytes, "inline SQL");
}
private static User user(String firstName, String lastName) {
return new User(0, firstName, lastName);
}
record User(int id, String firstName, String lastName) {
}
}

2
spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/users-schema-with-custom-schema.sql

@ -5,7 +5,7 @@ SET SCHEMA my_schema; @@ -5,7 +5,7 @@ SET SCHEMA my_schema;
DROP TABLE users IF EXISTS;
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
id INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL
);

2
spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/users-schema.sql

@ -1,7 +1,7 @@ @@ -1,7 +1,7 @@
DROP TABLE users IF EXISTS;
CREATE TABLE users (
id INTEGER GENERATED BY DEFAULT AS IDENTITY,
id INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) PRIMARY KEY,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL
);

Loading…
Cancel
Save