Skip to content

Commit

Permalink
#135: Run SQL script with multiple statements (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaklakariada authored Sep 24, 2024
1 parent db6e18d commit 390faa3
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 24 deletions.
3 changes: 2 additions & 1 deletion doc/changes/changes_3.6.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Code name: Fix CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-ja

This release fixes CVE-2024-7254 in test dependency `com.google.protobuf:protobuf-java:3.25.1`.

The release also speeds up inserting rows into a table by using batch insert, allows specifying a charset when creating MySQL tables, see the [user guide](../user_guide/user_guide.md#mysql-specific-database-objects) for details and supports databases that don't support transactions. TDBJ will then insert rows without a transaction.
The release also speeds up inserting rows into a table by using batch insert, allows specifying a charset when creating MySQL tables, see the [user guide](../user_guide/user_guide.md#mysql-specific-database-objects) for details and supports databases that don't support transactions. TDBJ will then insert rows without a transaction. Additionally, method `factory.executeSqlFile()` now supports running scripts with multiple statements, separated with `;`.

## Security

Expand All @@ -17,6 +17,7 @@ The release also speeds up inserting rows into a table by using batch insert, al
* #137: Updated `AbstractImmediateDatabaseObjectWriter#write()` to use batching for inserting rows
* #134: Allowed specifying charset for MySQL tables
* #136: Added support for databases without transaction support
* #135: Added support for running scripts with multiple statements

## Dependency Updates

Expand Down
2 changes: 2 additions & 0 deletions doc/user_guide/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ You can also run multiple SQL files in a row. They are executed in the order the
factory.executeSqlFile(file1, file2, file3);
```
SQL files may contain multiple statements separated with `;`.
## Populating Tables
Populating a table is really simple:
Expand Down
2 changes: 1 addition & 1 deletion error_code_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ error-tags:
TDBJ:
packages:
- com.exasol.dbbuilder
highest-index: 37
highest-index: 38
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.exasol.dbbuilder.dialects;

import static java.util.stream.Collectors.toList;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.*;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

Expand Down Expand Up @@ -158,15 +161,38 @@ protected String createCommaSeparatedObjectPrivilegeList(final ObjectPrivilege[]
@Override
public void executeSqlFile(final Path... sqlFiles) {
for (final Path sqlFile : sqlFiles) {
try (final Statement statement = this.connection.createStatement()) {
final String sql = Files.readString(sqlFile);
statement.execute(sql);
} catch (final IOException | SQLException exception) {
throw new DatabaseObjectException(
ExaError.messageBuilder("E-TDBJ-14")
.message("Unable to execute SQL from file: {{sqlFile}}", sqlFile).toString(),
exception);
}
executeSingleSqlFile(sqlFile);
}
}

private void executeSingleSqlFile(final Path sqlFile) {
final String sqlScriptContent = readFileContent(sqlFile);
final List<String> statements = splitIntoStatements(sqlScriptContent);
for (final String statement : statements) {
executeStatement(sqlFile, statement);
}
}

private String readFileContent(final Path sqlFile) {
try {
return Files.readString(sqlFile);
} catch (final IOException exception) {
throw new DatabaseObjectException(ExaError.messageBuilder("E-TDBJ-38")
.message("Unable to read SQL from file {{sqlFile}}", sqlFile).toString(), exception);
}
}

private List<String> splitIntoStatements(final String sqlScriptContent) {
return Arrays.stream(sqlScriptContent.split(";")).collect(toList());
}

private void executeStatement(final Path sqlFile, final String sql) {
try (final Statement statement = this.connection.createStatement()) {
statement.execute(sql);
} catch (final SQLException exception) {
throw new DatabaseObjectException(ExaError.messageBuilder("E-TDBJ-14")
.message("Unable to execute SQL statement {{statement}} from file {{sqlFile}}", sql, sqlFile)
.toString(), exception);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ public interface DatabaseObjectFactory {
public User createLoginUser(final String name, final String password);

/**
* Execute the contents of an SQL script file.
* Execute the contents of one or more SQL script files. Each file may contain multiple SQL statements, separated
* with {@code ;}.
*
* @param sqlFiles path to the script file
* @param sqlFiles paths to the script files
*/
// [impl->dsn~creating-objects-through-sql-files~1]
public void executeSqlFile(final Path... sqlFiles);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,7 @@ protected ExistsInDatabaseMatcher(final Connection connection) {
*/
protected abstract String getCheckCommand(final DatabaseObject object);

private boolean matchResult(final DatabaseObject object, final PreparedStatement objectExistenceStatement)
throws SQLException {
private boolean matchResult(final PreparedStatement objectExistenceStatement) throws SQLException {
try (final ResultSet resultSet = objectExistenceStatement.executeQuery()) {
return resultSet.next();
}
Expand All @@ -156,9 +155,11 @@ protected boolean matchesSafely(final DatabaseObject object) {
try (final PreparedStatement objectExistenceStatement = this.connection
.prepareStatement(getCheckCommand(object))) {
objectExistenceStatement.setString(1, object.getName());
return matchResult(object, objectExistenceStatement);
return matchResult(objectExistenceStatement);
} catch (final SQLException exception) {
throw new AssertionError(ExaError.messageBuilder("E-TDBJ-19").message("Unable to determine existence of object: {{object}}", object.getName()).toString(), exception);
throw new AssertionError(ExaError.messageBuilder("E-TDBJ-19")
.message("Unable to determine existence of object: {{object}}", object.getName()).toString(),
exception);
}
}

Expand All @@ -173,4 +174,4 @@ protected void describeMismatchSafely(final DatabaseObject item, final Descripti
.appendText(item.getFullyQualifiedName()).appendText(" in the database");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.io.IOException;
Expand Down Expand Up @@ -60,8 +61,10 @@ void testAttachToScriptFromSqlFile(@TempDir final Path tempDir) throws IOExcepti

// [itest->dsn~creating-objects-through-sql-files~1]
@Test
@SuppressWarnings("java:S5778") // creating lists is not an Exception throwing method
void testAttachToScriptThrowsExceptionOnNonExistingFile() {
assertThrows(DatabaseObjectException.class, () -> factory.executeSqlFile(Path.of("non/existent/file.sql")));
final Path path = Path.of("non/existent/file.sql");
final DatabaseObjectException exception = assertThrows(DatabaseObjectException.class,
() -> factory.executeSqlFile(path));
assertThat(exception.getMessage(), startsWith("E-TDBJ-38: Unable to read SQL from file"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.*;

import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -478,6 +476,25 @@ void testInsertIntoTable() {
}
}

@Test
void testRunSqlScriptFails(@TempDir final Path tempDir) throws IOException {
final Path script = tempDir.resolve("script.sql");
Files.writeString(script, "invalid");
final DatabaseObjectException exception = assertThrows(DatabaseObjectException.class,
() -> factory.executeSqlFile(script));
assertThat(exception.getMessage(),
startsWith("E-TDBJ-14: Unable to execute SQL statement 'invalid' from file"));
}

@Test
void testRunSqlScriptWithMultipleStatements(@TempDir final Path tempDir) throws IOException {
final Path createScript = tempDir.resolve("create-script.sql");
final Path dropScript = tempDir.resolve("drop-script.sql");
Files.writeString(createScript, "CREATE SCHEMA script_schema_1; CREATE SCHEMA script_schema_2;");
Files.writeString(dropScript, "DROP SCHEMA script_schema_1; DROP SCHEMA script_schema_2;");
assertDoesNotThrow(() -> factory.executeSqlFile(createScript, dropScript));
}

@Override
protected Matcher<DatabaseObject> existsInDatabase() {
return new ExistsInDatabaseMatcher(this.adminConnection);
Expand Down

0 comments on commit 390faa3

Please sign in to comment.