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::{TableProvider, 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, ExecutionPlanProperties, PlanProperties,
30 SendableRecordBatchStream,
31 analyze::AnalyzeExec,
32 coalesce_partitions::CoalescePartitionsExec,
33 display::DisplayableExecutionPlan,
34 execution_plan::{Boundedness, CardinalityEffect, EmissionType},
35 metrics::MetricValue,
36 sorts::sort_preserving_merge::SortPreservingMergeExec,
37 stream::RecordBatchStreamAdapter,
38 streaming::PartitionStream,
39 },
40};
41use datafusion::{execution::memory_pool::TrackConsumersPool, physical_plan::metrics::MetricType};
42use datafusion_common::{DataFusionError, Statistics};
43use datafusion_physical_expr::{EquivalenceProperties, Partitioning};
44
45use futures::{StreamExt, stream};
46use lance_arrow::SchemaExt;
47use lance_core::{
48 Error, Result,
49 utils::{
50 futures::FinallyStreamExt,
51 tracing::{EXECUTION_PLAN_RUN, StreamTracingExt, TRACE_EXECUTION},
52 },
53};
54use log::{debug, info, warn};
55use tracing::Span;
56
57use crate::udf::register_functions;
58use crate::{
59 chunker::StrictBatchSizeStream,
60 utils::{
61 BYTES_READ_METRIC, INDEX_CACHE_HITS_METRIC, INDEX_CACHE_MISSES_METRIC,
62 INDEX_COMPARISONS_METRIC, INDICES_LOADED_METRIC, IOPS_METRIC, MetricsExt,
63 PARTS_LOADED_METRIC, REQUESTS_METRIC,
64 },
65};
66
67pub struct OneShotExec {
75 stream: Mutex<Option<SendableRecordBatchStream>>,
76 schema: Arc<ArrowSchema>,
79 properties: Arc<PlanProperties>,
80}
81
82impl OneShotExec {
83 pub fn new(stream: SendableRecordBatchStream) -> Self {
85 let schema = stream.schema();
86 Self {
87 stream: Mutex::new(Some(stream)),
88 schema: schema.clone(),
89 properties: Arc::new(PlanProperties::new(
90 EquivalenceProperties::new(schema),
91 Partitioning::RoundRobinBatch(1),
92 EmissionType::Incremental,
93 Boundedness::Bounded,
94 )),
95 }
96 }
97
98 pub fn from_batch(batch: RecordBatch) -> Self {
99 let schema = batch.schema();
100 let stream = Box::pin(RecordBatchStreamAdapter::new(
101 schema,
102 stream::iter(vec![Ok(batch)]),
103 ));
104 Self::new(stream)
105 }
106}
107
108impl std::fmt::Debug for OneShotExec {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 let stream = self.stream.lock().unwrap();
111 f.debug_struct("OneShotExec")
112 .field("exhausted", &stream.is_none())
113 .field("schema", self.schema.as_ref())
114 .finish()
115 }
116}
117
118impl DisplayAs for OneShotExec {
119 fn fmt_as(
120 &self,
121 t: datafusion::physical_plan::DisplayFormatType,
122 f: &mut std::fmt::Formatter,
123 ) -> std::fmt::Result {
124 let stream = self.stream.lock().unwrap();
125 let exhausted = if stream.is_some() { "" } else { "EXHAUSTED" };
126 let columns = self
127 .schema
128 .field_names()
129 .iter()
130 .cloned()
131 .cloned()
132 .collect::<Vec<_>>();
133 match t {
134 DisplayFormatType::Default | DisplayFormatType::Verbose => {
135 write!(
136 f,
137 "OneShotStream: {}columns=[{}]",
138 exhausted,
139 columns.join(",")
140 )
141 }
142 DisplayFormatType::TreeRender => {
143 write!(
144 f,
145 "OneShotStream\nexhausted={}\ncolumns=[{}]",
146 exhausted,
147 columns.join(",")
148 )
149 }
150 }
151 }
152}
153
154impl ExecutionPlan for OneShotExec {
155 fn name(&self) -> &str {
156 "OneShotExec"
157 }
158
159 fn schema(&self) -> arrow_schema::SchemaRef {
160 self.schema.clone()
161 }
162
163 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
164 vec![]
165 }
166
167 fn with_new_children(
168 self: Arc<Self>,
169 children: Vec<Arc<dyn ExecutionPlan>>,
170 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
171 if !children.is_empty() {
173 return Err(datafusion_common::DataFusionError::Internal(
174 "OneShotExec does not support children".to_string(),
175 ));
176 }
177 Ok(self)
178 }
179
180 fn execute(
181 &self,
182 _partition: usize,
183 _context: Arc<datafusion::execution::TaskContext>,
184 ) -> datafusion_common::Result<SendableRecordBatchStream> {
185 let stream = self
186 .stream
187 .lock()
188 .map_err(|err| DataFusionError::Execution(err.to_string()))?
189 .take();
190 if let Some(stream) = stream {
191 Ok(stream)
192 } else {
193 Err(DataFusionError::Execution(
194 "OneShotExec has already been executed".to_string(),
195 ))
196 }
197 }
198
199 fn properties(&self) -> &Arc<datafusion::physical_plan::PlanProperties> {
200 &self.properties
201 }
202}
203
204struct TracedExec {
205 input: Arc<dyn ExecutionPlan>,
206 properties: Arc<PlanProperties>,
207 span: Span,
208}
209
210impl TracedExec {
211 pub fn new(input: Arc<dyn ExecutionPlan>, span: Span) -> Self {
212 Self {
213 properties: input.properties().clone(),
214 input,
215 span,
216 }
217 }
218}
219
220impl DisplayAs for TracedExec {
221 fn fmt_as(
222 &self,
223 t: datafusion::physical_plan::DisplayFormatType,
224 f: &mut std::fmt::Formatter,
225 ) -> std::fmt::Result {
226 match t {
227 DisplayFormatType::Default
228 | DisplayFormatType::Verbose
229 | DisplayFormatType::TreeRender => {
230 write!(f, "TracedExec")
231 }
232 }
233 }
234}
235
236impl std::fmt::Debug for TracedExec {
237 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
238 write!(f, "TracedExec")
239 }
240}
241impl ExecutionPlan for TracedExec {
242 fn name(&self) -> &str {
243 "TracedExec"
244 }
245
246 fn properties(&self) -> &Arc<PlanProperties> {
247 &self.properties
248 }
249
250 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
251 vec![&self.input]
252 }
253
254 fn with_new_children(
255 self: Arc<Self>,
256 children: Vec<Arc<dyn ExecutionPlan>>,
257 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
258 Ok(Arc::new(Self {
259 input: children[0].clone(),
260 properties: self.properties.clone(),
261 span: self.span.clone(),
262 }))
263 }
264
265 fn execute(
266 &self,
267 partition: usize,
268 context: Arc<TaskContext>,
269 ) -> datafusion_common::Result<SendableRecordBatchStream> {
270 let _guard = self.span.enter();
271 let stream = self.input.execute(partition, context)?;
272 let schema = stream.schema();
273 let stream = stream.stream_in_span(self.span.clone());
274 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
275 }
276}
277
278pub type ExecutionStatsCallback = Arc<dyn Fn(&ExecutionSummaryCounts) + Send + Sync>;
280
281#[derive(Default, Clone)]
282pub struct LanceExecutionOptions {
283 pub use_spilling: bool,
284 pub mem_pool_size: Option<u64>,
285 pub max_temp_directory_size: Option<u64>,
286 pub batch_size: Option<usize>,
287 pub target_partition: Option<usize>,
288 pub execution_stats_callback: Option<ExecutionStatsCallback>,
289 pub skip_logging: bool,
290}
291
292impl std::fmt::Debug for LanceExecutionOptions {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 f.debug_struct("LanceExecutionOptions")
295 .field("use_spilling", &self.use_spilling)
296 .field("mem_pool_size", &self.mem_pool_size)
297 .field("max_temp_directory_size", &self.max_temp_directory_size)
298 .field("batch_size", &self.batch_size)
299 .field("target_partition", &self.target_partition)
300 .field("skip_logging", &self.skip_logging)
301 .field(
302 "execution_stats_callback",
303 &self.execution_stats_callback.is_some(),
304 )
305 .finish()
306 }
307}
308
309const DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION: u64 = 150 * 1024 * 1024;
310const DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE: u64 = 100 * 1024 * 1024 * 1024; impl LanceExecutionOptions {
313 pub fn mem_pool_size(&self) -> u64 {
314 let num_partitions = self.target_partition.unwrap_or(1) as u64;
315 self.mem_pool_size.unwrap_or_else(|| {
316 std::env::var("LANCE_MEM_POOL_SIZE")
317 .map(|s| match s.parse::<u64>() {
318 Ok(v) => v,
319 Err(e) => {
320 warn!("Failed to parse LANCE_MEM_POOL_SIZE: {}, using default", e);
321 DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION * num_partitions
322 }
323 })
324 .unwrap_or(DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION * num_partitions)
325 })
326 }
327
328 pub fn max_temp_directory_size(&self) -> u64 {
329 self.max_temp_directory_size.unwrap_or_else(|| {
330 std::env::var("LANCE_MAX_TEMP_DIRECTORY_SIZE")
331 .map(|s| match s.parse::<u64>() {
332 Ok(v) => v,
333 Err(e) => {
334 warn!(
335 "Failed to parse LANCE_MAX_TEMP_DIRECTORY_SIZE: {}, using default",
336 e
337 );
338 DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE
339 }
340 })
341 .unwrap_or(DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE)
342 })
343 }
344
345 pub fn use_spilling(&self) -> bool {
346 if !self.use_spilling {
347 return false;
348 }
349 std::env::var("LANCE_BYPASS_SPILLING")
350 .map(|_| {
351 info!("Bypassing spilling because LANCE_BYPASS_SPILLING is set");
352 false
353 })
354 .unwrap_or(true)
355 }
356}
357
358pub fn new_session_context(options: &LanceExecutionOptions) -> SessionContext {
359 let mut session_config = SessionConfig::new();
360 let mut runtime_env_builder = RuntimeEnvBuilder::new();
361 if let Some(target_partition) = options.target_partition {
362 session_config = session_config.with_target_partitions(target_partition);
363 }
364 if options.use_spilling() {
365 let sort_spill_reservation_bytes =
370 (options.mem_pool_size() / 3).min(40 * 1024 * 1024) as usize;
371 session_config =
372 session_config.with_sort_spill_reservation_bytes(sort_spill_reservation_bytes);
373 let disk_manager_builder = DiskManagerBuilder::default()
374 .with_max_temp_directory_size(options.max_temp_directory_size());
375 runtime_env_builder = runtime_env_builder
376 .with_disk_manager_builder(disk_manager_builder)
377 .with_memory_pool(Arc::new(TrackConsumersPool::new(
378 FairSpillPool::new(options.mem_pool_size() as usize),
379 NonZero::try_from(16).unwrap(),
380 )));
381 }
382 let runtime_env = runtime_env_builder.build_arc().unwrap();
383
384 let ctx = SessionContext::new_with_config_rt(session_config, runtime_env);
385 register_functions(&ctx);
386
387 ctx
388}
389
390#[derive(Clone, Debug, PartialEq, Eq, Hash)]
392struct SessionContextCacheKey {
393 mem_pool_size: u64,
394 max_temp_directory_size: u64,
395 target_partition: Option<usize>,
396 use_spilling: bool,
397}
398
399impl SessionContextCacheKey {
400 fn from_options(options: &LanceExecutionOptions) -> Self {
401 Self {
402 mem_pool_size: options.mem_pool_size(),
403 max_temp_directory_size: options.max_temp_directory_size(),
404 target_partition: options.target_partition,
405 use_spilling: options.use_spilling(),
406 }
407 }
408}
409
410struct CachedSessionContext {
411 context: SessionContext,
412 last_access: std::time::Instant,
413}
414
415fn get_session_cache() -> &'static Mutex<HashMap<SessionContextCacheKey, CachedSessionContext>> {
416 static SESSION_CACHE: OnceLock<Mutex<HashMap<SessionContextCacheKey, CachedSessionContext>>> =
417 OnceLock::new();
418 SESSION_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
419}
420
421fn get_max_cache_size() -> usize {
422 const DEFAULT_CACHE_SIZE: usize = 4;
423 static MAX_CACHE_SIZE: OnceLock<usize> = OnceLock::new();
424 *MAX_CACHE_SIZE.get_or_init(|| {
425 std::env::var("LANCE_SESSION_CACHE_SIZE")
426 .ok()
427 .and_then(|v| v.parse().ok())
428 .unwrap_or(DEFAULT_CACHE_SIZE)
429 })
430}
431
432pub fn get_session_context(options: &LanceExecutionOptions) -> SessionContext {
433 let key = SessionContextCacheKey::from_options(options);
434 let mut cache = get_session_cache()
435 .lock()
436 .unwrap_or_else(|e| e.into_inner());
437
438 if let Some(entry) = cache.get_mut(&key) {
440 entry.last_access = std::time::Instant::now();
441 return entry.context.clone();
442 }
443
444 if cache.len() >= get_max_cache_size()
446 && let Some(lru_key) = cache
447 .iter()
448 .min_by_key(|(_, v)| v.last_access)
449 .map(|(k, _)| k.clone())
450 {
451 cache.remove(&lru_key);
452 }
453
454 let context = new_session_context(options);
455 cache.insert(
456 key,
457 CachedSessionContext {
458 context: context.clone(),
459 last_access: std::time::Instant::now(),
460 },
461 );
462 context
463}
464
465fn get_task_context(
466 session_ctx: &SessionContext,
467 options: &LanceExecutionOptions,
468) -> Arc<TaskContext> {
469 let mut state = session_ctx.state();
470 if let Some(batch_size) = options.batch_size.as_ref() {
471 state.config_mut().options_mut().execution.batch_size = *batch_size;
472 }
473
474 state.task_ctx()
475}
476
477#[derive(Default, Clone, Debug, PartialEq, Eq)]
478pub struct ExecutionSummaryCounts {
479 pub iops: usize,
481 pub requests: usize,
484 pub bytes_read: usize,
486 pub indices_loaded: usize,
488 pub parts_loaded: usize,
490 pub index_comparisons: usize,
492 pub all_counts: HashMap<String, usize>,
499 pub all_times: HashMap<String, usize>,
502}
503
504impl ExecutionSummaryCounts {
505 pub fn index_cache_hits(&self) -> usize {
533 self.all_counts
534 .get(INDEX_CACHE_HITS_METRIC)
535 .copied()
536 .unwrap_or(0)
537 }
538
539 pub fn index_cache_misses(&self) -> usize {
548 self.all_counts
549 .get(INDEX_CACHE_MISSES_METRIC)
550 .copied()
551 .unwrap_or(0)
552 }
553
554 pub fn index_cache_hit_ratio(&self) -> f32 {
564 let hits = self.index_cache_hits() as u128;
567 let total = hits + self.index_cache_misses() as u128;
568 if total == 0 {
569 0.0
570 } else {
571 hits as f32 / total as f32
572 }
573 }
574}
575
576pub fn collect_execution_metrics(node: &dyn ExecutionPlan, counts: &mut ExecutionSummaryCounts) {
577 if let Some(metrics) = node.metrics() {
578 for (metric_name, count) in metrics.iter_counts() {
579 match metric_name.as_ref() {
580 IOPS_METRIC => counts.iops += count.value(),
581 REQUESTS_METRIC => counts.requests += count.value(),
582 BYTES_READ_METRIC => counts.bytes_read += count.value(),
583 INDICES_LOADED_METRIC => counts.indices_loaded += count.value(),
584 PARTS_LOADED_METRIC => counts.parts_loaded += count.value(),
585 INDEX_COMPARISONS_METRIC => counts.index_comparisons += count.value(),
586 _ => {
587 let existing = counts
588 .all_counts
589 .entry(metric_name.as_ref().to_string())
590 .or_insert(0);
591 *existing += count.value();
592 }
593 }
594 }
595 for (metric_name, time) in metrics.iter_times() {
596 let existing = counts
597 .all_times
598 .entry(metric_name.as_ref().to_string())
599 .or_insert(0);
600 *existing += time.value();
601 }
602 for (metric_name, gauge) in metrics.iter_gauges() {
604 match metric_name.as_ref() {
605 IOPS_METRIC => counts.iops += gauge.value(),
606 REQUESTS_METRIC => counts.requests += gauge.value(),
607 BYTES_READ_METRIC => counts.bytes_read += gauge.value(),
608 _ => {}
609 }
610 }
611 }
612 for child in node.children() {
613 collect_execution_metrics(child.as_ref(), counts);
614 }
615}
616
617fn report_plan_summary_metrics(plan: &dyn ExecutionPlan, options: &LanceExecutionOptions) {
618 let output_rows = plan
619 .metrics()
620 .map(|m| m.output_rows().unwrap_or(0))
621 .unwrap_or(0);
622 let mut counts = ExecutionSummaryCounts::default();
623 collect_execution_metrics(plan, &mut counts);
624 if !options.skip_logging {
625 tracing::info!(
626 target: TRACE_EXECUTION,
627 r#type = EXECUTION_PLAN_RUN,
628 plan_summary = display_plan_one_liner(plan),
629 output_rows,
630 iops = counts.iops,
631 requests = counts.requests,
632 bytes_read = counts.bytes_read,
633 indices_loaded = counts.indices_loaded,
634 parts_loaded = counts.parts_loaded,
635 index_comparisons = counts.index_comparisons,
636 index_cache_hits = counts.index_cache_hits(),
637 index_cache_misses = counts.index_cache_misses(),
638 );
639 }
640 if let Some(callback) = options.execution_stats_callback.as_ref() {
641 callback(&counts);
642 }
643}
644
645fn display_plan_one_liner(plan: &dyn ExecutionPlan) -> String {
652 let mut output = String::new();
653
654 display_plan_one_liner_impl(plan, &mut output);
655
656 output
657}
658
659fn display_plan_one_liner_impl(plan: &dyn ExecutionPlan, output: &mut String) {
660 let name = plan.name().trim_end_matches("Exec");
662 output.push_str(name);
663
664 let children = plan.children();
665 if !children.is_empty() {
666 output.push('(');
667 for (i, child) in children.iter().enumerate() {
668 if i > 0 {
669 output.push(',');
670 }
671 display_plan_one_liner_impl(child.as_ref(), output);
672 }
673 output.push(')');
674 }
675}
676
677pub fn execute_plan(
681 plan: Arc<dyn ExecutionPlan>,
682 options: LanceExecutionOptions,
683) -> Result<SendableRecordBatchStream> {
684 if !options.skip_logging {
685 debug!(
686 "Executing plan:\n{}",
687 DisplayableExecutionPlan::new(plan.as_ref()).indent(true)
688 );
689 }
690
691 let session_ctx = get_session_context(&options);
692
693 let plan: Arc<dyn ExecutionPlan> = if plan.properties().partitioning.partition_count() == 1 {
704 plan
705 } else if let Some(ordering) = plan.output_ordering() {
706 Arc::new(SortPreservingMergeExec::new(ordering.clone(), plan))
707 } else {
708 Arc::new(CoalescePartitionsExec::new(plan))
709 };
710
711 let stream = plan.execute(0, get_task_context(&session_ctx, &options))?;
712
713 let schema = stream.schema();
714 let stream = stream.finally(move || {
715 if !options.skip_logging || options.execution_stats_callback.is_some() {
716 report_plan_summary_metrics(plan.as_ref(), &options);
717 }
718 });
719 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
720}
721
722pub async fn analyze_plan(
723 plan: Arc<dyn ExecutionPlan>,
724 options: LanceExecutionOptions,
725) -> Result<String> {
726 analyze_plan_with_context(plan, options, None).await
727}
728
729pub async fn analyze_plan_with_context(
738 plan: Arc<dyn ExecutionPlan>,
739 options: LanceExecutionOptions,
740 task_context: Option<Arc<TaskContext>>,
741) -> Result<String> {
742 let plan = Arc::new(TracedExec::new(plan, Span::current()));
745
746 let schema = plan.schema();
747 let analyze = Arc::new(AnalyzeExec::new(
749 true,
750 true,
751 vec![MetricType::Summary],
752 None,
753 plan,
754 schema,
755 ));
756
757 let session_ctx = get_session_context(&options);
758 let task_context = task_context.unwrap_or_else(|| get_task_context(&session_ctx, &options));
759 assert_eq!(analyze.properties().partitioning.partition_count(), 1);
760 let mut stream = analyze
761 .execute(0, task_context)
762 .map_err(|err| Error::io(format!("Failed to execute analyze plan: {}", err)))?;
763
764 while (stream.next().await).is_some() {}
766
767 let result = format_plan(analyze);
768 Ok(result)
769}
770
771pub fn format_plan(plan: Arc<dyn ExecutionPlan>) -> String {
772 struct CalculateVisitor {
774 highest_index: usize,
775 index_to_elapsed: HashMap<usize, Duration>,
776 }
777
778 struct SubtreeMetrics {
780 min_start: Option<DateTime<Utc>>,
781 max_end: Option<DateTime<Utc>>,
782 }
783
784 impl CalculateVisitor {
785 fn calculate_metrics(&mut self, plan: &Arc<dyn ExecutionPlan>) -> SubtreeMetrics {
786 self.highest_index += 1;
787 let plan_index = self.highest_index;
788
789 let (mut min_start, mut max_end) = Self::node_timerange(plan);
791
792 for child in plan.children() {
794 let child_metrics = self.calculate_metrics(child);
795 min_start = Self::min_option(min_start, child_metrics.min_start);
796 max_end = Self::max_option(max_end, child_metrics.max_end);
797 }
798
799 let elapsed = match (min_start, max_end) {
801 (Some(start), Some(end)) => Some((end - start).to_std().unwrap_or_default()),
802 _ => None,
803 };
804
805 if let Some(e) = elapsed {
806 self.index_to_elapsed.insert(plan_index, e);
807 }
808
809 SubtreeMetrics { min_start, max_end }
810 }
811
812 fn node_timerange(
813 plan: &Arc<dyn ExecutionPlan>,
814 ) -> (Option<DateTime<Utc>>, Option<DateTime<Utc>>) {
815 let Some(metrics) = plan.metrics() else {
816 return (None, None);
817 };
818 let min_start = metrics
819 .iter()
820 .filter_map(|m| match m.value() {
821 MetricValue::StartTimestamp(ts) => ts.value(),
822 _ => None,
823 })
824 .min();
825 let max_end = metrics
826 .iter()
827 .filter_map(|m| match m.value() {
828 MetricValue::EndTimestamp(ts) => ts.value(),
829 _ => None,
830 })
831 .max();
832 (min_start, max_end)
833 }
834
835 fn min_option(a: Option<DateTime<Utc>>, b: Option<DateTime<Utc>>) -> Option<DateTime<Utc>> {
836 [a, b].into_iter().flatten().min()
837 }
838
839 fn max_option(a: Option<DateTime<Utc>>, b: Option<DateTime<Utc>>) -> Option<DateTime<Utc>> {
840 [a, b].into_iter().flatten().max()
841 }
842 }
843
844 struct PrintVisitor {
846 highest_index: usize,
847 indent: usize,
848 }
849 impl PrintVisitor {
850 fn write_output(
851 &mut self,
852 plan: &Arc<dyn ExecutionPlan>,
853 f: &mut Formatter,
854 calcs: &CalculateVisitor,
855 ) -> std::fmt::Result {
856 self.highest_index += 1;
857 write!(f, "{:indent$}", "", indent = self.indent * 2)?;
858
859 let displayable =
861 datafusion::physical_plan::display::DisplayableExecutionPlan::new(plan.as_ref());
862 let plan_str = displayable.one_line().to_string();
863 let plan_str = plan_str.trim();
864
865 match calcs.index_to_elapsed.get(&self.highest_index) {
867 Some(elapsed) => match plan_str.find(": ") {
868 Some(i) => write!(
869 f,
870 "{}: elapsed={elapsed:?}, {}",
871 &plan_str[..i],
872 &plan_str[i + 2..]
873 )?,
874 None => write!(f, "{plan_str}, elapsed={elapsed:?}")?,
875 },
876 None => write!(f, "{plan_str}")?,
877 }
878
879 if let Some(metrics) = plan.metrics() {
880 let metrics = metrics
881 .aggregate_by_name()
882 .sorted_for_display()
883 .timestamps_removed();
884
885 write!(f, ", metrics=[{metrics}]")?;
886 } else {
887 write!(f, ", metrics=[]")?;
888 }
889 writeln!(f)?;
890 self.indent += 1;
891 for child in plan.children() {
892 self.write_output(child, f, calcs)?;
893 }
894 self.indent -= 1;
895 std::fmt::Result::Ok(())
896 }
897 }
898 struct PrintWrapper {
900 plan: Arc<dyn ExecutionPlan>,
901 }
902 impl fmt::Display for PrintWrapper {
903 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
904 let mut calcs = CalculateVisitor {
905 highest_index: 0,
906 index_to_elapsed: HashMap::new(),
907 };
908 calcs.calculate_metrics(&self.plan);
909 let mut prints = PrintVisitor {
910 highest_index: 0,
911 indent: 0,
912 };
913 prints.write_output(&self.plan, f, &calcs)
914 }
915 }
916 let wrapper = PrintWrapper { plan };
917 format!("{}", wrapper)
918}
919
920pub trait SessionContextExt {
921 fn read_one_shot(
925 &self,
926 data: SendableRecordBatchStream,
927 ) -> datafusion::common::Result<DataFrame>;
928}
929
930pub struct OneShotPartitionStream {
931 data: Arc<Mutex<Option<SendableRecordBatchStream>>>,
932 schema: Arc<ArrowSchema>,
933}
934
935impl std::fmt::Debug for OneShotPartitionStream {
936 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
937 let data = self.data.lock().unwrap();
938 f.debug_struct("OneShotPartitionStream")
939 .field("exhausted", &data.is_none())
940 .field("schema", self.schema.as_ref())
941 .finish()
942 }
943}
944
945impl OneShotPartitionStream {
946 pub fn new(data: SendableRecordBatchStream) -> Self {
947 let schema = data.schema();
948 Self {
949 data: Arc::new(Mutex::new(Some(data))),
950 schema,
951 }
952 }
953}
954
955impl PartitionStream for OneShotPartitionStream {
956 fn schema(&self) -> &arrow_schema::SchemaRef {
957 &self.schema
958 }
959
960 fn execute(&self, _ctx: Arc<TaskContext>) -> SendableRecordBatchStream {
961 let mut stream = self.data.lock().unwrap();
962 stream
963 .take()
964 .expect("Attempt to consume a one shot dataframe multiple times")
965 }
966}
967
968impl SessionContextExt for SessionContext {
969 fn read_one_shot(
970 &self,
971 data: SendableRecordBatchStream,
972 ) -> datafusion::common::Result<DataFrame> {
973 let schema = data.schema();
974 let part_stream = Arc::new(OneShotPartitionStream::new(data));
975 let provider = StreamingTable::try_new(schema, vec![part_stream])?;
976 self.read_table(Arc::new(provider))
977 }
978}
979
980pub async fn provider_to_stream(
1010 provider: Arc<dyn TableProvider>,
1011) -> Result<SendableRecordBatchStream> {
1012 let ctx = SessionContext::new();
1013 let plan = provider.scan(&ctx.state(), None, &[], None).await?;
1014 let plan: Arc<dyn ExecutionPlan> =
1015 if plan.properties().output_partitioning().partition_count() > 1 {
1016 Arc::new(CoalescePartitionsExec::new(plan))
1017 } else {
1018 plan
1019 };
1020 Ok(plan.execute(0, ctx.task_ctx())?)
1021}
1022
1023#[derive(Clone, Debug)]
1024pub struct StrictBatchSizeExec {
1025 input: Arc<dyn ExecutionPlan>,
1026 batch_size: usize,
1027}
1028
1029impl StrictBatchSizeExec {
1030 pub fn new(input: Arc<dyn ExecutionPlan>, batch_size: usize) -> Self {
1031 Self { input, batch_size }
1032 }
1033}
1034
1035impl DisplayAs for StrictBatchSizeExec {
1036 fn fmt_as(
1037 &self,
1038 _t: datafusion::physical_plan::DisplayFormatType,
1039 f: &mut std::fmt::Formatter,
1040 ) -> std::fmt::Result {
1041 write!(f, "StrictBatchSizeExec")
1042 }
1043}
1044
1045impl ExecutionPlan for StrictBatchSizeExec {
1046 fn name(&self) -> &str {
1047 "StrictBatchSizeExec"
1048 }
1049
1050 fn properties(&self) -> &Arc<PlanProperties> {
1051 self.input.properties()
1052 }
1053
1054 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1055 vec![&self.input]
1056 }
1057
1058 fn with_new_children(
1059 self: Arc<Self>,
1060 children: Vec<Arc<dyn ExecutionPlan>>,
1061 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
1062 Ok(Arc::new(Self {
1063 input: children[0].clone(),
1064 batch_size: self.batch_size,
1065 }))
1066 }
1067
1068 fn execute(
1069 &self,
1070 partition: usize,
1071 context: Arc<TaskContext>,
1072 ) -> datafusion_common::Result<SendableRecordBatchStream> {
1073 let stream = self.input.execute(partition, context)?;
1074 let schema = stream.schema();
1075 let stream = StrictBatchSizeStream::new(stream, self.batch_size);
1076 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
1077 }
1078
1079 fn maintains_input_order(&self) -> Vec<bool> {
1080 vec![true]
1081 }
1082
1083 fn benefits_from_input_partitioning(&self) -> Vec<bool> {
1084 vec![false]
1085 }
1086
1087 fn partition_statistics(
1088 &self,
1089 partition: Option<usize>,
1090 ) -> datafusion_common::Result<std::sync::Arc<Statistics>> {
1091 self.input.partition_statistics(partition)
1092 }
1093
1094 fn cardinality_effect(&self) -> CardinalityEffect {
1095 CardinalityEffect::Equal
1096 }
1097
1098 fn supports_limit_pushdown(&self) -> bool {
1099 true
1100 }
1101}
1102
1103#[derive(Clone, Debug)]
1126pub struct HardCapBatchSizeExec {
1127 input: Arc<dyn ExecutionPlan>,
1128 max_bytes: usize,
1129}
1130
1131impl HardCapBatchSizeExec {
1132 pub fn new(input: Arc<dyn ExecutionPlan>, max_bytes: usize) -> Self {
1133 Self { input, max_bytes }
1134 }
1135}
1136
1137impl DisplayAs for HardCapBatchSizeExec {
1138 fn fmt_as(
1139 &self,
1140 _t: datafusion::physical_plan::DisplayFormatType,
1141 f: &mut std::fmt::Formatter,
1142 ) -> std::fmt::Result {
1143 write!(f, "HardCapBatchSizeExec(max_bytes={})", self.max_bytes)
1144 }
1145}
1146
1147impl ExecutionPlan for HardCapBatchSizeExec {
1148 fn name(&self) -> &str {
1149 "HardCapBatchSizeExec"
1150 }
1151
1152 fn properties(&self) -> &Arc<PlanProperties> {
1153 self.input.properties()
1154 }
1155
1156 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1157 vec![&self.input]
1158 }
1159
1160 fn with_new_children(
1161 self: Arc<Self>,
1162 children: Vec<Arc<dyn ExecutionPlan>>,
1163 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
1164 Ok(Arc::new(Self {
1165 input: children[0].clone(),
1166 max_bytes: self.max_bytes,
1167 }))
1168 }
1169
1170 fn execute(
1171 &self,
1172 partition: usize,
1173 context: Arc<TaskContext>,
1174 ) -> datafusion_common::Result<SendableRecordBatchStream> {
1175 let stream = self.input.execute(partition, context)?;
1176 let schema = stream.schema();
1177 let max_bytes = self.max_bytes;
1178 let rechunked = lance_arrow::stream::rechunk_stream_by_size_deep_copy(
1179 stream,
1180 schema.clone(),
1181 0,
1182 max_bytes,
1183 );
1184 let validated = rechunked.map(move |result| {
1186 let batch = result?;
1187 if batch.num_rows() == 1 && batch.get_array_memory_size() > max_bytes {
1188 return Err(DataFusionError::External(Box::new(Error::invalid_input(
1189 format!(
1190 "a single row is {} bytes which exceeds the maximum allowed batch \
1191 size of {} bytes",
1192 batch.get_array_memory_size(),
1193 max_bytes,
1194 ),
1195 ))));
1196 }
1197 Ok(batch)
1198 });
1199 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, validated)))
1200 }
1201
1202 fn maintains_input_order(&self) -> Vec<bool> {
1203 vec![true]
1204 }
1205
1206 fn benefits_from_input_partitioning(&self) -> Vec<bool> {
1207 vec![false]
1208 }
1209
1210 fn partition_statistics(
1211 &self,
1212 partition: Option<usize>,
1213 ) -> datafusion_common::Result<std::sync::Arc<Statistics>> {
1214 self.input.partition_statistics(partition)
1215 }
1216
1217 fn cardinality_effect(&self) -> CardinalityEffect {
1218 CardinalityEffect::Equal
1219 }
1220
1221 fn supports_limit_pushdown(&self) -> bool {
1222 true
1223 }
1224}
1225
1226#[cfg(test)]
1227mod tests {
1228 use super::*;
1229
1230 static CACHE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1232
1233 #[test]
1234 fn test_session_context_cache() {
1235 let _lock = CACHE_TEST_LOCK.lock().unwrap();
1236 let cache = get_session_cache();
1237
1238 cache.lock().unwrap().clear();
1240
1241 let opts1 = LanceExecutionOptions::default();
1243 let _ctx1 = get_session_context(&opts1);
1244
1245 {
1246 let cache_guard = cache.lock().unwrap();
1247 assert_eq!(cache_guard.len(), 1);
1248 }
1249
1250 let _ctx1_again = get_session_context(&opts1);
1252 {
1253 let cache_guard = cache.lock().unwrap();
1254 assert_eq!(cache_guard.len(), 1);
1255 }
1256
1257 let opts2 = LanceExecutionOptions {
1259 use_spilling: true,
1260 ..Default::default()
1261 };
1262 let _ctx2 = get_session_context(&opts2);
1263 {
1264 let cache_guard = cache.lock().unwrap();
1265 assert_eq!(cache_guard.len(), 2);
1266 }
1267 }
1268
1269 #[test]
1270 fn test_session_context_cache_lru_eviction() {
1271 let _lock = CACHE_TEST_LOCK.lock().unwrap();
1272 let cache = get_session_cache();
1273
1274 cache.lock().unwrap().clear();
1276
1277 let configs: Vec<LanceExecutionOptions> = (0..4)
1279 .map(|i| LanceExecutionOptions {
1280 mem_pool_size: Some((i + 1) as u64 * 1024 * 1024),
1281 ..Default::default()
1282 })
1283 .collect();
1284
1285 for config in &configs {
1286 let _ctx = get_session_context(config);
1287 }
1288
1289 {
1290 let cache_guard = cache.lock().unwrap();
1291 assert_eq!(cache_guard.len(), 4);
1292 }
1293
1294 std::thread::sleep(std::time::Duration::from_millis(1));
1297 let _ctx = get_session_context(&configs[0]);
1298
1299 let opts5 = LanceExecutionOptions {
1301 mem_pool_size: Some(5 * 1024 * 1024),
1302 ..Default::default()
1303 };
1304 let _ctx5 = get_session_context(&opts5);
1305
1306 {
1307 let cache_guard = cache.lock().unwrap();
1308 assert_eq!(cache_guard.len(), 4);
1309
1310 let key0 = SessionContextCacheKey::from_options(&configs[0]);
1312 assert!(
1313 cache_guard.contains_key(&key0),
1314 "config[0] should still be cached after recent access"
1315 );
1316
1317 let key1 = SessionContextCacheKey::from_options(&configs[1]);
1319 assert!(
1320 !cache_guard.contains_key(&key1),
1321 "config[1] should have been evicted"
1322 );
1323
1324 let key5 = SessionContextCacheKey::from_options(&opts5);
1326 assert!(
1327 cache_guard.contains_key(&key5),
1328 "new config should be cached"
1329 );
1330 }
1331 }
1332
1333 #[test]
1334 fn test_mem_pool_size_scales_with_partitions() {
1335 let default_per_partition = DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION;
1336
1337 let opts = LanceExecutionOptions::default();
1339 assert_eq!(opts.mem_pool_size(), default_per_partition);
1340
1341 let opts = LanceExecutionOptions {
1343 target_partition: Some(4),
1344 ..Default::default()
1345 };
1346 assert_eq!(opts.mem_pool_size(), default_per_partition * 4);
1347
1348 let opts = LanceExecutionOptions {
1350 target_partition: Some(8),
1351 ..Default::default()
1352 };
1353 assert_eq!(opts.mem_pool_size(), default_per_partition * 8);
1354
1355 let opts = LanceExecutionOptions {
1357 mem_pool_size: Some(50 * 1024 * 1024),
1358 target_partition: Some(8),
1359 ..Default::default()
1360 };
1361 assert_eq!(opts.mem_pool_size(), 50 * 1024 * 1024);
1362 }
1363
1364 #[derive(Debug)]
1366 struct RequiredExtension;
1367
1368 #[derive(Debug)]
1372 struct NeedsExtensionExec {
1373 properties: Arc<PlanProperties>,
1374 executed: Arc<std::sync::atomic::AtomicBool>,
1378 }
1379
1380 impl NeedsExtensionExec {
1381 fn new(executed: Arc<std::sync::atomic::AtomicBool>) -> Self {
1382 let schema = Arc::new(ArrowSchema::empty());
1383 Self {
1384 properties: Arc::new(PlanProperties::new(
1385 EquivalenceProperties::new(schema),
1386 Partitioning::UnknownPartitioning(1),
1387 EmissionType::Incremental,
1388 Boundedness::Bounded,
1389 )),
1390 executed,
1391 }
1392 }
1393 }
1394
1395 impl DisplayAs for NeedsExtensionExec {
1396 fn fmt_as(&self, _t: DisplayFormatType, f: &mut Formatter) -> fmt::Result {
1397 write!(f, "NeedsExtensionExec")
1398 }
1399 }
1400
1401 impl ExecutionPlan for NeedsExtensionExec {
1402 fn name(&self) -> &str {
1403 "NeedsExtensionExec"
1404 }
1405 fn properties(&self) -> &Arc<PlanProperties> {
1406 &self.properties
1407 }
1408 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1409 vec![]
1410 }
1411 fn with_new_children(
1412 self: Arc<Self>,
1413 _children: Vec<Arc<dyn ExecutionPlan>>,
1414 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
1415 Ok(self)
1416 }
1417 fn execute(
1418 &self,
1419 _partition: usize,
1420 context: Arc<TaskContext>,
1421 ) -> datafusion_common::Result<SendableRecordBatchStream> {
1422 if context
1423 .session_config()
1424 .get_extension::<RequiredExtension>()
1425 .is_none()
1426 {
1427 return Err(DataFusionError::Execution(
1428 "missing required session-config extension".to_string(),
1429 ));
1430 }
1431 self.executed
1432 .store(true, std::sync::atomic::Ordering::SeqCst);
1433 let schema = self.schema();
1434 Ok(Box::pin(RecordBatchStreamAdapter::new(
1435 schema,
1436 stream::empty(),
1437 )))
1438 }
1439 }
1440
1441 #[tokio::test]
1446 async fn test_analyze_plan_uses_provided_task_context() {
1447 use std::sync::atomic::{AtomicBool, Ordering};
1448
1449 let executed = Arc::new(AtomicBool::new(false));
1450 let plan: Arc<dyn ExecutionPlan> = Arc::new(NeedsExtensionExec::new(executed.clone()));
1451
1452 let report = analyze_plan(plan.clone(), LanceExecutionOptions::default())
1457 .await
1458 .expect("AnalyzeExec swallows the node's execute error into an Ok report");
1459 assert!(
1460 report.contains("NeedsExtensionExec, metrics=[]"),
1461 "expected an empty, unexecuted NeedsExtensionExec node, got: {report}"
1462 );
1463 assert!(
1464 !executed.load(Ordering::SeqCst),
1465 "node must not execute successfully without the extension"
1466 );
1467
1468 let options = LanceExecutionOptions::default();
1470 let session_ctx = get_session_context(&options);
1471 let config = session_ctx
1472 .task_ctx()
1473 .session_config()
1474 .clone()
1475 .with_extension(Arc::new(RequiredExtension));
1476 let task_ctx = session_ctx.task_ctx();
1477 let task_ctx = Arc::new(TaskContext::new(
1478 task_ctx.task_id(),
1479 task_ctx.session_id(),
1480 config,
1481 task_ctx.scalar_functions().clone(),
1482 task_ctx.higher_order_functions().clone(),
1483 task_ctx.aggregate_functions().clone(),
1484 task_ctx.window_functions().clone(),
1485 task_ctx.runtime_env(),
1486 ));
1487 let report = analyze_plan_with_context(plan, options, Some(task_ctx))
1488 .await
1489 .expect("analyze should succeed when the extension is present");
1490 assert!(report.contains("NeedsExtensionExec"));
1491 assert!(
1494 executed.load(Ordering::SeqCst),
1495 "supplied context must be forwarded so the node executes"
1496 );
1497 }
1498}