1use crate::output_plan::OutputPlan;
64use crate::Table;
65#[cfg(feature = "progress")]
66use std::collections::BTreeMap;
67#[cfg(feature = "progress")]
68use std::fmt;
69#[cfg(feature = "progress")]
70use std::sync::Arc;
71
72#[cfg(feature = "progress")]
81pub trait ProgressTracker: Send + Sync + fmt::Debug {
82 fn register(&self, _table: Table, _total_units: u64) {}
89
90 fn increment(&self, table: Table, units: u64);
96
97 fn finish(&self) {}
102}
103
104#[derive(Debug, Clone, Default)]
110#[cfg(feature = "progress")]
111pub(crate) struct RunProgress {
112 tracker: Option<Arc<dyn ProgressTracker>>,
113}
114
115#[cfg(feature = "progress")]
116impl RunProgress {
117 pub(crate) fn with_tracker(tracker: Arc<dyn ProgressTracker>) -> Self {
118 Self {
119 tracker: Some(tracker),
120 }
121 }
122
123 pub(crate) fn register_totals(&self, plans: &[OutputPlan]) {
124 if let Some(tracker) = self.tracker.as_ref() {
125 let mut totals: BTreeMap<Table, u64> = BTreeMap::new();
126 for plan in plans {
127 *totals.entry(plan.table()).or_insert(0) += plan.chunk_count() as u64;
128 }
129 for (table, total) in totals {
130 tracker.register(table, total);
131 }
132 }
133 }
134
135 pub(crate) fn increment_for_existing(&self, plan: &OutputPlan) {
136 if let Some(tracker) = self.tracker.as_ref() {
137 tracker.increment(plan.table(), plan.chunk_count() as u64);
138 }
139 }
140
141 pub(crate) fn for_table(&self, table: Table) -> TableProgress {
142 TableProgress::for_table(self.tracker.clone(), table)
143 }
144
145 pub(crate) fn finish(self) {
146 if let Some(tracker) = self.tracker {
147 tracker.finish();
148 }
149 }
150}
151
152#[derive(Debug, Clone, Default)]
154#[cfg(not(feature = "progress"))]
155pub(crate) struct RunProgress;
156
157#[cfg(not(feature = "progress"))]
158impl RunProgress {
159 pub(crate) fn register_totals(&self, _plans: &[OutputPlan]) {}
160
161 pub(crate) fn increment_for_existing(&self, _plan: &OutputPlan) {}
162
163 pub(crate) fn for_table(&self, _table: Table) -> TableProgress {
164 TableProgress
165 }
166
167 pub(crate) fn finish(self) {}
168}
169
170#[derive(Clone, Default)]
175#[cfg(feature = "progress")]
176pub(crate) struct TableProgress {
177 tracker: Option<(Arc<dyn ProgressTracker>, Table)>,
178}
179
180#[cfg(feature = "progress")]
181impl TableProgress {
182 pub(crate) fn for_table(progress: Option<Arc<dyn ProgressTracker>>, table: Table) -> Self {
183 Self {
184 tracker: progress.map(|progress| (progress, table)),
185 }
186 }
187
188 pub(crate) fn increment_output_unit(&self) {
189 if let Some((progress, table)) = self.tracker.as_ref() {
190 progress.increment(*table, 1);
191 }
192 }
193}
194
195#[derive(Clone, Default)]
197#[cfg(not(feature = "progress"))]
198pub(crate) struct TableProgress;
199
200#[cfg(not(feature = "progress"))]
201impl TableProgress {
202 pub(crate) fn increment_output_unit(&self) {}
203}
204
205#[cfg(feature = "indicatif-progress")]
206pub use indicatif_impl::IndicatifProgress;
207
208#[cfg(feature = "indicatif-progress")]
209mod indicatif_impl {
210 use super::ProgressTracker;
211 use crate::Table;
212 use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
213 use std::collections::BTreeMap;
214 use std::io::{self, Write};
215 use std::sync::{OnceLock, RwLock};
216
217 #[derive(Debug)]
226 pub struct IndicatifProgress {
227 multi: MultiProgress,
228 tables: RwLock<BTreeMap<Table, ProgressBar>>,
229 }
230
231 impl IndicatifProgress {
232 pub fn new() -> Self {
235 Self {
236 multi: MultiProgress::new(),
237 tables: RwLock::new(BTreeMap::new()),
238 }
239 }
240
241 pub fn log_writer(&self) -> Box<dyn io::Write + Send + 'static> {
244 Box::new(IndicatifLogWriter {
245 multi: self.multi.clone(),
246 })
247 }
248 }
249
250 impl Default for IndicatifProgress {
251 fn default() -> Self {
252 Self::new()
253 }
254 }
255
256 impl ProgressTracker for IndicatifProgress {
257 fn register(&self, table: Table, total_units: u64) {
258 let Ok(mut tables) = self.tables.write() else {
259 return;
260 };
261
262 let pb = self.multi.add(ProgressBar::new(total_units));
263 pb.set_style(bar_style().clone());
264 pb.set_message(table.to_string());
265 let pb = pb.with_finish(ProgressFinish::AndLeave);
266 tables.insert(table, pb);
269 }
270
271 fn increment(&self, table: Table, units: u64) {
272 let bar = {
276 let Ok(tables) = self.tables.read() else {
277 return;
278 };
279 tables.get(&table).cloned()
280 };
281 if let Some(bar) = bar {
282 bar.inc(units);
283 }
284 }
285
286 fn finish(&self) {
287 let bars = {
288 let Ok(tables) = self.tables.read() else {
289 return;
290 };
291 tables.values().cloned().collect::<Vec<_>>()
292 };
293 for bar in bars {
294 bar.finish_using_style();
295 }
296 }
297 }
298
299 fn bar_style() -> &'static ProgressStyle {
300 static STYLE: OnceLock<ProgressStyle> = OnceLock::new();
301 STYLE.get_or_init(|| {
302 ProgressStyle::default_bar()
303 .template("{msg:10} [{bar:28}] Progress: {percent:>3}%")
304 .expect("static progress bar template is valid")
305 .progress_chars("█▓░")
306 })
307 }
308
309 struct IndicatifLogWriter {
310 multi: MultiProgress,
311 }
312
313 impl Write for IndicatifLogWriter {
314 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
315 self.multi.suspend(|| {
316 let mut stderr = io::stderr().lock();
317 stderr.write(buf)
318 })
319 }
320
321 fn flush(&mut self) -> io::Result<()> {
322 self.multi.suspend(|| {
323 let mut stderr = io::stderr().lock();
324 stderr.flush()
325 })
326 }
327 }
328
329 #[cfg(test)]
330 mod tests {
331 use super::*;
332
333 #[test]
334 fn registers_and_increments() {
335 let t = IndicatifProgress::new();
336 t.register(Table::Lineitem, 60);
337 t.register(Table::Orders, 15);
338 t.increment(Table::Lineitem, 1);
339 t.increment(Table::Orders, 5);
340
341 let tables = t.tables.read().unwrap();
342 assert_eq!(tables[&Table::Lineitem].position(), 1);
343 assert_eq!(tables[&Table::Orders].position(), 5);
344 }
345
346 #[test]
347 fn reaches_total() {
348 let t = IndicatifProgress::new();
349 t.register(Table::Orders, 5);
350 for _ in 0..5 {
351 t.increment(Table::Orders, 1);
352 }
353 assert_eq!(t.tables.read().unwrap()[&Table::Orders].position(), 5);
354 }
355
356 #[test]
357 fn unknown_table_is_no_op() {
358 let t = IndicatifProgress::new();
360 t.register(Table::Orders, 1);
361 t.increment(Table::Lineitem, 1);
362 assert_eq!(t.tables.read().unwrap()[&Table::Orders].position(), 0);
363 }
364
365 #[test]
366 fn finish_marks_registered_bars_finished() {
367 let t = IndicatifProgress::new();
368 t.register(Table::Orders, 2);
369 t.increment(Table::Orders, 2);
370 t.finish();
371
372 assert!(t.tables.read().unwrap()[&Table::Orders].is_finished());
373 }
374 }
375}
376
377#[cfg(all(test, feature = "progress"))]
378mod tests {
379 use super::*;
380 use crate::Table;
381 use std::sync::{
382 atomic::{AtomicU64, Ordering},
383 Arc, Mutex,
384 };
385
386 #[derive(Debug, Default)]
390 struct MockTracker {
391 registered: Mutex<Vec<(Table, u64)>>,
392 total_increments: AtomicU64,
393 finished: AtomicU64,
394 }
395
396 impl ProgressTracker for MockTracker {
397 fn register(&self, table: Table, total_units: u64) {
398 self.registered.lock().unwrap().push((table, total_units));
399 }
400 fn increment(&self, _table: Table, units: u64) {
401 self.total_increments.fetch_add(units, Ordering::Relaxed);
402 }
403 fn finish(&self) {
404 self.finished.fetch_add(1, Ordering::Relaxed);
405 }
406 }
407
408 #[test]
409 fn mock_tracker_works_through_arc_dyn() {
410 let mock = Arc::new(MockTracker::default());
411 let dynamic: Arc<dyn ProgressTracker> = mock.clone();
412 dynamic.register(Table::Lineitem, 10);
413 dynamic.register(Table::Orders, 4);
414 dynamic.increment(Table::Lineitem, 3);
415 dynamic.increment(Table::Orders, 1);
416 dynamic.finish();
417
418 assert_eq!(
419 *mock.registered.lock().unwrap(),
420 vec![(Table::Lineitem, 10), (Table::Orders, 4)]
421 );
422 assert_eq!(mock.total_increments.load(Ordering::Relaxed), 4);
423 assert_eq!(mock.finished.load(Ordering::Relaxed), 1);
424 }
425
426 #[test]
427 fn default_register_and_finish_are_noops() {
428 #[derive(Debug)]
430 struct Minimal(AtomicU64);
431 impl ProgressTracker for Minimal {
432 fn increment(&self, _t: Table, c: u64) {
433 self.0.fetch_add(c, Ordering::Relaxed);
434 }
435 }
436 let m = Minimal(AtomicU64::new(0));
437 m.register(Table::Region, 99); m.increment(Table::Region, 7);
439 m.finish(); assert_eq!(m.0.load(Ordering::Relaxed), 7);
441 }
442}