Skip to content

Commit

Permalink
Add QuestDB module (#5995)
Browse files Browse the repository at this point in the history
Introduce Testcontainers implementation for `QuestDB`, `QuestDBContainer`.
  • Loading branch information
Vangreen committed Oct 21, 2022
1 parent f9edf87 commit 643b815
Show file tree
Hide file tree
Showing 14 changed files with 253 additions and 0 deletions.
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yaml
Expand Up @@ -41,6 +41,7 @@ body:
- PostgreSQL
- Presto
- Pulsar
- QuestDB
- RabbitMQ
- Redpanda
- Selenium
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/enhancement.yaml
Expand Up @@ -41,6 +41,7 @@ body:
- PostgreSQL
- Presto
- Pulsar
- QuestDB
- RabbitMQ
- Redpanda
- Selenium
Expand Down
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/feature.yaml
Expand Up @@ -39,6 +39,7 @@ body:
- Oracle-XE
- OrientDB
- PostgreSQL
- QuestDB
- Presto
- Pulsar
- RabbitMQ
Expand Down
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions .github/labeler.yml
Expand Up @@ -65,6 +65,8 @@
- modules/presto/*
"modules/pulsar":
- modules/pulsar/*
"modules/questdb":
- modules/questdb/*
"modules/r2dbc":
- modules/r2dbc/*
"modules/rabbitmq":
Expand Down
29 changes: 29 additions & 0 deletions 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

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>questdb</artifactId>
<version>{{latest_version}}</version>
<scope>test</scope>
</dependency>
```
1 change: 1 addition & 0 deletions mkdocs.yml
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions 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'
}
@@ -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<QuestDBContainer> {

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);
}
}
@@ -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));
}
}
@@ -0,0 +1 @@
org.testcontainers.containers.QuestDBProvider
@@ -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");
}
@@ -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<Object[]> data() {
return Arrays.asList(
new Object[][] { { "jdbc:tc:postgresql://hostname/databasename", EnumSet.of(Options.PmdKnownBroken) } }
);
}
}
@@ -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();
}
}
}
}

0 comments on commit 643b815

Please sign in to comment.