Skip to main content

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}