Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#135: Run SQL script with multiple statements #143

Merged
merged 19 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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