diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index abb8b0f7901..270f292dcf7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -42,6 +42,7 @@ body: - MySQL - Neo4j - NGINX + - OceanBase - OpenFGA - Oracle Free - Oracle XE diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index 54868eea96d..97297c9ddb2 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -42,6 +42,7 @@ body: - MySQL - Neo4j - NGINX + - OceanBase - OpenFGA - Oracle Free - Oracle XE diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index 2d28aca91d6..80048f00fb0 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -42,6 +42,7 @@ body: - MySQL - Neo4j - NGINX + - OceanBase - OpenFGA - Oracle Free - Oracle XE diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ddf7a8b7d9a..22cbe2c2748 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -219,6 +219,11 @@ updates: schedule: interval: "weekly" open-pull-requests-limit: 10 + - package-ecosystem: "gradle" + directory: "/modules/oceanbase" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/openfga" schedule: diff --git a/.github/labeler.yml b/.github/labeler.yml index 8107870a59a..2b9dd3bc899 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -135,6 +135,10 @@ - changed-files: - any-glob-to-any-file: - modules/nginx/**/* +"modules/oceanbase": + - changed-files: + - any-glob-to-any-file: + - modules/oceanbase/**/* "modules/openfga": - changed-files: - any-glob-to-any-file: diff --git a/.github/settings.yml b/.github/settings.yml index ad711b60307..301fdcdacfd 100644 --- a/.github/settings.yml +++ b/.github/settings.yml @@ -193,6 +193,9 @@ labels: - name: modules/nginx color: '#006b75' + - name: modules/oceanbase + color: '#006b75' + - name: modules/openfga color: '#006b75' diff --git a/docs/modules/databases/jdbc.md b/docs/modules/databases/jdbc.md index dc11954153a..c1d0171c59e 100644 --- a/docs/modules/databases/jdbc.md +++ b/docs/modules/databases/jdbc.md @@ -55,6 +55,10 @@ Insert `tc:` after `jdbc:` as follows. Note that the hostname, port and database `jdbc:tc:sqlserver:2017-CU12:///databasename` +#### Using OceanBase + +`jdbc:tc:oceanbasece:4.2.2:///databasename` + #### Using Oracle `jdbc:tc:oracle:21-slim-faststart:///databasename` diff --git a/docs/modules/databases/oceanbase.md b/docs/modules/databases/oceanbase.md new file mode 100644 index 00000000000..b90a492a6e4 --- /dev/null +++ b/docs/modules/databases/oceanbase.md @@ -0,0 +1,25 @@ +# OceanBase Module + +See [Database containers](./index.md) for documentation and usage that is common to all relational database container types. + +## Adding this module to your project dependencies + +Add the following dependency to your `pom.xml`/`build.gradle` file: + +=== "Gradle" + ```groovy + testImplementation "org.testcontainers:oceanbase:{{latest_version}}" + ``` + +=== "Maven" + ```xml + + org.testcontainers + oceanbase + {{latest_version}} + test + + ``` + +!!! hint +Adding this Testcontainers library JAR will not automatically add a database driver JAR to your project. You should ensure that your project also has a suitable database driver as a dependency. diff --git a/mkdocs.yml b/mkdocs.yml index 8c5a0a769f1..ce0c33dcf16 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -64,6 +64,7 @@ nav: - modules/databases/mssqlserver.md - modules/databases/mysql.md - modules/databases/neo4j.md + - modules/databases/oceanbase.md - modules/databases/oraclefree.md - modules/databases/oraclexe.md - modules/databases/orientdb.md diff --git a/modules/oceanbase/build.gradle b/modules/oceanbase/build.gradle new file mode 100644 index 00000000000..e1f13a7a6a1 --- /dev/null +++ b/modules/oceanbase/build.gradle @@ -0,0 +1,8 @@ +description = "Testcontainers :: JDBC :: OceanBase" + +dependencies { + api project(':jdbc') + + testImplementation project(':jdbc-test') + testRuntimeOnly 'mysql:mysql-connector-java:8.0.33' +} diff --git a/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainer.java b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainer.java new file mode 100644 index 00000000000..6c08ff165f2 --- /dev/null +++ b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainer.java @@ -0,0 +1,82 @@ +package org.testcontainers.oceanbase; + +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.utility.DockerImageName; + +/** + * Testcontainers implementation for OceanBase Community Edition. + *

+ * Supported image: {@code oceanbase/oceanbase-ce} + *

+ * Exposed ports: + *

+ */ +public class OceanBaseCEContainer extends JdbcDatabaseContainer { + + static final String NAME = "oceanbasece"; + + static final String DOCKER_IMAGE_NAME = "oceanbase/oceanbase-ce"; + + private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse(DOCKER_IMAGE_NAME); + + private static final Integer SQL_PORT = 2881; + + private static final Integer RPC_PORT = 2882; + + private static final String DEFAULT_TEST_TENANT_NAME = "test"; + + private static final String DEFAULT_USERNAME = "root"; + + private static final String DEFAULT_PASSWORD = ""; + + private static final String DEFAULT_DATABASE_NAME = "test"; + + public OceanBaseCEContainer(String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public OceanBaseCEContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + + addExposedPorts(SQL_PORT, RPC_PORT); + } + + @Override + public String getDriverClassName() { + return OceanBaseJdbcUtils.getDriverClass(); + } + + @Override + public String getJdbcUrl() { + String additionalUrlParams = constructUrlParameters("?", "&"); + String prefix = OceanBaseJdbcUtils.isMySQLDriver(getDriverClassName()) ? "jdbc:mysql://" : "jdbc:oceanbase://"; + return prefix + getHost() + ":" + getMappedPort(SQL_PORT) + "/" + DEFAULT_DATABASE_NAME + additionalUrlParams; + } + + @Override + public String getDatabaseName() { + return DEFAULT_DATABASE_NAME; + } + + @Override + public String getUsername() { + // In OceanBase, the jdbc username is related to the name of user, tenant and cluster, + // if a tenant name other than the default value 'test' is used, you should manually + // construct the jdbc username by yourself. + return DEFAULT_USERNAME + "@" + DEFAULT_TEST_TENANT_NAME; + } + + @Override + public String getPassword() { + return DEFAULT_PASSWORD; + } + + @Override + protected String getTestQueryString() { + return "SELECT 1"; + } +} diff --git a/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainerProvider.java b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainerProvider.java new file mode 100644 index 00000000000..d0f651bc338 --- /dev/null +++ b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseCEContainerProvider.java @@ -0,0 +1,32 @@ +package org.testcontainers.oceanbase; + +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.containers.JdbcDatabaseContainerProvider; +import org.testcontainers.utility.DockerImageName; + +/** + * Factory for OceanBase Community Edition containers. + */ +public class OceanBaseCEContainerProvider extends JdbcDatabaseContainerProvider { + + private static final String DEFAULT_TAG = "4.2.2"; + + @Override + public boolean supports(String databaseType) { + return databaseType.equals(OceanBaseCEContainer.NAME); + } + + @Override + public JdbcDatabaseContainer newInstance() { + return newInstance(DEFAULT_TAG); + } + + @Override + public JdbcDatabaseContainer newInstance(String tag) { + if (tag != null) { + return new OceanBaseCEContainer(DockerImageName.parse(OceanBaseCEContainer.DOCKER_IMAGE_NAME).withTag(tag)); + } else { + return newInstance(); + } + } +} diff --git a/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseJdbcUtils.java b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseJdbcUtils.java new file mode 100644 index 00000000000..d2e90f48ee6 --- /dev/null +++ b/modules/oceanbase/src/main/java/org/testcontainers/oceanbase/OceanBaseJdbcUtils.java @@ -0,0 +1,45 @@ +package org.testcontainers.oceanbase; + +import java.util.Arrays; +import java.util.List; + +/** + * Utils for OceanBase Jdbc Connection. + */ +class OceanBaseJdbcUtils { + + static final String MYSQL_JDBC_DRIVER = "com.mysql.cj.jdbc.Driver"; + + static final String MYSQL_LEGACY_JDBC_DRIVER = "com.mysql.jdbc.Driver"; + + static final String OCEANBASE_JDBC_DRIVER = "com.oceanbase.jdbc.Driver"; + + static final String OCEANBASE_LEGACY_JDBC_DRIVER = "com.alipay.oceanbase.jdbc.Driver"; + + static final List SUPPORTED_DRIVERS = Arrays.asList( + OCEANBASE_JDBC_DRIVER, + OCEANBASE_LEGACY_JDBC_DRIVER, + MYSQL_JDBC_DRIVER, + MYSQL_LEGACY_JDBC_DRIVER + ); + + static String getDriverClass() { + for (String driverClass : SUPPORTED_DRIVERS) { + try { + Class.forName(driverClass); + return driverClass; + } catch (ClassNotFoundException e) { + // try to load next driver + } + } + throw new RuntimeException("Can't find valid driver class for OceanBase"); + } + + static boolean isMySQLDriver(String driverClassName) { + return MYSQL_JDBC_DRIVER.equals(driverClassName) || MYSQL_LEGACY_JDBC_DRIVER.equals(driverClassName); + } + + static boolean isOceanBaseDriver(String driverClassName) { + return OCEANBASE_JDBC_DRIVER.equals(driverClassName) || OCEANBASE_LEGACY_JDBC_DRIVER.equals(driverClassName); + } +} diff --git a/modules/oceanbase/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/oceanbase/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..505bfe5e088 --- /dev/null +++ b/modules/oceanbase/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.oceanbase.OceanBaseCEContainerProvider diff --git a/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/OceanBaseJdbcDriverTest.java b/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/OceanBaseJdbcDriverTest.java new file mode 100644 index 00000000000..a4f63b1eb90 --- /dev/null +++ b/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/OceanBaseJdbcDriverTest.java @@ -0,0 +1,19 @@ +package org.testcontainers.oceanbase; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.testcontainers.jdbc.AbstractJDBCDriverTest; + +import java.util.Arrays; +import java.util.EnumSet; + +@RunWith(Parameterized.class) +public class OceanBaseJdbcDriverTest extends AbstractJDBCDriverTest { + + @Parameterized.Parameters(name = "{index} - {0}") + public static Iterable data() { + return Arrays.asList( + new Object[][] { { "jdbc:tc:oceanbasece://hostname/databasename", EnumSet.noneOf(Options.class) } } + ); + } +} diff --git a/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/SimpleOceanBaseCETest.java b/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/SimpleOceanBaseCETest.java new file mode 100644 index 00000000000..7c4f20e4943 --- /dev/null +++ b/modules/oceanbase/src/test/java/org/testcontainers/oceanbase/SimpleOceanBaseCETest.java @@ -0,0 +1,69 @@ +package org.testcontainers.oceanbase; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.db.AbstractContainerDatabaseTest; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SimpleOceanBaseCETest extends AbstractContainerDatabaseTest { + + private static final Logger logger = LoggerFactory.getLogger(SimpleOceanBaseCETest.class); + + private final OceanBaseCEContainerProvider containerProvider = new OceanBaseCEContainerProvider(); + + @SuppressWarnings("resource") + private OceanBaseCEContainer testContainer() { + return ((OceanBaseCEContainer) containerProvider.newInstance()).withEnv("MODE", "slim") + .withEnv("FASTBOOT", "true") + .withLogConsumer(new Slf4jLogConsumer(logger)); + } + + @Test + public void testSimple() throws SQLException { + try (OceanBaseCEContainer container = testContainer()) { + container.start(); + + ResultSet resultSet = performQuery(container, "SELECT 1"); + int resultSetInt = resultSet.getInt(1); + assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); + assertHasCorrectExposedAndLivenessCheckPorts(container); + } + } + + @Test + public void testExplicitInitScript() throws SQLException { + try (OceanBaseCEContainer container = testContainer().withInitScript("init.sql")) { + container.start(); + + ResultSet resultSet = performQuery(container, "SELECT foo FROM bar"); + String firstColumnValue = resultSet.getString(1); + assertThat(firstColumnValue).as("Value from init script should equal real value").isEqualTo("hello world"); + } + } + + @Test + public void testWithAdditionalUrlParamInJdbcUrl() { + try (OceanBaseCEContainer container = testContainer().withUrlParam("useSSL", "false")) { + container.start(); + + String jdbcUrl = container.getJdbcUrl(); + assertThat(jdbcUrl).contains("?"); + assertThat(jdbcUrl).contains("useSSL=false"); + } + } + + private void assertHasCorrectExposedAndLivenessCheckPorts(OceanBaseCEContainer container) { + int sqlPort = 2881; + int rpcPort = 2882; + + assertThat(container.getExposedPorts()).containsExactlyInAnyOrder(sqlPort, rpcPort); + assertThat(container.getLivenessCheckPortNumbers()) + .containsExactlyInAnyOrder(container.getMappedPort(sqlPort), container.getMappedPort(rpcPort)); + } +} diff --git a/modules/oceanbase/src/test/resources/init.sql b/modules/oceanbase/src/test/resources/init.sql new file mode 100644 index 00000000000..98d6889b078 --- /dev/null +++ b/modules/oceanbase/src/test/resources/init.sql @@ -0,0 +1,45 @@ +CREATE TABLE bar ( + foo VARCHAR(255) +); + +DROP PROCEDURE IF EXISTS -- ; + count_foo; + +SELECT "a /* string literal containing comment characters like -- here"; +SELECT "a 'quoting' \"scenario ` involving BEGIN keyword\" here"; +SELECT * from `bar`; + +-- What about a line comment containing imbalanced string delimiters? " + +CREATE PROCEDURE count_foo() +BEGIN + + BEGIN + SELECT * + FROM bar; + SELECT 1 + FROM dual; + END; + + BEGIN + select * from bar; + END; + + -- we can do comments + + /* including block + comments + */ + + /* what if BEGIN appears inside a comment? */ + + select "or what if BEGIN appears inside a literal?"; + +END /*; */; + +/* or a block comment + containing imbalanced string delimiters? + ' " + */ + +INSERT INTO bar (foo) /* ; */ VALUES ('hello world'); diff --git a/modules/oceanbase/src/test/resources/logback-test.xml b/modules/oceanbase/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..83ef7a1a3ef --- /dev/null +++ b/modules/oceanbase/src/test/resources/logback-test.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} %-5level %logger - %msg%n + + + + + + + + +