// STRYKE-CLICKHOUSE — CLICKHOUSE CLIENT FOR STRYKE // QUERY + INSERT + DDL + INTROSPECTION

stryke package · namespace Clickhouse · cdylib libstryke_clickhouse · opt-in (kept out of stryke core)

Report GitHub Issues

>_STRYKE-CLICKHOUSE

Columnar OLAP, one stryke pipe at a time. ClickHouse client for stryke — SELECTs, bulk insert via JSONEachRow, DDL, and schema introspection against any ClickHouse server over its HTTP interface (default port 8123). Transport is ureq over rustls — synchronous, pure-Rust, no tokio, no OpenSSL. A keep-alive connection pool is cached per (base_url, auth, database). An opt-in package, kept out of the stryke core binary so the daily-driver install stays slim.

Install

s add github.com/MenkeTechnologies/stryke-clickhouse

On first use Clickhouse, stryke dlopens the cdylib in-process and registers every clickhouse__* export.

Quick start

use Clickhouse

var %conn
$conn{url} = "http://127.0.0.1:8123"

Clickhouse::create_table("events", columns => "ts DateTime, user UInt64, kind String", order_by => "(ts, user)", %conn)
Clickhouse::insert("events", [{ ts => "2026-06-20 12:00:00", user => 1, kind => "click" }], %conn)

p Clickhouse::count("events", %conn)
p Clickhouse::query_value("SELECT uniqExact(user) FROM events", %conn)
val @rows = Clickhouse::query_rows("SELECT kind, count() c FROM events GROUP BY kind", %conn)

Connecting

Connection params come from the %conn opts hash on every call (or $CLICKHOUSE_URL when neither url nor host is given).

urlFull base URL, e.g. https://ch.example.com:8443
host / port127.0.0.1 / 8123 — used with tls when url is absent
tlsfalse; true selects the https scheme
username / passwordHTTP Basic auth (default user default, empty password)
databasedefault — sent as ?database= on every request
paramsExtra ClickHouse settings as URL query params

Connection params can be passed inline per call or kept in a hash and splatted into every call:

var %conn
$conn{host}     = "ch.internal"
$conn{port}     = 8123
$conn{tls}      = 0
$conn{username} = "reader"
$conn{password} = $ENV{CH_PASS}
$conn{database} = "analytics"

p Clickhouse::ping(%conn)
val @rows = Clickhouse::query_rows("SELECT now()", %conn)

When neither url nor host is present, the client falls back to the $CLICKHOUSE_URL environment variable, so a script can run unchanged across hosts:

export CLICKHOUSE_URL="https://default:secret@ch.example.com:8443/analytics"
s run report.stk     # no %conn needed

A ureq::Agent (HTTP keep-alive pool) is cached per (base_url, auth, database) tuple for the life of the stryke process, so repeated calls to the same target reuse the underlying connection. The database rides on the query string as ?database=, and credentials travel as HTTP Basic (default user default, empty password).

API surface

Livenessversion, ping, server_version
Queryquery, query_rows, query_row, query_value, exec, insert, delete_where, raw
Introspectiondatabases, tables, describe, columns, count, table_exists, database_exists, settings, parts, disk_usage, mutations, processes
DDLcreate_database, drop_database, create_table, drop_table, truncate_table, rename_table, add_column, drop_column, optimize
Pure helpersescape, escape_like, quote_literal, quote_ident, valid_identifier, format_value, format_in_list, format_array, build_url, parse_url, redact_url

43 functions in the Clickhouse namespace. Every call (except the pure helpers) takes the trailing %opts connection hash. query returns the full result object; query_rows / query_row / query_value peel off the array / first row / first scalar. exec is for statements that return no rows.

Reference — liveness

FunctionReturns / behaviour
version()Package version string (from the cdylib, no server).
ping(%opts)Connectivity probe (GET /ping) — 1 on success, 0 otherwise.
server_version(%opts)Server version string (SELECT version()).

Reference — query & write

FunctionReturns / behaviour
query($sql, %opts)Run a SELECT; returns the full { meta, data, rows, statistics } object.
query_rows($sql, %opts)Run a SELECT; returns just the array of row objects.
query_row($sql, %opts)Run a SELECT; returns the first row object (or undef).
query_value($sql, %opts)Run a SELECT; returns the first column of the first row (a scalar).
exec($sql, %opts)Run a no-row statement (CREATE/INSERT/ALTER/DROP/…). Returns truthy on ok.
insert($table, $rows, %opts)Bulk insert an arrayref of hashrefs into $table via JSONEachRow. Returns the number of rows sent.
delete_where($table, $where, %opts)Lightweight delete: DELETE FROM $table WHERE $where (ClickHouse 22.8+).
raw($sql, %opts)Escape hatch: run arbitrary $sql. json opt (default true) parses a SELECT result; pass json => 0 for a no-row statement.

Reference — introspection

FunctionReturns / behaviour
databases(%opts)Array of database names (system.databases).
tables(%opts)Array of table names in %opts{database} (default default).
describe($table, %opts)DESCRIBE TABLE $table — array of column descriptors.
columns($table, %opts)Per-column metadata from system.columns: name, type, default kind/expression, compressed/uncompressed bytes, comment.
count($table, %opts)Row count of $table.
table_exists($table, %opts)True when $table exists in %opts{database}.
database_exists($name, %opts)True when database $name exists (system.databases).
settings(%opts)All server settings (system.settings): name, value, changed, description.
parts($table, %opts)Active parts from system.parts: partition, rows, on-disk + compressed/uncompressed bytes, modification time.
disk_usage($table, %opts)Aggregate storage of active parts: rows, on-disk + compressed + uncompressed bytes, and compression ratio. One hashref.
mutations($table, %opts)Pending/recent mutations from system.mutations: mutation_id, command, create_time, parts_to_do, is_done, latest_fail_reason.
processes(%opts)Currently executing queries from system.processes: query_id, user, query, elapsed, read_rows, read_bytes, memory_usage.

Reference — DDL

FunctionEmitted statement
create_database($name, %opts)CREATE DATABASE IF NOT EXISTS $name.
drop_database($name, %opts)DROP DATABASE IF EXISTS $name.
create_table($name, %opts)Either %opts{sql} (full CREATE) or $name + columns ("col Type, …") + engine (default MergeTree) + order_by.
drop_table($table, %opts)DROP TABLE IF EXISTS $table.
truncate_table($table, %opts)TRUNCATE TABLE IF EXISTS $table.
rename_table($from, $to, %opts)RENAME TABLE $from TO $to.
add_column($table, $name, $type, %opts)ALTER TABLE $table ADD COLUMN IF NOT EXISTS $name $type; %opts{default} appends a DEFAULT expression.
drop_column($table, $name, %opts)ALTER TABLE $table DROP COLUMN IF EXISTS $name.
optimize($table, %opts)OPTIMIZE TABLE $table; %opts{final} appends FINAL.

Reference — pure helpers

These take no connection and are unit-tested in-crate, so they validate in CI with no server present. Use them to build SQL safely.

FunctionBehaviour
escape($value)Escape $value for a single-quoted SQL literal (backslash + single-quote escaped).
escape_like($value)Escape \, %, _ so $value matches literally inside a LIKE pattern.
quote_literal($value)Wrap $value as a single-quoted, backslash-escaped string literal.
quote_ident($value)Backtick-quote $value as an identifier (embedded backticks escaped).
valid_identifier($value)True when $value is a valid unquoted identifier ([A-Za-z_][A-Za-z0-9_]*).
format_value($value)Format a value as a ClickHouse literal: string→'…', number→as-is, bool→true/false, null→NULL, array→[…].
format_in_list($values)Render an arrayref into an IN (…) list (empty→(NULL), matches nothing).
format_array($values)Render an arrayref into a ClickHouse array literal […].
build_url(%opts)Resolve the base URL these opts connect to.
parse_url($url)Decompose $url into scheme/host/port/username/password/database/tls.
redact_url($url)Replace userinfo in $url with *** for safe logging.

Examples

Safe parameterization with the escape helper:

val $name = Clickhouse::escape($user_input)
val @hits = Clickhouse::query_rows("SELECT * FROM events WHERE kind = '$name'", %conn)

Build an IN (…) list and a LIKE pattern without hand-escaping:

val $ids = Clickhouse::format_in_list([1, 2, 3])          # (1, 2, 3)
val @rows = Clickhouse::query_rows("SELECT * FROM events WHERE user IN $ids", %conn)

val $needle = Clickhouse::escape_like($q)
val @found = Clickhouse::query_rows("SELECT * FROM events WHERE kind LIKE '%$needle%'", %conn)

Introspect storage and schema:

p Clickhouse::table_exists("events", %conn)
val @cols = Clickhouse::columns("events", %conn)
val $usage = Clickhouse::disk_usage("events", %conn)
p $usage->{compression_ratio}

for val $proc (Clickhouse::processes(%conn)) {
    p "$proc->{query_id}: $proc->{elapsed}s"
}

DDL and lightweight delete:

Clickhouse::add_column("events", "session", "String", default => "''", %conn)
Clickhouse::optimize("events", final => 1, %conn)
Clickhouse::delete_where("events", "kind = 'spam'", %conn)
Clickhouse::rename_table("events", "events_old", %conn)

Redact a URL before logging it:

p Clickhouse::redact_url("https://user:pw@ch.example.com:8443")   # https://***@ch.example.com:8443

Build & test

make debug       # cargo build
make test        # cargo test, then `s test t/`
make install     # s pkg install -g .

cargo test runs the in-crate unit tests (string/identifier escaping, URL parse/redact, connection key, result extraction) with no server required. t/test_stryke_clickhouse_surface.stk pins the wrapper surface and exercises the pure helpers; t/test_clickhouse.stk runs end-to-end DDL + insert + query against a live server (needs $CLICKHOUSE_URL or 127.0.0.1:8123) and short-circuits when none answers.

Why a package, not a builtin

The HTTP + TLS transport (ureq, rustls) ships once, on demand, as an opt-in package — stryke core is never linked against it. On first use Clickhouse, stryke dlopens the cdylib in-process and registers every clickhouse__* export. The cdylib caches a ureq::Agent keep-alive pool per (base_url, auth, database), so repeated queries reuse the connection.

Sibling packages

Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo: