use std::path::PathBuf;
pub const CODE_UNSUPPORTED: i32 = -2;
pub const ABI_REVISION: i32 = 4;
pub const CODE_ARTIFACT_MISSING: &str = "CHTYPES_ARTIFACT_MISSING";
pub const CODE_ARTIFACT_UNTRUSTED: &str = "CHTYPES_ARTIFACT_UNTRUSTED";
pub const CODE_ARTIFACT_CORRUPT: &str = "CHTYPES_ARTIFACT_CORRUPT";
pub const CODE_ARTIFACT_PINNED: &str = "CHTYPES_ARTIFACT_PINNED";
pub const CODE_ARTIFACT_UNPUBLISHED: &str = "CHTYPES_ARTIFACT_UNPUBLISHED";
pub const CODE_SOURCE_UNREACHABLE: &str = "CHTYPES_SOURCE_UNREACHABLE";
pub const FETCH_COMMAND: &str = "cargo install chtypes && chtypes fetch";
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("chtypes: registry {dir}: {source}")]
Registry {
dir: PathBuf,
#[source]
source: std::io::Error,
},
#[error("chtypes: dlopen {path}: {message}")]
Load {
path: PathBuf,
message: String,
},
#[error("chtypes: {path} does not export the chtypes C API (missing {symbol})")]
NotAnArtifact {
path: PathBuf,
symbol: &'static str,
},
#[error("chtypes: {path}: manifest says {expected} bytes, file is {actual}")]
CorruptArtifact {
path: PathBuf,
expected: u64,
actual: u64,
},
#[error("chtypes: {path}: library reports ClickHouse {reported}, manifest says {manifest}")]
VersionMismatch {
path: PathBuf,
reported: String,
manifest: String,
},
#[error("chtypes: chs_init failed for {path}: rc={rc}: {message}")]
Init {
path: PathBuf,
rc: i32,
message: String,
},
#[error(
"chtypes: {path} is already initialized with timezone {have:?}; \
cannot re-initialize with {want:?} (one image per path — \
chs_init runs at most once)"
)]
InitConflict {
path: PathBuf,
have: String,
want: String,
},
#[error("chtypes: no version artifacts under {dir}")]
EmptyRegistry {
dir: PathBuf,
},
#[error("chtypes: no vendored build for ClickHouse {requested} (have {loaded})")]
NoSuchVersion {
requested: String,
loaded: String,
},
#[error("chtypes: ${var} is not set")]
NoRegistryEnv {
var: &'static str,
},
#[error("{}", schema_display(*code, message, column.as_deref()))]
Schema {
code: i32,
message: String,
column: Option<String>,
},
#[error("chtypes: [-2] {message}")]
Unsupported {
message: String,
},
#[error("chtypes: [-2] this artifact predates {feature} (rebuild it)")]
PredatesFeature {
feature: &'static str,
},
#[error("chtypes: bad result document at byte {offset}: {message}")]
BadDocument {
message: String,
offset: usize,
},
#[error("chtypes: interior NUL byte in {what}")]
Nul {
what: &'static str,
},
#[error("chtypes: {message}")]
Discovery {
message: String,
},
#[error(
"chtypes: filter (ClickHouse {filter_version}) and block (ClickHouse {block_version}) \
come from different libraries"
)]
CrossLibrary {
filter_version: String,
block_version: String,
},
#[error("{}", artifact_missing_display(line, platform, looked_in))]
ArtifactMissing {
line: String,
platform: String,
looked_in: Vec<PathBuf>,
},
#[error("chtypes: {origin}: SHA256SUMS is not trusted: {reason}")]
ArtifactUntrusted {
origin: String,
reason: String,
},
#[error("chtypes: {subject}: sha256 is {actual}, expected {expected}")]
ArtifactCorrupt {
subject: String,
expected: String,
actual: String,
},
#[error("chtypes: {key}: {message}")]
ArtifactPinned {
key: String,
message: String,
},
#[error(
"chtypes: {origin} publishes no artifact for ClickHouse {requested} on {platform} (it has: {offered})"
)]
ArtifactUnpublished {
requested: String,
platform: String,
origin: String,
offered: String,
},
#[error("chtypes: {origin}: {message}")]
SourceUnreachable {
origin: String,
message: String,
},
#[error("chtypes: fetch: {message}")]
Fetch {
message: String,
},
}
impl Error {
pub fn code(&self) -> Option<i32> {
match self {
Error::Schema { code, .. } => Some(*code),
Error::Unsupported { .. } | Error::PredatesFeature { .. } => Some(CODE_UNSUPPORTED),
_ => None,
}
}
pub fn is_unsupported(&self) -> bool {
self.code() == Some(CODE_UNSUPPORTED)
}
pub fn artifact_code(&self) -> Option<&'static str> {
match self {
Error::ArtifactMissing { .. } => Some(CODE_ARTIFACT_MISSING),
Error::ArtifactUntrusted { .. } => Some(CODE_ARTIFACT_UNTRUSTED),
Error::ArtifactCorrupt { .. } => Some(CODE_ARTIFACT_CORRUPT),
Error::ArtifactPinned { .. } => Some(CODE_ARTIFACT_PINNED),
Error::ArtifactUnpublished { .. } => Some(CODE_ARTIFACT_UNPUBLISHED),
Error::SourceUnreachable { .. } => Some(CODE_SOURCE_UNREACHABLE),
_ => None,
}
}
pub(crate) fn from_code(code: i32, message: String) -> Error {
if code < 0 {
Error::Unsupported { message }
} else {
Error::Schema {
code,
message,
column: None,
}
}
}
}
fn schema_display(code: i32, message: &str, column: Option<&str>) -> String {
match column {
Some(c) => format!("chtypes: column {c:?}: [{code}] {message}"),
None => format!("chtypes: [{code}] {message}"),
}
}
fn artifact_missing_display(line: &str, platform: &str, looked_in: &[PathBuf]) -> String {
let dirs: Vec<String> = looked_in.iter().map(|d| d.display().to_string()).collect();
format!(
"chtypes: no artifact for ClickHouse {line} ({platform}). Looked in: {}.\n\
Install it: {FETCH_COMMAND} {line}\n\
or set CHTYPES_AUTOFETCH=1 to fetch on first use.",
dirs.join(", ")
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_code_keys_on_the_sign() {
assert!(matches!(
Error::from_code(115, "bad name".into()),
Error::Schema { code: 115, .. }
));
assert!(matches!(
Error::from_code(50, "unknown family".into()),
Error::Schema { code: 50, .. }
));
assert!(matches!(
Error::from_code(-2, "declined".into()),
Error::Unsupported { .. }
));
assert!(matches!(
Error::from_code(-1, "guarded exception".into()),
Error::Unsupported { .. }
));
assert!(matches!(
Error::from_code(-3, "future sentinel".into()),
Error::Unsupported { .. }
));
}
#[test]
fn the_rendered_sentinel_shape_is_frozen() {
let decline = Error::Unsupported {
message: "engine not modelled".into(),
};
assert_eq!(decline.to_string(), "chtypes: [-2] engine not modelled");
let predates = Error::PredatesFeature {
feature: "chs_schema_engine",
};
assert_eq!(
predates.to_string(),
"chtypes: [-2] this artifact predates chs_schema_engine (rebuild it)"
);
let refusal = Error::Schema {
code: 115,
message: "bad name".into(),
column: None,
};
assert_eq!(refusal.to_string(), "chtypes: [115] bad name");
let attributed = Error::Schema {
code: 469,
message: "constraint".into(),
column: Some("e".into()),
};
assert_eq!(
attributed.to_string(),
"chtypes: column \"e\": [469] constraint"
);
}
#[test]
fn the_artifact_missing_message_is_the_spec_s_verbatim() {
let err = Error::ArtifactMissing {
line: "25.8".into(),
platform: "linux-arm64".into(),
looked_in: vec![
PathBuf::from("/home/u/.cache/chtypes/artifacts/linux-arm64"),
PathBuf::from("/usr/local/share/chtypes/artifacts/linux-arm64"),
PathBuf::from("/opt/chtypes/artifacts/linux-arm64"),
],
};
assert_eq!(
err.to_string(),
"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.\n\
Install it: cargo install chtypes && chtypes fetch 25.8\n\
or set CHTYPES_AUTOFETCH=1 to fetch on first use."
);
assert_eq!(err.artifact_code(), Some("CHTYPES_ARTIFACT_MISSING"));
assert_eq!(err.code(), None);
}
#[test]
fn every_artifact_code_is_the_shared_spelling() {
let cases: Vec<(Error, &str)> = vec![
(
Error::ArtifactUntrusted {
origin: "s".into(),
reason: "r".into(),
},
"CHTYPES_ARTIFACT_UNTRUSTED",
),
(
Error::ArtifactCorrupt {
subject: "x".into(),
expected: "aa".into(),
actual: "bb".into(),
},
"CHTYPES_ARTIFACT_CORRUPT",
),
(
Error::ArtifactPinned {
key: "linux-arm64/25.8".into(),
message: "m".into(),
},
"CHTYPES_ARTIFACT_PINNED",
),
(
Error::ArtifactUnpublished {
requested: "25.8".into(),
platform: "linux-arm64".into(),
origin: "s".into(),
offered: "nothing".into(),
},
"CHTYPES_ARTIFACT_UNPUBLISHED",
),
(
Error::SourceUnreachable {
origin: "s".into(),
message: "offline".into(),
},
"CHTYPES_SOURCE_UNREACHABLE",
),
];
for (err, code) in cases {
assert_eq!(err.artifact_code(), Some(code), "{err}");
assert_eq!(err.code(), None, "{err}");
}
let plain = Error::Fetch {
message: "m".into(),
};
assert_eq!(plain.artifact_code(), None);
assert_eq!(
Error::Unsupported {
message: "m".into()
}
.artifact_code(),
None
);
}
}