use crate::DbkitError;
use crate::base_handler::{FetchMode, QueryResult, WriteOp};
use crate::value::DbValue;
use sqlx::postgres::{PgArguments, PgRow};
use sqlx::query::Query;
use sqlx::{AssertSqlSafe, PgPool, Postgres};
use tracing::warn;
use unicode_normalization::UnicodeNormalization;
#[cfg(feature = "duckdb")]
use crate::analytical::RecordBatch;
#[cfg(feature = "duckdb")]
use crate::read::{ReadEngine, duckdb::DuckEngine};
fn bind_pg<'q>(
mut q: Query<'q, Postgres, PgArguments>,
params: &[DbValue],
) -> Query<'q, Postgres, PgArguments> {
for p in params {
q = match p {
DbValue::Null => q.bind(Option::<i64>::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()),
DbValue::Date(d) => q.bind(*d),
DbValue::DateTime(dt) => q.bind(*dt),
DbValue::TimestampTz(dt) => q.bind(*dt),
DbValue::Json(j) => q.bind(j.clone()),
DbValue::Uuid(u) => q.bind(*u),
DbValue::Time(t) => q.bind(*t),
DbValue::TextArray(v) => q.bind(v.clone()),
DbValue::FloatArray(v) => q.bind(v.clone()),
DbValue::OptFloatArray(v) => q.bind(v.clone()),
};
}
q
}
pub struct PgHandler {
pool: PgPool,
#[cfg(feature = "duckdb")]
duck: Option<DuckEngine>,
}
impl PgHandler {
pub fn new(pool: PgPool) -> Self {
Self {
pool,
#[cfg(feature = "duckdb")]
duck: None,
}
}
#[cfg(feature = "duckdb")]
pub fn with_duckdb(pool: PgPool) -> Result<Self, DbkitError> {
Ok(Self {
pool,
duck: Some(DuckEngine::new_in_memory()?),
})
}
#[cfg(feature = "duckdb")]
pub fn with_duckdb_attached_postgres(
pool: PgPool,
pg_connection_string: &str,
) -> Result<Self, DbkitError> {
let duck = DuckEngine::new_in_memory()?;
duck.attach_postgres(pg_connection_string)?;
Ok(Self {
pool,
duck: Some(duck),
})
}
pub fn has_read_engine(&self) -> bool {
#[cfg(feature = "duckdb")]
{
self.duck.is_some()
}
#[cfg(not(feature = "duckdb"))]
{
false
}
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
pub fn normalize_name(name: &str) -> String {
name.nfd().collect::<String>().to_lowercase()
}
pub async fn execute_write(
&self,
op: WriteOp<'_>,
) -> Result<QueryResult<PgRow>, DbkitError> {
match op {
WriteOp::Single {
query,
params,
mode,
} => self.query(query, params, mode).await,
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,
} => {
if params_list.is_empty() {
return Ok(QueryResult::None);
}
let total = params_list.len();
let mut tx = self.pool.begin().await?;
let mut failed = 0usize;
for (idx, params) in params_list.iter().enumerate() {
let q = bind_pg(sqlx::query(AssertSqlSafe(query)), params);
if let Err(e) = q.execute(&mut *tx).await {
warn!("BatchParams row {}/{} failed: {:?}", idx + 1, total, e);
failed += 1;
}
}
tx.commit().await?;
if failed > 0 {
warn!(
"BatchParams: {}/{} succeeded, {} failed",
total - failed,
total,
failed
);
}
Ok(QueryResult::None)
}
}
}
pub async fn query(
&self,
query: &str,
params: Vec<DbValue>,
mode: FetchMode,
) -> Result<QueryResult<PgRow>, DbkitError> {
let q = bind_pg(sqlx::query(AssertSqlSafe(query)), ¶ms);
match mode {
FetchMode::None => {
q.execute(&self.pool).await?;
Ok(QueryResult::None)
}
FetchMode::One => Ok(QueryResult::One(q.fetch_one(&self.pool).await?)),
FetchMode::Optional => Ok(QueryResult::Optional(q.fetch_optional(&self.pool).await?)),
FetchMode::All => Ok(QueryResult::All(q.fetch_all(&self.pool).await?)),
}
}
#[cfg(feature = "duckdb")]
pub async fn execute_read(
&self,
sql: &str,
params: &[DbValue],
) -> Result<Vec<RecordBatch>, DbkitError> {
self.duck
.as_ref()
.ok_or(DbkitError::NoReadEngine)?
.query_arrow(sql, params)
.await
}
}