Expand description
chtypes — ClickHouse’s own type system, per ClickHouse version, from Rust.
chtypes answers one question, exactly: if this row were inserted into this
ClickHouse table on this ClickHouse version, what would happen? It answers
it by running ClickHouse’s own C++ machinery — DataTypeFactory,
ISerialization, ReadHelpers, evaluateMissingDefaults, the TTL
algorithms, MergeTreeDataWriter::mergeBlock — vendored per release and
linked behind the frozen chs_* C ABI. Nothing here reimplements a coercion
rule, which is why the answers are exact by construction.
This crate is a peer SDK over that ABI, alongside Go, Python and TypeScript.
The language-neutral contract is spec/ in this repository; where this crate
and spec/ disagree, the spec wins and this is a bug.
use chtypes::{Format, Registry, NO_SETTINGS};
let registry = Registry::from_env_or_default()?; // $CHTYPES_REGISTRY, else the per-user cache
let lib = registry.for_version("25.8")?; // minor line or exact patch
let schema = lib.compile("ts DateTime, seq UInt8").compile()?;
let batch = schema.rows(
Format::JsonEachRow,
br#"{"ts":"2026-01-15 10:30:00","seq":256}"#,
NO_SETTINGS,
)?;
println!("{} {:?}", batch.outcome, batch.rows[0].values);
for t in &batch.transformed {
// seq: 256 -> 0, overflow_wrap, lossy — and ClickHouse returned success.
println!("row {} {}: {} -> {} ({})", t.row, t.column, t.input, t.stored, t.reason);
}§What this crate will not do
- Never map
Error::Unsupportedonto a rejection or an acceptance.-2(CODE_UNSUPPORTED) means “a real server might well have accepted this; I decline to guess”. Mapping it to a rejection manufactures an over-reject; mapping it to an acceptance manufactures an over-accept, which is the cardinal sin — rows stream to subscribers and then the insert fails. - Never infer one version’s answer from another’s. Behaviour is not
monotonic: 25.10 rejects a mixed-type DEFAULT that 24.8 through 25.8 and
26.6 onward all accept;
JSONis rejected on 24.8 and accepted from 25.3.Registry::for_versionfails, naming what is loaded, rather than answering from the nearest artifact. - Never treat a per-row
acceptedas “stored”. A TTL-expired row is accepted per row and not stored per batch.BatchResult::transformedfolds in the batch-levelstorage_transforms, andBatchResult::engine_rows— when present — is the stored truth, notBatchResult::rows. - Never route a value through a float. Settings values cross as strings
and stored values stay raw JSON text;
18446744073709551615must not become18446744073709552000. - Never decode a stored value into a language type before comparing it.
A ClickHouse
Stringholds arbitrary bytes, so a stored rendering isRawText— bytes, with a fallible UTF-8 view — and never aStringthat silently carries U+FFFD where the value had bytes. SeeValue::text.
§Getting artifacts
An artifact is one ClickHouse release compiled behind the C ABI — 166–302 MB
each, hours of C++ compute. Fetch prebuilt, signed ones with the crate’s own
command (cargo install chtypes → chtypes fetch 25.8) or from Rust with
ensure — the docs/fetch.md contract, behind the default-on fetch
feature; scripts/fetch.sh is the reference implementation of the same
chain. A local build lands in the same per-user cache
(~/.cache/chtypes/artifacts/<os>-<arch>). Registry::from_search_path
looks there, in $CHTYPES_REGISTRY and in the system locations, and names
every place it looked when a line is missing (Error::ArtifactMissing);
Registry::new loads one explicit directory.
§Platform
Unix only — the loader is dlopen. Linux is the shipping target; macOS is a
development floor and not an oracle: its long double is 53-bit, so float
parses diverge from a real server (the float corpus matches 395/395 on Linux
and 0/395 on macOS). Any float expectation must come from a Linux artifact or
a live server.
Re-exports§
pub use fetch::Action;pub use fetch::EnsureOptions;pub use fetch::Installed;pub use fetch::ensure;
Modules§
- fetch
- Fetching, verifying and installing artifacts —
docs/fetch.md, the contract every SDK implements identically. Behind thefetchfeature (on by default). - reason
- The stable reason strings. The harness groups on them, so these spellings are fixed by the spec and must not drift.
Structs§
- Batch
Result - The outcome of one request body, which may hold many rows.
- Block
- One body, parsed ONCE under one schema handle and one clock instant —
see
Schema::parse_blockfor the parse contract andFilter::evalfor the evaluation side. The borrow IS the free-order enforcement, exactly as forFilter: aBlockcannot outlive itsSchema, and itsDropruns first by construction. - Column
- One column of a compiled schema, as this build canonicalised it.
- Compile
Request - A compile request, built with
Library::compile. See the module docs for the common case and the settings-profile case side by side. - Computed
- One
MATERIALIZEDcolumn’s value: stored at insert, frozen thereafter. - Discovered
Column - One row of
QUERY_TABLE_COLUMNS. - DocFlags
- Which document GROUPS the per-row documents carry (revision 3;
spec/c-abi.md§Document flags). The verdict channel — batch and per-row outcome/code/err,rows_read,rows_skipped,unsupported_settings,engine_rows,storage_transforms— is ALWAYS emitted and is not a flag. - Filter
- One boolean SQL expression compiled against a
Schema’s columns — seeSchema::compile_filterfor the compile contract and the lifetime argument (the borrow IS the free-order enforcement). - Filter
Result - One
crate::Filter::rowsanswer. - Filter
RowError - One
'e'or'd'row, itemized: the row’s 0-based index and the code and message verbatim — ClickHouse’s own for an error row, this library’s decline for a declined row. - Library
- One
dlopen’d vendored ClickHouse build. - Manifest
- One artifact’s
manifest.json. - RawText
- ClickHouse’s own rendering of a value, as bytes.
- Registry
- Every artifact under one directory, indexed by version — or, built with
Registry::from_search_path, thedocs/fetch.md§1 search path opened one line at a time. - Registry
Options - Options for a search-path registry (
Registry::from_search_path_with). The default is the §1 search path for this host,UTC, and autofetch fromCHTYPES_AUTOFETCH. - RowResult
- The outcome of validating and coercing one row.
- Schema
- A schema compiled inside one specific version’s library.
- Server
Profile - What discovery learns about one deployment: the exact release and the settings it runs changed from defaults. Cache one per deployment (or per tenant on bring-your-own-ClickHouse) and declare it.
- Span
- One row’s byte range inside an export payload:
payload[off..off + len]IS that row’s complete serialized line, terminating\nincluded, and is itself a valid one-row body in the export format. Spans are index-aligned withBatchResult::rows; a non-accepted row (rejected / skipped / unsupported) carries{0, 0}. The concatenation of all non-zero spans reproduces the payload exactly, which is what lets batches merge by byte concatenation. - Substitution
- One volatile DEFAULT this library resolved instead of the server.
- Transform
- One silent change: input
256intoUInt8stored as0. - Value
- One coerced column value, as ClickHouse itself renders it.
Enums§
- Compile
Mode - The compile MODE — mirrors
enum chs_compile_modeinchtypes.h. Numeric values are part of the ABI, exactly likecrate::Format. - Default
Kind - What a column’s DEFAULT clause is.
- Error
- Everything that can go wrong loading an artifact or asking it a question.
- Filter
Outcome - The CALL-level verdict of
crate::Filter::rows— whether evaluation completed at all; per-row failures live in the verdicts, not here. The default (and the degradation for an outcome spelling this crate does not recognise) isUnsupported, neverRejected— the same vocabulary-drift rule asOutcome. - Format
- The input encoding of a row, as the
chs_formatinteger codes. The numbers are part of the ABI and must not be renumbered. - Outcome
- The verdict on one row or one batch.
- Verdict
- One row’s answer from
crate::Filter::rows. Two of the four states are ANSWERS and two are NOT, and the split is load-bearing: a caller enforcing visibility MUST fail closed (hide the row / fail the request) onVerdict::ErrorandVerdict::Decline— collapsing either into “false the answer” inverts fail-closed into fail-open underNOT, the measured leak class (spec/bindings.md§Revision 3). The default isDecline, so an unset or unknown verdict is fail-closed by construction.
Constants§
- ABI_
REVISION - The
chs_*ABI revision this crate was written against —CHS_ABI_REVISIONininclude/chtypes.h. - AUTOFETCH_
ENV CHTYPES_AUTOFETCH=1turns lazy fetch on for a search-path registry (Registry::from_search_path): opening a missing line runscrate::ensurefirst (docs/fetch.md§6). Off by default, because a production process must not begin a 250 MB download inside a request.- CODE_
ARTIFACT_ CORRUPT CHTYPES_ARTIFACT_CORRUPT— any hash mismatch anywhere in the chain (§3).- CODE_
ARTIFACT_ MISSING CHTYPES_ARTIFACT_MISSING— no installed artifact answers for the line (docs/fetch.md§7). The code every SDK shares forError::ArtifactMissing.- CODE_
ARTIFACT_ PINNED CHTYPES_ARTIFACT_PINNED— the release offers something other than what the lock file pins (§5).- CODE_
ARTIFACT_ UNPUBLISHED CHTYPES_ARTIFACT_UNPUBLISHED— the release publishes nothing for the requested line or exact patch on this platform (§2).- CODE_
ARTIFACT_ UNTRUSTED CHTYPES_ARTIFACT_UNTRUSTED— the release’sSHA256SUMSis unsigned or mis-signed (§3 step 0); nothing was downloaded around it.- CODE_
SOURCE_ UNREACHABLE CHTYPES_SOURCE_UNREACHABLE— the source could not be reached, or was not consulted because the fetch was offline.- CODE_
UNSUPPORTED CHS_CODE_UNSUPPORTEDfrominclude/chtypes.h: “this build refuses to answer”, and never a real ClickHouse error code.- DEFAULT_
TIMEZONE - The server timezone assumed for bare
DateTime/DateTime64columns. - FETCH_
COMMAND - This SDK’s fetch command, as the “Install it:” line of
Error::ArtifactMissingspells it (docs/fetch.md§6: the crate’s[[bin]], reached throughcargo install chtypes). - NO_
PARAMS - An empty query-parameter map, for the common
Schema::compile_filtercall with no{name:Type}parameters. Positionally, like the settings sliceSchema::rowsandSchema::set_enginealready take — the crate’s one-optional-parameter convention (spec/bindings.md§One compile function). - NO_
SETTINGS - An empty settings map, for the common call with no per-request settings.
- QUERY_
CHANGED_ SETTINGS - Every query setting the deployment runs at a NON-default value — the whole
declared profile, from the server itself. One row per setting:
{"name":"flatten_nested","value":"0"}.system.settings.valueis already a String; the parsed pairs feedcrate::Library::compile’s.settings(...)and per-call settings verbatim. - QUERY_
SERVER_ VERSION QUERY_SERVER_VERSION’s result: the deployment’s exact release — the stringcrate::Registry::for_versionresolves (minor line or exact patch both work). One row:{"version":"25.8.28.1"}.- QUERY_
TABLE_ COLUMNS - One existing table, in declaration order, with everything a
column-declaration list needs — INCLUDING
default_kindanddefault_expression, without which a reconstructed schema silently loses its DEFAULT/MATERIALIZED semantics. Uses ClickHouse’s own query parameters: sendparam_db/param_table(HTTP) or bind{db}/{table}(native). - REGISTRY_
ENV - The environment variable a host may point at a registry directory.
- SETTING_
CLOCK_ OFFSET_ NANOS - The caller’s measured
(server - client)offset, added to every clock read. - SETTING_
DEFAULT_ EVAL_ MEMORY_ BYTES - Admission ceiling on DEFAULT/TTL evaluation memory. Process-wide: settable
only through
Library::set_default_settings. - SETTING_
DEFAULT_ EVAL_ WALL_ NANOS - Admission ceiling on DEFAULT/TTL evaluation wall time. Process-wide, as above.
- SETTING_
MAX_ CLOCK_ SKEW_ NANOS - Refuse to substitute a volatile DEFAULT when
|offset|exceeds this;0means no budget, and with none of these set the tolerated skew is unbounded. - SETTING_
NOW_ EPOCH_ NANOS - Pin the batch instant outright — tests, replay, anything that must be
reproducible. The value is a 19-digit nanosecond epoch and must cross as a
string: as a JSON number through a float it becomes
1.7e+18and the setting is silently ignored. - SYSTEM_
ARTIFACT_ ROOTS - The system registry roots,
docs/fetch.md§1 item 4 — searched after the per-user cache, never written by fetch.<os>-<arch>is appended.
Functions§
- cache_
dir_ for - The per-user artifact cache for one platform —
${XDG_CACHE_HOME:-~/.cache}/chtypes/artifacts/<platform>(docs/fetch.md§1 item 3). Where fetch installs, where a core-repository build lands. - default_
registry_ dir - The per-user artifact cache for this host —
${XDG_CACHE_HOME:-~/.cache}/chtypes/artifacts/<os>-<arch>,<arch>spelled the artifact way (amd64,arm64). Wherescripts/fetch.shandcrate::ensureinstall, where a core-repository build lands, and what every SDK’s tests and playgrounds fall back to when$CHTYPES_REGISTRYis unset — one directory the four SDKs agree on. A path, not a promise:Registry::newstill errors if nothing is there. - host_
platform - This host’s platform key,
<os>-<arch>in the artifact spelling:linuxordarwin,arm64oramd64(docs/fetch.md§1). - install_
dir - Where fetch writes for this host: the first of
explicit,$CHTYPES_REGISTRYand the per-user cache that is set — never a system location (docs/fetch.md§1). - install_
dir_ for install_dirfor an arbitrary platform key; seesearch_path_forfor why$CHTYPES_REGISTRYonly counts for the host platform.- installed_
lines - Every minor line installed somewhere on
dirs, each with the FIRST directory that holds it (the one lookup would take), in numeric release order. A subdirectory counts when it holds amanifest.json. - locate
- The first directory on this host’s search path that holds
line—<dir>/<minor>/manifest.jsonexists — returned as<dir>/<minor>.Nonewhen no directory does.linemay be a minor line or an exact patch; the minor is what is looked for, asRegistry::for_versionresolves it. - locate_
in locateover an explicit list of registry directories, in order.- parse_
changed_ settings_ result - Read
QUERY_CHANGED_SETTINGS’ JSONEachRow body. - parse_
columns_ result - Read
QUERY_TABLE_COLUMNS’ JSONEachRow body, in the query’sORDER BY positionorder. - parse_
version_ result - Read
QUERY_SERVER_VERSION’s JSONEachRow body. - reconstruct_
ddl - Turn
QUERY_TABLE_COLUMNS’ rows back into the column-declaration listcrate::Library::compiletakes. It is a spelling exercise, not a semantic one: types and expressions are the server’s own text, passed through verbatim, and the library’s own compile is the judge of the result. - registry_
search_ path - The
docs/fetch.md§1 search path for this host, in order:explicit,$CHTYPES_REGISTRY, the per-user cache, then the system locations (SYSTEM_ARTIFACT_ROOTS). Unset entries are absent; directories that do not exist are kept, so the §7 message can name every place looked in. - search_
path_ for registry_search_pathfor an arbitrary platform key.$CHTYPES_REGISTRYnames this host’s registry and joins the path only for the host platform; a foreign platform (Linux artifacts fetched on a Mac, for a container) is looked for in its own cache and system directories.