use std::sync::Arc;
use duckdb::Connection;
use parking_lot::Mutex;
use serde_json::Value as JsonValue;
use tracing::{debug, instrument};
use crate::config::DuckDbConfig;
use crate::error::{DuckDbError, DuckDbResult};
use crate::types::{DuckDbParam, duckdb_value_ref_to_json};
use prax_query::filter::FilterValue;
#[derive(Clone)]
pub struct DuckDbConnection {
conn: Arc<Mutex<Connection>>,
}
impl DuckDbConnection {
pub fn new(config: &DuckDbConfig) -> DuckDbResult<Self> {
let conn = if config.is_in_memory() {
Connection::open_in_memory()?
} else {
Connection::open(config.path_str())?
};
Self::apply_config(&conn, config)?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
})
}
pub fn open_in_memory() -> DuckDbResult<Self> {
let conn = Connection::open_in_memory()?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
})
}
pub fn open(path: &str) -> DuckDbResult<Self> {
let conn = Connection::open(path)?;
Ok(Self {
conn: Arc::new(Mutex::new(conn)),
})
}
fn apply_config(conn: &Connection, config: &DuckDbConfig) -> DuckDbResult<()> {
if let Some(threads) = config.threads {
conn.execute(&format!("SET threads = {}", threads), [])?;
}
if let Some(ref limit) = config.memory_limit {
conn.execute(&format!("SET memory_limit = '{}'", limit), [])?;
}
if let Some(ref max) = config.max_memory {
conn.execute(&format!("SET max_memory = '{}'", max), [])?;
}
if let Some(ref temp_dir) = config.temp_directory {
let path = temp_dir.to_string_lossy();
conn.execute(&format!("SET temp_directory = '{}'", path), [])?;
}
if !config.enable_external_access {
conn.execute("SET enable_external_access = false", [])?;
}
if !config.enable_object_cache {
conn.execute("SET enable_object_cache = false", [])?;
}
if let Some(ref order) = config.default_null_order {
conn.execute(&format!("SET default_null_order = '{}'", order), [])?;
}
if let Some(ref order) = config.default_order {
conn.execute(&format!("SET default_order = '{}'", order), [])?;
}
if config.enable_progress_bar {
conn.execute("SET enable_progress_bar = true", [])?;
}
Ok(())
}
#[instrument(skip(self, params), fields(sql = %sql))]
pub fn query(&self, sql: &str, params: &[FilterValue]) -> DuckDbResult<Vec<JsonValue>> {
debug!("Executing query");
let conn = self.conn.lock();
let mut stmt = conn.prepare(sql)?;
let duckdb_params: Vec<DuckDbParam<'_>> = params.iter().map(DuckDbParam).collect();
let param_refs: Vec<&dyn duckdb::ToSql> = duckdb_params
.iter()
.map(|p| p as &dyn duckdb::ToSql)
.collect();
let mut rows = stmt.query(param_refs.as_slice())?;
let column_names: Vec<String> = rows
.as_ref()
.map(|stmt| stmt.column_names())
.unwrap_or_default();
let mut results = Vec::new();
while let Some(row) = rows.next()? {
let mut obj = serde_json::Map::new();
for (i, name) in column_names.iter().enumerate() {
let value = row.get_ref(i)?;
obj.insert(name.clone(), duckdb_value_ref_to_json(value));
}
results.push(JsonValue::Object(obj));
}
Ok(results)
}
#[instrument(skip(self, params), fields(sql = %sql))]
pub fn query_one(&self, sql: &str, params: &[FilterValue]) -> DuckDbResult<JsonValue> {
let results = self.query(sql, params)?;
results
.into_iter()
.next()
.ok_or_else(|| DuckDbError::query("Query returned no rows"))
}
#[instrument(skip(self, params), fields(sql = %sql))]
pub fn query_optional(
&self,
sql: &str,
params: &[FilterValue],
) -> DuckDbResult<Option<JsonValue>> {
let results = self.query(sql, params)?;
Ok(results.into_iter().next())
}
#[instrument(skip(self, params), fields(sql = %sql))]
pub fn execute(&self, sql: &str, params: &[FilterValue]) -> DuckDbResult<usize> {
debug!("Executing statement");
let conn = self.conn.lock();
let mut stmt = conn.prepare(sql)?;
let duckdb_params: Vec<DuckDbParam<'_>> = params.iter().map(DuckDbParam).collect();
let param_refs: Vec<&dyn duckdb::ToSql> = duckdb_params
.iter()
.map(|p| p as &dyn duckdb::ToSql)
.collect();
let affected = stmt.execute(param_refs.as_slice())?;
Ok(affected)
}
#[instrument(skip(self), fields(sql_len = %sql.len()))]
pub fn execute_batch(&self, sql: &str) -> DuckDbResult<()> {
debug!("Executing batch");
let conn = self.conn.lock();
conn.execute_batch(sql)?;
Ok(())
}
#[instrument(skip(self, params), fields(sql = %sql))]
pub fn insert(&self, sql: &str, params: &[FilterValue]) -> DuckDbResult<usize> {
debug!("Executing insert");
self.execute(sql, params)
}
#[instrument(skip(self, params), fields(sql = %sql))]
pub fn insert_returning(
&self,
sql: &str,
params: &[FilterValue],
) -> DuckDbResult<Vec<JsonValue>> {
debug!("Executing insert with returning");
self.query(sql, params)
}
pub fn begin_transaction(&self) -> DuckDbResult<()> {
self.execute_batch("BEGIN TRANSACTION")
}
pub fn commit(&self) -> DuckDbResult<()> {
self.execute_batch("COMMIT")
}
pub fn rollback(&self) -> DuckDbResult<()> {
self.execute_batch("ROLLBACK")
}
pub fn savepoint(&self, name: &str) -> DuckDbResult<()> {
self.execute_batch(&format!("SAVEPOINT {}", name))
}
pub fn release_savepoint(&self, name: &str) -> DuckDbResult<()> {
self.execute_batch(&format!("RELEASE SAVEPOINT {}", name))
}
pub fn rollback_to_savepoint(&self, name: &str) -> DuckDbResult<()> {
self.execute_batch(&format!("ROLLBACK TO SAVEPOINT {}", name))
}
#[instrument(skip(self), fields(query_len = %query.len()))]
pub fn copy_to_parquet(&self, query: &str, path: &str) -> DuckDbResult<()> {
let sql = format!("COPY ({}) TO '{}' (FORMAT PARQUET)", query, path);
self.execute_batch(&sql)
}
#[instrument(skip(self), fields(query_len = %query.len()))]
pub fn copy_to_csv(&self, query: &str, path: &str, header: bool) -> DuckDbResult<()> {
let sql = format!(
"COPY ({}) TO '{}' (FORMAT CSV, HEADER {})",
query,
path,
if header { "TRUE" } else { "FALSE" }
);
self.execute_batch(&sql)
}
pub fn query_parquet(&self, path: &str) -> DuckDbResult<Vec<JsonValue>> {
let sql = format!("SELECT * FROM read_parquet('{}')", path);
self.query(&sql, &[])
}
pub fn query_csv(&self, path: &str, header: bool) -> DuckDbResult<Vec<JsonValue>> {
let sql = format!(
"SELECT * FROM read_csv('{}', header = {})",
path,
if header { "true" } else { "false" }
);
self.query(&sql, &[])
}
pub fn query_json(&self, path: &str) -> DuckDbResult<Vec<JsonValue>> {
let sql = format!("SELECT * FROM read_json_auto('{}')", path);
self.query(&sql, &[])
}
pub fn version(&self) -> DuckDbResult<String> {
let result = self.query_one("SELECT version()", &[])?;
result
.as_object()
.and_then(|obj| obj.values().next())
.and_then(|v| v.as_str())
.map(String::from)
.ok_or_else(|| DuckDbError::query("Failed to get version"))
}
pub fn explain(&self, query: &str) -> DuckDbResult<String> {
let sql = format!("EXPLAIN {}", query);
let results = self.query(&sql, &[])?;
let mut plan = String::new();
for row in results {
if let Some(obj) = row.as_object() {
for value in obj.values() {
if let Some(s) = value.as_str() {
plan.push_str(s);
plan.push('\n');
}
}
}
}
Ok(plan)
}
pub fn explain_analyze(&self, query: &str) -> DuckDbResult<String> {
let sql = format!("EXPLAIN ANALYZE {}", query);
let results = self.query(&sql, &[])?;
let mut plan = String::new();
for row in results {
if let Some(obj) = row.as_object() {
for value in obj.values() {
if let Some(s) = value.as_str() {
plan.push_str(s);
plan.push('\n');
}
}
}
}
Ok(plan)
}
}
impl std::fmt::Debug for DuckDbConnection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DuckDbConnection").finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_in_memory() {
let conn = DuckDbConnection::open_in_memory().unwrap();
let version = conn.version().unwrap();
assert!(!version.is_empty());
}
#[test]
fn test_execute_batch() {
let conn = DuckDbConnection::open_in_memory().unwrap();
conn.execute_batch(
"CREATE TABLE test (id INTEGER, name VARCHAR);
INSERT INTO test VALUES (1, 'Alice'), (2, 'Bob');",
)
.unwrap();
let results = conn.query("SELECT * FROM test ORDER BY id", &[]).unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_query_with_params() {
let conn = DuckDbConnection::open_in_memory().unwrap();
conn.execute_batch("CREATE TABLE users (id INTEGER, name VARCHAR);")
.unwrap();
conn.execute(
"INSERT INTO users VALUES (?, ?)",
&[
FilterValue::Int(1),
FilterValue::String("Alice".to_string()),
],
)
.unwrap();
let results = conn
.query("SELECT * FROM users WHERE id = ?", &[FilterValue::Int(1)])
.unwrap();
assert_eq!(results.len(), 1);
}
#[test]
fn test_transaction() {
let conn = DuckDbConnection::open_in_memory().unwrap();
conn.execute_batch("CREATE TABLE test (id INTEGER);")
.unwrap();
conn.begin_transaction().unwrap();
conn.execute("INSERT INTO test VALUES (?)", &[FilterValue::Int(1)])
.unwrap();
conn.rollback().unwrap();
let results = conn.query("SELECT * FROM test", &[]).unwrap();
assert!(results.is_empty());
}
}