chtypes/lib.rs
1//! chtypes — ClickHouse's own type system, per ClickHouse version, from Rust.
2//!
3//! chtypes answers one question, exactly: **if this row were inserted into this
4//! ClickHouse table on this ClickHouse version, what would happen?** It answers
5//! it by running ClickHouse's own C++ machinery — `DataTypeFactory`,
6//! `ISerialization`, `ReadHelpers`, `evaluateMissingDefaults`, the TTL
7//! algorithms, `MergeTreeDataWriter::mergeBlock` — vendored per release and
8//! linked behind the frozen `chs_*` C ABI. Nothing here reimplements a coercion
9//! rule, which is why the answers are exact by construction.
10//!
11//! This crate is a peer SDK over that ABI, alongside Go, Python and TypeScript.
12//! The language-neutral contract is `spec/` in this repository; where this crate
13//! and `spec/` disagree, the spec wins and this is a bug.
14//!
15//! ```no_run
16//! use chtypes::{Format, Registry, NO_SETTINGS};
17//!
18//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
19//! let registry = Registry::from_env_or_default()?; // $CHTYPES_REGISTRY, else the per-user cache
20//! let lib = registry.for_version("25.8")?; // minor line or exact patch
21//! let schema = lib.compile("ts DateTime, seq UInt8").compile()?;
22//!
23//! let batch = schema.rows(
24//! Format::JsonEachRow,
25//! br#"{"ts":"2026-01-15 10:30:00","seq":256}"#,
26//! NO_SETTINGS,
27//! )?;
28//!
29//! println!("{} {:?}", batch.outcome, batch.rows[0].values);
30//! for t in &batch.transformed {
31//! // seq: 256 -> 0, overflow_wrap, lossy — and ClickHouse returned success.
32//! println!("row {} {}: {} -> {} ({})", t.row, t.column, t.input, t.stored, t.reason);
33//! }
34//! # Ok(()) }
35//! ```
36//!
37//! # What this crate will not do
38//!
39//! * **Never map [`Error::Unsupported`] onto a rejection or an acceptance.**
40//! `-2` ([`CODE_UNSUPPORTED`]) means "a real server might well have accepted
41//! this; I decline to guess". Mapping it to a rejection manufactures an
42//! over-reject; mapping it to an acceptance manufactures an over-accept, which
43//! is the cardinal sin — rows stream to subscribers and then the insert fails.
44//! * **Never infer one version's answer from another's.** Behaviour is not
45//! monotonic: 25.10 rejects a mixed-type DEFAULT that 24.8 through 25.8 and
46//! 26.6 onward all accept; `JSON` is rejected on 24.8 and accepted from 25.3.
47//! [`Registry::for_version`] fails, naming what is loaded, rather than
48//! answering from the nearest artifact.
49//! * **Never treat a per-row `accepted` as "stored".** A TTL-expired row is
50//! accepted per row and not stored per batch. [`BatchResult::transformed`]
51//! folds in the batch-level `storage_transforms`, and
52//! [`BatchResult::engine_rows`] — when present — is the stored truth, not
53//! [`BatchResult::rows`].
54//! * **Never route a value through a float.** Settings values cross as strings
55//! and stored values stay raw JSON text; `18446744073709551615` must not
56//! become `18446744073709552000`.
57//! * **Never decode a stored value into a language type before comparing it.**
58//! A ClickHouse `String` holds arbitrary bytes, so a stored rendering is
59//! [`RawText`] — bytes, with a *fallible* UTF-8 view — and never a `String`
60//! that silently carries U+FFFD where the value had bytes. See [`Value::text`].
61//!
62//! # Getting artifacts
63//!
64//! An artifact is one ClickHouse release compiled behind the C ABI — 166–302 MB
65//! each, hours of C++ compute. Fetch prebuilt, signed ones with the crate's own
66//! command (`cargo install chtypes` → `chtypes fetch 25.8`) or from Rust with
67//! [`ensure`] — the `docs/fetch.md` contract, behind the default-on `fetch`
68//! feature; `scripts/fetch.sh` is the reference implementation of the same
69//! chain. A local build lands in the same per-user cache
70//! (`~/.cache/chtypes/artifacts/<os>-<arch>`). [`Registry::from_search_path`]
71//! looks there, in `$CHTYPES_REGISTRY` and in the system locations, and names
72//! every place it looked when a line is missing ([`Error::ArtifactMissing`]);
73//! [`Registry::new`] loads one explicit directory.
74//!
75//! # Platform
76//!
77//! Unix only — the loader is `dlopen`. Linux is the shipping target; macOS is a
78//! development floor and **not** an oracle: its `long double` is 53-bit, so float
79//! parses diverge from a real server (the float corpus matches 395/395 on Linux
80//! and 0/395 on macOS). Any float expectation must come from a Linux artifact or
81//! a live server.
82
83#![deny(missing_docs)]
84#![warn(clippy::undocumented_unsafe_blocks)]
85
86#[cfg(not(unix))]
87compile_error!("chtypes loads artifacts with dlopen and supports unix targets only");
88
89mod compile;
90mod discover;
91mod doc;
92mod error;
93#[cfg(feature = "fetch")]
94pub mod fetch;
95mod ffi;
96mod json;
97mod library;
98mod raw;
99mod registry;
100mod result;
101mod schema;
102mod transform;
103
104pub use compile::{CompileMode, CompileRequest};
105pub use discover::{
106 DiscoveredColumn, QUERY_CHANGED_SETTINGS, QUERY_SERVER_VERSION, QUERY_TABLE_COLUMNS,
107 ServerProfile, parse_changed_settings_result, parse_columns_result, parse_version_result,
108 reconstruct_ddl,
109};
110pub use error::{
111 ABI_REVISION, CODE_ARTIFACT_CORRUPT, CODE_ARTIFACT_MISSING, CODE_ARTIFACT_PINNED,
112 CODE_ARTIFACT_UNPUBLISHED, CODE_ARTIFACT_UNTRUSTED, CODE_SOURCE_UNREACHABLE, CODE_UNSUPPORTED,
113 Error, FETCH_COMMAND, Result,
114};
115#[cfg(feature = "fetch")]
116pub use fetch::{Action, EnsureOptions, Installed, ensure};
117pub use library::{Column, DEFAULT_TIMEZONE, DefaultKind, Library};
118pub use raw::RawText;
119pub use registry::{
120 AUTOFETCH_ENV, Manifest, REGISTRY_ENV, Registry, RegistryOptions, SYSTEM_ARTIFACT_ROOTS,
121 cache_dir_for, default_registry_dir, host_platform, install_dir, install_dir_for,
122 installed_lines, locate, locate_in, registry_search_path, search_path_for,
123};
124pub use result::{
125 BatchResult, Computed, DocFlags, FilterOutcome, FilterResult, FilterRowError, Format, Outcome,
126 RowResult, Span, Substitution, Transform, Value, Verdict,
127};
128pub use schema::{
129 Block, Filter, NO_PARAMS, NO_SETTINGS, SETTING_CLOCK_OFFSET_NANOS,
130 SETTING_DEFAULT_EVAL_MEMORY_BYTES, SETTING_DEFAULT_EVAL_WALL_NANOS,
131 SETTING_MAX_CLOCK_SKEW_NANOS, SETTING_NOW_EPOCH_NANOS, Schema,
132};
133pub use transform::reason;
134
135#[cfg(test)]
136mod thread_contract {
137 use super::*;
138
139 /// `chtypes.h`: a single handle must not be used from two threads at once,
140 /// but the library is thread-safe for concurrent calls on distinct handles.
141 /// So [`Schema`] is `Send` and `!Sync`, and these assertions fail to compile
142 /// if that ever changes.
143 fn assert_send<T: Send>() {}
144 fn assert_send_sync<T: Send + Sync>() {}
145
146 trait AmbiguousIfSync<A> {
147 fn tag() {}
148 }
149 impl<T: ?Sized> AmbiguousIfSync<()> for T {}
150 impl<T: ?Sized + Sync> AmbiguousIfSync<u8> for T {}
151
152 #[test]
153 fn schema_is_send_and_not_sync() {
154 assert_send::<Schema>();
155 // Resolves only while Schema is NOT Sync: a second impl would make the
156 // call ambiguous and this would stop compiling.
157 let _ = <Schema as AmbiguousIfSync<_>>::tag;
158 }
159
160 #[test]
161 fn a_library_and_registry_are_shareable() {
162 // Every call takes the library's mutex, so sharing these is safe.
163 assert_send_sync::<Library>();
164 assert_send_sync::<Registry>();
165 assert_send_sync::<std::sync::Arc<Library>>();
166 }
167}