From b2a2dd869d598e3b9f021da0bd6c25797fdd9459 Mon Sep 17 00:00:00 2001 From: Hyun Lee Date: Wed, 16 Sep 2026 12:46:03 -0500 Subject: [PATCH] fix: support explicit init script charsets --- docs/modules/databases/jdbc.md | 14 ++++ .../org/testcontainers/ext/ScriptUtils.java | 17 ++++- .../testcontainers/ext/ScriptUtilsTest.java | 69 +++++++++++++++++++ .../src/test/resources/init-utf-8.sql | 1 + .../src/test/resources/init-windows-1252.sql | 1 + .../containers/JdbcDatabaseContainer.java | 18 ++++- .../containers/JdbcDatabaseContainerTest.java | 57 ++++++++++++++- .../jdbc/src/test/resources/init-utf-8.sql | 1 + .../src/test/resources/init-windows-1252.sql | 1 + 9 files changed, 175 insertions(+), 4 deletions(-) create mode 100644 modules/database-commons/src/test/java/org/testcontainers/ext/ScriptUtilsTest.java create mode 100644 modules/database-commons/src/test/resources/init-utf-8.sql create mode 100644 modules/database-commons/src/test/resources/init-windows-1252.sql create mode 100644 modules/jdbc/src/test/resources/init-utf-8.sql create mode 100644 modules/jdbc/src/test/resources/init-windows-1252.sql diff --git a/docs/modules/databases/jdbc.md b/docs/modules/databases/jdbc.md index 72539aef44e..d3d87b0d980 100644 --- a/docs/modules/databases/jdbc.md +++ b/docs/modules/databases/jdbc.md @@ -174,3 +174,17 @@ Examples/Tests: * [MySQL](https://github.com/testcontainers/testcontainers-java/blob/main/modules/mysql/src/test/java/org/testcontainers/junit/mysql/SimpleMySQLTest.java) * [PostgreSQL](https://github.com/testcontainers/testcontainers-java/blob/main/modules/postgresql/src/test/java/org/testcontainers/junit/postgresql/SimplePostgreSQLTest.java) + +## Init script encoding for container instances + +Scripts configured with `withInitScript` or `withInitScripts` are read as UTF-8 by default. +For scripts saved with another encoding, set the charset on the container before starting it: + +```java +container.withInitScript("init.sql") + .withInitScriptCharset(java.nio.charset.Charset.forName("windows-1252")); +``` + +The charset applies to all scripts configured on that container. It controls how the script files +are read, not the database's character set. Scripts loaded through the JDBC URL parameter +`TC_INITSCRIPT` continue to use UTF-8. 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 b78ac4d63c4..c56a869a930 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 @@ -24,6 +24,7 @@ import java.io.IOException; import java.net.URL; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; @@ -192,12 +193,24 @@ public static boolean containsSqlScriptDelimiters( } /** - * Load script from classpath and apply it to the given database + * Load a UTF-8 script from the classpath and apply it to the given database. * * @param databaseDelegate database delegate for script execution * @param initScriptPath the resource to load the init script from */ public static void runInitScript(DatabaseDelegate databaseDelegate, String initScriptPath) { + runInitScript(databaseDelegate, initScriptPath, StandardCharsets.UTF_8); + } + + /** + * Load a script from the classpath using the given charset and apply it to the given database. + * + * @param databaseDelegate database delegate for script execution + * @param initScriptPath the resource to load the init script from + * @param charset the charset used to read the script; never {@code null} + */ + public static void runInitScript(DatabaseDelegate databaseDelegate, String initScriptPath, Charset charset) { + checkArgument(charset != null, "charset must not be null"); try { URL resource = Thread.currentThread().getContextClassLoader().getResource(initScriptPath); if (resource == null) { @@ -209,7 +222,7 @@ public static void runInitScript(DatabaseDelegate databaseDelegate, String initS ); } } - String scripts = IOUtils.toString(resource, StandardCharsets.UTF_8); + String scripts = IOUtils.toString(resource, charset); executeDatabaseScript(databaseDelegate, initScriptPath, scripts); } catch (IOException e) { LOGGER.warn("Could not load classpath init script: {}", initScriptPath); 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 new file mode 100644 index 00000000000..e2c0a3b4cb9 --- /dev/null +++ b/modules/database-commons/src/test/java/org/testcontainers/ext/ScriptUtilsTest.java @@ -0,0 +1,69 @@ +package org.testcontainers.ext; + +import org.junit.jupiter.api.Test; +import org.testcontainers.delegate.AbstractDatabaseDelegate; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ScriptUtilsTest { + + private final RecordingDatabaseDelegate delegate = new RecordingDatabaseDelegate(); + + @Test + void usesUtf8ByDefault() { + ScriptUtils.runInitScript(delegate, "init-utf-8.sql"); + + assertThat(delegate.statements).containsExactly("SELECT 'café'"); + } + + @Test + void readsWindows1252Script() { + ScriptUtils.runInitScript(delegate, "init-windows-1252.sql", Charset.forName("windows-1252")); + + assertThat(delegate.statements).containsExactly("SELECT 'café'"); + } + + @Test + void readsExplicitUtf8Script() { + ScriptUtils.runInitScript(delegate, "init-utf-8.sql", StandardCharsets.UTF_8); + + assertThat(delegate.statements).containsExactly("SELECT 'café'"); + } + + @Test + void rejectsNullCharset() { + assertThatThrownBy(() -> ScriptUtils.runInitScript(delegate, "init-utf-8.sql", null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("charset must not be null"); + } + + private static class RecordingDatabaseDelegate extends AbstractDatabaseDelegate { + + private final List statements = new ArrayList<>(); + + @Override + public void execute( + String statement, + String scriptPath, + int lineNumber, + boolean continueOnError, + boolean ignoreFailedDrops + ) { + statements.add(statement); + } + + @Override + protected Void createNewConnection() { + return null; + } + + @Override + protected void closeConnectionQuietly(Void connection) {} + } +} diff --git a/modules/database-commons/src/test/resources/init-utf-8.sql b/modules/database-commons/src/test/resources/init-utf-8.sql new file mode 100644 index 00000000000..a90ec0ec243 --- /dev/null +++ b/modules/database-commons/src/test/resources/init-utf-8.sql @@ -0,0 +1 @@ +SELECT 'café'; diff --git a/modules/database-commons/src/test/resources/init-windows-1252.sql b/modules/database-commons/src/test/resources/init-windows-1252.sql new file mode 100644 index 00000000000..b6dcf5effe7 --- /dev/null +++ b/modules/database-commons/src/test/resources/init-windows-1252.sql @@ -0,0 +1 @@ +SELECT 'café'; diff --git a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java index cf6c995528f..f8d9c26b74f 100644 --- a/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java +++ b/modules/jdbc/src/main/java/org/testcontainers/containers/JdbcDatabaseContainer.java @@ -13,6 +13,8 @@ import org.testcontainers.utility.DockerImageName; import org.testcontainers.utility.MountableFile; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.Driver; import java.sql.SQLException; @@ -41,6 +43,8 @@ public abstract class JdbcDatabaseContainer initScriptPaths = new ArrayList<>(); + private Charset initScriptCharset = StandardCharsets.UTF_8; + protected Map parameters = new HashMap<>(); protected Map urlParameters = new HashMap<>(); @@ -171,6 +175,18 @@ public SELF withInitScripts(Iterable initScriptPaths) { return self(); } + /** + * Sets the charset used to read all scripts configured with {@link #withInitScript(String)} + * or {@link #withInitScripts(String...)}. Defaults to UTF-8. + * + * @param charset the charset used to read the scripts + * @return self + */ + public SELF withInitScriptCharset(@NonNull Charset charset) { + this.initScriptCharset = charset; + return self(); + } + @SneakyThrows(InterruptedException.class) @Override protected void waitUntilContainerStarted() { @@ -365,7 +381,7 @@ protected void runInitScriptIfRequired() { initScriptPaths .stream() .filter(Objects::nonNull) - .forEach(path -> ScriptUtils.runInitScript(getDatabaseDelegate(), path)); + .forEach(path -> ScriptUtils.runInitScript(getDatabaseDelegate(), path, initScriptCharset)); } public void setParameters(Map parameters) { diff --git a/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java b/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java index ca41c3f5d1c..0e2373f3a2a 100644 --- a/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java +++ b/modules/jdbc/src/test/java/org/testcontainers/containers/JdbcDatabaseContainerTest.java @@ -3,12 +3,18 @@ import lombok.NonNull; import org.junit.jupiter.api.Test; import org.slf4j.Logger; +import org.testcontainers.delegate.DatabaseDelegate; +import java.nio.charset.Charset; import java.sql.Connection; import java.sql.SQLException; +import java.util.Collections; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; class JdbcDatabaseContainerTest { @@ -20,7 +26,56 @@ void anExceptionIsThrownIfJdbcIsNotAvailable() { assertThatExceptionOfType(IllegalStateException.class).isThrownBy(jdbcContainer::waitUntilContainerStarted); } - static class JdbcDatabaseContainerStub extends JdbcDatabaseContainer { + @Test + void readsInitScriptAsUtf8ByDefault() { + JdbcDatabaseContainerStub container = new JdbcDatabaseContainerStub("mysql:latest"); + container.withInitScript("init-utf-8.sql"); + + container.runInitScriptIfRequired(); + + verify(container.delegate).execute(Collections.singletonList("SELECT 'café'"), "init-utf-8.sql", false, false); + } + + @Test + void readsInitScriptWithConfiguredCharset() { + JdbcDatabaseContainerStub container = new JdbcDatabaseContainerStub("mysql:latest"); + assertThat(container.withInitScriptCharset(Charset.forName("windows-1252"))).isSameAs(container); + container.withInitScript("init-windows-1252.sql"); + + container.runInitScriptIfRequired(); + + verify(container.delegate) + .execute(Collections.singletonList("SELECT 'café'"), "init-windows-1252.sql", false, false); + } + + @Test + void readsAllInitScriptsWithConfiguredCharset() { + JdbcDatabaseContainerStub container = new JdbcDatabaseContainerStub("mysql:latest"); + container + .withInitScripts("init-windows-1252.sql", null, "init-windows-1252.sql") + .withInitScriptCharset(Charset.forName("windows-1252")); + + container.runInitScriptIfRequired(); + + verify(container.delegate, times(2)) + .execute(Collections.singletonList("SELECT 'café'"), "init-windows-1252.sql", false, false); + } + + @Test + void rejectsNullInitScriptCharset() { + JdbcDatabaseContainerStub container = new JdbcDatabaseContainerStub("mysql:latest"); + + assertThatExceptionOfType(NullPointerException.class).isThrownBy(() -> container.withInitScriptCharset(null)); + } + + static class JdbcDatabaseContainerStub extends JdbcDatabaseContainer { + + private final DatabaseDelegate delegate = mock(DatabaseDelegate.class); + + @Override + protected DatabaseDelegate getDatabaseDelegate() { + return delegate; + } public JdbcDatabaseContainerStub(@NonNull String dockerImageName) { super(dockerImageName); diff --git a/modules/jdbc/src/test/resources/init-utf-8.sql b/modules/jdbc/src/test/resources/init-utf-8.sql new file mode 100644 index 00000000000..a90ec0ec243 --- /dev/null +++ b/modules/jdbc/src/test/resources/init-utf-8.sql @@ -0,0 +1 @@ +SELECT 'café'; diff --git a/modules/jdbc/src/test/resources/init-windows-1252.sql b/modules/jdbc/src/test/resources/init-windows-1252.sql new file mode 100644 index 00000000000..b6dcf5effe7 --- /dev/null +++ b/modules/jdbc/src/test/resources/init-windows-1252.sql @@ -0,0 +1 @@ +SELECT 'café';