diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index c31dd05e048..d9ac5ec368d 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -15,6 +15,7 @@ body:
options:
- Core
- ActiveMQ
+ - ArcadeDB
- Azure
- Cassandra
- ChromaDB
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml
index 9b9a06ecf6a..8b0f936639b 100644
--- a/.github/ISSUE_TEMPLATE/enhancement.yaml
+++ b/.github/ISSUE_TEMPLATE/enhancement.yaml
@@ -15,6 +15,7 @@ body:
options:
- Core
- ActiveMQ
+ - ArcadeDB
- Azure
- Cassandra
- ChromaDB
diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml
index b655b4ac505..9ab49a81e19 100644
--- a/.github/ISSUE_TEMPLATE/feature.yaml
+++ b/.github/ISSUE_TEMPLATE/feature.yaml
@@ -15,6 +15,7 @@ body:
options:
- Core
- ActiveMQ
+ - ArcadeDB
- Azure
- Cassandra
- ChromaDB
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 72a6d9110b6..c2107f19d32 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -37,6 +37,11 @@ updates:
schedule:
interval: "monthly"
open-pull-requests-limit: 10
+ - package-ecosystem: "gradle"
+ directory: "/modules/arcadedb"
+ schedule:
+ interval: "monthly"
+ open-pull-requests-limit: 10
- package-ecosystem: "gradle"
directory: "/modules/azure"
schedule:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index f4649bd7f99..bc74faccade 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -23,6 +23,10 @@
- changed-files:
- any-glob-to-any-file:
- modules/activemq/**/*
+"modules/arcadedb":
+ - changed-files:
+ - any-glob-to-any-file:
+ - modules/arcadedb/**/*
"modules/azure":
- changed-files:
- any-glob-to-any-file:
diff --git a/docs/modules/databases/arcadedb.md b/docs/modules/databases/arcadedb.md
new file mode 100644
index 00000000000..9a2765d13ef
--- /dev/null
+++ b/docs/modules/databases/arcadedb.md
@@ -0,0 +1,56 @@
+# ArcadeDB Module
+
+Testcontainers module for [ArcadeDB](https://hub.docker.com/u/arcadedata)
+
+## Usage example
+
+You can start an ArcadeDB container instance from any Java application by using:
+
+
+[Container creation](../../../modules/arcadedb/src/test/java/org/testcontainers/containers/ArcadeDBContainerTest.java) inside_block:container
+
+
+## Adding this module to your project dependencies
+
+Add the following dependency to your `pom.xml`/`build.gradle` file:
+
+=== "Gradle"
+```groovy
+testImplementation "org.testcontainers:arcadedb:25.3.2"
+```
+=== "Maven"
+```xml
+
+ org.testcontainers
+ orientdb
+ 25.3.2
+ test
+
+```
+
+!!! hint
+Add the following dependencies if you plan to access the Testcontainer:
+
+ === "Gradle"
+ ```groovy
+ compile "com.arcadedb:arcadedb-engine:25.3.2"
+ compile "com.arcadedb:arcadedb-network:25.3.2"
+ ```
+
+ === "Maven"
+ ```xml
+
+ com.arcadedb
+ arcadedb-engine
+ 25.7.1
+
+
+ com.arcadedb
+ arcadedb-network
+ 25.7.1
+
+ ```
+
+
+
+
diff --git a/modules/arcadedb/build.gradle b/modules/arcadedb/build.gradle
new file mode 100644
index 00000000000..8a5ae2abfa1
--- /dev/null
+++ b/modules/arcadedb/build.gradle
@@ -0,0 +1,17 @@
+description = "Testcontainers :: ArcadeDB"
+
+dependencies {
+ api project(":testcontainers")
+
+ api "com.arcadedb:arcadedb-engine:25.3.2"
+ api "com.arcadedb:arcadedb-network:25.3.2"
+ testImplementation 'org.assertj:assertj-core:3.27.4'
+ testImplementation 'org.apache.tinkerpop:gremlin-driver:3.7.3'
+ testImplementation "com.arcadedb:arcadedb-gremlin:25.3.2"
+}
+
+tasks.japicmp {
+ classExcludes = [
+ "org.testcontainers.containers.ArcadeDBContainer"
+ ]
+}
diff --git a/modules/arcadedb/src/main/java/org/testcontainers/containers/ArcadeDBContainer.java b/modules/arcadedb/src/main/java/org/testcontainers/containers/ArcadeDBContainer.java
new file mode 100644
index 00000000000..3adfc42bfc9
--- /dev/null
+++ b/modules/arcadedb/src/main/java/org/testcontainers/containers/ArcadeDBContainer.java
@@ -0,0 +1,185 @@
+package org.testcontainers.containers;
+
+import com.arcadedb.remote.RemoteDatabase;
+import com.arcadedb.remote.RemoteServer;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import lombok.Getter;
+import lombok.NonNull;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.wait.strategy.Wait;
+import org.testcontainers.utility.DockerImageName;
+
+import java.io.IOException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+/**
+ * Testcontainers implementation for ArcadeDB.
+ *
+ * Supported image: {@code arcadedb} with JDK version up to 17
+ *
+ * Exposed ports:
+ *
+ * - Database: 2424
+ * - Remote database and Studio: 2480
+ *
+ */
+public class ArcadeDBContainer extends GenericContainer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ArcadeDBContainer.class);
+
+ private static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("arcadedata/arcadedb");
+
+ private static final String DEFAULT_SERVER_PASSWORD = "playwithdata";
+
+ private static final String DEFAULT_DATABASE_NAME = "testcontainers";
+
+ private static final int DEFAULT_BINARY_PORT = 2424;
+
+ private static final int DEFAULT_HTTP_PORT = 2480;
+
+ @Getter
+ private String databaseName;
+
+ private String serverPassword;
+
+ private int serverPort;
+
+ private Optional scriptPath = Optional.empty();
+
+ private RemoteServer remoteServer;
+
+ private RemoteDatabase database;
+
+ public ArcadeDBContainer(@NonNull String dockerImageName) {
+ this(DockerImageName.parse(dockerImageName));
+ }
+
+ public ArcadeDBContainer(final DockerImageName dockerImageName) {
+ super(dockerImageName);
+ dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);
+
+ serverPort = DEFAULT_HTTP_PORT;
+ serverPassword = DEFAULT_SERVER_PASSWORD;
+ databaseName = DEFAULT_DATABASE_NAME;
+
+ waitStrategy = Wait.forLogMessage(".*ArcadeDB Server started.*", 1);
+
+ addExposedPorts(DEFAULT_BINARY_PORT, DEFAULT_HTTP_PORT);
+ }
+
+ @Override
+ protected void configure() {
+ final String javaOpts = String.format("-Darcadedb.server.rootPassword=%s", serverPassword);
+
+ addEnv("JAVA_OPTS", javaOpts);
+ }
+
+ public synchronized RemoteDatabase getDatabase() {
+ final String host = getHost();
+ final Integer port = getMappedPort(serverPort);
+ if (remoteServer == null) {
+ try {
+ remoteServer = new RemoteServer(host, port, "root", serverPassword);
+ } catch (Exception e) {
+ final String msg = String.format(
+ "Could not connect to server %s:%d with user 'root' due to %s",
+ host,
+ port,
+ e.getMessage()
+ );
+ LOGGER.error(msg, e);
+ throw new IllegalStateException(msg, e);
+ }
+ }
+
+ if (!remoteServer.exists(getDatabaseName())) {
+ remoteServer.create(getDatabaseName());
+ }
+
+ if (database != null && database.isOpen()) {
+ return database;
+ }
+
+ try {
+ database = new RemoteDatabase(host, port, getDatabaseName(), "root", serverPassword);
+ scriptPath.ifPresent(path -> loadScript(path, database));
+ return database;
+ } catch (Exception e) {
+ final String msg = String.format(
+ "Could not connect to database %s on server %s:%d due to %s",
+ getDatabaseName(),
+ host,
+ port,
+ e.getMessage()
+ );
+ LOGGER.error(msg, e);
+ throw new IllegalStateException(msg, e);
+ }
+ }
+
+ public ArcadeDBContainer withDatabaseName(final String databaseName) {
+ this.databaseName = databaseName;
+ return self();
+ }
+
+ public ArcadeDBContainer withServerPassword(final String serverPassword) {
+ this.serverPassword = serverPassword;
+ return self();
+ }
+
+ public ArcadeDBContainer withServerPort(final int serverPort) {
+ this.serverPort = serverPort;
+ return self();
+ }
+
+ public ArcadeDBContainer withScriptPath(String scriptPath) {
+ this.scriptPath = Optional.of(scriptPath);
+ return self();
+ }
+
+ @Override
+ protected void containerIsStarted(InspectContainerResponse containerInfo) {
+ final String host = getHost();
+ final Integer port = getMappedPort(serverPort);
+
+ try {
+ remoteServer = new RemoteServer(host, port, "root", serverPassword);
+ } catch (Exception e) {
+ final String msg = String.format(
+ "Could not connect to server %s:%d with user 'root' due to %s",
+ host,
+ port,
+ e.getMessage()
+ );
+ LOGGER.error(msg, e);
+ throw new IllegalStateException(msg, e);
+ }
+ }
+
+ private void loadScript(String path, RemoteDatabase db) {
+ try {
+ URL resource = getClass().getClassLoader().getResource(path);
+
+ if (resource == null) {
+ LOGGER.warn("Could not load classpath init script: {}", scriptPath);
+ throw new RuntimeException(
+ "Could not load classpath init script: " + scriptPath + ". Resource not found."
+ );
+ }
+
+ String script = IOUtils.toString(resource, StandardCharsets.UTF_8);
+
+ db.command("sqlscript", script);
+ } catch (IOException e) {
+ LOGGER.warn("Could not load classpath init script: {}", scriptPath);
+ throw new RuntimeException("Could not load classpath init script: " + scriptPath, e);
+ } catch (UnsupportedOperationException e) {
+ LOGGER.error("Error while executing init script: {}", scriptPath, e);
+ throw new RuntimeException("Error while executing init script: " + scriptPath, e);
+ }
+ }
+}
diff --git a/modules/arcadedb/src/test/java/org/testcontainers/containers/ArcadeDBContainerTest.java b/modules/arcadedb/src/test/java/org/testcontainers/containers/ArcadeDBContainerTest.java
new file mode 100644
index 00000000000..d8611d64dcf
--- /dev/null
+++ b/modules/arcadedb/src/test/java/org/testcontainers/containers/ArcadeDBContainerTest.java
@@ -0,0 +1,59 @@
+package org.testcontainers.containers;
+
+import com.arcadedb.remote.RemoteDatabase;
+import org.junit.Test;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ArcadeDBContainerTest {
+
+ private static final DockerImageName ARCADEDB_IMAGE = DockerImageName
+ .parse("arcadedata/arcadedb:24.4.1")
+ .asCompatibleSubstituteFor("arcadedb");
+
+ @Test
+ public void shouldReturnTheSameSession() {
+ try ( // container {
+ ArcadeDBContainer arcadedb = new ArcadeDBContainer("arcadedata/arcadedb:25.3.2")
+ // }
+ ) {
+ arcadedb.start();
+
+ final RemoteDatabase database = arcadedb.getDatabase();
+ final RemoteDatabase database2 = arcadedb.getDatabase();
+
+ assertThat(database).isSameAs(database2);
+ }
+ }
+
+ @Test
+ public void shouldInitializeWithCommands() {
+ try (ArcadeDBContainer arcadedb = new ArcadeDBContainer(ARCADEDB_IMAGE)) {
+ arcadedb.start();
+
+ final RemoteDatabase db = arcadedb.getDatabase();
+
+ db.command("sql", "create vertex type Person");
+ db.command("sql", "INSERT INTO Person set name='john'");
+ db.command("sql", "INSERT INTO Person set name='jane'");
+
+ assertThat(db.query("sql", "SELECT FROM Person").stream()).hasSize(2);
+ }
+ }
+
+ @Test
+ public void shouldInitializeDatabaseFromScript() {
+ try (
+ ArcadeDBContainer arcadedb = new ArcadeDBContainer(ARCADEDB_IMAGE)
+ .withScriptPath("initscript.sql")
+ .withDatabaseName("persons")
+ ) {
+ arcadedb.start();
+
+ final RemoteDatabase database = arcadedb.getDatabase();
+
+ assertThat(database.query("sql", "SELECT FROM Person").stream()).hasSize(4);
+ }
+ }
+}
diff --git a/modules/arcadedb/src/test/resources/initscript.sql b/modules/arcadedb/src/test/resources/initscript.sql
new file mode 100644
index 00000000000..4fc582de942
--- /dev/null
+++ b/modules/arcadedb/src/test/resources/initscript.sql
@@ -0,0 +1,6 @@
+CREATE VERTEX Type Person;
+
+INSERT INTO Person set name="john";
+INSERT INTO Person set name="paul";
+INSERT INTO Person set name="luke";
+INSERT INTO Person set name="albert";
diff --git a/modules/arcadedb/src/test/resources/logback-test.xml b/modules/arcadedb/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..d91138d8b74
--- /dev/null
+++ b/modules/arcadedb/src/test/resources/logback-test.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
+
+
+
+
+
+
+
+
+
+
+