use std::sync::Arc;
use crate::config::ClickHouseConfig;
use crate::error::ClickHouseError;
use crate::metrics::{Metrics, Outcome};
#[derive(Clone)]
pub struct ClickHouse {
client: clickhouse::Client,
config: ClickHouseConfig,
metrics: Arc<Metrics>,
}
impl std::fmt::Debug for ClickHouse {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClickHouse")
.field("client", &"<clickhouse::Client>")
.field("config", &self.config)
.field("metrics", &self.metrics)
.finish()
}
}
impl ClickHouse {
#[must_use]
pub fn new(config: &ClickHouseConfig) -> Self {
let client = clickhouse::Client::default()
.with_url(config.url.clone())
.with_user(config.user.clone())
.with_password(config.password.clone())
.with_database(config.database.clone());
Self::with_client(client, config.clone())
}
pub(crate) fn with_client(client: clickhouse::Client, config: ClickHouseConfig) -> Self {
Self {
client,
config,
metrics: Arc::new(Metrics::default()),
}
}
#[must_use]
pub const fn config(&self) -> &ClickHouseConfig {
&self.config
}
#[must_use]
pub fn metrics(&self) -> &Metrics {
&self.metrics
}
#[must_use]
pub const fn client(&self) -> &clickhouse::Client {
&self.client
}
pub async fn ping(&self) -> Result<(), ClickHouseError> {
let result = self.call(self.client.query("SELECT 1").execute()).await;
self.metrics.query_ended(Outcome::of(&result));
result
}
pub async fn fetch_all<T>(&self, sql: &str) -> Result<Vec<T>, ClickHouseError>
where
T: clickhouse::RowOwned + clickhouse::RowRead,
{
let result = self.call(self.client.query(sql).fetch_all::<T>()).await;
self.metrics.query_ended(Outcome::of(&result));
result
}
pub async fn fetch_one<T>(&self, sql: &str) -> Result<T, ClickHouseError>
where
T: clickhouse::RowOwned + clickhouse::RowRead,
{
let result = self.call(self.client.query(sql).fetch_one::<T>()).await;
self.metrics.query_ended(Outcome::of(&result));
result
}
pub async fn execute(&self, sql: &str) -> Result<(), ClickHouseError> {
let result = self.call(self.client.query(sql).execute()).await;
self.metrics.ddl_ended(Outcome::of(&result));
result
}
pub async fn insert<T>(&self, table: &str, rows: &[T]) -> Result<(), ClickHouseError>
where
T: clickhouse::RowOwned + serde::Serialize + Sync,
{
let result = self.insert_inner(table, rows).await;
let outcome = Outcome::of(&result);
let count = if outcome == Outcome::Succeeded {
row_count(rows.len())
} else {
0
};
self.metrics.insert_ended(outcome, count);
result
}
#[must_use]
pub fn inserter<T>(&self, table: &str) -> Inserter<T>
where
T: clickhouse::RowOwned + serde::Serialize + Sync,
{
let inner = self
.client
.inserter::<T>(table)
.with_max_rows(self.config.inserter_max_rows)
.with_max_bytes(self.config.inserter_max_bytes)
.with_period(Some(self.config.inserter_period()));
Inserter {
inner,
timeout: self.config.timeout(),
metrics: Arc::clone(&self.metrics),
}
}
async fn call<T>(
&self,
future: impl std::future::Future<Output = clickhouse::error::Result<T>>,
) -> Result<T, ClickHouseError> {
let timeout = self.config.timeout();
tokio::time::timeout(timeout, future).await.map_or_else(
|_| Err(ClickHouseError::Timeout { timeout }),
|result| result.map_err(ClickHouseError::from),
)
}
async fn insert_inner<T>(&self, table: &str, rows: &[T]) -> Result<(), ClickHouseError>
where
T: clickhouse::RowOwned + serde::Serialize + Sync,
{
let mut insert = self.call(self.client.insert::<T>(table)).await?;
for row in rows {
self.call(insert.write(row)).await?;
}
self.call(insert.end()).await?;
Ok(())
}
}
pub struct Inserter<T> {
inner: clickhouse::inserter::Inserter<T>,
timeout: std::time::Duration,
metrics: Arc<Metrics>,
}
impl<T> Inserter<T>
where
T: clickhouse::RowOwned + serde::Serialize + Sync,
{
pub async fn write(&mut self, row: &T) -> Result<(), ClickHouseError> {
let timeout = self.timeout;
let result = tokio::time::timeout(timeout, self.inner.write(row))
.await
.map_or_else(
|_| Err(ClickHouseError::Timeout { timeout }),
|inner| inner.map_err(ClickHouseError::from),
);
if result.is_err() {
self.metrics.insert_ended(Outcome::of(&result), 0);
}
result
}
pub async fn commit(&mut self) -> Result<InsertStats, ClickHouseError> {
let timeout = self.timeout;
let result = tokio::time::timeout(timeout, self.inner.force_commit())
.await
.map_or_else(
|_| Err(ClickHouseError::Timeout { timeout }),
|inner| inner.map_err(ClickHouseError::from),
);
let outcome = Outcome::of(&result);
let stats = result.map(InsertStats::from)?;
self.metrics.insert_ended(outcome, stats.rows);
Ok(stats)
}
pub async fn end(self) -> Result<InsertStats, ClickHouseError> {
let Self {
inner,
timeout,
metrics,
} = self;
let result = tokio::time::timeout(timeout, inner.end())
.await
.map_or_else(
|_| Err(ClickHouseError::Timeout { timeout }),
|inner| inner.map_err(ClickHouseError::from),
);
let outcome = Outcome::of(&result);
let stats = result.map(InsertStats::from)?;
metrics.insert_ended(outcome, stats.rows);
Ok(stats)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct InsertStats {
pub rows: u64,
pub bytes: u64,
pub transactions: u64,
}
impl From<clickhouse::inserter::Quantities> for InsertStats {
fn from(quantities: clickhouse::inserter::Quantities) -> Self {
Self {
rows: quantities.rows,
bytes: quantities.bytes,
transactions: quantities.transactions,
}
}
}
fn row_count(len: usize) -> u64 {
u64::try_from(len).unwrap_or(u64::MAX)
}
#[cfg(test)]
mod tests;