chtypes — Rust SDK
ClickHouse's own type system, schema validation, DEFAULT/TTL semantics and
coercion — per ClickHouse version, in one process. A peer SDK over the
chs_* C ABI (pre-1.0 only the names and the format integers are frozen —
signatures can still change by deliberate cycle, which is why the loader
checks chs_abi_revision before trusting them), alongside Go, Python and
TypeScript. ../../spec/ is the
contract; where this crate and the spec disagree, the spec wins.
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 and what each one means.
Install (pre-publish)
The crate is not published; consume it as a path dependency. Unix only — the
loader is dlopen.
[]
= { = "../chtypes/rust" }
Artifact prerequisite. The crate answers nothing by itself: it dlopens
one artifact per ClickHouse release (166–302 MB each — real ClickHouse,
compiled). Get them with the crate's own command (see
Fetching artifacts below),
with scripts/fetch.sh 25.8 (the reference implementation, same result), or
build one in the core repository; all land under
~/.cache/chtypes/artifacts/<os>-<arch>/<minor>/ — the per-user cache
Registry::from_env_or_default() resolves to — so a registry looks like:
<registry>/
24.8/ manifest.json libchtypes_s1.so unsafe_families.txt
25.8/ manifest.json libchtypes.dylib unsafe_families.txt
...
How loading works, and what the loader checks. Registry scans one
subdirectory per version. The shared library's file name comes from
manifest.json's library field — never constructed from the platform (the
Linux artifacts still ship the historical libchtypes_s1.so name). Each
library is dlopen'd with RTLD_NOW | RTLD_LOCAL, names itself via
chs_clickhouse_version() (nothing is inferred from the directory), and is
cross-checked against the manifest's size and version fields. The loader then
compares the artifact's chs_abi_revision() against this crate's hand-kept
chtypes::ABI_REVISION: a different nonzero revision is refused at load
(Error::Load — calling through mismatched declarations is undefined), while
0 means the artifact predates the probe and loads with per-symbol
degradation (missing entry points answer Error::PredatesFeature at call
time, never at load).
Quickstart
use ;
let registry = from_search_path; // docs/fetch.md §1: $CHTYPES_REGISTRY, the per-user cache, the system locations
let lib = registry.for_version?; // minor line or exact patch; Error::ArtifactMissing if nowhere
let schema = lib.compile
.settings // the deployment's profile
.compile?;
// A good row — accepted, but silently coerced: 256 into UInt8 stores 0.
let ok = schema.rows?;
assert_eq!;
assert_eq!;
assert_eq!;
// A bad row — rejected with ClickHouse's own code, in the Ok value.
let bad = schema.rows?;
assert_eq!;
println!; // ClickHouse's message
The verdict is in the Ok value. A row the server would reject is Ok
with Outcome::Rejected; the Err arm is for the machinery (loading,
marshalling, an unreadable document). Three outcomes must never be conflated:
rejected (the server itself would refuse — a real ClickHouse code),
unsupported (this build declines to guess — Error::Unsupported /
Outcome::Unsupported, code -2; fall back to the server), and accepted,
possibly with coercions reported in transformed and volatile DEFAULTs in
substituted (send those columns explicitly in the INSERT).
Fetching artifacts
docs/fetch.md is the contract, identical in the four SDKs; this crate
implements it behind the Cargo feature fetch (on by default —
default-features = false drops the command, ensure, autofetch and every
dependency they bring, leaving the loader alone).
The command. cargo install chtypes puts a chtypes binary on the path:
chtypes fetch <line>... [--all] [--platform <os-arch>] [--dest <dir>]
[--tag <t> | --url <base>] [--lock <file>] [--frozen]
[--force] [--offline]
chtypes verify [--dest <dir>] re-hash every installed line against its manifest
chtypes list [--dest <dir>] what is installed, and what the release offers
chtypes where the registry directory fetch would write to
Progress prints on stderr; fetch prints each installed directory alone on
stdout. Exit codes: 0 ok · 1 verification failed · 2 usage · 3 source
unreachable · 4 not published for this platform/line.
The function. chtypes::ensure(line, &opts) is the same operation from
Rust — idempotent: once the line is installed and its library hashes what the
signed release lists, nothing is downloaded (the release's three small files
are read, never the tarball; offline: true reads no source and trusts the
installed manifest):
use ;
let installed = ensure?; // AlreadyInstalled | Installed | Replaced
let registry = new?;
EnsureOptions carries the flags (dest, platform, url/tag, lock,
frozen, force, offline, progress) and the trust policy
(trusted_keys, allow_unsigned; None reads the environment).
Where. Lookup walks docs/fetch.md §1 in order — an explicit directory,
$CHTYPES_REGISTRY, ${XDG_CACHE_HOME:-~/.cache}/chtypes/artifacts/<os>-<arch>,
then /usr/local/share/chtypes/artifacts/<os>-<arch> and
/opt/chtypes/artifacts/<os>-<arch> — and takes the first directory holding
the line (registry_search_path, locate). Fetch writes to the first of the
first three that is set (install_dir), never to a system location. With an
explicit --dest, "already installed" means installed in that directory:
a container build's --dest /opt/chtypes/artifacts is never satisfied by
the builder's own cache.
Verification, in order — nothing else is a verdict. SHA256SUMS.sig must
verify (ed25519) over the exact bytes of SHA256SUMS under a trusted key;
index.json must agree with the signed sums about the asset; the tarball is
hashed as it streams and never unpacked on a mismatch; it is unpacked flat
(plain top-level files only — no paths, links or traversal) into a temporary
sibling, renamed into <registry>/<minor>/, and the installed library is
re-hashed in place. The release key is embedded (fetch::RELEASE_PUBLIC_KEY,
id deb275922dbff76e); CHTYPES_TRUSTED_KEYS=<hex>[,<hex>…] replaces it for
a mirror; CHTYPES_ALLOW_UNSIGNED=1 skips the signature with one loud
warning naming the source — never the default, never silent.
Pinning. --lock chtypes.lock records, per <os>-<arch>/<minor>, the
asset and sha256 that were installed; --frozen (or
EnsureOptions { frozen: true, .. }) refuses anything else with
CHTYPES_ARTIFACT_PINNED.
Lazy fetch on first open is opt-in: RegistryOptions { autofetch: Some(true), .. } or CHTYPES_AUTOFETCH=1 makes
Registry::from_search_path_with(opts).for_version(line) run ensure first
for a line found nowhere — once per process per line, under one process-wide
lock, so concurrent opens fetch once. Off by default: a production process
must not begin a 250 MB download inside a request.
The error. A line found nowhere on the search path is
Error::ArtifactMissing { line, platform, looked_in }, whose message is the
one every SDK renders:
chtypes: no artifact for ClickHouse 25.8 (linux-arm64). Looked in: /home/u/.cache/chtypes/artifacts/linux-arm64, /usr/local/share/chtypes/artifacts/linux-arm64, /opt/chtypes/artifacts/linux-arm64.
Install it: cargo install chtypes && chtypes fetch 25.8
or set CHTYPES_AUTOFETCH=1 to fetch on first use.
Error::artifact_code() answers the shared code string —
CHTYPES_ARTIFACT_MISSING, …_UNTRUSTED (Error::ArtifactUntrusted),
…_CORRUPT (ArtifactCorrupt, any hash mismatch), …_PINNED
(ArtifactPinned), …_UNPUBLISHED (ArtifactUnpublished) and
CHTYPES_SOURCE_UNREACHABLE (SourceUnreachable) — and None for
everything else. It is distinct from Error::code(), which stays the
ClickHouse error code of a rejection. A registry over one explicit
directory (Registry::new) keeps answering Error::NoSuchVersion, naming
what is loaded.
Tests. cargo test --test fetch runs every fixture in
spec/fixtures/fetch/ (signed, bad signature, unsigned, tampered tarball,
sums/index mismatch, the lock) through both the library and the binary,
offline, reading the verdicts from the fixtures' own expected.json; it
skips loudly when the fixtures are absent.
API reference
Every public item, with the chs_* entry point underneath it. "derived" means
the crate computes it with no C call.
| Item | C function | Takes | Returns | Errors |
|---|---|---|---|---|
Registry::new(dir) / from_env() / from_env_or(dir) / with_timezone(dir, tz) |
chs_clickhouse_version + chs_abi_revision + chs_init per artifact |
registry directory; optional timezone (default UTC) |
Registry (eager: every artifact in the directory) |
Registry, Load, NotAnArtifact, CorruptArtifact, VersionMismatch, Init, InitConflict, EmptyRegistry; NoRegistryEnv (from_env) |
Registry::from_search_path() / from_search_path_with(RegistryOptions) |
— (loads nothing until asked) | optional explicit dir, timezone, autofetch (+ fetch: EnsureOptions) |
Registry (lazy: the docs/fetch.md §1 search path, one line per open) |
— |
Registry::for_version(v) |
derived; lazy: the per-artifact load above, on first open | minor line or exact patch | Arc<Library> |
eager: NoSuchVersion (names what IS loaded; no nearest fallback); lazy: ArtifactMissing (§7), or the fetch's own error under autofetch |
Registry::versions() / libraries() / dir() / search_path() / autofetch() |
derived | — | minor lines, numeric order (lazy: what is installed) / loaded libraries, release order (Vec<Arc<Library>>) / path / the directories looked in / the setting |
— |
ensure(line, &EnsureOptions) (feature fetch) |
— | line or exact patch; the flags of chtypes fetch |
Installed { dir, line, version, library, library_sha256, platform, action, asset } |
ArtifactUntrusted, ArtifactCorrupt, ArtifactPinned, ArtifactUnpublished, SourceUnreachable, Fetch |
fetch::ensure_all / verify_installed(dir) / release_info(&opts) / install_dir(&opts) / search_path(&opts) / parse_line (feature fetch) |
— | as chtypes fetch --all / verify / list / where |
Vec<Installed> / Vec<Verification> / ReleaseInfo / path / paths / minor |
as ensure |
fetch::TrustPolicy, LockFile, IndexRow, verify_signature, sha256_file, RELEASE_PUBLIC_KEY[_HEX], RELEASE_KEY_ID (feature fetch) |
— | the pieces of the chain, for a host that wants them separately | — | Fetch (a malformed key list) |
registry_search_path(explicit) / search_path_for(platform, explicit) / install_dir[_for] / locate[_in] / installed_lines / host_platform / cache_dir_for / default_registry_dir |
derived | — | the §1 search path, where fetch writes, <dir>/<minor> of the first hit, what is installed, <os>-<arch> |
— |
Error::artifact_code() |
derived | — | the shared CHTYPES_ARTIFACT_* / CHTYPES_SOURCE_UNREACHABLE code, None otherwise |
— |
Registry::shutdown() |
chs_shutdown per library |
— | — | — |
Library::load(path, tz) |
dlopen + the four mandatory symbols |
artifact path, timezone | Library |
Load, NotAnArtifact, Init, InitConflict, Nul |
Library::version() / minor() / path() |
chs_clickhouse_version (cached) |
— | "25.8.28.1-lts" / "25.8" / path |
— |
Library::abi_revision() |
chs_abi_revision |
— | artifact's revision, 0 = predates probe |
— |
Library::validate_type(expr) |
chs_validate_type |
type expression | canonical spelling, ClickHouse's own text | Schema (rejected, e.g. 50), Unsupported, PredatesFeature, Nul |
Library::compile(ddl) |
— (builder) | column-declaration list (not a CREATE TABLE) |
CompileRequest |
— |
CompileRequest::settings(...) / .mode(...) |
— (builder) | declared profile; CompileMode::Declared |
Self |
— |
CompileRequest::compile() |
chs_schema_compile + column introspection group |
— | Schema |
Schema (server rejects: 115 unknown setting, 455/44 declared gate, 691 Enum DEFAULT, …), Unsupported (−2 declines), Nul |
Library::set_default_settings(&[(k,v)]) |
chs_set_default_settings |
process-wide seed; admission budgets live here | () |
Schema (115, wholesale — nothing committed), PredatesFeature, Nul |
Library::reference_type(expr) |
chs_reference_type |
type expression | Option<String> (None = no wider type) |
PredatesFeature, Nul |
Library::registered_families() |
chs_registered_families |
— | every type family in this build | PredatesFeature |
Library::function_flags() |
chs_function_flags |
— | TSV volatility audit | PredatesFeature |
Library::has_compile_settings() |
symbol probe | — | true (mandatory symbol) |
— |
Library::shutdown() |
chs_shutdown |
— | — (idempotent; required before any dlclose) |
— |
Schema::columns() |
chs_schema_column_* (read at compile) |
— | &[Column], canonicalised |
— |
Schema::library() |
derived | — | &Arc<Library> |
— |
Schema::set_engine(engine, order_by, mt_settings) |
chs_schema_engine |
SHOW CREATE spelling, sorting key, MergeTree-namespace settings |
() |
sign rule: Schema (rc>0, server refusal — 115), Unsupported (rc<0, decline), PredatesFeature, Nul |
Schema::set_ttl(sql) |
chs_schema_ttl |
table-level rows TTL | () |
Unsupported (any nonzero), PredatesFeature, Nul |
Schema::row(format, raw) / row_with_settings(...) |
chs_row |
format code, counted bytes, settings | RowResult (verdict inside) |
PredatesFeature, BadDocument, Nul |
Schema::rows(format, body, settings) |
chs_rows |
one request body = one clock instant | BatchResult (verdict inside) |
PredatesFeature, BadDocument, Nul |
Schema::compile_filter(expr, params) |
chs_filter_compile |
one boolean expression; params binds {name:Type} positionally (NO_PARAMS for none — values are STRINGS, never hand-escaped) |
Filter<'_> (borrows the schema — the free order is a compile-time fact) |
Schema (47 unknown identifier, 456 unbound param, 457 unparseable param value), Unsupported (clock reads), PredatesFeature, Nul |
Filter::rows(format, body, settings) |
chs_filter_rows |
same contract as Schema::rows |
FilterResult — per-row Verdicts; Error/Decline are NOT answers, fail closed |
PredatesFeature, BadDocument, Nul |
Schema::parse_block(format, body, settings) |
chs_block_parse |
parse a body ONCE; per-row parse failures live IN the block | Block<'_> (borrows the schema), evaluable by K filters with no re-parse |
Schema (115 unknown setting, framing, decode fault — no block, no partial answers), Unsupported, PredatesFeature, Nul |
Filter::eval(&block) |
chs_filter_eval |
an already-parsed Block |
the same FilterResult rows answers — eval(parse_block(body)) ≡ rows(body); a cross-schema pair answers Rejected/1002 |
CrossLibrary (a pair from two libraries), BadDocument, PredatesFeature |
Filter / Block drop |
chs_filter_free / chs_block_free |
— | — (the borrow makes drop-before-schema a compile-time fact) | — |
Schema drop |
chs_schema_free |
— | — | — |
parse_version_result / parse_changed_settings_result / parse_columns_result |
derived (parsers) | JSONEachRow bytes from the three queries | version / Vec<(name, value)> / Vec<DiscoveredColumn> |
Discovery |
reconstruct_ddl(&cols) |
derived | discovered columns | column-declaration list for compile |
Discovery |
RowResult / BatchResult / Value / Transform (+ lossy()) / Substitution / Computed / Outcome |
derived from the result document | — | see rustdoc; BatchResult::engine_rows_bytes() is the byte-exact stored truth |
— |
RawText |
— | — | byte-exact stored rendering; as_str() is fallible, to_lossy() explicit |
— |
Constants: Format (codes 0–9), CompileMode::Declared=0, CODE_UNSUPPORTED=−2, ABI_REVISION (4), DEFAULT_TIMEZONE, NO_SETTINGS, NO_PARAMS, REGISTRY_ENV, AUTOFETCH_ENV, SYSTEM_ARTIFACT_ROOTS, CODE_ARTIFACT_* / CODE_SOURCE_UNREACHABLE, FETCH_COMMAND, SETTING_* (the chtypes keys), reason::* (24 stable spellings) |
— | — | — | — |
Filters, query parameters and the block twin (ABI revisions 3–4)
Schema::compile_filter(expr, NO_PARAMS) compiles one boolean expression
against the schema's physical columns and Filter::rows(...) answers
per-row verdicts with WHERE-side semantics (x = 256 over UInt8
promotes — false for every row — it never wraps), computed by ClickHouse's
own comparison functions. Four verdicts: Verdict::True/False are
answers; Verdict::Error (the predicate threw — a real server fails the
whole query) and Verdict::Decline (this library declines) are NOT, and an
enforcing caller MUST fail closed on both. Enforcement gate: no
read-side security may be enforced on this surface until the WHERE-truth rig
gates green — until then it is shadow/replay only (spec/c-abi.md
§Filters).
Params (revision 4). The expression may contain {name:Type} query
parameters, bound positionally — compile_filter("t = {t:String}", &[("t", "acme")]), the crate's slice convention (NO_PARAMS for none) —
values are strings, exactly as the server's own parameter channels carry
them. Substitution is the server's own ReplaceQueryParameterVisitor: each
value is deserialized by the DECLARED type's own reader and injected as a
typed literal AFTER SQL parsing, so a value is never SQL text and must
never be hand-escaped into the expression — injection safety is by
construction, and a hostile value (' OR 1=1 --) compares as exactly that
literal. An UNBOUND parameter is the server's own 456 ("Substitution
name is not set"), an unparseable value the server's own 457 — both
Error::Schema, verbatim; a bound name the expression never uses is
ignored. The compiled handle bakes the values in — identity is per
(schema, expr, params) — so a caller compiling filters from
tenant-influenced values MUST bound its cache and its compile rate: a
bounded LRU keyed on (schema generation, expr, params-hash) plus a
per-principal compile throttle; an unbounded cache is a memory DoS and an
unmetered compile path is a CPU DoS (spec/c-abi.md §Filters, normative).
Choose the brace type for the value's domain. A bound value is
deserialized by the brace type's OWN reader (deserializeTextEscaped),
which wraps an out-of-domain integer — {p:UInt8} given "256" binds
0 and matches every genuine zero (measured, uniform 24.8–26.7,
server-matched) — while the same constant written as a literal in the
expression PROMOTES (x = 256 over UInt8 is simply never true). A
too-narrow parameter type therefore silently matches the wrong rows: size
the type for the tenant-supplied domain ({p:UInt64}, {p:String}) or
validate the value before binding it. Malformed integer spellings refuse
loudly with the server's own 457 ("-1", "+7", "007" as UInt8);
an empty string refuses with 32. If one name is bound twice, the LAST
binding wins — the server's own insert_or_assign rule (reachable here:
params is a slice of pairs, and the last pair with a name wins).
One naming trap, the transport's rather than this library's: on a real
server's TCP channel a parameter named limit or offset fails at
the protocol layer with code 26 even when unused (params ride in a Settings
block there); HTTP is fine, and this library matches the HTTP/substitution
semantics. Avoid those two names for anything that will ever cross TCP.
The block twin (revision 4). Schema::parse_block(format, body, settings) parses a body ONCE into a Block<'_> — the parse half of
Filter::rows — and Filter::eval(&block) answers the same FilterResult
with no re-parse: the live-SSE hot path is K filters × 1 event, and the
re-parse is shed. Normative equivalence: eval(&parse_block(body)?) ≡
rows(body) for every verdict class (volatile DEFAULTs resolve against the
PARSE call's clock instant — pin chtypes_now_epoch_nanos for cross-call
identity). Evaluation is a pure function of (filter, block), takes no
settings, and never consumes or mutates the block. Filter and block MUST
come from the SAME schema: a mismatched same-library pair answers
FilterOutcome::Rejected/1002 loudly, and a cross-library pair is
Error::CrossLibrary before any C call. A Block borrows its Schema
exactly as a Filter does — the wrong free order does not compile.
Parse-once does not open enforcement earlier: the twin is under the same
enforcement gate.
Settings and precedence
The row path resolves three settings channels in one order — later wins:
per-call map > handle compile profile > library defaults (set_default_settings) > ClickHouse defaults
One documented exception: a type gate named in the compile profile binds at compile — where a real server binds it, at CREATE — and then outranks the per-call map for that handle's life (a profile declaring it at a refusing value fails the compile with the server's own 455/44). Gates the profile does not name keep the per-row re-check.
Rules that are enforced, not advisory:
- Values are strings at the boundary. The crate's
&[(k, v)]shape makes this structural:chtypes_now_epoch_nanosis a 19-digit nanosecond epoch, and a caller that routes it through a float sends1.7e+18— the setting is silently ignored. Never stringify through a float. - An unknown setting name rejects the whole call with the server's own
code 115 (did-you-mean hint included), on every channel;
set_default_settingsrefuses its payload wholesale, nothing committed. - The six
chtypes_*keys are the entire reserved namespace — three per-call (SETTING_NOW_EPOCH_NANOS,SETTING_CLOCK_OFFSET_NANOS,SETTING_MAX_CLOCK_SKEW_NANOS) and three per-process (SETTING_DEFAULT_EVAL_MEMORY_BYTES,SETTING_DEFAULT_EVAL_WALL_NANOS, pluschtypes_custom_settings_prefixes). A per-process key handed torows()comes back onunsupported_settings— a decline, and the row is promoted toOutcome::Unsupported, never scored as agreement. NO_SETTINGSis the spelled-out empty map for the common call.
Discovery
This crate never connects to ClickHouse — the app runs three canonical queries at connect time with whatever client it already has, and declares the answers back:
| Constant | Feeds |
|---|---|
QUERY_SERVER_VERSION |
Registry::for_version |
QUERY_CHANGED_SETTINGS |
the compile profile AND per-call settings |
QUERY_TABLE_COLUMNS |
parse_columns_result → reconstruct_ddl → compile |
// once per connection/tenant; cache the profile per deployment
let version = parse_version_result?;
let settings = parse_changed_settings_result?;
let lib = registry.for_version?;
let cols = parse_columns_result?;
let schema = lib.compile
.settings
.compile?; // a typo'd setting = the server's own 115, here
let batch = schema.rows?; // same profile per call
Never ask the customer for their settings — ask their server. The parsers keep
integer fields exact through both quoted and bare spellings (never through a
float), and reconstruct_ddl carries default_kind/default_expression,
without which DEFAULT/MATERIALIZED/EPHEMERAL semantics are silently lost.
system.columns reports the table as stored, so reconstruction is
shape-faithful exactly when the discovered profile is declared at compile.
Multi-version use and thread-safety
- Every artifact loads with
RTLD_NOW | RTLD_LOCAL— that is the whole mechanism letting several ClickHouse builds coexist in one process. Cost, measured: ~120 MB resident per loaded version. for_versionresolves a minor line ("25.8") or an exact patch ("25.8.28.1-lts"); a drifted patch resolves to its minor line. Failure names what is loaded — there is deliberately no nearest-version fallback, because version behaviour is not monotonic (25.10 rejects a DEFAULT that 25.8 and 26.6 both accept).- One mutex per loaded image serialises every call into a
Library— includingset_default_settings, which the ABI requires be excluded against everything else on the same image.LibraryandRegistryareSend + Sync; share them freely. SchemaisSendand deliberately notSync: one native handle must not be used from two threads at once, and the type system enforces it. Parallelism comes from more schemas, not shared ones.Registry::shutdown()/Library::shutdown()join the DEFAULT evaluator's threads.chs_initregisters the same viaatexit, so an ordinary process needs no call; it is required beforedlclose— which this crate never performs itself (the handle is intentionally leaked to the process).
Known limitations
- The error model is normative —
spec/c-abi.md§Error model. A decline (Error::Unsupported/Outcome::Unsupported, code −2) is neither an acceptance nor a rejection; both over-accepts and over-rejects are budgeted at zero. - EPHEMERAL columns: an INSERT whose explicit column list names an
EPHEMERAL column cannot be previewed by
row/rows(a format stream carries no column list). Detect viaSchema::columns()'sDefaultKind::Ephemeraland decline that intersection —spec/bindings.md§EPHEMERAL. - macOS is a dev floor, not an oracle: its
long doubleis 53-bit, so float parses diverge from a real server (Linux matches 395/395 of the float corpus; macOS 0/395). Float expectations must come from a Linux artifact or a live server. - Declined by design: unmodelled engines and sorting keys,
WHERE/GROUP BYTTLs,TO DISK/VOLUMEmoves,RECOMPRESS, clock-reading TTL expressions, server/session-property DEFAULTs (hostName(),currentUser()), blocking DEFAULTs (sleep), DEFAULTs past the admission budgets, MergeTree settings declared at non-default values. Each isError::Unsupported— fall back to the server. Format::Native/Buffers/RowBinary*support depends on the loaded artifact's age, not this crate's version — probe the artifact (spec/bindings.md§Values a binding must accept and reject).
More
cargo run --example demo— the product in one screen (overflow, pinnednow(), TTL, reject, decline).playground/rust/— a nine-section runnable tour; the go/, python/ and ts/ tours beside it print the same nine sections.cargo test— integration tests and the golden set skip without a registry;CHTYPES_REGISTRY=/path/to/registry cargo testruns them. The fetch suite (tests/fetch.rs) needs onlyspec/fixtures/fetch/and runs offline.cargo build --no-default-features— the loader without thefetchfeature: no binary, noensure, and none of the fetch dependencies (ed25519-dalek,sha2,ureq,base64,tar,flate2).