>_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).
| url | Full base URL, e.g. https://ch.example.com:8443 |
| host / port | 127.0.0.1 / 8123 — used with tls when url is absent |
| tls | false; true selects the https scheme |
| username / password | HTTP Basic auth (default user default, empty password) |
| database | default — sent as ?database= on every request |
| params | Extra 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
| Liveness | version, ping, server_version |
| Query | query, query_rows, query_row, query_value, exec, insert, delete_where, raw |
| Introspection | databases, tables, describe, columns, count, table_exists, database_exists, settings, parts, disk_usage, mutations, processes |
| DDL | create_database, drop_database, create_table, drop_table, truncate_table, rename_table, add_column, drop_column, optimize |
| Pure helpers | escape, 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
| Function | Returns / 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
| Function | Returns / 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
| Function | Returns / 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
| Function | Emitted 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.
| Function | Behaviour |
|---|---|
| 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:
- stryke-arrow — Apache Arrow / Parquet / Feather
- stryke-aws — S3, DynamoDB, SQS, Lambda, STS
- stryke-azure — Blob, Queues, Cosmos DB, Key Vault, Entra
- stryke-demo — live demos for every connector
- stryke-docker — Docker daemon API
- stryke-duckdb — embedded DuckDB SQL engine
- stryke-email — email + campaign client
- stryke-fleet — parallel expect/PTY automation
- stryke-gcp — Cloud Storage, Pub/Sub, Secret Manager, BigQuery, Firestore
- stryke-grpc — reflection-based gRPC client
- stryke-gui — mouse / keyboard / screen / clipboard automation
- stryke-k8s — Kubernetes
- stryke-kafka — Apache Kafka
- stryke-mcpd — MCP servers without a runtime
- stryke-mongo — MongoDB
- stryke-mssql — Microsoft SQL Server
- stryke-mysql — MySQL / MariaDB
- stryke-neo4j — Neo4j graph (Bolt)
- stryke-office — Office docs / PDF / images
- stryke-parquet — Parquet file inspector
- stryke-polars — Polars + ndarray + linalg + FFT
- stryke-postgres — PostgreSQL
- stryke-redis — Redis / Valkey
- stryke-scrape — web scraping / crawling
- stryke-scylla — ScyllaDB / Cassandra
- stryke-search — Elasticsearch / OpenSearch
- stryke-selenium — browser automation (WebDriver)
- stryke-spark — Spark Connect (no JVM)
- stryke-utils — pure-stryke utility belt
- stryke-zmq — ZeroMQ messaging