use std::sync::Arc;
use crate::{Connection, Result, ffi, inner_connection::ConnectionCell, inner_connection::InnerConnection};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct QueryProgress {
percentage: f64,
rows_processed: u64,
total_rows_to_process: u64,
}
impl QueryProgress {
#[inline]
fn unavailable() -> Self {
Self {
percentage: -1.0,
rows_processed: 0,
total_rows_to_process: 0,
}
}
#[inline]
fn from_raw(raw: ffi::duckdb_query_progress_type) -> Self {
Self {
percentage: raw.percentage,
rows_processed: raw.rows_processed,
total_rows_to_process: raw.total_rows_to_process,
}
}
#[inline]
pub fn percentage(&self) -> Option<f64> {
self.is_available().then_some(self.percentage)
}
#[inline]
pub fn rows_processed(&self) -> u64 {
self.rows_processed
}
#[inline]
pub fn total_rows_to_process(&self) -> u64 {
self.total_rows_to_process
}
#[inline]
pub fn is_available(&self) -> bool {
self.percentage >= 0.0 && (self.rows_processed != 0 || self.total_rows_to_process != 0)
}
}
fn read_progress(cell: &ConnectionCell) -> QueryProgress {
cell.with(|conn| QueryProgress::from_raw(unsafe { ffi::duckdb_query_progress(conn) }))
.unwrap_or_else(QueryProgress::unavailable)
}
#[derive(Clone)]
pub struct ProgressHandle {
cell: Arc<ConnectionCell>,
}
impl ProgressHandle {
#[inline]
pub(crate) fn new(cell: Arc<ConnectionCell>) -> Self {
Self { cell }
}
pub fn query_progress(&self) -> QueryProgress {
read_progress(&self.cell)
}
pub fn is_connected(&self) -> bool {
self.cell.with(|_| ()).is_some()
}
}
impl std::fmt::Debug for ProgressHandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ProgressHandle").finish_non_exhaustive()
}
}
impl InnerConnection {
pub fn query_progress(&self) -> QueryProgress {
read_progress(&self.cell)
}
pub fn get_progress_handle(&self) -> ProgressHandle {
ProgressHandle::new(self.cell.clone())
}
}
impl Connection {
#[inline]
pub fn query_progress(&self) -> QueryProgress {
self.db.borrow().query_progress()
}
#[inline]
pub fn progress_handle(&self) -> ProgressHandle {
self.db.borrow().get_progress_handle()
}
pub fn set_query_progress_tracking(&self, enabled: bool) -> Result<()> {
self.execute_batch(&format!(
"SET enable_progress_bar = {enabled}; SET enable_progress_bar_print = false;"
))
}
}
#[cfg(test)]
mod test {
use std::{
sync::{Arc, mpsc},
thread,
time::Duration,
};
use super::{ProgressHandle, QueryProgress};
use crate::{Connection, Result, ffi};
const SCAN_QUERY: &str = "select * from t";
const BATCHES: usize = 40;
fn tracked_connection() -> Result<Connection> {
let conn = Connection::open_in_memory()?;
conn.set_query_progress_tracking(true)?;
Ok(conn)
}
fn scan_connection() -> Result<Connection> {
let conn = tracked_connection()?;
conn.execute_batch("set threads = 1")?;
conn.execute_batch("create table t as select i, i::varchar as s from range(4000000) t(i)")?;
Ok(conn)
}
fn with_query_in_flight<T>(conn: Connection, inspect: impl FnOnce() -> T) -> T {
let (ready_tx, ready_rx) = mpsc::channel();
let (release_tx, release_rx) = mpsc::channel();
let worker = thread::spawn(move || {
let mut stmt = conn.prepare(SCAN_QUERY).unwrap();
let mut stream = stmt.stream_arrow([]).unwrap();
for _ in 0..BATCHES {
assert!(stream.next().is_some(), "the scan finished early");
}
ready_tx.send(()).unwrap();
release_rx.recv().unwrap();
});
ready_rx
.recv_timeout(Duration::from_secs(60))
.expect("the query thread never reached its ready point");
let observed = inspect();
release_tx.send(()).unwrap();
worker.join().unwrap();
observed
}
#[test]
fn handles_are_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ProgressHandle>();
assert_send_sync::<crate::InterruptHandle>();
assert_send_sync::<Arc<crate::InterruptHandle>>();
}
#[test]
fn tracking_does_not_carry_to_another_connection() -> Result<()> {
let conn = tracked_connection()?;
let cloned = conn.try_clone()?;
let mut stmt = cloned.prepare("select current_setting('enable_progress_bar')")?;
let enabled: bool = stmt.query_row([], |row| row.get(0))?;
assert!(!enabled, "a second connection must opt in separately");
Ok(())
}
#[test]
fn is_connected_distinguishes_a_closed_connection() -> Result<()> {
let conn = tracked_connection()?;
let progress = conn.progress_handle();
assert!(progress.is_connected());
assert_eq!(progress.query_progress().percentage(), None);
drop(conn);
assert!(!progress.is_connected());
assert_eq!(progress.query_progress().percentage(), None);
Ok(())
}
#[test]
fn progress_is_unavailable_when_tracking_is_off() -> Result<()> {
let conn = Connection::open_in_memory()?;
conn.execute_batch("select 42")?;
let progress = conn.query_progress();
assert_eq!(progress.percentage(), None);
assert!(!progress.is_available());
assert_eq!(progress.rows_processed(), 0);
assert_eq!(progress.total_rows_to_process(), 0);
Ok(())
}
#[test]
fn progress_is_unavailable_before_the_query_reports() {
let raw = |percentage, rows_processed, total_rows_to_process| {
QueryProgress::from_raw(ffi::duckdb_query_progress_type {
percentage,
rows_processed,
total_rows_to_process,
})
};
assert_eq!(raw(0.0, 0, 0).percentage(), None);
assert_eq!(raw(-1.0, 0, 0).percentage(), None);
assert_eq!(raw(0.0, 0, 100).percentage(), Some(0.0));
assert_eq!(raw(50.0, 50, 100).percentage(), Some(50.0));
}
#[test]
fn progress_is_unavailable_when_no_query_is_running() -> Result<()> {
let conn = tracked_connection()?;
assert_eq!(conn.query_progress().percentage(), None);
Ok(())
}
#[test]
fn progress_is_unavailable_once_the_query_is_done() -> Result<()> {
let conn = tracked_connection()?;
conn.execute_batch("select count(*) from range(1000)")?;
assert_eq!(conn.query_progress().percentage(), None);
Ok(())
}
#[test]
fn progress_advances_as_a_streaming_result_is_consumed() -> Result<()> {
let conn = scan_connection()?;
let mut stmt = conn.prepare(SCAN_QUERY)?;
let mut stream = stmt.stream_arrow([])?;
let mut samples = Vec::new();
for _ in 0..6 {
for _ in 0..BATCHES {
assert!(stream.next().is_some(), "the scan finished early");
}
samples.push(conn.query_progress());
}
let seen: Vec<_> = samples.iter().map(|s| (s.percentage(), s.rows_processed())).collect();
for sample in &samples {
assert!(sample.is_available(), "progress went unavailable mid-stream: {seen:?}");
assert!(sample.rows_processed() > 0, "no rows reported: {seen:?}");
assert!(sample.rows_processed() <= sample.total_rows_to_process());
let percentage = sample.percentage().unwrap();
assert!((0.0..=100.0).contains(&percentage), "percentage out of range: {seen:?}");
}
for pair in samples.windows(2) {
assert!(
pair[1].rows_processed() >= pair[0].rows_processed(),
"progress went backwards: {seen:?}"
);
}
assert!(
samples.last().unwrap().rows_processed() > samples.first().unwrap().rows_processed(),
"progress never advanced: {seen:?}"
);
Ok(())
}
#[test]
fn handle_reports_progress_while_a_query_runs_on_another_thread() -> Result<()> {
let conn = scan_connection()?;
let progress = conn.progress_handle();
let snapshot = with_query_in_flight(conn, || progress.query_progress());
assert!(
snapshot.is_available(),
"progress should be readable from another thread"
);
assert!(snapshot.rows_processed() > 0);
assert!(snapshot.rows_processed() <= snapshot.total_rows_to_process());
let percentage = snapshot.percentage().unwrap();
assert!(
(0.0..=100.0).contains(&percentage),
"percentage out of range: {percentage}"
);
Ok(())
}
#[test]
fn tracking_can_be_turned_back_off() -> Result<()> {
let conn = scan_connection()?;
conn.set_query_progress_tracking(false)?;
let progress = conn.progress_handle();
let snapshot = with_query_in_flight(conn, || progress.query_progress());
assert_eq!(
snapshot.percentage(),
None,
"tracking is off, so there is nothing to report"
);
assert_eq!(snapshot.rows_processed(), 0);
Ok(())
}
#[test]
fn handle_outliving_its_connection_reports_unavailable() -> Result<()> {
let conn = tracked_connection()?;
let progress = conn.progress_handle();
drop(conn);
assert_eq!(progress.query_progress().percentage(), None);
assert_eq!(progress.query_progress().rows_processed(), 0);
Ok(())
}
#[test]
fn handle_is_cloneable_and_shared_across_threads() -> Result<()> {
let conn = tracked_connection()?;
let progress = conn.progress_handle();
let clone = progress.clone();
let joined = thread::spawn(move || clone.query_progress()).join().unwrap();
assert_eq!(joined.percentage(), None);
assert_eq!(progress.query_progress().percentage(), None);
Ok(())
}
}