ClickHouse¶
dev.rightsize.modules.ClickHouseContainer — a single-node ClickHouse container,
queried over its HTTP interface. Defaults to a test/test user/password pair and a
test database.
Defaults¶
| Default image | clickhouse/clickhouse-server:latest — this image's floating reference (see below) |
| Exposed ports | 8123 (HTTP interface — what the helpers use), 9000 (native protocol, exposed but not wrapped by a helper) |
| Env | CLICKHOUSE_USER=test, CLICKHOUSE_PASSWORD=test, CLICKHOUSE_DB=test |
| Wait strategy | Wait.forHttp("/ping").forPort(8123) |
With no image given, this module tracks upstream's latest tag rather than a version
this library pins, so the version moves with ClickHouse's own releases instead of this
library's release cycle. The facts below were verified against
clickhouse/clickhouse-server:25.8 (LTS) specifically — pass that image explicitly to
pin it:
Helpers¶
| Member | Returns |
|---|---|
httpUrl: String |
The HTTP interface's base URI — POST a SQL body, basic-auth'd |
username / password / databaseName: String |
The configured credentials/database (default test/test/test) |
withUsername(username: String): ClickHouseContainer |
Overrides CLICKHOUSE_USER |
withPassword(password: String): ClickHouseContainer |
Overrides CLICKHOUSE_PASSWORD |
withDatabase(database: String): ClickHouseContainer |
Overrides CLICKHOUSE_DB |
Call the withX overrides before start(). This module is deliberately HTTP-first —
the HTTP interface needs no client dependency in the JVM (java.net.http.HttpClient
covers it), which is why there's no separate jdbcUrl helper.
Example¶
package dev.rightsize.modules
import dev.rightsize.modules.ClickHouseContainer
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.util.Base64
class ClickHouseContainerTest {
private val http = HttpClient.newHttpClient()
private fun ClickHouseContainer.query(sql: String): HttpResponse<String> = http.send(
HttpRequest.newBuilder(URI(httpUrl))
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString("$username:$password".toByteArray()))
.POST(HttpRequest.BodyPublishers.ofString(sql))
.build(),
HttpResponse.BodyHandlers.ofString(),
)
@Test
fun `creates a table, inserts, and selects`() {
val ch = ClickHouseContainer()
ch.start()
try {
assertEquals(200, ch.query("CREATE TABLE t (x Int32) ENGINE=Memory").statusCode())
assertEquals(200, ch.query("INSERT INTO t VALUES (1)").statusCode())
val select = ch.query("SELECT x FROM t")
assertEquals(200, select.statusCode())
assertEquals("1", select.body().trim())
} finally {
ch.stop()
}
}
}
Backend notes¶
Readiness is genuinely simple here, no race to work around. GET /ping on the
HTTP port returns the literal body Ok.\n as soon as the HTTP server is accepting
connections — verified directly against a real ClickHouse 25.8 boot. There's no
restart/double-boot race the way Postgres/MySQL/MariaDB's entrypoints have, so
Wait.forHttp("/ping") at the default 200 status is both the simplest and the correct
signal, with nothing further to work around.
Env var names were confirmed against a real boot, not assumed from documentation:
booting with CLICKHOUSE_USER/CLICKHOUSE_PASSWORD/CLICKHOUSE_DB set produces
create new user 'test' instead 'default' and create database 'test' in the logs,
and an authenticated curl -u test:test against the HTTP interface runs queries
successfully.
No withMemoryLimit override is needed — a single-node ClickHouse server isn't a JVM
process at all, so it has none of the Paketo/QuickStart-style fixed-heap-region
problems that Spring Cloud Config and Pinot ran
into (observed ~12s integration-test round-trip on the microsandbox backend with no
memory-ladder escalation needed).
Compatibility checking¶
Passing an explicit image checks its repository against the one this module
understands (clickhouse/clickhouse-server) before any port, wait-strategy, or backend
work runs — a mismatched image fails fast with a typed IncompatibleImageException
naming both repositories, rather than degrading into a bare wait-strategy timeout. To
use a differently-named image on purpose (a private mirror, a hardened rebuild), wrap
it with the escape hatch:
ClickHouseContainer(
DockerImageName.parse("mycorp/clickhouse-hardened:25.8")
.asCompatibleSubstituteFor("clickhouse/clickhouse-server"))
See Core Concepts for DockerImageName itself.