1use std::{
7 collections::HashMap,
8 fmt::{self, Formatter},
9 num::NonZero,
10 sync::{Arc, Mutex, OnceLock},
11 time::Duration,
12};
13
14use chrono::{DateTime, Utc};
15
16use arrow_array::RecordBatch;
17use arrow_schema::Schema as ArrowSchema;
18use datafusion::{
19 catalog::streaming::StreamingTable,
20 dataframe::DataFrame,
21 execution::{
22 TaskContext,
23 context::{SessionConfig, SessionContext},
24 disk_manager::DiskManagerBuilder,
25 memory_pool::FairSpillPool,
26 runtime_env::RuntimeEnvBuilder,
27 },
28 physical_plan::{
29 DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties, SendableRecordBatchStream,
30 analyze::AnalyzeExec,
31 coalesce_partitions::CoalescePartitionsExec,
32 display::DisplayableExecutionPlan,
33 execution_plan::{Boundedness, CardinalityEffect, EmissionType},
34 metrics::MetricValue,
35 stream::RecordBatchStreamAdapter,
36 streaming::PartitionStream,
37 },
38};
39use datafusion::{execution::memory_pool::TrackConsumersPool, physical_plan::metrics::MetricType};
40use datafusion_common::{DataFusionError, Statistics};
41use datafusion_physical_expr::{EquivalenceProperties, Partitioning};
42
43use futures::{StreamExt, stream};
44use lance_arrow::SchemaExt;
45use lance_core::{
46 Error, Result,
47 utils::{
48 futures::FinallyStreamExt,
49 tracing::{EXECUTION_PLAN_RUN, StreamTracingExt, TRACE_EXECUTION},
50 },
51};
52use log::{debug, info, warn};
53use tracing::Span;
54
55use crate::udf::register_functions;
56use crate::{
57 chunker::StrictBatchSizeStream,
58 utils::{
59 BYTES_READ_METRIC, INDEX_COMPARISONS_METRIC, INDICES_LOADED_METRIC, IOPS_METRIC,
60 MetricsExt, PARTS_LOADED_METRIC, REQUESTS_METRIC,
61 },
62};
63
64pub struct OneShotExec {
72 stream: Mutex<Option<SendableRecordBatchStream>>,
73 schema: Arc<ArrowSchema>,
76 properties: Arc<PlanProperties>,
77}
78
79impl OneShotExec {
80 pub fn new(stream: SendableRecordBatchStream) -> Self {
82 let schema = stream.schema();
83 Self {
84 stream: Mutex::new(Some(stream)),
85 schema: schema.clone(),
86 properties: Arc::new(PlanProperties::new(
87 EquivalenceProperties::new(schema),
88 Partitioning::RoundRobinBatch(1),
89 EmissionType::Incremental,
90 Boundedness::Bounded,
91 )),
92 }
93 }
94
95 pub fn from_batch(batch: RecordBatch) -> Self {
96 let schema = batch.schema();
97 let stream = Box::pin(RecordBatchStreamAdapter::new(
98 schema,
99 stream::iter(vec![Ok(batch)]),
100 ));
101 Self::new(stream)
102 }
103}
104
105impl std::fmt::Debug for OneShotExec {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 let stream = self.stream.lock().unwrap();
108 f.debug_struct("OneShotExec")
109 .field("exhausted", &stream.is_none())
110 .field("schema", self.schema.as_ref())
111 .finish()
112 }
113}
114
115impl DisplayAs for OneShotExec {
116 fn fmt_as(
117 &self,
118 t: datafusion::physical_plan::DisplayFormatType,
119 f: &mut std::fmt::Formatter,
120 ) -> std::fmt::Result {
121 let stream = self.stream.lock().unwrap();
122 let exhausted = if stream.is_some() { "" } else { "EXHAUSTED" };
123 let columns = self
124 .schema
125 .field_names()
126 .iter()
127 .cloned()
128 .cloned()
129 .collect::<Vec<_>>();
130 match t {
131 DisplayFormatType::Default | DisplayFormatType::Verbose => {
132 write!(
133 f,
134 "OneShotStream: {}columns=[{}]",
135 exhausted,
136 columns.join(",")
137 )
138 }
139 DisplayFormatType::TreeRender => {
140 write!(
141 f,
142 "OneShotStream\nexhausted={}\ncolumns=[{}]",
143 exhausted,
144 columns.join(",")
145 )
146 }
147 }
148 }
149}
150
151impl ExecutionPlan for OneShotExec {
152 fn name(&self) -> &str {
153 "OneShotExec"
154 }
155
156 fn schema(&self) -> arrow_schema::SchemaRef {
157 self.schema.clone()
158 }
159
160 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
161 vec![]
162 }
163
164 fn with_new_children(
165 self: Arc<Self>,
166 children: Vec<Arc<dyn ExecutionPlan>>,
167 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
168 if !children.is_empty() {
170 return Err(datafusion_common::DataFusionError::Internal(
171 "OneShotExec does not support children".to_string(),
172 ));
173 }
174 Ok(self)
175 }
176
177 fn execute(
178 &self,
179 _partition: usize,
180 _context: Arc<datafusion::execution::TaskContext>,
181 ) -> datafusion_common::Result<SendableRecordBatchStream> {
182 let stream = self
183 .stream
184 .lock()
185 .map_err(|err| DataFusionError::Execution(err.to_string()))?
186 .take();
187 if let Some(stream) = stream {
188 Ok(stream)
189 } else {
190 Err(DataFusionError::Execution(
191 "OneShotExec has already been executed".to_string(),
192 ))
193 }
194 }
195
196 fn properties(&self) -> &Arc<datafusion::physical_plan::PlanProperties> {
197 &self.properties
198 }
199}
200
201struct TracedExec {
202 input: Arc<dyn ExecutionPlan>,
203 properties: Arc<PlanProperties>,
204 span: Span,
205}
206
207impl TracedExec {
208 pub fn new(input: Arc<dyn ExecutionPlan>, span: Span) -> Self {
209 Self {
210 properties: input.properties().clone(),
211 input,
212 span,
213 }
214 }
215}
216
217impl DisplayAs for TracedExec {
218 fn fmt_as(
219 &self,
220 t: datafusion::physical_plan::DisplayFormatType,
221 f: &mut std::fmt::Formatter,
222 ) -> std::fmt::Result {
223 match t {
224 DisplayFormatType::Default
225 | DisplayFormatType::Verbose
226 | DisplayFormatType::TreeRender => {
227 write!(f, "TracedExec")
228 }
229 }
230 }
231}
232
233impl std::fmt::Debug for TracedExec {
234 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
235 write!(f, "TracedExec")
236 }
237}
238impl ExecutionPlan for TracedExec {
239 fn name(&self) -> &str {
240 "TracedExec"
241 }
242
243 fn properties(&self) -> &Arc<PlanProperties> {
244 &self.properties
245 }
246
247 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
248 vec![&self.input]
249 }
250
251 fn with_new_children(
252 self: Arc<Self>,
253 children: Vec<Arc<dyn ExecutionPlan>>,
254 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
255 Ok(Arc::new(Self {
256 input: children[0].clone(),
257 properties: self.properties.clone(),
258 span: self.span.clone(),
259 }))
260 }
261
262 fn execute(
263 &self,
264 partition: usize,
265 context: Arc<TaskContext>,
266 ) -> datafusion_common::Result<SendableRecordBatchStream> {
267 let _guard = self.span.enter();
268 let stream = self.input.execute(partition, context)?;
269 let schema = stream.schema();
270 let stream = stream.stream_in_span(self.span.clone());
271 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
272 }
273}
274
275pub type ExecutionStatsCallback = Arc<dyn Fn(&ExecutionSummaryCounts) + Send + Sync>;
277
278#[derive(Default, Clone)]
279pub struct LanceExecutionOptions {
280 pub use_spilling: bool,
281 pub mem_pool_size: Option<u64>,
282 pub max_temp_directory_size: Option<u64>,
283 pub batch_size: Option<usize>,
284 pub target_partition: Option<usize>,
285 pub execution_stats_callback: Option<ExecutionStatsCallback>,
286 pub skip_logging: bool,
287}
288
289impl std::fmt::Debug for LanceExecutionOptions {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 f.debug_struct("LanceExecutionOptions")
292 .field("use_spilling", &self.use_spilling)
293 .field("mem_pool_size", &self.mem_pool_size)
294 .field("max_temp_directory_size", &self.max_temp_directory_size)
295 .field("batch_size", &self.batch_size)
296 .field("target_partition", &self.target_partition)
297 .field("skip_logging", &self.skip_logging)
298 .field(
299 "execution_stats_callback",
300 &self.execution_stats_callback.is_some(),
301 )
302 .finish()
303 }
304}
305
306const DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION: u64 = 150 * 1024 * 1024;
307const DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE: u64 = 100 * 1024 * 1024 * 1024; impl LanceExecutionOptions {
310 pub fn mem_pool_size(&self) -> u64 {
311 let num_partitions = self.target_partition.unwrap_or(1) as u64;
312 self.mem_pool_size.unwrap_or_else(|| {
313 std::env::var("LANCE_MEM_POOL_SIZE")
314 .map(|s| match s.parse::<u64>() {
315 Ok(v) => v,
316 Err(e) => {
317 warn!("Failed to parse LANCE_MEM_POOL_SIZE: {}, using default", e);
318 DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION * num_partitions
319 }
320 })
321 .unwrap_or(DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION * num_partitions)
322 })
323 }
324
325 pub fn max_temp_directory_size(&self) -> u64 {
326 self.max_temp_directory_size.unwrap_or_else(|| {
327 std::env::var("LANCE_MAX_TEMP_DIRECTORY_SIZE")
328 .map(|s| match s.parse::<u64>() {
329 Ok(v) => v,
330 Err(e) => {
331 warn!(
332 "Failed to parse LANCE_MAX_TEMP_DIRECTORY_SIZE: {}, using default",
333 e
334 );
335 DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE
336 }
337 })
338 .unwrap_or(DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE)
339 })
340 }
341
342 pub fn use_spilling(&self) -> bool {
343 if !self.use_spilling {
344 return false;
345 }
346 std::env::var("LANCE_BYPASS_SPILLING")
347 .map(|_| {
348 info!("Bypassing spilling because LANCE_BYPASS_SPILLING is set");
349 false
350 })
351 .unwrap_or(true)
352 }
353}
354
355pub fn new_session_context(options: &LanceExecutionOptions) -> SessionContext {
356 let mut session_config = SessionConfig::new();
357 let mut runtime_env_builder = RuntimeEnvBuilder::new();
358 if let Some(target_partition) = options.target_partition {
359 session_config = session_config.with_target_partitions(target_partition);
360 }
361 if options.use_spilling() {
362 let sort_spill_reservation_bytes =
367 (options.mem_pool_size() / 3).min(40 * 1024 * 1024) as usize;
368 session_config =
369 session_config.with_sort_spill_reservation_bytes(sort_spill_reservation_bytes);
370 let disk_manager_builder = DiskManagerBuilder::default()
371 .with_max_temp_directory_size(options.max_temp_directory_size());
372 runtime_env_builder = runtime_env_builder
373 .with_disk_manager_builder(disk_manager_builder)
374 .with_memory_pool(Arc::new(TrackConsumersPool::new(
375 FairSpillPool::new(options.mem_pool_size() as usize),
376 NonZero::try_from(16).unwrap(),
377 )));
378 }
379 let runtime_env = runtime_env_builder.build_arc().unwrap();
380
381 let ctx = SessionContext::new_with_config_rt(session_config, runtime_env);
382 register_functions(&ctx);
383
384 ctx
385}
386
387#[derive(Clone, Debug, PartialEq, Eq, Hash)]
389struct SessionContextCacheKey {
390 mem_pool_size: u64,
391 max_temp_directory_size: u64,
392 target_partition: Option<usize>,
393 use_spilling: bool,
394}
395
396impl SessionContextCacheKey {
397 fn from_options(options: &LanceExecutionOptions) -> Self {
398 Self {
399 mem_pool_size: options.mem_pool_size(),
400 max_temp_directory_size: options.max_temp_directory_size(),
401 target_partition: options.target_partition,
402 use_spilling: options.use_spilling(),
403 }
404 }
405}
406
407struct CachedSessionContext {
408 context: SessionContext,
409 last_access: std::time::Instant,
410}
411
412fn get_session_cache() -> &'static Mutex<HashMap<SessionContextCacheKey, CachedSessionContext>> {
413 static SESSION_CACHE: OnceLock<Mutex<HashMap<SessionContextCacheKey, CachedSessionContext>>> =
414 OnceLock::new();
415 SESSION_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
416}
417
418fn get_max_cache_size() -> usize {
419 const DEFAULT_CACHE_SIZE: usize = 4;
420 static MAX_CACHE_SIZE: OnceLock<usize> = OnceLock::new();
421 *MAX_CACHE_SIZE.get_or_init(|| {
422 std::env::var("LANCE_SESSION_CACHE_SIZE")
423 .ok()
424 .and_then(|v| v.parse().ok())
425 .unwrap_or(DEFAULT_CACHE_SIZE)
426 })
427}
428
429pub fn get_session_context(options: &LanceExecutionOptions) -> SessionContext {
430 let key = SessionContextCacheKey::from_options(options);
431 let mut cache = get_session_cache()
432 .lock()
433 .unwrap_or_else(|e| e.into_inner());
434
435 if let Some(entry) = cache.get_mut(&key) {
437 entry.last_access = std::time::Instant::now();
438 return entry.context.clone();
439 }
440
441 if cache.len() >= get_max_cache_size()
443 && let Some(lru_key) = cache
444 .iter()
445 .min_by_key(|(_, v)| v.last_access)
446 .map(|(k, _)| k.clone())
447 {
448 cache.remove(&lru_key);
449 }
450
451 let context = new_session_context(options);
452 cache.insert(
453 key,
454 CachedSessionContext {
455 context: context.clone(),
456 last_access: std::time::Instant::now(),
457 },
458 );
459 context
460}
461
462fn get_task_context(
463 session_ctx: &SessionContext,
464 options: &LanceExecutionOptions,
465) -> Arc<TaskContext> {
466 let mut state = session_ctx.state();
467 if let Some(batch_size) = options.batch_size.as_ref() {
468 state.config_mut().options_mut().execution.batch_size = *batch_size;
469 }
470
471 state.task_ctx()
472}
473
474#[derive(Default, Clone, Debug, PartialEq, Eq)]
475pub struct ExecutionSummaryCounts {
476 pub iops: usize,
478 pub requests: usize,
481 pub bytes_read: usize,
483 pub indices_loaded: usize,
485 pub parts_loaded: usize,
487 pub index_comparisons: usize,
489 pub all_counts: HashMap<String, usize>,
492 pub all_times: HashMap<String, usize>,
495}
496
497pub fn collect_execution_metrics(node: &dyn ExecutionPlan, counts: &mut ExecutionSummaryCounts) {
498 if let Some(metrics) = node.metrics() {
499 for (metric_name, count) in metrics.iter_counts() {
500 match metric_name.as_ref() {
501 IOPS_METRIC => counts.iops += count.value(),
502 REQUESTS_METRIC => counts.requests += count.value(),
503 BYTES_READ_METRIC => counts.bytes_read += count.value(),
504 INDICES_LOADED_METRIC => counts.indices_loaded += count.value(),
505 PARTS_LOADED_METRIC => counts.parts_loaded += count.value(),
506 INDEX_COMPARISONS_METRIC => counts.index_comparisons += count.value(),
507 _ => {
508 let existing = counts
509 .all_counts
510 .entry(metric_name.as_ref().to_string())
511 .or_insert(0);
512 *existing += count.value();
513 }
514 }
515 }
516 for (metric_name, time) in metrics.iter_times() {
517 let existing = counts
518 .all_times
519 .entry(metric_name.as_ref().to_string())
520 .or_insert(0);
521 *existing += time.value();
522 }
523 for (metric_name, gauge) in metrics.iter_gauges() {
525 match metric_name.as_ref() {
526 IOPS_METRIC => counts.iops += gauge.value(),
527 REQUESTS_METRIC => counts.requests += gauge.value(),
528 BYTES_READ_METRIC => counts.bytes_read += gauge.value(),
529 _ => {}
530 }
531 }
532 }
533 for child in node.children() {
534 collect_execution_metrics(child.as_ref(), counts);
535 }
536}
537
538fn report_plan_summary_metrics(plan: &dyn ExecutionPlan, options: &LanceExecutionOptions) {
539 let output_rows = plan
540 .metrics()
541 .map(|m| m.output_rows().unwrap_or(0))
542 .unwrap_or(0);
543 let mut counts = ExecutionSummaryCounts::default();
544 collect_execution_metrics(plan, &mut counts);
545 if !options.skip_logging {
546 tracing::info!(
547 target: TRACE_EXECUTION,
548 r#type = EXECUTION_PLAN_RUN,
549 plan_summary = display_plan_one_liner(plan),
550 output_rows,
551 iops = counts.iops,
552 requests = counts.requests,
553 bytes_read = counts.bytes_read,
554 indices_loaded = counts.indices_loaded,
555 parts_loaded = counts.parts_loaded,
556 index_comparisons = counts.index_comparisons,
557 );
558 }
559 if let Some(callback) = options.execution_stats_callback.as_ref() {
560 callback(&counts);
561 }
562}
563
564fn display_plan_one_liner(plan: &dyn ExecutionPlan) -> String {
571 let mut output = String::new();
572
573 display_plan_one_liner_impl(plan, &mut output);
574
575 output
576}
577
578fn display_plan_one_liner_impl(plan: &dyn ExecutionPlan, output: &mut String) {
579 let name = plan.name().trim_end_matches("Exec");
581 output.push_str(name);
582
583 let children = plan.children();
584 if !children.is_empty() {
585 output.push('(');
586 for (i, child) in children.iter().enumerate() {
587 if i > 0 {
588 output.push(',');
589 }
590 display_plan_one_liner_impl(child.as_ref(), output);
591 }
592 output.push(')');
593 }
594}
595
596pub fn execute_plan(
600 plan: Arc<dyn ExecutionPlan>,
601 options: LanceExecutionOptions,
602) -> Result<SendableRecordBatchStream> {
603 if !options.skip_logging {
604 debug!(
605 "Executing plan:\n{}",
606 DisplayableExecutionPlan::new(plan.as_ref()).indent(true)
607 );
608 }
609
610 let session_ctx = get_session_context(&options);
611
612 let plan: Arc<dyn ExecutionPlan> = if plan.properties().partitioning.partition_count() == 1 {
616 plan
617 } else {
618 Arc::new(CoalescePartitionsExec::new(plan))
619 };
620
621 let stream = plan.execute(0, get_task_context(&session_ctx, &options))?;
622
623 let schema = stream.schema();
624 let stream = stream.finally(move || {
625 if !options.skip_logging || options.execution_stats_callback.is_some() {
626 report_plan_summary_metrics(plan.as_ref(), &options);
627 }
628 });
629 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
630}
631
632pub async fn analyze_plan(
633 plan: Arc<dyn ExecutionPlan>,
634 options: LanceExecutionOptions,
635) -> Result<String> {
636 let plan = Arc::new(TracedExec::new(plan, Span::current()));
639
640 let schema = plan.schema();
641 let analyze = Arc::new(AnalyzeExec::new(
643 true,
644 true,
645 vec![MetricType::Summary],
646 None,
647 plan,
648 schema,
649 ));
650
651 let session_ctx = get_session_context(&options);
652 assert_eq!(analyze.properties().partitioning.partition_count(), 1);
653 let mut stream = analyze
654 .execute(0, get_task_context(&session_ctx, &options))
655 .map_err(|err| Error::io(format!("Failed to execute analyze plan: {}", err)))?;
656
657 while (stream.next().await).is_some() {}
659
660 let result = format_plan(analyze);
661 Ok(result)
662}
663
664pub fn format_plan(plan: Arc<dyn ExecutionPlan>) -> String {
665 struct CalculateVisitor {
667 highest_index: usize,
668 index_to_elapsed: HashMap<usize, Duration>,
669 }
670
671 struct SubtreeMetrics {
673 min_start: Option<DateTime<Utc>>,
674 max_end: Option<DateTime<Utc>>,
675 }
676
677 impl CalculateVisitor {
678 fn calculate_metrics(&mut self, plan: &Arc<dyn ExecutionPlan>) -> SubtreeMetrics {
679 self.highest_index += 1;
680 let plan_index = self.highest_index;
681
682 let (mut min_start, mut max_end) = Self::node_timerange(plan);
684
685 for child in plan.children() {
687 let child_metrics = self.calculate_metrics(child);
688 min_start = Self::min_option(min_start, child_metrics.min_start);
689 max_end = Self::max_option(max_end, child_metrics.max_end);
690 }
691
692 let elapsed = match (min_start, max_end) {
694 (Some(start), Some(end)) => Some((end - start).to_std().unwrap_or_default()),
695 _ => None,
696 };
697
698 if let Some(e) = elapsed {
699 self.index_to_elapsed.insert(plan_index, e);
700 }
701
702 SubtreeMetrics { min_start, max_end }
703 }
704
705 fn node_timerange(
706 plan: &Arc<dyn ExecutionPlan>,
707 ) -> (Option<DateTime<Utc>>, Option<DateTime<Utc>>) {
708 let Some(metrics) = plan.metrics() else {
709 return (None, None);
710 };
711 let min_start = metrics
712 .iter()
713 .filter_map(|m| match m.value() {
714 MetricValue::StartTimestamp(ts) => ts.value(),
715 _ => None,
716 })
717 .min();
718 let max_end = metrics
719 .iter()
720 .filter_map(|m| match m.value() {
721 MetricValue::EndTimestamp(ts) => ts.value(),
722 _ => None,
723 })
724 .max();
725 (min_start, max_end)
726 }
727
728 fn min_option(a: Option<DateTime<Utc>>, b: Option<DateTime<Utc>>) -> Option<DateTime<Utc>> {
729 [a, b].into_iter().flatten().min()
730 }
731
732 fn max_option(a: Option<DateTime<Utc>>, b: Option<DateTime<Utc>>) -> Option<DateTime<Utc>> {
733 [a, b].into_iter().flatten().max()
734 }
735 }
736
737 struct PrintVisitor {
739 highest_index: usize,
740 indent: usize,
741 }
742 impl PrintVisitor {
743 fn write_output(
744 &mut self,
745 plan: &Arc<dyn ExecutionPlan>,
746 f: &mut Formatter,
747 calcs: &CalculateVisitor,
748 ) -> std::fmt::Result {
749 self.highest_index += 1;
750 write!(f, "{:indent$}", "", indent = self.indent * 2)?;
751
752 let displayable =
754 datafusion::physical_plan::display::DisplayableExecutionPlan::new(plan.as_ref());
755 let plan_str = displayable.one_line().to_string();
756 let plan_str = plan_str.trim();
757
758 match calcs.index_to_elapsed.get(&self.highest_index) {
760 Some(elapsed) => match plan_str.find(": ") {
761 Some(i) => write!(
762 f,
763 "{}: elapsed={elapsed:?}, {}",
764 &plan_str[..i],
765 &plan_str[i + 2..]
766 )?,
767 None => write!(f, "{plan_str}, elapsed={elapsed:?}")?,
768 },
769 None => write!(f, "{plan_str}")?,
770 }
771
772 if let Some(metrics) = plan.metrics() {
773 let metrics = metrics
774 .aggregate_by_name()
775 .sorted_for_display()
776 .timestamps_removed();
777
778 write!(f, ", metrics=[{metrics}]")?;
779 } else {
780 write!(f, ", metrics=[]")?;
781 }
782 writeln!(f)?;
783 self.indent += 1;
784 for child in plan.children() {
785 self.write_output(child, f, calcs)?;
786 }
787 self.indent -= 1;
788 std::fmt::Result::Ok(())
789 }
790 }
791 struct PrintWrapper {
793 plan: Arc<dyn ExecutionPlan>,
794 }
795 impl fmt::Display for PrintWrapper {
796 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
797 let mut calcs = CalculateVisitor {
798 highest_index: 0,
799 index_to_elapsed: HashMap::new(),
800 };
801 calcs.calculate_metrics(&self.plan);
802 let mut prints = PrintVisitor {
803 highest_index: 0,
804 indent: 0,
805 };
806 prints.write_output(&self.plan, f, &calcs)
807 }
808 }
809 let wrapper = PrintWrapper { plan };
810 format!("{}", wrapper)
811}
812
813pub trait SessionContextExt {
814 fn read_one_shot(
818 &self,
819 data: SendableRecordBatchStream,
820 ) -> datafusion::common::Result<DataFrame>;
821}
822
823pub struct OneShotPartitionStream {
824 data: Arc<Mutex<Option<SendableRecordBatchStream>>>,
825 schema: Arc<ArrowSchema>,
826}
827
828impl std::fmt::Debug for OneShotPartitionStream {
829 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
830 let data = self.data.lock().unwrap();
831 f.debug_struct("OneShotPartitionStream")
832 .field("exhausted", &data.is_none())
833 .field("schema", self.schema.as_ref())
834 .finish()
835 }
836}
837
838impl OneShotPartitionStream {
839 pub fn new(data: SendableRecordBatchStream) -> Self {
840 let schema = data.schema();
841 Self {
842 data: Arc::new(Mutex::new(Some(data))),
843 schema,
844 }
845 }
846}
847
848impl PartitionStream for OneShotPartitionStream {
849 fn schema(&self) -> &arrow_schema::SchemaRef {
850 &self.schema
851 }
852
853 fn execute(&self, _ctx: Arc<TaskContext>) -> SendableRecordBatchStream {
854 let mut stream = self.data.lock().unwrap();
855 stream
856 .take()
857 .expect("Attempt to consume a one shot dataframe multiple times")
858 }
859}
860
861impl SessionContextExt for SessionContext {
862 fn read_one_shot(
863 &self,
864 data: SendableRecordBatchStream,
865 ) -> datafusion::common::Result<DataFrame> {
866 let schema = data.schema();
867 let part_stream = Arc::new(OneShotPartitionStream::new(data));
868 let provider = StreamingTable::try_new(schema, vec![part_stream])?;
869 self.read_table(Arc::new(provider))
870 }
871}
872
873#[derive(Clone, Debug)]
874pub struct StrictBatchSizeExec {
875 input: Arc<dyn ExecutionPlan>,
876 batch_size: usize,
877}
878
879impl StrictBatchSizeExec {
880 pub fn new(input: Arc<dyn ExecutionPlan>, batch_size: usize) -> Self {
881 Self { input, batch_size }
882 }
883}
884
885impl DisplayAs for StrictBatchSizeExec {
886 fn fmt_as(
887 &self,
888 _t: datafusion::physical_plan::DisplayFormatType,
889 f: &mut std::fmt::Formatter,
890 ) -> std::fmt::Result {
891 write!(f, "StrictBatchSizeExec")
892 }
893}
894
895impl ExecutionPlan for StrictBatchSizeExec {
896 fn name(&self) -> &str {
897 "StrictBatchSizeExec"
898 }
899
900 fn properties(&self) -> &Arc<PlanProperties> {
901 self.input.properties()
902 }
903
904 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
905 vec![&self.input]
906 }
907
908 fn with_new_children(
909 self: Arc<Self>,
910 children: Vec<Arc<dyn ExecutionPlan>>,
911 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
912 Ok(Arc::new(Self {
913 input: children[0].clone(),
914 batch_size: self.batch_size,
915 }))
916 }
917
918 fn execute(
919 &self,
920 partition: usize,
921 context: Arc<TaskContext>,
922 ) -> datafusion_common::Result<SendableRecordBatchStream> {
923 let stream = self.input.execute(partition, context)?;
924 let schema = stream.schema();
925 let stream = StrictBatchSizeStream::new(stream, self.batch_size);
926 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
927 }
928
929 fn maintains_input_order(&self) -> Vec<bool> {
930 vec![true]
931 }
932
933 fn benefits_from_input_partitioning(&self) -> Vec<bool> {
934 vec![false]
935 }
936
937 fn partition_statistics(
938 &self,
939 partition: Option<usize>,
940 ) -> datafusion_common::Result<std::sync::Arc<Statistics>> {
941 self.input.partition_statistics(partition)
942 }
943
944 fn cardinality_effect(&self) -> CardinalityEffect {
945 CardinalityEffect::Equal
946 }
947
948 fn supports_limit_pushdown(&self) -> bool {
949 true
950 }
951}
952
953#[derive(Clone, Debug)]
976pub struct HardCapBatchSizeExec {
977 input: Arc<dyn ExecutionPlan>,
978 max_bytes: usize,
979}
980
981impl HardCapBatchSizeExec {
982 pub fn new(input: Arc<dyn ExecutionPlan>, max_bytes: usize) -> Self {
983 Self { input, max_bytes }
984 }
985}
986
987impl DisplayAs for HardCapBatchSizeExec {
988 fn fmt_as(
989 &self,
990 _t: datafusion::physical_plan::DisplayFormatType,
991 f: &mut std::fmt::Formatter,
992 ) -> std::fmt::Result {
993 write!(f, "HardCapBatchSizeExec(max_bytes={})", self.max_bytes)
994 }
995}
996
997impl ExecutionPlan for HardCapBatchSizeExec {
998 fn name(&self) -> &str {
999 "HardCapBatchSizeExec"
1000 }
1001
1002 fn properties(&self) -> &Arc<PlanProperties> {
1003 self.input.properties()
1004 }
1005
1006 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1007 vec![&self.input]
1008 }
1009
1010 fn with_new_children(
1011 self: Arc<Self>,
1012 children: Vec<Arc<dyn ExecutionPlan>>,
1013 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
1014 Ok(Arc::new(Self {
1015 input: children[0].clone(),
1016 max_bytes: self.max_bytes,
1017 }))
1018 }
1019
1020 fn execute(
1021 &self,
1022 partition: usize,
1023 context: Arc<TaskContext>,
1024 ) -> datafusion_common::Result<SendableRecordBatchStream> {
1025 let stream = self.input.execute(partition, context)?;
1026 let schema = stream.schema();
1027 let max_bytes = self.max_bytes;
1028 let rechunked = lance_arrow::stream::rechunk_stream_by_size_deep_copy(
1029 stream,
1030 schema.clone(),
1031 0,
1032 max_bytes,
1033 );
1034 let validated = rechunked.map(move |result| {
1036 let batch = result?;
1037 if batch.num_rows() == 1 && batch.get_array_memory_size() > max_bytes {
1038 return Err(DataFusionError::External(Box::new(Error::invalid_input(
1039 format!(
1040 "a single row is {} bytes which exceeds the maximum allowed batch \
1041 size of {} bytes",
1042 batch.get_array_memory_size(),
1043 max_bytes,
1044 ),
1045 ))));
1046 }
1047 Ok(batch)
1048 });
1049 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, validated)))
1050 }
1051
1052 fn maintains_input_order(&self) -> Vec<bool> {
1053 vec![true]
1054 }
1055
1056 fn benefits_from_input_partitioning(&self) -> Vec<bool> {
1057 vec![false]
1058 }
1059
1060 fn partition_statistics(
1061 &self,
1062 partition: Option<usize>,
1063 ) -> datafusion_common::Result<std::sync::Arc<Statistics>> {
1064 self.input.partition_statistics(partition)
1065 }
1066
1067 fn cardinality_effect(&self) -> CardinalityEffect {
1068 CardinalityEffect::Equal
1069 }
1070
1071 fn supports_limit_pushdown(&self) -> bool {
1072 true
1073 }
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078 use super::*;
1079
1080 static CACHE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1082
1083 #[test]
1084 fn test_session_context_cache() {
1085 let _lock = CACHE_TEST_LOCK.lock().unwrap();
1086 let cache = get_session_cache();
1087
1088 cache.lock().unwrap().clear();
1090
1091 let opts1 = LanceExecutionOptions::default();
1093 let _ctx1 = get_session_context(&opts1);
1094
1095 {
1096 let cache_guard = cache.lock().unwrap();
1097 assert_eq!(cache_guard.len(), 1);
1098 }
1099
1100 let _ctx1_again = get_session_context(&opts1);
1102 {
1103 let cache_guard = cache.lock().unwrap();
1104 assert_eq!(cache_guard.len(), 1);
1105 }
1106
1107 let opts2 = LanceExecutionOptions {
1109 use_spilling: true,
1110 ..Default::default()
1111 };
1112 let _ctx2 = get_session_context(&opts2);
1113 {
1114 let cache_guard = cache.lock().unwrap();
1115 assert_eq!(cache_guard.len(), 2);
1116 }
1117 }
1118
1119 #[test]
1120 fn test_session_context_cache_lru_eviction() {
1121 let _lock = CACHE_TEST_LOCK.lock().unwrap();
1122 let cache = get_session_cache();
1123
1124 cache.lock().unwrap().clear();
1126
1127 let configs: Vec<LanceExecutionOptions> = (0..4)
1129 .map(|i| LanceExecutionOptions {
1130 mem_pool_size: Some((i + 1) as u64 * 1024 * 1024),
1131 ..Default::default()
1132 })
1133 .collect();
1134
1135 for config in &configs {
1136 let _ctx = get_session_context(config);
1137 }
1138
1139 {
1140 let cache_guard = cache.lock().unwrap();
1141 assert_eq!(cache_guard.len(), 4);
1142 }
1143
1144 std::thread::sleep(std::time::Duration::from_millis(1));
1147 let _ctx = get_session_context(&configs[0]);
1148
1149 let opts5 = LanceExecutionOptions {
1151 mem_pool_size: Some(5 * 1024 * 1024),
1152 ..Default::default()
1153 };
1154 let _ctx5 = get_session_context(&opts5);
1155
1156 {
1157 let cache_guard = cache.lock().unwrap();
1158 assert_eq!(cache_guard.len(), 4);
1159
1160 let key0 = SessionContextCacheKey::from_options(&configs[0]);
1162 assert!(
1163 cache_guard.contains_key(&key0),
1164 "config[0] should still be cached after recent access"
1165 );
1166
1167 let key1 = SessionContextCacheKey::from_options(&configs[1]);
1169 assert!(
1170 !cache_guard.contains_key(&key1),
1171 "config[1] should have been evicted"
1172 );
1173
1174 let key5 = SessionContextCacheKey::from_options(&opts5);
1176 assert!(
1177 cache_guard.contains_key(&key5),
1178 "new config should be cached"
1179 );
1180 }
1181 }
1182
1183 #[test]
1184 fn test_mem_pool_size_scales_with_partitions() {
1185 let default_per_partition = DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION;
1186
1187 let opts = LanceExecutionOptions::default();
1189 assert_eq!(opts.mem_pool_size(), default_per_partition);
1190
1191 let opts = LanceExecutionOptions {
1193 target_partition: Some(4),
1194 ..Default::default()
1195 };
1196 assert_eq!(opts.mem_pool_size(), default_per_partition * 4);
1197
1198 let opts = LanceExecutionOptions {
1200 target_partition: Some(8),
1201 ..Default::default()
1202 };
1203 assert_eq!(opts.mem_pool_size(), default_per_partition * 8);
1204
1205 let opts = LanceExecutionOptions {
1207 mem_pool_size: Some(50 * 1024 * 1024),
1208 target_partition: Some(8),
1209 ..Default::default()
1210 };
1211 assert_eq!(opts.mem_pool_size(), 50 * 1024 * 1024);
1212 }
1213}