ClickHouse's own type system, as a library you can call.
Validate, coerce and preview rows before they reach a server —
answered by ClickHouse's real C++, not a model of it.
Docs · Install · Quickstart · Supported versions · How it compares · Examples
"What does ClickHouse do with 256 into a UInt8?" has exactly one correct answer, and it is whatever ClickHouse's code does — which changes between releases. chtypes compiles ClickHouse's real C++ (DataTypeFactory, ISerialization, ReadHelpers, evaluateMissingDefaults, the MergeTree insert-time merge) per release into a native artifact behind a small frozen C ABI, and hands it to Go, Python, TypeScript and Rust. Nothing semantic is reimplemented in any binding.
row + schema + ClickHouse version ─▶ accepted / rejected / poisoned
the stored value, byte for byte
every silent change, named
That last line is the point. ClickHouse will accept 256 into a UInt8, store 0, and never mention it. chtypes names it.
Two things, always: the binding for your language, and at least one artifact — the per-version native library it loads. The binding is small; the artifact is real ClickHouse, compiled.
go get github.com/wave-rf/chtypes/go # Go
uv add chtypes # Python (or: pip install chtypes)
pnpm add @wavehouse/chtypes # TypeScript
cargo add chtypes # RustThen fetch an artifact — verified, into the per-user cache every binding reads by default. Each binding ships the same command, so you need nothing from this repository:
go run github.com/wave-rf/chtypes/go/cmd/chtypes@latest fetch 25.8
python -m chtypes fetch 25.8
npx @wavehouse/chtypes fetch 25.8
cargo install chtypes && chtypes fetch 25.8An ed25519 signature over the release and the sha256 of every byte are checked before anything lands. fetch --all takes every published line. Full details: docs/install.md.
One artifact, one schema, one row — the same program in each language. The row is accepted, and 256 is silently stored as 0.
Go
reg, _ := chtypes.NewRegistry(chtypes.DefaultRegistryDir())
lib, _ := reg.For("25.8") // a line or an exact patch; never a nearest match
cs, _ := lib.CompileDDL("x UInt8, ts DateTime DEFAULT now()")
defer cs.Close()
r, _ := cs.Row(chtypes.JSONEachRow, []byte(`{"x":256}`))
fmt.Println(r.Outcome) // accepted
fmt.Println(r.Transformed[0].Reason) // overflow_wrap — 256 stored as 0, silently
fmt.Println(r.Substituted) // ts: send it explicitly, or preview != storedPython
from chtypes import Format, Registry
registry = Registry()
library = registry.for_version("25.8")
with library.compile_ddl("x UInt8, ts DateTime DEFAULT now()") as schema:
r = schema.rows(Format.JSON_EACH_ROW, b'{"x":256}\n')
r.outcome # Outcome.ACCEPTED
r.rows[0].value("x").text # '0' — what would actually be stored
r.transformed[0].reason # 'overflow_wrap' — which is the product
r.rows[0].substituted # ts: send it explicitly, or preview != storedTypeScript
import { Format, Registry } from '@wavehouse/chtypes';
const registry = new Registry();
const lib = registry.for('25.8');
const schema = lib.compileDdl('x UInt8, ts DateTime DEFAULT now()');
const r = schema.row(Format.JSONEachRow, Buffer.from('{"x":256}'));
console.log(r.outcome); // accepted
console.log(r.transformed[0]?.reason); // overflow_wrap — 256 stored as 0, silently
console.log(r.substituted[0]?.column); // ts — send it explicitly in the real INSERT
schema.close(); // or `using schema = …` on Node >= 24Rust
use chtypes::{Format, Registry, NO_SETTINGS};
let registry = Registry::from_search_path();
let lib = registry.for_version("25.8")?;
let schema = lib.compile("x UInt8, ts DateTime DEFAULT now()").compile()?;
let r = schema.rows(Format::JsonEachRow, br#"{"x":256}"#, NO_SETTINGS)?;
assert_eq!(r.outcome, chtypes::Outcome::Accepted);
assert_eq!(r.rows[0].values[0].text, "0"); // what would be stored
assert_eq!(r.transformed[0].reason, chtypes::reason::OVERFLOW_WRAP);A bad row is a verdict, not an error: outcome becomes rejected, carrying ClickHouse's own error code and message. Exceptions (or the Err arm) are for the machinery — a missing artifact, an unreadable document. Full quickstart · the same tour, runnable, in all four languages
| chtypes | hand-rolled validation | round-trip to a real server | |
|---|---|---|---|
| Exact ClickHouse semantics | ClickHouse's own C++ | an approximation that drifts | yes |
| Answers before the insert | yes | yes | no — the row has already gone |
| Names what was silently changed | yes, every coercion | no | no |
| Per-release answers | one artifact per ClickHouse line | no | only that one server's version |
| Cost per row | in-process call | in-process call | a network round trip |
| Needs a running ClickHouse | no | no | yes |
Transformedis the product. ClickHouse never says "I changed your value"; chtypes derives that report (overflow_wrap,date_clamp,poisoned,ttl_expired, …) and it is not optional.- Over-accepts and over-rejects are both budgeted at zero in the proof behind the artifacts: a row accepted here and rejected by the server ships before the insert fails; a row rejected here and accepted by the server is silent data loss.
unsupportedis an answer, never a guess. A binding surfaces the library's decline; it never papers over one.- The four bindings give one answer. The golden set is run by all of them, and each is scored against real ClickHouse servers at the same agreement as the reference.
Three axes — the language you call from, the platform you run on, and the ClickHouse line you want answers for. docs/support.md carries the full matrix, generated from this tree's manifests and the release's own index so it cannot drift from what actually ships.
The short version: Go, Python, TypeScript and Rust; linux-amd64, linux-arm64 and darwin-arm64 (Unix only — both loaders are dlopen); and every ClickHouse line with a committed run of record, today spanning 24.8 through 26.8 and growing as releases are certified.
| Install · Quickstart | getting a binding and an artifact, and the first program |
| Guides | artifacts, fetching, settings, batches, filters, discovery, multi-version, transformations |
| Reference | per-language API, the C ABI contract, the binding contract |
| Supported versions | languages, platforms, ClickHouse lines |
| Examples | four side-by-side runnable tours, same sections in every language |
Pre-1.0, and published to all four registries — the badges above read the live version from each. The C ABI is frozen at revision 4 (include/chtypes.h, 28 chs_* functions) and the four bindings pin that number at compile time. Package names, the artifact name libchtypes, the chs_ prefix and the enum chs_format numbers are frozen; function signatures froze at the first tag. Anything else may still move — each binding keeps its own CHANGELOG.
This repository is the SDK half of chtypes, Apache 2.0. The other half — the C++ wrapper, the per-version vendoring and build pipeline, the artifacts themselves, and the differential proof (tens of thousands of cases scored against real ClickHouse servers on every supported version) — is the core repository, under its own license. The bindings here contain no ClickHouse code: they load an artifact and speak the ABI. Artifacts carry their own license; see the LICENSE inside each release.
Issues and pull requests are welcome — start with CONTRIBUTING.md. A change to one binding's behavior lands in all four in the same cycle; that is the contract, not a preference. Report security issues privately: SECURITY.md.
Much of this repository was written with AI assistance and reviewed by a human before merge. The review gate is the same regardless of who or what authored a change, and the test suites are deliberately built to refuse a silent pass — every artifact-dependent test skips loudly by name, and a suite that ran nothing fails. If you find documentation that drifted from the code, please open an issue; that is the failure mode we most want reported.