chtypes 0.1.2

ClickHouse's own type system, schema validation, DEFAULT/TTL semantics and coercion, per ClickHouse version, over the frozen chs_* C ABI
Documentation

chtypes — Rust SDK

If this row were inserted into this table on this ClickHouse version, what would happen? chtypes answers with ClickHouse's own code: the real C++ type machinery, vendored per release into a native library behind the frozen chs_* C ABI and reached here through libloading. Nothing semantic is reimplemented, so "what does ClickHouse do with 256 into a UInt8?" is answered by ClickHouse rather than by a model of it. One peer binding among {go, python, ts, rust} — no language is privileged, and all four give one answer.

Unix only — the loader is dlopen.

Install

Two things: this crate, and at least one artifact — the per-version native library it dlopens at runtime.

cargo add chtypes
cargo install chtypes && chtypes fetch 25.8

The fetch lands in ~/.cache/chtypes/artifacts/<os>-<arch>/25.8/ — the per-user cache every chtypes binding reads by default — after checking an ed25519 signature over the release and the sha256 of every byte. $CHTYPES_REGISTRY overrides it.

The fetch feature is on by default and carries the binary, ensure and autofetch. default-features = false drops it and every dependency it brings (ed25519-dalek, sha2, ureq, base64, tar, flate2), leaving the loader alone.

Quickstart

use chtypes::{Format, Registry, NO_SETTINGS};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let registry = Registry::from_search_path();  // walks the search path
    let lib = registry.for_version("25.8")?;      // a line or an exact patch; never a nearest match
    let schema = lib.compile("x UInt8, ts DateTime DEFAULT now()").compile()?;

    let batch = schema.rows(Format::JsonEachRow, br#"{"x":256}"#, NO_SETTINGS)?;
    let row = &batch.rows[0];
    println!("{:?}", batch.outcome);           // Accepted
    println!("{}", row.values[0].text);        // 0             — what would actually be stored
    println!("{}", row.transformed[0].reason); // overflow_wrap — which is the product
    println!("{:?}", row.substituted);         // ts: send it explicitly, or preview != stored
    Ok(())
}

The row is accepted and 256 is silently stored as 0. That report — transformed — is the one derived answer in the system and the reason it exists.

ts was substituted rather than stored: send every substituted column as an explicit value in the real INSERT, or the server re-evaluates now() at its own instant and your preview is not what landed.

Three outcomes, and conflating any two is a bug

The verdict is in the Ok value. A row a server would reject is Ok with Outcome::Rejected, carrying ClickHouse's own code and message. The Err arm is for the machinery — loading, marshaling, an unreadable document — and for schema-level answers.

  • Error::Schema — the server itself would refuse this. Error::code() is a real ClickHouse code, as an Option<i32>.
  • Error::Unsupported / Outcome::Unsupported (code -2) — this build declines to guess, and a real server might well have accepted. Fall back to the server; a decline is neither an acceptance nor a rejection.

Documentation

cargo doc --no-deps --open is the full reference — every public item is documented (#![deny(missing_docs)]), including which Error variant each call can produce.

Quickstart the same program in all four languages
Rust API reference the map of the crate, with the C entry point under each item
Artifacts getting one, where it lands, verifying and pinning it
Batches always rows, and the two bad-row policies
Transformations the silent-change report, and the DEFAULTs you must echo back
Settings · Discovery the four channels; asking a real server what profile to validate under
Filters · Multi-version boolean expressions over rows; several ClickHouse versions in one process
Support matrix · Limitations what works where; what chtypes declines to answer

Where this crate and the normative spec disagree, the spec wins.

Three things specific to this binding

Two settings shapes, and the difference will catch you once. The compile builder's .settings(...) takes anything iterable, so [("k", "v")] is fine. rows, row_with_settings and Filter::rows take &[(K, V)] — a slice reference — so write &[("k", "v")], or NO_SETTINGS for the empty case.

Lifetimes do the work other bindings do at runtime. A Filter and a Block borrow their Schema, so freeing the schema first does not compile. Schema is Send and deliberately not Sync: one native handle must not reach two threads, and the type system enforces it. Parallelism comes from more schemas, not shared ones. Library and Registry are Send + Sync — share them freely.

Two constructors, two behaviors. Registry::from_search_path() is lazy and walks the search path, loading one line per open. Registry::new(dir) is the single-directory loader: eager, that directory only, answering Error::NoSuchVersion for a line it lacks.

Tests

cargo test. Integration tests and the golden set skip loudly without a registry; CHTYPES_REGISTRY=/path/to/registry cargo test runs them. The fetch suite (cargo test --test fetch) needs only tests/fixtures/fetch/ and runs offline, reading its verdicts from the fixtures' own expected.json.

cargo run --example demo is the product in one screen: overflow, a pinned now(), TTL, a rejection and a decline.

License

Apache 2.0. The artifacts this crate loads are Elastic License 2.0 — a separate license, shipped inside each artifact release.