use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::compile::{CompileMode, CompileRequest};
use crate::error::{Error, Result};
use crate::ffi::{Api, cstring};
use crate::schema::settings_json;
pub const DEFAULT_TIMEZONE: &str = "UTC";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DefaultKind {
None,
Default,
Materialized,
Alias,
Ephemeral,
Other(String),
}
impl DefaultKind {
fn parse(s: &str) -> DefaultKind {
match s {
"" => DefaultKind::None,
"DEFAULT" => DefaultKind::Default,
"MATERIALIZED" => DefaultKind::Materialized,
"ALIAS" => DefaultKind::Alias,
"EPHEMERAL" => DefaultKind::Ephemeral,
other => DefaultKind::Other(other.to_string()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Column {
pub name: String,
pub ty: String,
pub default_kind: DefaultKind,
pub default_expr: String,
pub default_is_literal: bool,
}
pub struct Library {
version: String,
minor: String,
path: PathBuf,
api: Api,
lock: Arc<Mutex<()>>,
}
fn image_lock(path: &Path) -> Arc<Mutex<()>> {
static LOCKS: Mutex<Option<std::collections::BTreeMap<PathBuf, Arc<Mutex<()>>>>> =
Mutex::new(None);
let key = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let mut guard = match LOCKS.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
Arc::clone(
guard
.get_or_insert_with(std::collections::BTreeMap::new)
.entry(key)
.or_insert_with(|| Arc::new(Mutex::new(()))),
)
}
impl Library {
pub fn load(path: impl AsRef<Path>, timezone: &str) -> Result<Library> {
let path = path.as_ref();
let api = Api::open(path)?;
let version = api.clickhouse_version();
let minor = minor_of(&version);
let families = std::fs::read_to_string(
path.parent()
.unwrap_or(Path::new("."))
.join("unsafe_families.txt"),
)
.map(|s| s.trim().to_string())
.unwrap_or_default();
static INITED: std::sync::Mutex<std::collections::BTreeMap<std::path::PathBuf, String>> =
std::sync::Mutex::new(std::collections::BTreeMap::new());
let key = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
{
let mut inited = INITED.lock().expect("init registry poisoned");
match inited.get(&key) {
Some(prev_tz) if prev_tz == timezone => {} Some(prev_tz) => {
return Err(Error::InitConflict {
path: path.to_path_buf(),
have: prev_tz.clone(),
want: timezone.to_string(),
});
}
None => {
let tz = cstring(timezone, "timezone")?;
let fams = cstring(&families, "unsafe_families")?;
let (rc, message) = api.init(&tz, &fams);
if rc != 0 {
return Err(Error::Init {
path: path.to_path_buf(),
rc,
message,
});
}
inited.insert(key, timezone.to_string());
}
}
}
Ok(Library {
version,
minor,
path: path.to_path_buf(),
api,
lock: image_lock(path),
})
}
pub fn version(&self) -> &str {
&self.version
}
pub fn abi_revision(&self) -> i32 {
self.api.abi_revision()
}
pub fn minor(&self) -> &str {
&self.minor
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn validate_type(&self, type_expr: &str) -> Result<String> {
let expr = cstring(type_expr, "type expression")?;
let _guard = self.lock();
self.api.validate_type(&expr)
}
pub fn compile(self: &Arc<Self>, columns_sql: &str) -> CompileRequest<'_> {
CompileRequest {
lib: self,
columns_sql: columns_sql.to_string(),
settings: Vec::new(),
mode: CompileMode::default(),
}
}
pub fn has_compile_settings(&self) -> bool {
self.api.has_compile_settings()
}
pub fn set_default_settings<K: AsRef<str>, V: AsRef<str>>(
&self,
settings: &[(K, V)],
) -> Result<()> {
let json = settings_json(settings)?;
let _guard = self.lock();
match self.api.set_default_settings(&json) {
None => Err(Error::PredatesFeature {
feature: "chs_set_default_settings",
}),
Some((0, _)) => Ok(()),
Some((rc, message)) => Err(Error::Schema {
code: rc,
message,
column: None,
}),
}
}
pub fn reference_type(&self, type_expr: &str) -> Result<Option<String>> {
let expr = cstring(type_expr, "type expression")?;
let _guard = self.lock();
self.api.reference_type(&expr)
}
pub fn registered_families(&self) -> Result<Vec<String>> {
let _guard = self.lock();
Ok(self
.api
.registered_families()?
.lines()
.filter(|l| !l.is_empty())
.map(str::to_string)
.collect())
}
pub fn function_flags(&self) -> Result<String> {
let _guard = self.lock();
self.api.function_flags()
}
pub fn shutdown(&self) {
let _guard = self.lock();
self.api.shutdown();
}
pub(crate) fn api(&self) -> &Api {
&self.api
}
pub(crate) fn lock(&self) -> std::sync::MutexGuard<'_, ()> {
match self.lock.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
}
}
impl std::fmt::Debug for Library {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Library")
.field("version", &self.version)
.field("minor", &self.minor)
.field("path", &self.path)
.finish()
}
}
pub(crate) fn columns_of(raw: Option<Vec<crate::ffi::RawColumn>>) -> Vec<Column> {
raw.unwrap_or_default()
.into_iter()
.map(|c| Column {
name: c.name,
ty: c.ty,
default_kind: DefaultKind::parse(&c.default_kind),
default_expr: c.default_expr,
default_is_literal: c.default_is_literal,
})
.collect()
}
pub(crate) fn minor_of(version: &str) -> String {
let mut it = version.splitn(3, '.');
match (it.next(), it.next()) {
(Some(a), Some(b)) => format!("{a}.{b}"),
_ => version.to_string(),
}
}
pub(crate) fn minor_order(minor: &str) -> (u64, u64) {
let mut it = minor.split('.');
let major = it.next().and_then(|s| s.parse().ok()).unwrap_or(0);
let line = it.next().and_then(|s| s.parse().ok()).unwrap_or(0);
(major, line)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minor_lines_come_from_the_reported_version() {
assert_eq!(minor_of("25.8.28.1-lts"), "25.8");
assert_eq!(minor_of("25.10.7.6-stable"), "25.10");
assert_eq!(minor_of("26.7"), "26.7");
assert_eq!(minor_of("head"), "head");
}
#[test]
fn minor_lines_order_numerically_not_lexically() {
let mut lines = vec!["25.8", "26.7", "25.10", "24.8", "25.3"];
lines.sort_by_key(|l| minor_order(l));
assert_eq!(lines, vec!["24.8", "25.3", "25.8", "25.10", "26.7"]);
}
#[test]
fn default_kinds_are_the_abi_spellings() {
assert_eq!(DefaultKind::parse(""), DefaultKind::None);
assert_eq!(DefaultKind::parse("DEFAULT"), DefaultKind::Default);
assert_eq!(
DefaultKind::parse("MATERIALIZED"),
DefaultKind::Materialized
);
assert_eq!(DefaultKind::parse("ALIAS"), DefaultKind::Alias);
assert_eq!(DefaultKind::parse("EPHEMERAL"), DefaultKind::Ephemeral);
assert_eq!(
DefaultKind::parse("FUTURE"),
DefaultKind::Other("FUTURE".into())
);
}
}