From 643b815a1adb825b948dc5d97906123d90d4eefd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Wangryn?= Date: Fri, 21 Oct 2022 18:49:33 +0200 Subject: [PATCH] Add QuestDB module (#5995) Introduce Testcontainers implementation for `QuestDB`, `QuestDBContainer`. --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 + .github/ISSUE_TEMPLATE/enhancement.yaml | 1 + .github/ISSUE_TEMPLATE/feature.yaml | 1 + .github/dependabot.yml | 5 ++ .github/labeler.yml | 2 + docs/modules/databases/questdb.md | 29 ++++++ mkdocs.yml | 1 + modules/questdb/build.gradle | 13 +++ .../containers/QuestDBContainer.java | 88 +++++++++++++++++++ .../containers/QuestDBProvider.java | 14 +++ ...s.containers.JdbcDatabaseContainerProvider | 1 + .../org/testcontainers/QuestDBTestImages.java | 7 ++ .../jdbc/questdb/QuestDBJDBCDriverTest.java | 19 ++++ .../junit/questdb/SimpleQuestDBTest.java | 71 +++++++++++++++ 14 files changed, 253 insertions(+) create mode 100644 docs/modules/databases/questdb.md create mode 100644 modules/questdb/build.gradle create mode 100644 modules/questdb/src/main/java/org/testcontainers/containers/QuestDBContainer.java create mode 100644 modules/questdb/src/main/java/org/testcontainers/containers/QuestDBProvider.java create mode 100644 modules/questdb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider create mode 100644 modules/questdb/src/test/java/org/testcontainers/QuestDBTestImages.java create mode 100644 modules/questdb/src/test/java/org/testcontainers/jdbc/questdb/QuestDBJDBCDriverTest.java create mode 100644 modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 8196451e88f..37609ce1424 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -41,6 +41,7 @@ body: - PostgreSQL - Presto - Pulsar + - QuestDB - RabbitMQ - Redpanda - Selenium diff --git a/.github/ISSUE_TEMPLATE/enhancement.yaml b/.github/ISSUE_TEMPLATE/enhancement.yaml index 9d65e1f12b8..2bbca5db3ae 100644 --- a/.github/ISSUE_TEMPLATE/enhancement.yaml +++ b/.github/ISSUE_TEMPLATE/enhancement.yaml @@ -41,6 +41,7 @@ body: - PostgreSQL - Presto - Pulsar + - QuestDB - RabbitMQ - Redpanda - Selenium diff --git a/.github/ISSUE_TEMPLATE/feature.yaml b/.github/ISSUE_TEMPLATE/feature.yaml index 581870d976d..d29d0178c39 100644 --- a/.github/ISSUE_TEMPLATE/feature.yaml +++ b/.github/ISSUE_TEMPLATE/feature.yaml @@ -39,6 +39,7 @@ body: - Oracle-XE - OrientDB - PostgreSQL + - QuestDB - Presto - Pulsar - RabbitMQ diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 46c7046300d..2e3afe8fa8a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -195,6 +195,11 @@ updates: schedule: interval: "monthly" open-pull-requests-limit: 10 + - package-ecosystem: "gradle" + directory: "/modules/questdb" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 - package-ecosystem: "gradle" directory: "/modules/r2dbc" schedule: diff --git a/.github/labeler.yml b/.github/labeler.yml index 85144b7d7f9..bda614305fa 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -65,6 +65,8 @@ - modules/presto/* "modules/pulsar": - modules/pulsar/* +"modules/questdb": + - modules/questdb/* "modules/r2dbc": - modules/r2dbc/* "modules/rabbitmq": diff --git a/docs/modules/databases/questdb.md b/docs/modules/databases/questdb.md new file mode 100644 index 00000000000..8783dd4b757 --- /dev/null +++ b/docs/modules/databases/questdb.md @@ -0,0 +1,29 @@ +# QuestDB Module + +Testcontainers module for [QuestDB](https://github.com/questdb/questdb). QuestDB is a high-performance, open-source SQL +database for applications in financial services, IoT, machine learning, DevOps and observability. + +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:questdb:{{latest_version}}" +``` + +=== "Maven" + +```xml + + + org.testcontainers + questdb + {{latest_version}} + test + +``` diff --git a/mkdocs.yml b/mkdocs.yml index 70421761e02..8871267cace 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -63,6 +63,7 @@ nav: - modules/databases/orientdb.md - modules/databases/postgres.md - modules/databases/presto.md + - modules/databases/questdb.md - modules/databases/tidb.md - modules/databases/trino.md - modules/azure.md diff --git a/modules/questdb/build.gradle b/modules/questdb/build.gradle new file mode 100644 index 00000000000..79fc0793879 --- /dev/null +++ b/modules/questdb/build.gradle @@ -0,0 +1,13 @@ +description = "Testcontainers :: QuestDB" + +dependencies { + api project(':testcontainers') + api project(':jdbc') + + testImplementation 'org.postgresql:postgresql:42.5.0' + testImplementation project(':jdbc-test') + testImplementation 'org.assertj:assertj-core:3.23.1' + testImplementation 'org.questdb:questdb:6.4.3-jdk8' + testImplementation 'org.awaitility:awaitility:4.2.0' + testImplementation 'org.apache.httpcomponents:httpclient:4.5.13' +} diff --git a/modules/questdb/src/main/java/org/testcontainers/containers/QuestDBContainer.java b/modules/questdb/src/main/java/org/testcontainers/containers/QuestDBContainer.java new file mode 100644 index 00000000000..dd933dd6c95 --- /dev/null +++ b/modules/questdb/src/main/java/org/testcontainers/containers/QuestDBContainer.java @@ -0,0 +1,88 @@ +package org.testcontainers.containers; + +import lombok.NonNull; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +/** + * Testcontainers implementation for QuestDB. + * + * @author vangreen + * @author jerrinot + */ +public class QuestDBContainer extends JdbcDatabaseContainer { + + static final String DATABASE_PROVIDER = "postgresql"; + + private static final String DEFAULT_DATABASE_NAME = "qdb"; + + private static final int DEFAULT_COMMIT_LAG_MS = 1000; + + private static final String DEFAULT_USERNAME = "admin"; + + private static final String DEFAULT_PASSWORD = "quest"; + + private static final Integer POSTGRES_PORT = 8812; + + private static final Integer REST_PORT = 9000; + + private static final Integer ILP_PORT = 9009; + + static final String TEST_QUERY = "SELECT 1"; + + static final DockerImageName DEFAULT_IMAGE_NAME = DockerImageName.parse("questdb/questdb"); + + public QuestDBContainer(@NonNull String dockerImageName) { + this(DockerImageName.parse(dockerImageName)); + } + + public QuestDBContainer(DockerImageName dockerImageName) { + super(dockerImageName); + dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME); + withExposedPorts(POSTGRES_PORT, REST_PORT, ILP_PORT); + addEnv("QDB_CAIRO_COMMIT_LAG", String.valueOf(DEFAULT_COMMIT_LAG_MS)); + waitingFor(Wait.forLogMessage("(?i).*A server-main enjoy.*", 1)); + } + + @Override + public String getDriverClassName() { + return "org.postgresql.Driver"; + } + + @Override + public String getJdbcUrl() { + return String.format("jdbc:postgresql://%s:%d/%s", getHost(), getMappedPort(8812), getDefaultDatabaseName()); + } + + @Override + public String getUsername() { + return DEFAULT_USERNAME; + } + + @Override + public String getPassword() { + return DEFAULT_PASSWORD; + } + + @Override + public String getTestQueryString() { + return TEST_QUERY; + } + + @Override + protected void waitUntilContainerStarted() { + getWaitStrategy().waitUntilReady(this); + } + + public String getDefaultDatabaseName() { + return DEFAULT_DATABASE_NAME; + } + + public String getIlpUrl() { + return getHost() + ":" + getMappedPort(ILP_PORT); + } + + public String getHttpUrl() { + return "http://" + getHost() + ":" + getMappedPort(REST_PORT); + } +} diff --git a/modules/questdb/src/main/java/org/testcontainers/containers/QuestDBProvider.java b/modules/questdb/src/main/java/org/testcontainers/containers/QuestDBProvider.java new file mode 100644 index 00000000000..0d4e9c6eeeb --- /dev/null +++ b/modules/questdb/src/main/java/org/testcontainers/containers/QuestDBProvider.java @@ -0,0 +1,14 @@ +package org.testcontainers.containers; + +public class QuestDBProvider extends JdbcDatabaseContainerProvider { + + @Override + public boolean supports(String databaseType) { + return databaseType.equals(QuestDBContainer.DATABASE_PROVIDER); + } + + @Override + public JdbcDatabaseContainer newInstance(String tag) { + return new QuestDBContainer(QuestDBContainer.DEFAULT_IMAGE_NAME.withTag(tag)); + } +} diff --git a/modules/questdb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider b/modules/questdb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider new file mode 100644 index 00000000000..922a12a1328 --- /dev/null +++ b/modules/questdb/src/main/resources/META-INF/services/org.testcontainers.containers.JdbcDatabaseContainerProvider @@ -0,0 +1 @@ +org.testcontainers.containers.QuestDBProvider diff --git a/modules/questdb/src/test/java/org/testcontainers/QuestDBTestImages.java b/modules/questdb/src/test/java/org/testcontainers/QuestDBTestImages.java new file mode 100644 index 00000000000..555b56f172c --- /dev/null +++ b/modules/questdb/src/test/java/org/testcontainers/QuestDBTestImages.java @@ -0,0 +1,7 @@ +package org.testcontainers; + +import org.testcontainers.utility.DockerImageName; + +public interface QuestDBTestImages { + DockerImageName QUESTDB_IMAGE = DockerImageName.parse("questdb/questdb:6.5.3"); +} diff --git a/modules/questdb/src/test/java/org/testcontainers/jdbc/questdb/QuestDBJDBCDriverTest.java b/modules/questdb/src/test/java/org/testcontainers/jdbc/questdb/QuestDBJDBCDriverTest.java new file mode 100644 index 00000000000..b19c2acc5fd --- /dev/null +++ b/modules/questdb/src/test/java/org/testcontainers/jdbc/questdb/QuestDBJDBCDriverTest.java @@ -0,0 +1,19 @@ +package org.testcontainers.jdbc.questdb; + +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 QuestDBJDBCDriverTest extends AbstractJDBCDriverTest { + + @Parameterized.Parameters(name = "{index} - {0}") + public static Iterable data() { + return Arrays.asList( + new Object[][] { { "jdbc:tc:postgresql://hostname/databasename", EnumSet.of(Options.PmdKnownBroken) } } + ); + } +} diff --git a/modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java b/modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java new file mode 100644 index 00000000000..c38524018c3 --- /dev/null +++ b/modules/questdb/src/test/java/org/testcontainers/junit/questdb/SimpleQuestDBTest.java @@ -0,0 +1,71 @@ +package org.testcontainers.junit.questdb; + +import io.questdb.client.Sender; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.util.EntityUtils; +import org.junit.Test; +import org.testcontainers.QuestDBTestImages; +import org.testcontainers.containers.QuestDBContainer; +import org.testcontainers.db.AbstractContainerDatabaseTest; + +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +public class SimpleQuestDBTest extends AbstractContainerDatabaseTest { + + private static final String TABLE_NAME = "mytable"; + + @Test + public void testSimple() throws SQLException { + try (QuestDBContainer questDB = new QuestDBContainer(QuestDBTestImages.QUESTDB_IMAGE)) { + questDB.start(); + + ResultSet resultSet = performQuery(questDB, questDB.getTestQueryString()); + + int resultSetInt = resultSet.getInt(1); + assertThat(resultSetInt).as("A basic SELECT query succeeds").isEqualTo(1); + } + } + + @Test + public void testRest() throws IOException { + try (QuestDBContainer questdb = new QuestDBContainer(QuestDBTestImages.QUESTDB_IMAGE)) { + questdb.start(); + populateByInfluxLineProtocol(questdb, 1_000); + try (CloseableHttpClient client = HttpClientBuilder.create().build()) { + String encodedSql = URLEncoder.encode("select * from " + TABLE_NAME, "UTF-8"); + HttpGet httpGet = new HttpGet(questdb.getHttpUrl() + "/exec?query=" + encodedSql); + await() + .untilAsserted(() -> { + try (CloseableHttpResponse response = client.execute(httpGet)) { + assertThat(response.getStatusLine().getStatusCode()).isEqualTo(200); + String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + assertThat(json.contains("\"count\":1000")).isTrue(); + } + }); + } + } + } + + private static void populateByInfluxLineProtocol(QuestDBContainer questdb, int rowCount) { + try (Sender sender = Sender.builder().address(questdb.getIlpUrl()).build()) { + for (int i = 0; i < rowCount; i++) { + sender + .table(TABLE_NAME) + .symbol("sym", "sym1" + i) + .stringColumn("str", "str1" + i) + .longColumn("long", i) + .atNow(); + } + } + } +}