use crate::DbkitError;
use crate::value::DbValue;
use sqlx::any::{AnyArguments, AnyRow};
use sqlx::query::Query;
use sqlx::{Any, AnyPool, AssertSqlSafe};
use tracing::warn;
use unicode_normalization::UnicodeNormalization;
#[cfg(any(feature = "duckdb", feature = "datafusion"))]
use crate::analytical::RecordBatch;
#[cfg(any(feature = "duckdb", feature = "datafusion"))]
use crate::read::ReadEngine;
pub enum WriteOp<'a> {
Single {
query: &'a str,
params: Vec<DbValue>,
mode: FetchMode,
},
BatchDDL { queries: &'a [&'a str] },
BatchParams {
query: &'a str,
params_list: Vec<Vec<DbValue>>,
isolate_rows: bool,
},
}
#[derive(Debug, Clone, Copy)]
pub enum FetchMode {
None,
One,
Optional,
All,
}
pub enum QueryResult<T> {
None,
One(T),
Optional(Option<T>),
All(Vec<T>),
}
impl<T> QueryResult<T> {
pub fn one(self) -> Result<T, DbkitError> {
match self {
Self::One(v) => Ok(v),
_ => Err(DbkitError::RowCount {
expected: "One".into(),
actual: 0,
}),
}
}
pub fn optional(self) -> Result<Option<T>, DbkitError> {
match self {
Self::Optional(v) => Ok(v),
Self::One(v) => Ok(Some(v)),
Self::None => Ok(None),
_ => Err(DbkitError::RowCount {
expected: "Optional".into(),
actual: 0,
}),
}
}
pub fn all(self) -> Result<Vec<T>, DbkitError> {
match self {
Self::All(v) => Ok(v),
_ => Err(DbkitError::RowCount {
expected: "All".into(),
actual: 0,
}),
}
}
}
fn bind_params<'q>(
mut q: Query<'q, Any, AnyArguments>,
params: &[DbValue],
) -> Query<'q, Any, AnyArguments> {
for p in params {
q = match p {
DbValue::Null => q.bind(Option::<String>::None),
DbValue::Bool(b) => q.bind(*b),
DbValue::Int(i) => q.bind(*i),
DbValue::Float(f) => q.bind(*f),
DbValue::Text(s) => q.bind(s.clone()),
DbValue::Bytes(b) => q.bind(b.clone()),
#[cfg(feature = "postgres-native")]
DbValue::Date(d) => q.bind(d.to_string()),
#[cfg(feature = "postgres-native")]
DbValue::DateTime(dt) => q.bind(dt.to_string()),
#[cfg(feature = "postgres-native")]
DbValue::TimestampTz(dt) => q.bind(dt.to_rfc3339()),
#[cfg(feature = "postgres-native")]
DbValue::Json(j) => q.bind(j.to_string()),
#[cfg(feature = "postgres-native")]
DbValue::Uuid(u) => q.bind(u.to_string()),
#[cfg(feature = "postgres-native")]
DbValue::Time(t) => q.bind(t.to_string()),
#[cfg(feature = "postgres-native")]
DbValue::TextArray(v) => q.bind(crate::value::pg_text_array_literal(v)),
#[cfg(feature = "postgres-native")]
DbValue::FloatArray(v) => {
q.bind(crate::value::pg_float_array_literal(v.iter().map(|x| Some(*x))))
}
#[cfg(feature = "postgres-native")]
DbValue::OptFloatArray(v) => {
q.bind(crate::value::pg_float_array_literal(v.iter().copied()))
}
};
}
q
}
pub struct BaseHandler {
pool: AnyPool,
#[cfg(any(feature = "duckdb", feature = "datafusion"))]
read_engine: Option<Box<dyn ReadEngine>>,
}
impl BaseHandler {
pub fn new(pool: AnyPool) -> Self {
Self {
pool,
#[cfg(any(feature = "duckdb", feature = "datafusion"))]
read_engine: None,
}
}
#[cfg(feature = "duckdb")]
pub fn with_duckdb(pool: AnyPool) -> Result<Self, DbkitError> {
let engine = crate::read::duckdb::DuckEngine::new_in_memory()?;
Ok(Self {
pool,
read_engine: Some(Box::new(engine)),
})
}
#[cfg(feature = "duckdb")]
pub fn with_duckdb_attached_postgres(
pool: AnyPool,
pg_connection_string: &str,
) -> Result<Self, DbkitError> {
let engine = crate::read::duckdb::DuckEngine::new_in_memory()?;
engine.attach_postgres(pg_connection_string)?;
Ok(Self {
pool,
read_engine: Some(Box::new(engine)),
})
}
#[cfg(feature = "datafusion")]
pub fn with_datafusion(pool: AnyPool) -> Result<Self, DbkitError> {
let engine = crate::read::datafusion::DataFusionEngine::new();
Ok(Self {
pool,
read_engine: Some(Box::new(engine)),
})
}
pub fn has_read_engine(&self) -> bool {
#[cfg(any(feature = "duckdb", feature = "datafusion"))]
{
self.read_engine.is_some()
}
#[cfg(not(any(feature = "duckdb", feature = "datafusion")))]
{
false
}
}
pub fn pool(&self) -> &AnyPool {
&self.pool
}
pub fn normalize_name(name: &str) -> String {
use unicode_normalization::char::is_combining_mark;
name.nfd()
.filter(|c| !is_combining_mark(*c))
.collect::<String>()
.to_lowercase()
}
pub async fn execute_write(
&self,
op: WriteOp<'_>,
) -> Result<QueryResult<AnyRow>, DbkitError> {
match op {
WriteOp::Single {
query,
params,
mode,
} => {
let has_null = params.iter().any(|v| matches!(v, DbValue::Null));
let q = bind_params(sqlx::query(AssertSqlSafe(query)), ¶ms)
.persistent(!has_null);
match mode {
FetchMode::None => {
q.execute(&self.pool).await?;
Ok(QueryResult::None)
}
FetchMode::One => {
let row = q.fetch_one(&self.pool).await?;
Ok(QueryResult::One(row))
}
FetchMode::Optional => {
let row = q.fetch_optional(&self.pool).await?;
Ok(QueryResult::Optional(row))
}
FetchMode::All => {
let rows = q.fetch_all(&self.pool).await?;
Ok(QueryResult::All(rows))
}
}
}
WriteOp::BatchDDL { queries } => {
let mut tx = self.pool.begin().await?;
for query in queries {
sqlx::query(AssertSqlSafe(*query)).execute(&mut *tx).await?;
}
tx.commit().await?;
Ok(QueryResult::None)
}
WriteOp::BatchParams {
query,
params_list,
isolate_rows,
} => {
if params_list.is_empty() {
return Ok(QueryResult::None);
}
let total = params_list.len();
let mut tx = self.pool.begin().await?;
if !isolate_rows {
for params in ¶ms_list {
let has_null = params.iter().any(|v| matches!(v, DbValue::Null));
bind_params(sqlx::query(AssertSqlSafe(query)), params)
.persistent(!has_null)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
return Ok(QueryResult::None);
}
let mut failed = 0usize;
for (idx, params) in params_list.iter().enumerate() {
sqlx::query(AssertSqlSafe("SAVEPOINT dbkit_row"))
.execute(&mut *tx)
.await?;
let has_null = params.iter().any(|v| matches!(v, DbValue::Null));
let q = bind_params(sqlx::query(AssertSqlSafe(query)), params)
.persistent(!has_null);
match q.execute(&mut *tx).await {
Ok(_) => {
sqlx::query(AssertSqlSafe("RELEASE SAVEPOINT dbkit_row"))
.execute(&mut *tx)
.await?;
}
Err(e) => {
warn!("BatchParams row {}/{} failed: {:?}", idx + 1, total, e);
failed += 1;
sqlx::query(AssertSqlSafe("ROLLBACK TO SAVEPOINT dbkit_row"))
.execute(&mut *tx)
.await?;
sqlx::query(AssertSqlSafe("RELEASE SAVEPOINT dbkit_row"))
.execute(&mut *tx)
.await?;
}
}
}
tx.commit().await?;
if failed > 0 {
warn!(
"BatchParams: {}/{} succeeded, {} failed",
total - failed,
total,
failed
);
}
Ok(QueryResult::None)
}
}
}
#[cfg(any(feature = "duckdb", feature = "datafusion"))]
pub async fn execute_read(
&self,
sql: &str,
params: &[DbValue],
) -> Result<Vec<RecordBatch>, DbkitError> {
self.read_engine
.as_ref()
.ok_or(DbkitError::NoReadEngine)?
.query_arrow(sql, params)
.await
}
#[cfg(any(feature = "duckdb", feature = "datafusion"))]
pub async fn execute_read_as<T>(
&self,
sql: &str,
params: &[DbValue],
) -> Result<Vec<T>, DbkitError>
where
T: serde::de::DeserializeOwned,
{
let batches = self.execute_read(sql, params).await?;
crate::analytical::deserialize_batches(&batches)
}
#[cfg(any(feature = "duckdb", feature = "datafusion"))]
pub async fn sync_query(
&self,
name: &str,
query: &str,
params: &[DbValue],
) -> Result<(), DbkitError> {
let engine = self.read_engine.as_ref().ok_or(DbkitError::NoReadEngine)?;
let q = bind_params(sqlx::query(AssertSqlSafe(query.to_string())), params);
let rows = q.fetch_all(&self.pool).await?;
match crate::read::rows_to_record_batch(&rows)? {
Some(batch) => engine.load_table(name, vec![batch]).await?,
None => engine.drop_table(name).await?,
}
Ok(())
}
#[cfg(any(feature = "duckdb", feature = "datafusion"))]
pub async fn sync_tables(&self, tables: &[&str]) -> Result<(), DbkitError> {
for table in tables {
self.sync_query(table, &format!("SELECT * FROM {table}"), &[])
.await?;
}
Ok(())
}
}