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, utils::get_available_parallelism};
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 MAX_SORT_SPILL_RESERVATION_BYTES: u64 = 40 * 1024 * 1024;
312const DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE: u64 = 100 * 1024 * 1024 * 1024; impl LanceExecutionOptions {
315 fn effective_target_partition(&self) -> u64 {
323 self.target_partition
324 .unwrap_or_else(get_available_parallelism) as u64
325 }
326
327 pub fn mem_pool_size(&self) -> u64 {
328 let num_partitions = self.effective_target_partition();
329 self.mem_pool_size.unwrap_or_else(|| {
330 std::env::var("LANCE_MEM_POOL_SIZE")
331 .map(|s| match s.parse::<u64>() {
332 Ok(v) => v,
333 Err(e) => {
334 warn!("Failed to parse LANCE_MEM_POOL_SIZE: {}, using default", e);
335 DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION * num_partitions
336 }
337 })
338 .unwrap_or(DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION * num_partitions)
339 })
340 }
341
342 pub fn max_temp_directory_size(&self) -> u64 {
343 self.max_temp_directory_size.unwrap_or_else(|| {
344 std::env::var("LANCE_MAX_TEMP_DIRECTORY_SIZE")
345 .map(|s| match s.parse::<u64>() {
346 Ok(v) => v,
347 Err(e) => {
348 warn!(
349 "Failed to parse LANCE_MAX_TEMP_DIRECTORY_SIZE: {}, using default",
350 e
351 );
352 DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE
353 }
354 })
355 .unwrap_or(DEFAULT_LANCE_MAX_TEMP_DIRECTORY_SIZE)
356 })
357 }
358
359 pub fn use_spilling(&self) -> bool {
360 if !self.use_spilling {
361 return false;
362 }
363 std::env::var("LANCE_BYPASS_SPILLING")
365 .map(|_| {
366 info!("Bypassing spilling because LANCE_BYPASS_SPILLING is set");
367 false
368 })
369 .unwrap_or(true)
370 }
371}
372
373pub fn new_session_context(options: &LanceExecutionOptions) -> SessionContext {
374 let mut session_config = SessionConfig::new();
375 let mut runtime_env_builder = RuntimeEnvBuilder::new();
376 if let Some(target_partition) = options.target_partition {
377 session_config = session_config.with_target_partitions(target_partition);
378 }
379 if options.use_spilling() {
380 let sort_spill_reservation_bytes =
385 (options.mem_pool_size() / 3).min(MAX_SORT_SPILL_RESERVATION_BYTES) as usize;
386 session_config =
387 session_config.with_sort_spill_reservation_bytes(sort_spill_reservation_bytes);
388 let disk_manager_builder = DiskManagerBuilder::default()
389 .with_max_temp_directory_size(options.max_temp_directory_size());
390 runtime_env_builder = runtime_env_builder
391 .with_disk_manager_builder(disk_manager_builder)
392 .with_memory_pool(Arc::new(TrackConsumersPool::new(
393 FairSpillPool::new(options.mem_pool_size() as usize),
394 NonZero::try_from(16).unwrap(),
395 )));
396 }
397 let runtime_env = runtime_env_builder.build_arc().unwrap();
400
401 let ctx = SessionContext::new_with_config_rt(session_config, runtime_env);
402 register_functions(&ctx);
403
404 ctx
405}
406
407#[derive(Clone, Debug, PartialEq, Eq, Hash)]
409struct SessionContextCacheKey {
410 mem_pool_size: u64,
411 max_temp_directory_size: u64,
412 target_partition: Option<usize>,
413}
414
415impl SessionContextCacheKey {
416 fn from_options(options: &LanceExecutionOptions) -> Self {
417 Self {
418 mem_pool_size: options.mem_pool_size(),
419 max_temp_directory_size: options.max_temp_directory_size(),
420 target_partition: options.target_partition,
421 }
422 }
423}
424
425struct CachedSessionContext {
426 context: SessionContext,
427 last_access: std::time::Instant,
428}
429
430fn get_session_cache() -> &'static Mutex<HashMap<SessionContextCacheKey, CachedSessionContext>> {
431 static SESSION_CACHE: OnceLock<Mutex<HashMap<SessionContextCacheKey, CachedSessionContext>>> =
432 OnceLock::new();
433 SESSION_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
434}
435
436fn get_max_cache_size() -> usize {
437 const DEFAULT_CACHE_SIZE: usize = 4;
438 static MAX_CACHE_SIZE: OnceLock<usize> = OnceLock::new();
439 *MAX_CACHE_SIZE.get_or_init(|| {
440 std::env::var("LANCE_SESSION_CACHE_SIZE")
441 .ok()
442 .and_then(|v| v.parse().ok())
443 .unwrap_or(DEFAULT_CACHE_SIZE)
444 })
445}
446
447pub fn get_session_context(options: &LanceExecutionOptions) -> SessionContext {
450 if options.use_spilling() {
451 return new_session_context(options);
452 }
453
454 let key = SessionContextCacheKey::from_options(options);
455 let mut cache = get_session_cache()
456 .lock()
457 .unwrap_or_else(|e| e.into_inner());
458
459 if let Some(entry) = cache.get_mut(&key) {
461 entry.last_access = std::time::Instant::now();
462 return entry.context.clone();
463 }
464
465 if cache.len() >= get_max_cache_size()
467 && let Some(lru_key) = cache
468 .iter()
469 .min_by_key(|(_, v)| v.last_access)
470 .map(|(k, _)| k.clone())
471 {
472 cache.remove(&lru_key);
473 }
474
475 let context = new_session_context(options);
476 cache.insert(
477 key,
478 CachedSessionContext {
479 context: context.clone(),
480 last_access: std::time::Instant::now(),
481 },
482 );
483 context
484}
485
486fn get_task_context(
492 session_ctx: &SessionContext,
493 options: &LanceExecutionOptions,
494) -> Arc<TaskContext> {
495 let task_ctx = TaskContext::from(session_ctx);
499 let Some(batch_size) = options.batch_size else {
500 return Arc::new(task_ctx);
501 };
502 let mut session_config = task_ctx.session_config().clone();
503 session_config.options_mut().execution.batch_size = batch_size;
504 Arc::new(task_ctx.with_session_config(session_config))
505}
506
507#[derive(Default, Clone, Debug, PartialEq, Eq)]
508pub struct ExecutionSummaryCounts {
509 pub iops: usize,
511 pub requests: usize,
514 pub bytes_read: usize,
516 pub indices_loaded: usize,
518 pub parts_loaded: usize,
520 pub index_comparisons: usize,
522 pub all_counts: HashMap<String, usize>,
529 pub all_times: HashMap<String, usize>,
535}
536
537impl ExecutionSummaryCounts {
538 pub fn index_cache_hits(&self) -> usize {
566 self.all_counts
567 .get(INDEX_CACHE_HITS_METRIC)
568 .copied()
569 .unwrap_or(0)
570 }
571
572 pub fn index_cache_misses(&self) -> usize {
581 self.all_counts
582 .get(INDEX_CACHE_MISSES_METRIC)
583 .copied()
584 .unwrap_or(0)
585 }
586
587 pub fn index_cache_hit_ratio(&self) -> f32 {
597 let hits = self.index_cache_hits() as u128;
600 let total = hits + self.index_cache_misses() as u128;
601 if total == 0 {
602 0.0
603 } else {
604 hits as f32 / total as f32
605 }
606 }
607}
608
609pub fn collect_execution_metrics(node: &dyn ExecutionPlan, counts: &mut ExecutionSummaryCounts) {
610 if let Some(metrics) = node.metrics() {
611 for (metric_name, count) in metrics.iter_counts() {
612 match metric_name.as_ref() {
613 IOPS_METRIC => counts.iops += count.value(),
614 REQUESTS_METRIC => counts.requests += count.value(),
615 BYTES_READ_METRIC => counts.bytes_read += count.value(),
616 INDICES_LOADED_METRIC => counts.indices_loaded += count.value(),
617 PARTS_LOADED_METRIC => counts.parts_loaded += count.value(),
618 INDEX_COMPARISONS_METRIC => counts.index_comparisons += count.value(),
619 _ => {
620 let existing = counts
621 .all_counts
622 .entry(metric_name.as_ref().to_string())
623 .or_insert(0);
624 *existing += count.value();
625 }
626 }
627 }
628 for (metric_name, time) in metrics.iter_times() {
629 let existing = counts
630 .all_times
631 .entry(metric_name.as_ref().to_string())
632 .or_insert(0);
633 *existing += time.value();
634 }
635 if let Some(elapsed) = metrics.elapsed_compute() {
638 *counts
639 .all_times
640 .entry(format!("{}_elapsed_compute", node.name()))
641 .or_default() += elapsed;
642 }
643 for (metric_name, gauge) in metrics.iter_gauges() {
645 match metric_name.as_ref() {
646 IOPS_METRIC => counts.iops += gauge.value(),
647 REQUESTS_METRIC => counts.requests += gauge.value(),
648 BYTES_READ_METRIC => counts.bytes_read += gauge.value(),
649 _ => {}
650 }
651 }
652 }
653 for child in node.children() {
654 collect_execution_metrics(child.as_ref(), counts);
655 }
656}
657
658fn report_plan_summary_metrics(plan: &dyn ExecutionPlan, options: &LanceExecutionOptions) {
659 let output_rows = plan
660 .metrics()
661 .map(|m| m.output_rows().unwrap_or(0))
662 .unwrap_or(0);
663 let mut counts = ExecutionSummaryCounts::default();
664 collect_execution_metrics(plan, &mut counts);
665 if !options.skip_logging {
666 tracing::info!(
667 target: TRACE_EXECUTION,
668 r#type = EXECUTION_PLAN_RUN,
669 plan_summary = display_plan_one_liner(plan),
670 output_rows,
671 iops = counts.iops,
672 requests = counts.requests,
673 bytes_read = counts.bytes_read,
674 indices_loaded = counts.indices_loaded,
675 parts_loaded = counts.parts_loaded,
676 index_comparisons = counts.index_comparisons,
677 index_cache_hits = counts.index_cache_hits(),
678 index_cache_misses = counts.index_cache_misses(),
679 );
680 }
681 if let Some(callback) = options.execution_stats_callback.as_ref() {
682 callback(&counts);
683 }
684}
685
686fn display_plan_one_liner(plan: &dyn ExecutionPlan) -> String {
693 let mut output = String::new();
694
695 display_plan_one_liner_impl(plan, &mut output);
696
697 output
698}
699
700fn display_plan_one_liner_impl(plan: &dyn ExecutionPlan, output: &mut String) {
701 let name = plan.name().trim_end_matches("Exec");
703 output.push_str(name);
704
705 let children = plan.children();
706 if !children.is_empty() {
707 output.push('(');
708 for (i, child) in children.iter().enumerate() {
709 if i > 0 {
710 output.push(',');
711 }
712 display_plan_one_liner_impl(child.as_ref(), output);
713 }
714 output.push(')');
715 }
716}
717
718pub fn execute_plan(
722 plan: Arc<dyn ExecutionPlan>,
723 options: LanceExecutionOptions,
724) -> Result<SendableRecordBatchStream> {
725 if !options.skip_logging {
726 debug!(
727 "Executing plan:\n{}",
728 DisplayableExecutionPlan::new(plan.as_ref()).indent(true)
729 );
730 }
731
732 let session_ctx = get_session_context(&options);
733
734 let plan: Arc<dyn ExecutionPlan> = if plan.properties().partitioning.partition_count() == 1 {
745 plan
746 } else if let Some(ordering) = plan.output_ordering() {
747 Arc::new(SortPreservingMergeExec::new(ordering.clone(), plan))
748 } else {
749 Arc::new(CoalescePartitionsExec::new(plan))
750 };
751
752 let stream = plan.execute(0, get_task_context(&session_ctx, &options))?;
753
754 let schema = stream.schema();
755 let stream = stream.finally(move || {
756 if !options.skip_logging || options.execution_stats_callback.is_some() {
757 report_plan_summary_metrics(plan.as_ref(), &options);
758 }
759 });
760 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
761}
762
763pub async fn analyze_plan(
764 plan: Arc<dyn ExecutionPlan>,
765 options: LanceExecutionOptions,
766) -> Result<String> {
767 analyze_plan_with_context(plan, options, None).await
768}
769
770pub async fn analyze_plan_with_context(
779 plan: Arc<dyn ExecutionPlan>,
780 options: LanceExecutionOptions,
781 task_context: Option<Arc<TaskContext>>,
782) -> Result<String> {
783 let plan = Arc::new(TracedExec::new(plan, Span::current()));
786
787 let schema = plan.schema();
788 let analyze = Arc::new(AnalyzeExec::new(
790 true,
791 true,
792 vec![MetricType::Summary],
793 None,
794 plan,
795 schema,
796 ));
797
798 let session_ctx = get_session_context(&options);
799 let task_context = task_context.unwrap_or_else(|| get_task_context(&session_ctx, &options));
800 assert_eq!(analyze.properties().partitioning.partition_count(), 1);
801 let mut stream = analyze
802 .execute(0, task_context)
803 .map_err(|err| Error::io(format!("Failed to execute analyze plan: {}", err)))?;
804
805 while (stream.next().await).is_some() {}
807
808 let result = format_plan(analyze);
809 Ok(result)
810}
811
812pub fn format_plan(plan: Arc<dyn ExecutionPlan>) -> String {
813 struct CalculateVisitor {
815 highest_index: usize,
816 index_to_elapsed: HashMap<usize, Duration>,
817 }
818
819 struct SubtreeMetrics {
821 min_start: Option<DateTime<Utc>>,
822 max_end: Option<DateTime<Utc>>,
823 }
824
825 impl CalculateVisitor {
826 fn calculate_metrics(&mut self, plan: &Arc<dyn ExecutionPlan>) -> SubtreeMetrics {
827 self.highest_index += 1;
828 let plan_index = self.highest_index;
829
830 let (mut min_start, mut max_end) = Self::node_timerange(plan);
832
833 for child in plan.children() {
835 let child_metrics = self.calculate_metrics(child);
836 min_start = Self::min_option(min_start, child_metrics.min_start);
837 max_end = Self::max_option(max_end, child_metrics.max_end);
838 }
839
840 let elapsed = match (min_start, max_end) {
842 (Some(start), Some(end)) => Some((end - start).to_std().unwrap_or_default()),
843 _ => None,
844 };
845
846 if let Some(e) = elapsed {
847 self.index_to_elapsed.insert(plan_index, e);
848 }
849
850 SubtreeMetrics { min_start, max_end }
851 }
852
853 fn node_timerange(
854 plan: &Arc<dyn ExecutionPlan>,
855 ) -> (Option<DateTime<Utc>>, Option<DateTime<Utc>>) {
856 let Some(metrics) = plan.metrics() else {
857 return (None, None);
858 };
859 let min_start = metrics
860 .iter()
861 .filter_map(|m| match m.value() {
862 MetricValue::StartTimestamp(ts) => ts.value(),
863 _ => None,
864 })
865 .min();
866 let max_end = metrics
867 .iter()
868 .filter_map(|m| match m.value() {
869 MetricValue::EndTimestamp(ts) => ts.value(),
870 _ => None,
871 })
872 .max();
873 (min_start, max_end)
874 }
875
876 fn min_option(a: Option<DateTime<Utc>>, b: Option<DateTime<Utc>>) -> Option<DateTime<Utc>> {
877 [a, b].into_iter().flatten().min()
878 }
879
880 fn max_option(a: Option<DateTime<Utc>>, b: Option<DateTime<Utc>>) -> Option<DateTime<Utc>> {
881 [a, b].into_iter().flatten().max()
882 }
883 }
884
885 struct PrintVisitor {
887 highest_index: usize,
888 indent: usize,
889 }
890 impl PrintVisitor {
891 fn write_output(
892 &mut self,
893 plan: &Arc<dyn ExecutionPlan>,
894 f: &mut Formatter,
895 calcs: &CalculateVisitor,
896 ) -> std::fmt::Result {
897 self.highest_index += 1;
898 write!(f, "{:indent$}", "", indent = self.indent * 2)?;
899
900 let displayable =
902 datafusion::physical_plan::display::DisplayableExecutionPlan::new(plan.as_ref());
903 let plan_str = displayable.one_line().to_string();
904 let plan_str = plan_str.trim();
905
906 match calcs.index_to_elapsed.get(&self.highest_index) {
908 Some(elapsed) => match plan_str.find(": ") {
909 Some(i) => write!(
910 f,
911 "{}: elapsed={elapsed:?}, {}",
912 &plan_str[..i],
913 &plan_str[i + 2..]
914 )?,
915 None => write!(f, "{plan_str}, elapsed={elapsed:?}")?,
916 },
917 None => write!(f, "{plan_str}")?,
918 }
919
920 if let Some(metrics) = plan.metrics() {
921 let metrics = metrics
922 .aggregate_by_name()
923 .sorted_for_display()
924 .timestamps_removed();
925
926 write!(f, ", metrics=[{metrics}]")?;
927 } else {
928 write!(f, ", metrics=[]")?;
929 }
930 writeln!(f)?;
931 self.indent += 1;
932 for child in plan.children() {
933 self.write_output(child, f, calcs)?;
934 }
935 self.indent -= 1;
936 std::fmt::Result::Ok(())
937 }
938 }
939 struct PrintWrapper {
941 plan: Arc<dyn ExecutionPlan>,
942 }
943 impl fmt::Display for PrintWrapper {
944 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
945 let mut calcs = CalculateVisitor {
946 highest_index: 0,
947 index_to_elapsed: HashMap::new(),
948 };
949 calcs.calculate_metrics(&self.plan);
950 let mut prints = PrintVisitor {
951 highest_index: 0,
952 indent: 0,
953 };
954 prints.write_output(&self.plan, f, &calcs)
955 }
956 }
957 let wrapper = PrintWrapper { plan };
958 format!("{}", wrapper)
959}
960
961pub trait SessionContextExt {
962 fn read_one_shot(
966 &self,
967 data: SendableRecordBatchStream,
968 ) -> datafusion::common::Result<DataFrame>;
969}
970
971pub struct OneShotPartitionStream {
972 data: Arc<Mutex<Option<SendableRecordBatchStream>>>,
973 schema: Arc<ArrowSchema>,
974}
975
976impl std::fmt::Debug for OneShotPartitionStream {
977 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
978 let data = self.data.lock().unwrap();
979 f.debug_struct("OneShotPartitionStream")
980 .field("exhausted", &data.is_none())
981 .field("schema", self.schema.as_ref())
982 .finish()
983 }
984}
985
986impl OneShotPartitionStream {
987 pub fn new(data: SendableRecordBatchStream) -> Self {
988 let schema = data.schema();
989 Self {
990 data: Arc::new(Mutex::new(Some(data))),
991 schema,
992 }
993 }
994}
995
996impl PartitionStream for OneShotPartitionStream {
997 fn schema(&self) -> &arrow_schema::SchemaRef {
998 &self.schema
999 }
1000
1001 fn execute(&self, _ctx: Arc<TaskContext>) -> SendableRecordBatchStream {
1002 let mut stream = self.data.lock().unwrap();
1003 stream
1004 .take()
1005 .expect("Attempt to consume a one shot dataframe multiple times")
1006 }
1007}
1008
1009impl SessionContextExt for SessionContext {
1010 fn read_one_shot(
1011 &self,
1012 data: SendableRecordBatchStream,
1013 ) -> datafusion::common::Result<DataFrame> {
1014 let schema = data.schema();
1015 let part_stream = Arc::new(OneShotPartitionStream::new(data));
1016 let provider = StreamingTable::try_new(schema, vec![part_stream])?;
1017 self.read_table(Arc::new(provider))
1018 }
1019}
1020
1021pub async fn provider_to_stream(
1051 provider: Arc<dyn TableProvider>,
1052) -> Result<SendableRecordBatchStream> {
1053 let ctx = SessionContext::new();
1054 let plan = provider.scan(&ctx.state(), None, &[], None).await?;
1055 let plan: Arc<dyn ExecutionPlan> =
1056 if plan.properties().output_partitioning().partition_count() > 1 {
1057 Arc::new(CoalescePartitionsExec::new(plan))
1058 } else {
1059 plan
1060 };
1061 Ok(plan.execute(0, ctx.task_ctx())?)
1062}
1063
1064#[derive(Clone, Debug)]
1065pub struct StrictBatchSizeExec {
1066 input: Arc<dyn ExecutionPlan>,
1067 batch_size: usize,
1068}
1069
1070impl StrictBatchSizeExec {
1071 pub fn new(input: Arc<dyn ExecutionPlan>, batch_size: usize) -> Self {
1072 Self { input, batch_size }
1073 }
1074}
1075
1076impl DisplayAs for StrictBatchSizeExec {
1077 fn fmt_as(
1078 &self,
1079 _t: datafusion::physical_plan::DisplayFormatType,
1080 f: &mut std::fmt::Formatter,
1081 ) -> std::fmt::Result {
1082 write!(f, "StrictBatchSizeExec")
1083 }
1084}
1085
1086impl ExecutionPlan for StrictBatchSizeExec {
1087 fn name(&self) -> &str {
1088 "StrictBatchSizeExec"
1089 }
1090
1091 fn properties(&self) -> &Arc<PlanProperties> {
1092 self.input.properties()
1093 }
1094
1095 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1096 vec![&self.input]
1097 }
1098
1099 fn with_new_children(
1100 self: Arc<Self>,
1101 children: Vec<Arc<dyn ExecutionPlan>>,
1102 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
1103 Ok(Arc::new(Self {
1104 input: children[0].clone(),
1105 batch_size: self.batch_size,
1106 }))
1107 }
1108
1109 fn execute(
1110 &self,
1111 partition: usize,
1112 context: Arc<TaskContext>,
1113 ) -> datafusion_common::Result<SendableRecordBatchStream> {
1114 let stream = self.input.execute(partition, context)?;
1115 let schema = stream.schema();
1116 let stream = StrictBatchSizeStream::new(stream, self.batch_size);
1117 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, stream)))
1118 }
1119
1120 fn maintains_input_order(&self) -> Vec<bool> {
1121 vec![true]
1122 }
1123
1124 fn benefits_from_input_partitioning(&self) -> Vec<bool> {
1125 vec![false]
1126 }
1127
1128 fn partition_statistics(
1129 &self,
1130 partition: Option<usize>,
1131 ) -> datafusion_common::Result<std::sync::Arc<Statistics>> {
1132 self.input.partition_statistics(partition)
1133 }
1134
1135 fn cardinality_effect(&self) -> CardinalityEffect {
1136 CardinalityEffect::Equal
1137 }
1138
1139 fn supports_limit_pushdown(&self) -> bool {
1140 true
1141 }
1142}
1143
1144#[derive(Clone, Debug)]
1169pub struct HardCapBatchSizeExec {
1170 input: Arc<dyn ExecutionPlan>,
1171 max_bytes: usize,
1172}
1173
1174impl HardCapBatchSizeExec {
1175 pub fn new(input: Arc<dyn ExecutionPlan>, max_bytes: usize) -> Self {
1176 Self { input, max_bytes }
1177 }
1178}
1179
1180impl DisplayAs for HardCapBatchSizeExec {
1181 fn fmt_as(
1182 &self,
1183 _t: datafusion::physical_plan::DisplayFormatType,
1184 f: &mut std::fmt::Formatter,
1185 ) -> std::fmt::Result {
1186 write!(f, "HardCapBatchSizeExec(max_bytes={})", self.max_bytes)
1187 }
1188}
1189
1190impl ExecutionPlan for HardCapBatchSizeExec {
1191 fn name(&self) -> &str {
1192 "HardCapBatchSizeExec"
1193 }
1194
1195 fn properties(&self) -> &Arc<PlanProperties> {
1196 self.input.properties()
1197 }
1198
1199 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1200 vec![&self.input]
1201 }
1202
1203 fn with_new_children(
1204 self: Arc<Self>,
1205 children: Vec<Arc<dyn ExecutionPlan>>,
1206 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
1207 Ok(Arc::new(Self {
1208 input: children[0].clone(),
1209 max_bytes: self.max_bytes,
1210 }))
1211 }
1212
1213 fn execute(
1214 &self,
1215 partition: usize,
1216 context: Arc<TaskContext>,
1217 ) -> datafusion_common::Result<SendableRecordBatchStream> {
1218 let stream = self.input.execute(partition, context)?;
1219 let schema = stream.schema();
1220 let max_bytes = self.max_bytes;
1221 let rechunked = lance_arrow::stream::rechunk_stream_by_size_deep_copy(
1222 stream,
1223 schema.clone(),
1224 0,
1225 max_bytes,
1226 );
1227 let validated = rechunked.map(move |result| {
1229 let batch = result?;
1230 if batch.num_rows() == 1 && batch.get_array_memory_size() > max_bytes {
1231 return Err(DataFusionError::External(Box::new(Error::invalid_input(
1232 format!(
1233 "a single row is {} bytes which exceeds the maximum allowed batch \
1234 size of {} bytes",
1235 batch.get_array_memory_size(),
1236 max_bytes,
1237 ),
1238 ))));
1239 }
1240 Ok(batch)
1241 });
1242 Ok(Box::pin(RecordBatchStreamAdapter::new(schema, validated)))
1243 }
1244
1245 fn maintains_input_order(&self) -> Vec<bool> {
1246 vec![true]
1247 }
1248
1249 fn benefits_from_input_partitioning(&self) -> Vec<bool> {
1250 vec![false]
1251 }
1252
1253 fn partition_statistics(
1254 &self,
1255 partition: Option<usize>,
1256 ) -> datafusion_common::Result<std::sync::Arc<Statistics>> {
1257 self.input.partition_statistics(partition)
1258 }
1259
1260 fn cardinality_effect(&self) -> CardinalityEffect {
1261 CardinalityEffect::Equal
1262 }
1263
1264 fn supports_limit_pushdown(&self) -> bool {
1265 true
1266 }
1267}
1268
1269#[cfg(test)]
1270mod tests {
1271 use super::*;
1272 use datafusion::execution::memory_pool::MemoryConsumer;
1273
1274 use arrow_array::{Int32Array, cast::AsArray, record_batch, types::Int32Type};
1275 use arrow_select::concat::concat_batches;
1276 use datafusion::execution::memory_pool::MemoryLimit;
1277 use datafusion::physical_plan::sorts::sort::SortExec;
1278 use datafusion_physical_expr::{LexOrdering, PhysicalSortExpr, expressions::col};
1279 use futures::TryStreamExt;
1280 use rstest::rstest;
1281
1282 static CACHE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
1284
1285 #[test]
1286 fn test_session_context_cache() {
1287 let _lock = CACHE_TEST_LOCK.lock().unwrap();
1288 let cache = get_session_cache();
1289
1290 cache.lock().unwrap().clear();
1292
1293 let opts1 = LanceExecutionOptions::default();
1295 let _ctx1 = get_session_context(&opts1);
1296
1297 {
1298 let cache_guard = cache.lock().unwrap();
1299 assert_eq!(cache_guard.len(), 1);
1300 }
1301
1302 let _ctx1_again = get_session_context(&opts1);
1304 {
1305 let cache_guard = cache.lock().unwrap();
1306 assert_eq!(cache_guard.len(), 1);
1307 }
1308
1309 let opts2 = LanceExecutionOptions {
1311 target_partition: Some(1),
1312 ..Default::default()
1313 };
1314 let _ctx2 = get_session_context(&opts2);
1315 {
1316 let cache_guard = cache.lock().unwrap();
1317 assert_eq!(cache_guard.len(), 2);
1318 }
1319 }
1320
1321 #[test]
1322 fn test_spilling_executions_have_independent_memory_pools() {
1323 let options = LanceExecutionOptions {
1324 use_spilling: true,
1325 mem_pool_size: Some(DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION),
1326 target_partition: Some(1),
1327 ..Default::default()
1328 };
1329 let contexts: Vec<_> = (0..3).map(|_| get_session_context(&options)).collect();
1330 let mut reservations = Vec::with_capacity(contexts.len());
1331
1332 for context in &contexts {
1334 let task_context = context.task_ctx();
1335 let pool = task_context.memory_pool();
1336 let reservation = MemoryConsumer::new("ExternalSorterMerge[0]").register(pool);
1337 reservation.try_grow(100 * 1024 * 1024).unwrap();
1338 assert_eq!(pool.reserved(), 100 * 1024 * 1024);
1339 reservations.push(reservation);
1340 }
1341
1342 assert_eq!(reservations.len(), 3);
1343 }
1344
1345 #[test]
1346 fn test_session_context_cache_lru_eviction() {
1347 let _lock = CACHE_TEST_LOCK.lock().unwrap();
1348 let cache = get_session_cache();
1349
1350 cache.lock().unwrap().clear();
1352
1353 let configs: Vec<LanceExecutionOptions> = (0..4)
1355 .map(|i| LanceExecutionOptions {
1356 mem_pool_size: Some((i + 1) as u64 * 1024 * 1024),
1357 ..Default::default()
1358 })
1359 .collect();
1360
1361 for config in &configs {
1362 let _ctx = get_session_context(config);
1363 }
1364
1365 {
1366 let cache_guard = cache.lock().unwrap();
1367 assert_eq!(cache_guard.len(), 4);
1368 }
1369
1370 std::thread::sleep(std::time::Duration::from_millis(1));
1373 let _ctx = get_session_context(&configs[0]);
1374
1375 let opts5 = LanceExecutionOptions {
1377 mem_pool_size: Some(5 * 1024 * 1024),
1378 ..Default::default()
1379 };
1380 let _ctx5 = get_session_context(&opts5);
1381
1382 {
1383 let cache_guard = cache.lock().unwrap();
1384 assert_eq!(cache_guard.len(), 4);
1385
1386 let key0 = SessionContextCacheKey::from_options(&configs[0]);
1388 assert!(
1389 cache_guard.contains_key(&key0),
1390 "config[0] should still be cached after recent access"
1391 );
1392
1393 let key1 = SessionContextCacheKey::from_options(&configs[1]);
1395 assert!(
1396 !cache_guard.contains_key(&key1),
1397 "config[1] should have been evicted"
1398 );
1399
1400 let key5 = SessionContextCacheKey::from_options(&opts5);
1402 assert!(
1403 cache_guard.contains_key(&key5),
1404 "new config should be cached"
1405 );
1406 }
1407 }
1408
1409 #[test]
1410 fn test_task_context_per_execution() {
1411 let _lock = CACHE_TEST_LOCK.lock().unwrap();
1412
1413 let default_options = LanceExecutionOptions::default();
1414 let session_ctx = get_session_context(&default_options);
1415 let task_ctx = get_task_context(&session_ctx, &default_options);
1416 assert!(!Arc::ptr_eq(
1418 &task_ctx,
1419 &get_task_context(&session_ctx, &default_options)
1420 ));
1421 assert_eq!(task_ctx.session_id(), session_ctx.session_id());
1422 assert_eq!(
1423 task_ctx.session_config().batch_size(),
1424 session_ctx.copied_config().batch_size()
1425 );
1426 assert!(Arc::ptr_eq(
1427 &task_ctx.runtime_env(),
1428 &session_ctx.runtime_env()
1429 ));
1430 assert!(task_ctx.scalar_functions().contains_key("contains_tokens"));
1431
1432 let spill_options = LanceExecutionOptions {
1433 use_spilling: true,
1434 mem_pool_size: Some(64 * 1024 * 1024),
1435 target_partition: Some(3),
1436 batch_size: Some(17),
1437 ..Default::default()
1438 };
1439 let spill_session_ctx = get_session_context(&spill_options);
1440 let spill_task_ctx = get_task_context(&spill_session_ctx, &spill_options);
1441 assert_eq!(spill_task_ctx.session_config().batch_size(), 17);
1442 assert_eq!(spill_task_ctx.session_config().target_partitions(), 3);
1443 assert!(
1444 spill_task_ctx
1445 .scalar_functions()
1446 .contains_key("contains_tokens")
1447 );
1448 assert_ne!(spill_session_ctx.copied_config().batch_size(), 17);
1451 assert!(Arc::ptr_eq(
1452 &spill_task_ctx.runtime_env(),
1453 &spill_session_ctx.runtime_env()
1454 ));
1455 assert!(matches!(
1456 spill_task_ctx.memory_pool().memory_limit(),
1457 MemoryLimit::Finite(limit) if limit == 64 * 1024 * 1024
1458 ));
1459 }
1460
1461 #[rstest]
1462 #[case::session_batch_size(None, &[6])]
1463 #[case::batch_size_override(Some(4), &[4, 2])]
1464 #[tokio::test]
1465 async fn test_execute_plan_batch_size(
1466 #[case] batch_size: Option<usize>,
1467 #[case] expected_batch_rows: &[usize],
1468 ) {
1469 let batch = record_batch!(("x", Int32, [5, 3, 1, 4, 2, 0])).unwrap();
1470 let sort_expr = PhysicalSortExpr::new_default(col("x", &batch.schema()).unwrap());
1471 let plan = Arc::new(SortExec::new(
1472 LexOrdering::new([sort_expr]).unwrap(),
1473 Arc::new(OneShotExec::from_batch(batch.clone())),
1474 ));
1475 let options = LanceExecutionOptions {
1476 batch_size,
1477 ..Default::default()
1478 };
1479 let batches: Vec<RecordBatch> = execute_plan(plan, options)
1480 .unwrap()
1481 .try_collect()
1482 .await
1483 .unwrap();
1484 let batch_rows: Vec<usize> = batches.iter().map(RecordBatch::num_rows).collect();
1485 assert_eq!(batch_rows, expected_batch_rows);
1486 let sorted = concat_batches(&batch.schema(), &batches).unwrap();
1487 assert_eq!(
1488 sorted["x"].as_primitive::<Int32Type>(),
1489 &Int32Array::from(vec![0, 1, 2, 3, 4, 5])
1490 );
1491 }
1492
1493 #[test]
1494 fn test_mem_pool_size_scales_with_partitions() {
1495 let default_per_partition = DEFAULT_LANCE_MEM_POOL_SIZE_PER_PARTITION;
1496
1497 let opts = LanceExecutionOptions::default();
1501 assert_eq!(
1502 opts.mem_pool_size(),
1503 default_per_partition * get_available_parallelism() as u64
1504 );
1505
1506 let opts = LanceExecutionOptions {
1508 target_partition: Some(4),
1509 ..Default::default()
1510 };
1511 assert_eq!(opts.mem_pool_size(), default_per_partition * 4);
1512
1513 let opts = LanceExecutionOptions {
1515 target_partition: Some(8),
1516 ..Default::default()
1517 };
1518 assert_eq!(opts.mem_pool_size(), default_per_partition * 8);
1519
1520 let opts = LanceExecutionOptions {
1522 mem_pool_size: Some(50 * 1024 * 1024),
1523 target_partition: Some(8),
1524 ..Default::default()
1525 };
1526 assert_eq!(opts.mem_pool_size(), 50 * 1024 * 1024);
1527 }
1528
1529 #[test]
1530 fn test_default_pool_fits_sort_merge_reservations_per_partition() {
1531 let options = LanceExecutionOptions {
1532 use_spilling: true,
1533 ..Default::default()
1534 };
1535 let num_partitions = options.effective_target_partition() as usize;
1536 let session_ctx = new_session_context(&options);
1537 let task_ctx = session_ctx.task_ctx();
1538 let pool = task_ctx.memory_pool();
1539
1540 let num_reservations = num_partitions * 3;
1546 let mut reservations = Vec::with_capacity(num_reservations);
1547 for _ in 0..num_reservations {
1548 let reservation = MemoryConsumer::new("ExternalSorterMerge[0]").register(pool);
1549 reservation
1550 .try_grow(MAX_SORT_SPILL_RESERVATION_BYTES as usize)
1551 .unwrap();
1552 reservations.push(reservation);
1553 }
1554
1555 assert_eq!(
1556 pool.reserved(),
1557 num_reservations * MAX_SORT_SPILL_RESERVATION_BYTES as usize
1558 );
1559 }
1560
1561 #[derive(Debug)]
1563 struct RequiredExtension;
1564
1565 #[derive(Debug)]
1569 struct NeedsExtensionExec {
1570 properties: Arc<PlanProperties>,
1571 executed: Arc<std::sync::atomic::AtomicBool>,
1575 }
1576
1577 impl NeedsExtensionExec {
1578 fn new(executed: Arc<std::sync::atomic::AtomicBool>) -> Self {
1579 let schema = Arc::new(ArrowSchema::empty());
1580 Self {
1581 properties: Arc::new(PlanProperties::new(
1582 EquivalenceProperties::new(schema),
1583 Partitioning::UnknownPartitioning(1),
1584 EmissionType::Incremental,
1585 Boundedness::Bounded,
1586 )),
1587 executed,
1588 }
1589 }
1590 }
1591
1592 impl DisplayAs for NeedsExtensionExec {
1593 fn fmt_as(&self, _t: DisplayFormatType, f: &mut Formatter) -> fmt::Result {
1594 write!(f, "NeedsExtensionExec")
1595 }
1596 }
1597
1598 impl ExecutionPlan for NeedsExtensionExec {
1599 fn name(&self) -> &str {
1600 "NeedsExtensionExec"
1601 }
1602 fn properties(&self) -> &Arc<PlanProperties> {
1603 &self.properties
1604 }
1605 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1606 vec![]
1607 }
1608 fn with_new_children(
1609 self: Arc<Self>,
1610 _children: Vec<Arc<dyn ExecutionPlan>>,
1611 ) -> datafusion_common::Result<Arc<dyn ExecutionPlan>> {
1612 Ok(self)
1613 }
1614 fn execute(
1615 &self,
1616 _partition: usize,
1617 context: Arc<TaskContext>,
1618 ) -> datafusion_common::Result<SendableRecordBatchStream> {
1619 if context
1620 .session_config()
1621 .get_extension::<RequiredExtension>()
1622 .is_none()
1623 {
1624 return Err(DataFusionError::Execution(
1625 "missing required session-config extension".to_string(),
1626 ));
1627 }
1628 self.executed
1629 .store(true, std::sync::atomic::Ordering::SeqCst);
1630 let schema = self.schema();
1631 Ok(Box::pin(RecordBatchStreamAdapter::new(
1632 schema,
1633 stream::empty(),
1634 )))
1635 }
1636 }
1637
1638 #[tokio::test]
1643 async fn test_analyze_plan_uses_provided_task_context() {
1644 use std::sync::atomic::{AtomicBool, Ordering};
1645
1646 let executed = Arc::new(AtomicBool::new(false));
1647 let plan: Arc<dyn ExecutionPlan> = Arc::new(NeedsExtensionExec::new(executed.clone()));
1648
1649 let report = analyze_plan(plan.clone(), LanceExecutionOptions::default())
1654 .await
1655 .expect("AnalyzeExec swallows the node's execute error into an Ok report");
1656 assert!(
1657 report.contains("NeedsExtensionExec, metrics=[]"),
1658 "expected an empty, unexecuted NeedsExtensionExec node, got: {report}"
1659 );
1660 assert!(
1661 !executed.load(Ordering::SeqCst),
1662 "node must not execute successfully without the extension"
1663 );
1664
1665 let options = LanceExecutionOptions::default();
1667 let session_ctx = get_session_context(&options);
1668 let config = session_ctx
1669 .task_ctx()
1670 .session_config()
1671 .clone()
1672 .with_extension(Arc::new(RequiredExtension));
1673 let task_ctx = session_ctx.task_ctx();
1674 let task_ctx = Arc::new(TaskContext::new(
1675 task_ctx.task_id(),
1676 task_ctx.session_id(),
1677 config,
1678 task_ctx.scalar_functions().clone(),
1679 task_ctx.higher_order_functions().clone(),
1680 task_ctx.aggregate_functions().clone(),
1681 task_ctx.window_functions().clone(),
1682 task_ctx.runtime_env(),
1683 ));
1684 let report = analyze_plan_with_context(plan, options, Some(task_ctx))
1685 .await
1686 .expect("analyze should succeed when the extension is present");
1687 assert!(report.contains("NeedsExtensionExec"));
1688 assert!(
1691 executed.load(Ordering::SeqCst),
1692 "supplied context must be forwarded so the node executes"
1693 );
1694 }
1695}