chtypes/error.rs
1//! Errors, and the one sentinel that must never be confused with a rejection.
2
3use std::path::PathBuf;
4
5/// `CHS_CODE_UNSUPPORTED` from `include/chtypes.h`: "this build refuses to
6/// answer", and never a real ClickHouse error code.
7///
8/// Mapping it onto a rejection manufactures an over-reject the product never
9/// made; mapping it onto an acceptance manufactures an over-accept, which is
10/// the cardinal sin. It is exposed here so a caller can branch on it.
11pub const CODE_UNSUPPORTED: i32 = -2;
12
13/// The `chs_*` ABI revision this crate was written against — `CHS_ABI_REVISION`
14/// in `include/chtypes.h`.
15///
16/// This crate `dlopen`s artifacts rather than compiling against the header, so
17/// this is a hand-kept mirror and MUST be bumped in the same cycle the header
18/// is. [`crate::Library`] refuses to load an artifact reporting a different
19/// nonzero revision; 0 means the artifact predates the probe, which is
20/// ignorance rather than incompatibility (`spec/artifact.md` §Loading).
21///
22/// Revision 3 (2026-08-31): `chs_rows` gained `export_format` / `doc_flags` /
23/// `out_bytes`, and the `chs_filter_compile` / `chs_filter_free` /
24/// `chs_filter_rows` trio joined the surface.
25///
26/// Revision 4 (2026-08-31, the filter phase-2 cycle): `chs_filter_compile`
27/// gained `params_json` (`{name:Type}` query parameters), and the block twin
28/// joined — `chs_block_parse` / `chs_block_free` / `chs_filter_eval`. This
29/// crate therefore speaks 4 and refuses revision-3 artifacts: calling the
30/// 5-argument `chs_filter_compile` against the 4-argument revision-3 artifact
31/// is undefined behaviour, which is exactly what this gate exists to refuse.
32pub const ABI_REVISION: i32 = 4;
33
34/// `CHTYPES_ARTIFACT_MISSING` — no installed artifact answers for the line
35/// (`docs/fetch.md` §7). The code every SDK shares for [`Error::ArtifactMissing`].
36pub const CODE_ARTIFACT_MISSING: &str = "CHTYPES_ARTIFACT_MISSING";
37/// `CHTYPES_ARTIFACT_UNTRUSTED` — the release's `SHA256SUMS` is unsigned or
38/// mis-signed (§3 step 0); nothing was downloaded around it.
39pub const CODE_ARTIFACT_UNTRUSTED: &str = "CHTYPES_ARTIFACT_UNTRUSTED";
40/// `CHTYPES_ARTIFACT_CORRUPT` — any hash mismatch anywhere in the chain (§3).
41pub const CODE_ARTIFACT_CORRUPT: &str = "CHTYPES_ARTIFACT_CORRUPT";
42/// `CHTYPES_ARTIFACT_PINNED` — the release offers something other than what
43/// the lock file pins (§5).
44pub const CODE_ARTIFACT_PINNED: &str = "CHTYPES_ARTIFACT_PINNED";
45/// `CHTYPES_ARTIFACT_UNPUBLISHED` — the release publishes nothing for the
46/// requested line or exact patch on this platform (§2).
47pub const CODE_ARTIFACT_UNPUBLISHED: &str = "CHTYPES_ARTIFACT_UNPUBLISHED";
48/// `CHTYPES_SOURCE_UNREACHABLE` — the source could not be reached, or was not
49/// consulted because the fetch was offline.
50pub const CODE_SOURCE_UNREACHABLE: &str = "CHTYPES_SOURCE_UNREACHABLE";
51
52/// This SDK's fetch command, as the "Install it:" line of
53/// [`Error::ArtifactMissing`] spells it (`docs/fetch.md` §6: the crate's
54/// `[[bin]]`, reached through `cargo install chtypes`).
55pub const FETCH_COMMAND: &str = "cargo install chtypes && chtypes fetch";
56
57/// `Result` with this crate's [`Error`].
58pub type Result<T> = std::result::Result<T, Error>;
59
60/// Everything that can go wrong loading an artifact or asking it a question.
61///
62/// Three different answers travel through this one type, and a caller must
63/// keep them apart (`spec/c-abi.md` §Error model):
64///
65/// * **A rejection** — [`Error::Schema`]: ClickHouse itself refused, with its
66/// own code and message. The DDL or profile can never exist on that server
67/// and the tenant has to be told.
68/// * **A decline** — [`Error::Unsupported`] / [`Error::PredatesFeature`]:
69/// this build refuses to answer ([`CODE_UNSUPPORTED`]). A real server might
70/// well have accepted the input, so the caller must fall back to the server
71/// (validate cautiously, forward unpreviewed) rather than report a tenant
72/// error. Mapping a decline onto a rejection manufactures an over-reject;
73/// both over-accepts and over-rejects are budgeted at zero.
74/// * **Everything else** is the machinery: loading, parsing, argument
75/// marshalling. No ClickHouse verdict was reached at all
76/// ([`Error::code`] answers `None`).
77///
78/// Note what is *not* an error: a row the server would reject comes back as
79/// `Ok` with [`crate::Outcome::Rejected`] in the result — the `Result` is
80/// about whether the question could be asked, and the verdict lives in the
81/// answer.
82#[derive(Debug, thiserror::Error)]
83#[non_exhaustive]
84pub enum Error {
85 /// The registry directory could not be read.
86 #[error("chtypes: registry {dir}: {source}")]
87 Registry {
88 /// The directory that could not be read.
89 dir: PathBuf,
90 /// The underlying I/O failure.
91 #[source]
92 source: std::io::Error,
93 },
94
95 /// A directory carried a `manifest.json` and the library still would not
96 /// `dlopen`. That is broken, not absent, so it aborts the scan.
97 #[error("chtypes: dlopen {path}: {message}")]
98 Load {
99 /// The shared library that would not load.
100 path: PathBuf,
101 /// `dlerror()`'s text.
102 message: String,
103 },
104
105 /// The library loaded but does not export the four mandatory `chs_*`
106 /// symbols, so it is not a chtypes artifact.
107 #[error("chtypes: {path} does not export the chtypes C API (missing {symbol})")]
108 NotAnArtifact {
109 /// The library that loaded but is not a chtypes artifact.
110 path: PathBuf,
111 /// The first mandatory symbol found missing.
112 symbol: &'static str,
113 },
114
115 /// `manifest.library_bytes` disagrees with the file on disk. A move that
116 /// reported success and truncated a 232 MB library looks identical to one
117 /// that worked, which is exactly why this is checked.
118 #[error("chtypes: {path}: manifest says {expected} bytes, file is {actual}")]
119 CorruptArtifact {
120 /// The library whose size disagrees with its manifest.
121 path: PathBuf,
122 /// `manifest.library_bytes`.
123 expected: u64,
124 /// The size on disk.
125 actual: u64,
126 },
127
128 /// `chs_clickhouse_version()` disagrees with `manifest.clickhouse_version`:
129 /// the right bytes in the wrong directory, the one corruption a checksum
130 /// cannot catch.
131 #[error("chtypes: {path}: library reports ClickHouse {reported}, manifest says {manifest}")]
132 VersionMismatch {
133 /// The library that disagrees with its manifest.
134 path: PathBuf,
135 /// What `chs_clickhouse_version()` said — the authority.
136 reported: String,
137 /// What `manifest.clickhouse_version` claimed.
138 manifest: String,
139 },
140
141 /// `chs_init` returned nonzero. The one reachable failure is an unknown
142 /// `timezone`, and `message` is ClickHouse's own text saying so — a bare
143 /// `rc` cannot say which name was rejected.
144 #[error("chtypes: chs_init failed for {path}: rc={rc}: {message}")]
145 Init {
146 /// The library whose initialisation failed.
147 path: PathBuf,
148 /// `chs_init`'s nonzero return.
149 rc: i32,
150 /// ClickHouse's own message, from `chs_init`'s `out_err`.
151 message: String,
152 },
153
154 /// The same artifact image is already initialized with a different
155 /// configuration. `dlopen` refcounts one image per path, so `chs_init`
156 /// runs at most once per artifact — a second load asking for a different
157 /// timezone cannot be honoured and must not silently re-timezone the
158 /// first load's live libraries.
159 #[error(
160 "chtypes: {path} is already initialized with timezone {have:?}; \
161 cannot re-initialize with {want:?} (one image per path — \
162 chs_init runs at most once)"
163 )]
164 InitConflict {
165 /// The artifact whose image is already initialized.
166 path: PathBuf,
167 /// The timezone the image was initialized with.
168 have: String,
169 /// The conflicting timezone this load requested.
170 want: String,
171 },
172
173 /// The directory exists and holds no loadable artifact. An empty registry is
174 /// a configuration mistake, not an empty result.
175 #[error("chtypes: no version artifacts under {dir}")]
176 EmptyRegistry {
177 /// The directory that held no loadable artifact.
178 dir: PathBuf,
179 },
180
181 /// No artifact answers for the requested version. Naming what *is* loaded is
182 /// part of the contract: answering 26.7 semantics from a 25.8 artifact would
183 /// be a lie, so there is deliberately no nearest-match fallback.
184 #[error("chtypes: no vendored build for ClickHouse {requested} (have {loaded})")]
185 NoSuchVersion {
186 /// The version that was asked for.
187 requested: String,
188 /// The minor lines that *are* loaded.
189 loaded: String,
190 },
191
192 /// The environment variable naming a registry is unset.
193 #[error("chtypes: ${var} is not set")]
194 NoRegistryEnv {
195 /// The variable that is unset.
196 var: &'static str,
197 },
198
199 /// ClickHouse itself rejected the schema, with its own error code.
200 ///
201 /// `code` is ALWAYS a real ClickHouse error code: a decline is a
202 /// DIFFERENT variant ([`Error::Unsupported`] / [`Error::PredatesFeature`]),
203 /// never this one carrying a negative sentinel (spec/bindings.md rule 12).
204 #[error("{}", schema_display(*code, message, column.as_deref()))]
205 Schema {
206 /// ClickHouse's own error code.
207 code: i32,
208 /// ClickHouse's own message.
209 message: String,
210 /// The offending column, when the failure is attributable to one.
211 /// Never guessed: populated only when the C layer's own structured
212 /// answer names one — which no schema-path entry point does today, so
213 /// the compile/engine/TTL/validate paths always carry `None` and a
214 /// message that names a column rides through verbatim in `message`.
215 /// The field stays for callers that KNOW a column (a gateway's
216 /// EPHEMERAL decline names columns it detected itself).
217 column: Option<String>,
218 },
219
220 /// This build refuses to answer: [`CODE_UNSUPPORTED`]. A real server might
221 /// well have accepted the input — this is not a rejection and MUST NOT be
222 /// reported as one.
223 ///
224 /// The rendered message keeps the frozen `[-2]` shape [`Error::Schema`]
225 /// renders its code with (spec/bindings.md rule 12): the conformance
226 /// drivers put this exact string on the protocol wire as an `unsupported`
227 /// scope, so the rendering is part of the contract even though the
228 /// sentinel is not a field. Whatever negative integer the binding saw
229 /// internally (`-1` a guarded exception, `-2`), the rendered code is
230 /// always the header's `CHS_CODE_UNSUPPORTED`.
231 #[error("chtypes: [-2] {message}")]
232 Unsupported {
233 /// Why this build declines, in its own words.
234 message: String,
235 },
236
237 /// The artifact does not export a symbol this call needs, i.e. it predates
238 /// the feature. Reported as unsupported at call time, never as a load
239 /// failure. Renders with the same frozen `[-2]` shape as
240 /// [`Error::Unsupported`] — a binding-internal missing-symbol sentinel
241 /// must never leak into the rendering (spec/bindings.md rule 12).
242 #[error("chtypes: [-2] this artifact predates {feature} (rebuild it)")]
243 PredatesFeature {
244 /// The symbol or capability the artifact does not export.
245 feature: &'static str,
246 },
247
248 /// The result document could not be parsed even after the bare-denormal
249 /// repair.
250 ///
251 /// This is a hard error on purpose: the previous behaviour — retrying the
252 /// parse through `String::from_utf8_lossy` — silently replaced a `String`
253 /// column's bytes with U+FFFD, which then read downstream as a coercion that
254 /// never happened. A document this crate cannot read exactly is reported,
255 /// never approximated.
256 #[error("chtypes: bad result document at byte {offset}: {message}")]
257 BadDocument {
258 /// What the reader expected, in its own words.
259 message: String,
260 /// The byte offset in the (denormal-repaired) document.
261 offset: usize,
262 },
263
264 /// A string argument contained an interior NUL, so it cannot cross the C
265 /// boundary.
266 #[error("chtypes: interior NUL byte in {what}")]
267 Nul {
268 /// Which argument carried the NUL.
269 what: &'static str,
270 },
271
272 /// A discovery-query result could not be parsed, or a discovered table
273 /// description could not be reconstructed into DDL (`crate::discover`).
274 /// Client-side and carries no ClickHouse code: the server never saw a
275 /// question it could reject.
276 #[error("chtypes: {message}")]
277 Discovery {
278 /// What went wrong, in the parser's own words.
279 message: String,
280 },
281
282 /// A [`crate::Filter`] and a [`crate::Block`] from two DIFFERENT loaded
283 /// libraries were paired in an eval — refused here, because no handle
284 /// ever crosses a `dlopen`'d image boundary. A pair from two schemas of
285 /// the SAME library is NOT this error: the C layer itself answers that
286 /// with a rejected result document, code 1002 (`spec/c-abi.md` §Blocks).
287 #[error(
288 "chtypes: filter (ClickHouse {filter_version}) and block (ClickHouse {block_version}) \
289 come from different libraries"
290 )]
291 CrossLibrary {
292 /// The filter's library, by its own reported version.
293 filter_version: String,
294 /// The block's library, by its own reported version.
295 block_version: String,
296 },
297
298 /// No installed artifact answers for the requested ClickHouse line on this
299 /// platform: the §1 search path was walked and none of its directories
300 /// holds `<line>/manifest.json` (`docs/fetch.md` §7). The message is the
301 /// one every SDK renders, verbatim apart from the bracketed parts, and
302 /// [`Error::artifact_code`] answers [`CODE_ARTIFACT_MISSING`].
303 ///
304 /// Raised by the search-path registry ([`crate::Registry::from_search_path`])
305 /// with autofetch off; a registry over one explicit directory keeps
306 /// answering [`Error::NoSuchVersion`], which names what IS loaded.
307 #[error("{}", artifact_missing_display(line, platform, looked_in))]
308 ArtifactMissing {
309 /// The minor line that was asked for (`25.8`).
310 line: String,
311 /// `<os>-<arch>`, the artifact spelling (`linux-arm64`).
312 platform: String,
313 /// Every directory that was tried, in search order.
314 looked_in: Vec<PathBuf>,
315 },
316
317 /// The release's `SHA256SUMS` did not verify (`docs/fetch.md` §3 step 0):
318 /// no `SHA256SUMS.sig`, a malformed one, or a signature under no trusted
319 /// key. Nothing was downloaded around it. Code [`CODE_ARTIFACT_UNTRUSTED`].
320 #[error("chtypes: {origin}: SHA256SUMS is not trusted: {reason}")]
321 ArtifactUntrusted {
322 /// The source the release was read from.
323 origin: String,
324 /// Why, in the verifier's own words.
325 reason: String,
326 },
327
328 /// A hash disagreed somewhere in the chain (`docs/fetch.md` §3): the index
329 /// and `SHA256SUMS`, the downloaded tarball, the library inside it, or the
330 /// installed library re-hashed in place. Reported, never repaired. Code
331 /// [`CODE_ARTIFACT_CORRUPT`].
332 #[error("chtypes: {subject}: sha256 is {actual}, expected {expected}")]
333 ArtifactCorrupt {
334 /// What was hashed, or which two records disagree.
335 subject: String,
336 /// The sha256 the chain said it should be.
337 expected: String,
338 /// The sha256 that was found.
339 actual: String,
340 },
341
342 /// The release offers something other than what the lock file pins for
343 /// this `<os>-<arch>/<minor>` (`docs/fetch.md` §5, `--frozen`). Code
344 /// [`CODE_ARTIFACT_PINNED`].
345 #[error("chtypes: {key}: {message}")]
346 ArtifactPinned {
347 /// The lock key, `<os>-<arch>/<minor>`.
348 key: String,
349 /// What was pinned and what was offered.
350 message: String,
351 },
352
353 /// The release publishes nothing for the requested line (or exact patch —
354 /// a hard requirement) on this platform (`docs/fetch.md` §2). Code
355 /// [`CODE_ARTIFACT_UNPUBLISHED`].
356 #[error(
357 "chtypes: {origin} publishes no artifact for ClickHouse {requested} on {platform} (it has: {offered})"
358 )]
359 ArtifactUnpublished {
360 /// The line or exact patch that was asked for.
361 requested: String,
362 /// `<os>-<arch>`.
363 platform: String,
364 /// The source that was consulted.
365 origin: String,
366 /// What the release does publish, for the message.
367 offered: String,
368 },
369
370 /// The source could not be reached — or was not consulted at all because
371 /// the fetch was offline. Code [`CODE_SOURCE_UNREACHABLE`].
372 #[error("chtypes: {origin}: {message}")]
373 SourceUnreachable {
374 /// The source that was (or would have been) contacted.
375 origin: String,
376 /// The transport's own words, or `offline`.
377 message: String,
378 },
379
380 /// Fetch machinery that reached no verdict: an unreadable release listing,
381 /// an unwritable install directory, an unusable option. No artifact code.
382 #[error("chtypes: fetch: {message}")]
383 Fetch {
384 /// What went wrong.
385 message: String,
386 },
387}
388
389impl Error {
390 /// The ClickHouse error code, or [`CODE_UNSUPPORTED`] for the two
391 /// unsupported shapes. `None` for loader-level failures, which have no code.
392 pub fn code(&self) -> Option<i32> {
393 match self {
394 Error::Schema { code, .. } => Some(*code),
395 Error::Unsupported { .. } | Error::PredatesFeature { .. } => Some(CODE_UNSUPPORTED),
396 _ => None,
397 }
398 }
399
400 /// Whether this is the "I decline to guess" sentinel rather than a
401 /// ClickHouse rejection.
402 pub fn is_unsupported(&self) -> bool {
403 self.code() == Some(CODE_UNSUPPORTED)
404 }
405
406 /// The shared artifact code (`docs/fetch.md` §7) — `CHTYPES_ARTIFACT_MISSING`,
407 /// `…_UNTRUSTED`, `…_CORRUPT`, `…_PINNED`, `…_UNPUBLISHED` or
408 /// `CHTYPES_SOURCE_UNREACHABLE` — for the fetch and lookup failures, `None`
409 /// for everything else. Distinct from [`Error::code`], which is the
410 /// ClickHouse error code of a rejection.
411 pub fn artifact_code(&self) -> Option<&'static str> {
412 match self {
413 Error::ArtifactMissing { .. } => Some(CODE_ARTIFACT_MISSING),
414 Error::ArtifactUntrusted { .. } => Some(CODE_ARTIFACT_UNTRUSTED),
415 Error::ArtifactCorrupt { .. } => Some(CODE_ARTIFACT_CORRUPT),
416 Error::ArtifactPinned { .. } => Some(CODE_ARTIFACT_PINNED),
417 Error::ArtifactUnpublished { .. } => Some(CODE_ARTIFACT_UNPUBLISHED),
418 Error::SourceUnreachable { .. } => Some(CODE_SOURCE_UNREACHABLE),
419 _ => None,
420 }
421 }
422
423 /// Build the right variant from a C code. The SIGN decides
424 /// (spec/bindings.md rule 12, spec/c-abi.md §Error model): a positive
425 /// code is the server's own refusal and rides through verbatim; ANY
426 /// negative code is this library declining — `-2` "I will not guess",
427 /// `-1` a guarded exception, and any sentinel a later era adds — and
428 /// becomes [`Error::Unsupported`]. Keying on the sign rather than on
429 /// `== CODE_UNSUPPORTED` means a negative sentinel can never become
430 /// "an `Error::Schema` with a negative code", which that variant's own
431 /// contract forbids.
432 pub(crate) fn from_code(code: i32, message: String) -> Error {
433 if code < 0 {
434 Error::Unsupported { message }
435 } else {
436 Error::Schema {
437 code,
438 message,
439 column: None,
440 }
441 }
442 }
443}
444
445/// The frozen rendering the refusal variant shares with its peers in every
446/// SDK: `chtypes: [<code>] <msg>`, with `chtypes: column "<c>": …` when a
447/// column is attributed (spec/bindings.md rule 12 — the shape the conformance
448/// drivers put on the wire).
449fn schema_display(code: i32, message: &str, column: Option<&str>) -> String {
450 match column {
451 Some(c) => format!("chtypes: column {c:?}: [{code}] {message}"),
452 None => format!("chtypes: [{code}] {message}"),
453 }
454}
455
456/// The §7 message, verbatim apart from the bracketed parts: the line, the
457/// platform, the directories that were looked in, and this SDK's own fetch
458/// command ([`FETCH_COMMAND`]).
459fn artifact_missing_display(line: &str, platform: &str, looked_in: &[PathBuf]) -> String {
460 let dirs: Vec<String> = looked_in.iter().map(|d| d.display().to_string()).collect();
461 format!(
462 "chtypes: no artifact for ClickHouse {line} ({platform}). Looked in: {}.\n\
463 Install it: {FETCH_COMMAND} {line}\n\
464 or set CHTYPES_AUTOFETCH=1 to fetch on first use.",
465 dirs.join(", ")
466 )
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn from_code_keys_on_the_sign() {
475 // A positive code is the server's own refusal, verbatim; ANY negative
476 // code is a decline — -2 "I will not guess", -1 a guarded exception,
477 // and any sentinel a later era adds. A negative Error::Schema must be
478 // unmakeable through the funnel (spec/bindings.md rule 12).
479 assert!(matches!(
480 Error::from_code(115, "bad name".into()),
481 Error::Schema { code: 115, .. }
482 ));
483 assert!(matches!(
484 Error::from_code(50, "unknown family".into()),
485 Error::Schema { code: 50, .. }
486 ));
487 assert!(matches!(
488 Error::from_code(-2, "declined".into()),
489 Error::Unsupported { .. }
490 ));
491 assert!(matches!(
492 Error::from_code(-1, "guarded exception".into()),
493 Error::Unsupported { .. }
494 ));
495 assert!(matches!(
496 Error::from_code(-3, "future sentinel".into()),
497 Error::Unsupported { .. }
498 ));
499 }
500
501 #[test]
502 fn the_rendered_sentinel_shape_is_frozen() {
503 // The decline renders the header's -2 in the same shape the refusal
504 // renders its code — the conformance drivers put this exact string on
505 // the protocol wire as an `unsupported` scope (spec/bindings.md rule
506 // 12). Internal sentinels never leak into the rendering.
507 let decline = Error::Unsupported {
508 message: "engine not modelled".into(),
509 };
510 assert_eq!(decline.to_string(), "chtypes: [-2] engine not modelled");
511 let predates = Error::PredatesFeature {
512 feature: "chs_schema_engine",
513 };
514 assert_eq!(
515 predates.to_string(),
516 "chtypes: [-2] this artifact predates chs_schema_engine (rebuild it)"
517 );
518 let refusal = Error::Schema {
519 code: 115,
520 message: "bad name".into(),
521 column: None,
522 };
523 assert_eq!(refusal.to_string(), "chtypes: [115] bad name");
524 // Column-attributed shape — for callers that KNOW one, never guessed.
525 let attributed = Error::Schema {
526 code: 469,
527 message: "constraint".into(),
528 column: Some("e".into()),
529 };
530 assert_eq!(
531 attributed.to_string(),
532 "chtypes: column \"e\": [469] constraint"
533 );
534 }
535
536 #[test]
537 fn the_artifact_missing_message_is_the_spec_s_verbatim() {
538 // docs/fetch.md §7: one message in every SDK, verbatim apart from the
539 // bracketed parts; the "Install it:" line names THIS SDK's command.
540 let err = Error::ArtifactMissing {
541 line: "25.8".into(),
542 platform: "linux-arm64".into(),
543 looked_in: vec![
544 PathBuf::from("/home/u/.cache/chtypes/artifacts/linux-arm64"),
545 PathBuf::from("/usr/local/share/chtypes/artifacts/linux-arm64"),
546 PathBuf::from("/opt/chtypes/artifacts/linux-arm64"),
547 ],
548 };
549 assert_eq!(
550 err.to_string(),
551 "chtypes: no artifact for ClickHouse 25.8 (linux-arm64). Looked in: \
552 /home/u/.cache/chtypes/artifacts/linux-arm64, \
553 /usr/local/share/chtypes/artifacts/linux-arm64, \
554 /opt/chtypes/artifacts/linux-arm64.\n\
555 Install it: cargo install chtypes && chtypes fetch 25.8\n\
556 or set CHTYPES_AUTOFETCH=1 to fetch on first use."
557 );
558 assert_eq!(err.artifact_code(), Some("CHTYPES_ARTIFACT_MISSING"));
559 // The ClickHouse-code accessor stays what it was: no verdict here.
560 assert_eq!(err.code(), None);
561 }
562
563 #[test]
564 fn every_artifact_code_is_the_shared_spelling() {
565 let cases: Vec<(Error, &str)> = vec![
566 (
567 Error::ArtifactUntrusted {
568 origin: "s".into(),
569 reason: "r".into(),
570 },
571 "CHTYPES_ARTIFACT_UNTRUSTED",
572 ),
573 (
574 Error::ArtifactCorrupt {
575 subject: "x".into(),
576 expected: "aa".into(),
577 actual: "bb".into(),
578 },
579 "CHTYPES_ARTIFACT_CORRUPT",
580 ),
581 (
582 Error::ArtifactPinned {
583 key: "linux-arm64/25.8".into(),
584 message: "m".into(),
585 },
586 "CHTYPES_ARTIFACT_PINNED",
587 ),
588 (
589 Error::ArtifactUnpublished {
590 requested: "25.8".into(),
591 platform: "linux-arm64".into(),
592 origin: "s".into(),
593 offered: "nothing".into(),
594 },
595 "CHTYPES_ARTIFACT_UNPUBLISHED",
596 ),
597 (
598 Error::SourceUnreachable {
599 origin: "s".into(),
600 message: "offline".into(),
601 },
602 "CHTYPES_SOURCE_UNREACHABLE",
603 ),
604 ];
605 for (err, code) in cases {
606 assert_eq!(err.artifact_code(), Some(code), "{err}");
607 assert_eq!(err.code(), None, "{err}");
608 }
609 let plain = Error::Fetch {
610 message: "m".into(),
611 };
612 assert_eq!(plain.artifact_code(), None);
613 assert_eq!(
614 Error::Unsupported {
615 message: "m".into()
616 }
617 .artifact_code(),
618 None
619 );
620 }
621}