1mod cache;
21use cache::*;
22mod context;
23use context::*;
24mod task;
25
26use std::collections::HashMap;
27use std::future::Future;
28use std::sync::Arc;
29
30use arrow_array::RecordBatch;
31use futures::channel::mpsc::{Sender, channel};
32use futures::stream::BoxStream;
33use futures::{SinkExt, StreamExt, TryStreamExt};
34pub use task::*;
35
36use crate::arrow::ArrowReaderBuilder;
37pub use crate::arrow::{ScanMetrics, ScanResult};
38use crate::delete_file_index::DeleteFileIndex;
39use crate::error::invalid_data;
40use crate::expr::visitors::inclusive_metrics_evaluator::InclusiveMetricsEvaluator;
41use crate::expr::{Bind, BoundPredicate, Predicate};
42use crate::io::FileIO;
43use crate::metadata_columns::{
44 RESERVED_FIELD_ID_PARTITION, get_metadata_field_id, is_metadata_column_name,
45};
46use crate::runtime::Runtime;
47use crate::spec::{DataContentType, Schema, SchemaRef, SnapshotRef, SortOrderRef, StructType};
48use crate::table::Table;
49use crate::util::available_parallelism;
50use crate::util::snapshot::snapshot_id_as_of_time;
51use crate::{Error, ErrorKind, Result};
52
53pub type ArrowRecordBatchStream = BoxStream<'static, Result<RecordBatch>>;
55
56fn resolve_field_id(
58 schema: &Schema,
59 column_name: &str,
60 case_sensitive: bool,
61) -> Result<Option<i32>> {
62 if case_sensitive {
63 Ok(schema.field_id_by_name(column_name))
64 } else {
65 let field = schema.field_by_name_case_insensitive_checked(column_name)?;
66 Ok(field.map(|field| field.id))
67 }
68}
69
70fn collect_scan_field_ids(
71 schema: &Schema,
72 column_names: Option<&[String]>,
73 case_sensitive: bool,
74) -> Result<Vec<i32>> {
75 let Some(column_names) = column_names else {
76 return Ok(schema.as_struct().fields().iter().map(|f| f.id).collect());
77 };
78
79 column_names
80 .iter()
81 .map(|column_name| {
82 if is_metadata_column_name(column_name) {
83 return get_metadata_field_id(column_name);
84 }
85
86 let field_id = resolve_field_id(schema, column_name, case_sensitive)?.ok_or_else(|| {
87 invalid_data!("Column {column_name} not found in table. Schema: {schema}")
88 })?;
89
90 schema
91 .as_struct()
92 .field_by_id(field_id)
93 .ok_or_else(|| {
94 Error::new(
95 ErrorKind::FeatureUnsupported,
96 format!(
97 "Column {column_name} is not a direct child of schema but a nested field, which is not supported now. Schema: {schema}"
98 ),
99 )
100 })?;
101
102 Ok(field_id)
103 })
104 .collect()
105}
106
107fn bind_scan_predicate(
108 schema: &SchemaRef,
109 predicate: Option<&Predicate>,
110 case_sensitive: bool,
111) -> Result<Option<Arc<BoundPredicate>>> {
112 predicate
113 .map(|predicate| predicate.bind(schema.clone(), case_sensitive))
114 .transpose()
115 .map(|predicate| predicate.map(Arc::new))
116}
117
118fn projected_partition_type(
119 table: &Table,
120 schema: &Schema,
121 field_ids: &[i32],
122) -> Result<Option<Arc<StructType>>> {
123 if !field_ids.contains(&RESERVED_FIELD_ID_PARTITION) {
124 return Ok(None);
125 }
126
127 table
128 .metadata()
129 .unified_partition_type(schema)
130 .map(Arc::new)
131 .map(Some)
132}
133
134enum SnapshotSelection {
136 SnapshotId(i64),
137 AsOfTime(i64),
138}
139
140pub struct TableScanBuilder<'a> {
142 table: &'a Table,
143 column_names: Option<Vec<String>>,
145 snapshot_selection: Option<SnapshotSelection>,
146 batch_size: Option<usize>,
147 case_sensitive: bool,
148 filter: Option<Predicate>,
149 concurrency_limit_data_files: usize,
150 concurrency_limit_manifest_entries: usize,
151 concurrency_limit_manifest_files: usize,
152 row_group_filtering_enabled: bool,
153 row_selection_enabled: bool,
154 bloom_filter_enabled: bool,
155}
156
157impl<'a> TableScanBuilder<'a> {
158 pub(crate) fn new(table: &'a Table) -> Self {
159 let num_cpus = available_parallelism().get();
160
161 Self {
162 table,
163 column_names: None,
164 snapshot_selection: None,
165 batch_size: None,
166 case_sensitive: true,
167 filter: None,
168 concurrency_limit_data_files: num_cpus,
169 concurrency_limit_manifest_entries: num_cpus,
170 concurrency_limit_manifest_files: num_cpus,
171 row_group_filtering_enabled: true,
172 row_selection_enabled: false,
173 bloom_filter_enabled: false,
174 }
175 }
176
177 pub fn with_batch_size(mut self, batch_size: Option<usize>) -> Self {
180 self.batch_size = batch_size;
181 self
182 }
183
184 pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
186 self.case_sensitive = case_sensitive;
187 self
188 }
189
190 pub fn with_filter(mut self, predicate: Predicate) -> Self {
192 self.filter = Some(predicate.rewrite_not());
195 self
196 }
197
198 pub fn select_all(mut self) -> Self {
200 self.column_names = None;
201 self
202 }
203
204 pub fn select_empty(mut self) -> Self {
206 self.column_names = Some(vec![]);
207 self
208 }
209
210 pub fn select(mut self, column_names: impl IntoIterator<Item = impl ToString>) -> Self {
212 self.column_names = Some(
213 column_names
214 .into_iter()
215 .map(|item| item.to_string())
216 .collect(),
217 );
218 self
219 }
220
221 pub fn snapshot_id(mut self, snapshot_id: i64) -> Self {
228 self.snapshot_selection = Some(SnapshotSelection::SnapshotId(snapshot_id));
229 self
230 }
231
232 pub fn as_of_time(mut self, timestamp_ms: i64) -> Self {
253 self.snapshot_selection = Some(SnapshotSelection::AsOfTime(timestamp_ms));
254 self
255 }
256
257 pub fn with_concurrency_limit(mut self, limit: usize) -> Self {
260 self.concurrency_limit_manifest_files = limit;
261 self.concurrency_limit_manifest_entries = limit;
262 self.concurrency_limit_data_files = limit;
263 self
264 }
265
266 pub fn with_data_file_concurrency_limit(mut self, limit: usize) -> Self {
268 self.concurrency_limit_data_files = limit;
269 self
270 }
271
272 pub fn with_manifest_entry_concurrency_limit(mut self, limit: usize) -> Self {
274 self.concurrency_limit_manifest_entries = limit;
275 self
276 }
277
278 pub fn with_row_group_filtering_enabled(mut self, row_group_filtering_enabled: bool) -> Self {
287 self.row_group_filtering_enabled = row_group_filtering_enabled;
288 self
289 }
290
291 pub fn with_row_selection_enabled(mut self, row_selection_enabled: bool) -> Self {
306 self.row_selection_enabled = row_selection_enabled;
307 self
308 }
309
310 pub fn with_bloom_filter_enabled(mut self, bloom_filter_enabled: bool) -> Self {
320 self.bloom_filter_enabled = bloom_filter_enabled;
321 self
322 }
323
324 pub fn build(self) -> Result<TableScan> {
326 let selected_snapshot = match self.snapshot_selection {
327 Some(SnapshotSelection::SnapshotId(id)) => {
328 Some((id, format!("Snapshot with id {id} not found")))
329 }
330 Some(SnapshotSelection::AsOfTime(timestamp_ms)) => {
331 let id = snapshot_id_as_of_time(&self.table.metadata_ref(), timestamp_ms)?;
332 Some((
334 id,
335 format!(
336 "Snapshot with id {id} resolved from timestamp {timestamp_ms} ms not found; it may have expired"
337 ),
338 ))
339 }
340 None => None,
341 };
342 let is_time_travel = selected_snapshot.is_some();
343 let snapshot = match selected_snapshot {
344 Some((snapshot_id, not_found_message)) => self
345 .table
346 .metadata()
347 .snapshot_by_id(snapshot_id)
348 .ok_or_else(|| invalid_data!(not_found_message))?
349 .clone(),
350 None => {
351 let Some(current_snapshot_id) = self.table.metadata().current_snapshot() else {
352 return Ok(TableScan {
353 batch_size: self.batch_size,
354 column_names: self.column_names,
355 file_io: self.table.file_io().clone(),
356 plan_context: None,
357 concurrency_limit_data_files: self.concurrency_limit_data_files,
358 concurrency_limit_manifest_entries: self.concurrency_limit_manifest_entries,
359 concurrency_limit_manifest_files: self.concurrency_limit_manifest_files,
360 row_group_filtering_enabled: self.row_group_filtering_enabled,
361 row_selection_enabled: self.row_selection_enabled,
362 bloom_filter_enabled: self.bloom_filter_enabled,
363 runtime: self.table.runtime().clone(),
364 });
365 };
366 current_snapshot_id.clone()
367 }
368 };
369
370 let schema = if is_time_travel {
373 snapshot.schema(self.table.metadata())?
374 } else {
375 self.table.metadata().current_schema().clone()
376 };
377 let field_ids =
378 collect_scan_field_ids(&schema, self.column_names.as_deref(), self.case_sensitive)?;
379 let snapshot_bound_predicate =
380 bind_scan_predicate(&schema, self.filter.as_ref(), self.case_sensitive)?;
381 let name_mapping = self
382 .table
383 .metadata()
384 .table_properties()
385 .default_name_mapping()?
386 .map(Arc::new);
387 let unified_partition_type = projected_partition_type(self.table, &schema, &field_ids)?;
388
389 let sort_orders = Arc::new(
392 self.table
393 .metadata()
394 .sort_orders_iter()
395 .map(|order| (order.order_id, order.clone()))
396 .collect::<HashMap<i64, SortOrderRef>>(),
397 );
398
399 let plan_context = PlanContext {
400 snapshot,
401 table_metadata: self.table.metadata_ref(),
402 snapshot_schema: schema,
403 case_sensitive: self.case_sensitive,
404 predicate: self.filter.map(Arc::new),
405 snapshot_bound_predicate,
406 object_cache: self.table.object_cache(),
407 field_ids: Arc::new(field_ids),
408 name_mapping,
409 partition_filter_cache: Arc::new(PartitionFilterCache::new()),
410 manifest_evaluator_cache: Arc::new(ManifestEvaluatorCache::new()),
411 expression_evaluator_cache: Arc::new(ExpressionEvaluatorCache::new()),
412 unified_partition_type,
413 sort_orders,
414 };
415
416 Ok(TableScan {
417 batch_size: self.batch_size,
418 column_names: self.column_names,
419 file_io: self.table.file_io().clone(),
420 plan_context: Some(plan_context),
421 concurrency_limit_data_files: self.concurrency_limit_data_files,
422 concurrency_limit_manifest_entries: self.concurrency_limit_manifest_entries,
423 concurrency_limit_manifest_files: self.concurrency_limit_manifest_files,
424 row_group_filtering_enabled: self.row_group_filtering_enabled,
425 row_selection_enabled: self.row_selection_enabled,
426 bloom_filter_enabled: self.bloom_filter_enabled,
427 runtime: self.table.runtime().clone(),
428 })
429 }
430}
431
432#[derive(Debug)]
434pub struct TableScan {
435 plan_context: Option<PlanContext>,
439 batch_size: Option<usize>,
440 file_io: FileIO,
441 column_names: Option<Vec<String>>,
442 concurrency_limit_manifest_files: usize,
445
446 concurrency_limit_manifest_entries: usize,
449
450 concurrency_limit_data_files: usize,
453
454 row_group_filtering_enabled: bool,
455 row_selection_enabled: bool,
456 bloom_filter_enabled: bool,
457
458 runtime: Runtime,
459}
460
461impl TableScan {
462 pub async fn plan_files(&self) -> Result<FileScanTaskStream> {
464 self.plan_data_files(|ctx| ctx.into_file_scan_task()).await
465 }
466
467 pub(crate) async fn plan_cow_rewrite_files(
468 &self,
469 ) -> Result<BoxStream<'static, Result<crate::cow_rewrite::CowRewriteFile>>> {
470 self.plan_data_files(|ctx| ctx.into_cow_rewrite_file())
471 .await
472 }
473
474 async fn plan_data_files<T, F, Fut>(
475 &self,
476 build_result: F,
477 ) -> Result<BoxStream<'static, Result<T>>>
478 where
479 T: Send + 'static,
480 F: Fn(ManifestEntryContext) -> Fut + Copy + Send + Sync + 'static,
481 Fut: Future<Output = Result<T>> + Send + 'static,
482 {
483 let Some(plan_context) = self.plan_context.as_ref() else {
484 return Ok(Box::pin(futures::stream::empty()));
485 };
486
487 let concurrency_limit_manifest_files = self.concurrency_limit_manifest_files;
488 let concurrency_limit_manifest_entries = self.concurrency_limit_manifest_entries;
489
490 let (manifest_entry_data_ctx_tx, manifest_entry_data_ctx_rx) =
492 channel(concurrency_limit_manifest_files);
493 let (manifest_entry_delete_ctx_tx, manifest_entry_delete_ctx_rx) =
494 channel(concurrency_limit_manifest_files);
495
496 let (planned_file_tx, planned_file_rx) = channel(concurrency_limit_manifest_entries);
498
499 let (delete_file_idx, delete_file_tx) = DeleteFileIndex::new(self.runtime.clone());
500
501 let manifest_list = plan_context.get_manifest_list().await?;
502
503 let manifest_file_contexts = plan_context.build_manifest_file_contexts(
507 manifest_list,
508 manifest_entry_data_ctx_tx,
509 delete_file_idx.clone(),
510 manifest_entry_delete_ctx_tx,
511 )?;
512
513 let mut channel_for_manifest_error = planned_file_tx.clone();
514 let mut channel_for_data_manifest_entry_error = planned_file_tx.clone();
515 let mut channel_for_delete_manifest_entry_error = planned_file_tx.clone();
516
517 let rt = self.runtime.clone();
518
519 rt.io().spawn(async move {
521 let result = futures::stream::iter(manifest_file_contexts)
522 .try_for_each_concurrent(concurrency_limit_manifest_files, |ctx| async move {
523 ctx.fetch_manifest_and_stream_manifest_entries().await
524 })
525 .await;
526
527 if let Err(error) = result {
528 let _ = channel_for_manifest_error.send(Err(error)).await;
529 }
530 });
531
532 {
534 let rt = rt.clone();
535 let rt_inner = rt.clone();
536 rt.cpu().spawn(async move {
537 let result = manifest_entry_delete_ctx_rx
538 .map(|me_ctx| Ok((me_ctx, delete_file_tx.clone())))
539 .try_for_each_concurrent(
540 concurrency_limit_manifest_entries,
541 |(manifest_entry_context, tx)| {
542 let rt_inner = rt_inner.clone();
543 async move {
544 rt_inner
545 .cpu()
546 .spawn(async move {
547 Self::process_delete_manifest_entry(
548 manifest_entry_context,
549 tx,
550 )
551 .await
552 })
553 .await?
554 }
555 },
556 )
557 .await;
558
559 if let Err(error) = result {
560 let _ = channel_for_delete_manifest_entry_error
561 .send(Err(error))
562 .await;
563 }
564 });
565 }
566
567 {
569 let rt_inner = rt.clone();
570 rt.cpu().spawn(async move {
571 let result = manifest_entry_data_ctx_rx
572 .map(|me_ctx| Ok((me_ctx, planned_file_tx.clone())))
573 .try_for_each_concurrent(
574 concurrency_limit_manifest_entries,
575 |(manifest_entry_context, tx)| {
576 let rt_inner = rt_inner.clone();
577 async move {
578 rt_inner
579 .cpu()
580 .spawn(async move {
581 Self::process_data_manifest_entry(
582 manifest_entry_context,
583 tx,
584 build_result,
585 )
586 .await
587 })
588 .await?
589 }
590 },
591 )
592 .await;
593
594 if let Err(error) = result {
595 let _ = channel_for_data_manifest_entry_error.send(Err(error)).await;
596 }
597 });
598 }
599
600 Ok(planned_file_rx.boxed())
601 }
602
603 pub async fn to_arrow(&self) -> Result<ArrowRecordBatchStream> {
605 let mut arrow_reader_builder =
606 ArrowReaderBuilder::new(self.file_io.clone(), self.runtime.clone())
607 .with_data_file_concurrency_limit(self.concurrency_limit_data_files)
608 .with_row_group_filtering_enabled(self.row_group_filtering_enabled)
609 .with_row_selection_enabled(self.row_selection_enabled)
610 .with_bloom_filter_enabled(self.bloom_filter_enabled);
611
612 if let Some(batch_size) = self.batch_size {
613 arrow_reader_builder = arrow_reader_builder.with_batch_size(batch_size);
614 }
615
616 arrow_reader_builder
617 .build()
618 .read(self.plan_files().await?)
619 .map(|result| result.stream())
620 }
621
622 pub fn column_names(&self) -> Option<&[String]> {
624 self.column_names.as_deref()
625 }
626
627 pub fn snapshot(&self) -> Option<&SnapshotRef> {
629 self.plan_context.as_ref().map(|x| &x.snapshot)
630 }
631
632 async fn process_data_manifest_entry<T, F, Fut>(
633 manifest_entry_context: ManifestEntryContext,
634 mut planned_file_tx: Sender<Result<T>>,
635 build_result: F,
636 ) -> Result<()>
637 where
638 F: Fn(ManifestEntryContext) -> Fut + Send + Sync + 'static,
639 Fut: Future<Output = Result<T>> + Send + 'static,
640 {
641 if manifest_entry_context.manifest_entry.content_type() != DataContentType::Data {
643 return Err(Error::new(
644 ErrorKind::FeatureUnsupported,
645 "Encountered an entry for a delete file in a data file manifest",
646 ));
647 }
648
649 if let Some(ref bound_predicates) = manifest_entry_context.bound_predicates {
650 let BoundPredicates {
651 snapshot_bound_predicate,
652 partition_bound_predicate,
653 } = bound_predicates.as_ref();
654
655 let expression_evaluator_cache =
656 manifest_entry_context.expression_evaluator_cache.as_ref();
657
658 let expression_evaluator = expression_evaluator_cache.get(
659 manifest_entry_context.partition_spec_id,
660 partition_bound_predicate,
661 )?;
662
663 if !expression_evaluator.eval(manifest_entry_context.manifest_entry.data_file())? {
666 return Ok(());
667 }
668
669 if !InclusiveMetricsEvaluator::eval(
671 snapshot_bound_predicate,
672 manifest_entry_context.manifest_entry.data_file(),
673 false,
674 )? {
675 return Ok(());
676 }
677 }
678
679 planned_file_tx
683 .send(Ok(build_result(manifest_entry_context).await?))
684 .await?;
685
686 Ok(())
687 }
688
689 async fn process_delete_manifest_entry(
690 manifest_entry_context: ManifestEntryContext,
691 mut delete_file_ctx_tx: Sender<DeleteFileContext>,
692 ) -> Result<()> {
693 if manifest_entry_context.manifest_entry.content_type() == DataContentType::Data {
695 return Err(Error::new(
696 ErrorKind::FeatureUnsupported,
697 "Encountered an entry for a data file in a delete manifest",
698 ));
699 }
700
701 if let Some(ref bound_predicates) = manifest_entry_context.bound_predicates {
702 let expression_evaluator_cache =
703 manifest_entry_context.expression_evaluator_cache.as_ref();
704
705 let expression_evaluator = expression_evaluator_cache.get(
706 manifest_entry_context.partition_spec_id,
707 &bound_predicates.partition_bound_predicate,
708 )?;
709
710 if !expression_evaluator.eval(manifest_entry_context.manifest_entry.data_file())? {
713 return Ok(());
714 }
715 }
716
717 delete_file_ctx_tx
718 .send(DeleteFileContext {
719 manifest_entry: manifest_entry_context.manifest_entry.clone(),
720 partition_spec_id: manifest_entry_context.partition_spec_id,
721 })
722 .await?;
723
724 Ok(())
725 }
726}
727
728pub(crate) struct BoundPredicates {
729 partition_bound_predicate: BoundPredicate,
730 snapshot_bound_predicate: BoundPredicate,
731}
732
733#[cfg(test)]
734mod tests {
735 use std::collections::HashMap;
738 use std::fs;
739 use std::fs::File;
740 use std::sync::Arc;
741
742 use arrow_array::cast::AsArray;
743 use arrow_array::types::Int32Type;
744 use arrow_array::{
745 Array, ArrayRef, BooleanArray, Float64Array, Int32Array, Int64Array, RecordBatch, RunArray,
746 StringArray,
747 };
748 use futures::{TryStreamExt, stream};
749 use parquet::arrow::ArrowWriter;
750 use uuid::Uuid;
751
752 use super::resolve_field_id;
753 use crate::arrow::ArrowReaderBuilder;
754 use crate::expr::{BoundPredicate, Reference};
755 use crate::io::FileIO;
756 use crate::metadata_columns::{
757 RESERVED_COL_NAME_FILE, RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER,
758 RESERVED_COL_NAME_POS, RESERVED_COL_NAME_SPEC_ID, RESERVED_FIELD_ID_POS,
759 };
760 use crate::scan::{FileScanTask, FileScanTaskDeleteFile};
761 use crate::spec::{
762 DataContentType, DataFileBuilder, DataFileFormat, Datum, Literal, MAIN_BRANCH,
763 ManifestEntry, ManifestListWriter, ManifestStatus, ManifestWriterBuilder, MappedField,
764 NameMapping, NestedField, NullOrder, Operation, PartitionSpec, PrimitiveType, Schema,
765 Snapshot, SortDirection, SortField, SortOrder, Struct, Summary, TableMetadataBuilder,
766 TableProperties, Transform, Type, UnboundPartitionSpec,
767 };
768 use crate::table::Table;
769 use crate::test_utils::scan::{
770 ROWS_PER_FIXTURE_FILE, TableTestFixture, assert_last_updated_seq_all,
771 };
772 use crate::test_utils::test_runtime;
773 use crate::util::snapshot::snapshot_id_as_of_time;
774 use crate::{ErrorKind, TableIdent};
775
776 #[tokio::test]
777 async fn test_table_scan_columns() {
778 let table = TableTestFixture::new().table;
779
780 let table_scan = table.scan().select(["x", "y"]).build().unwrap();
781 assert_eq!(
782 Some(vec!["x".to_string(), "y".to_string()]),
783 table_scan.column_names
784 );
785
786 let table_scan = table
787 .scan()
788 .select(["x", "y"])
789 .select(["z"])
790 .build()
791 .unwrap();
792 assert_eq!(Some(vec!["z".to_string()]), table_scan.column_names);
793 }
794
795 #[tokio::test]
796 async fn test_select_all() {
797 let table = TableTestFixture::new().table;
798
799 let table_scan = table.scan().select_all().build().unwrap();
800 assert!(table_scan.column_names.is_none());
801 }
802
803 #[test]
804 fn test_select_no_exist_column() {
805 let table = TableTestFixture::new().table;
806
807 let table_scan = table.scan().select(["x", "y", "z", "a", "b"]).build();
808 assert!(table_scan.is_err());
809 }
810
811 #[test]
812 fn test_case_sensitive_scan_rejects_mismatched_column_case() {
813 let table = TableTestFixture::new().table;
814
815 assert!(table.scan().select(["X"]).build().is_err());
817 assert!(
818 table
819 .scan()
820 .with_filter(Reference::new("X").greater_than(Datum::long(1)))
821 .build()
822 .is_err()
823 );
824 }
825
826 #[test]
827 fn test_resolve_field_id_case_insensitive_rejects_ambiguous_name() {
828 let schema = Schema::builder()
829 .with_fields(vec![
830 NestedField::optional(1, "id", PrimitiveType::Int).into(),
831 NestedField::optional(2, "ID", PrimitiveType::Int).into(),
832 NestedField::optional(3, "data", PrimitiveType::Int).into(),
833 ])
834 .build()
835 .unwrap();
836
837 assert_eq!(resolve_field_id(&schema, "id", true).unwrap(), Some(1));
838 assert_eq!(resolve_field_id(&schema, "DATA", false).unwrap(), Some(3));
839 let error = resolve_field_id(&schema, "Id", false).unwrap_err();
840 assert_eq!(error.kind(), ErrorKind::DataInvalid);
841 assert!(error.message().contains("case-insensitively"), "{error}");
842 }
843
844 #[test]
845 fn test_case_insensitive_scan_with_ambiguous_schema() {
846 let fixture = TableTestFixture::new();
847 let mut metadata = fixture.table.metadata().clone();
848 let schema = Schema::builder()
849 .with_schema_id(metadata.current_schema_id())
850 .with_fields(vec![
851 NestedField::optional(1, "id", PrimitiveType::Int).into(),
852 NestedField::optional(2, "ID", PrimitiveType::Int).into(),
853 NestedField::optional(3, "data", PrimitiveType::Int).into(),
854 ])
855 .build()
856 .unwrap();
857 metadata
858 .schemas
859 .insert(metadata.current_schema_id(), Arc::new(schema));
860 let table = fixture.table.with_metadata(Arc::new(metadata));
861
862 table
863 .scan()
864 .with_case_sensitive(false)
865 .select(["DATA"])
866 .build()
867 .unwrap();
868 let error = table
869 .scan()
870 .with_case_sensitive(false)
871 .select(["Id"])
872 .build()
873 .unwrap_err();
874 assert_eq!(error.kind(), ErrorKind::DataInvalid);
875 assert!(error.message().contains("case-insensitively"), "{error}");
876 }
877
878 #[tokio::test]
879 async fn test_case_insensitive_scan_resolves_mismatched_column_case() {
880 let mut fixture = TableTestFixture::new();
881 fixture.setup_manifest_files().await;
882
883 let table_scan = fixture
886 .table
887 .scan()
888 .with_case_sensitive(false)
889 .select(["X", "Z"])
890 .with_filter(Reference::new("Y").greater_than(Datum::long(1)))
891 .build()
892 .unwrap();
893
894 let batches: Vec<_> = table_scan
895 .to_arrow()
896 .await
897 .unwrap()
898 .try_collect()
899 .await
900 .unwrap();
901
902 assert_eq!(batches[0].num_columns(), 2);
903 assert_eq!(
904 batches[0]
905 .column_by_name("x")
906 .unwrap()
907 .as_any()
908 .downcast_ref::<Int64Array>()
909 .unwrap()
910 .value(0),
911 1
912 );
913 assert_eq!(
914 batches[0]
915 .column_by_name("z")
916 .unwrap()
917 .as_any()
918 .downcast_ref::<Int64Array>()
919 .unwrap()
920 .value(0),
921 3
922 );
923 }
924
925 #[tokio::test]
926 async fn test_table_scan_default_snapshot_id() {
927 let table = TableTestFixture::new().table;
928
929 let table_scan = table.scan().build().unwrap();
930 assert_eq!(
931 table.metadata().current_snapshot().unwrap().snapshot_id(),
932 table_scan.snapshot().unwrap().snapshot_id()
933 );
934 }
935
936 #[test]
937 fn test_table_scan_non_exist_snapshot_id() {
938 let table = TableTestFixture::new().table;
939
940 let table_scan = table.scan().snapshot_id(1024).build();
941 assert!(table_scan.is_err());
942 }
943
944 #[tokio::test]
945 async fn test_table_scan_with_snapshot_id() {
946 let table = TableTestFixture::new().table;
947
948 let table_scan = table
949 .scan()
950 .snapshot_id(3051729675574597004)
951 .with_row_selection_enabled(true)
952 .build()
953 .unwrap();
954 assert_eq!(
955 table_scan.snapshot().unwrap().snapshot_id(),
956 3051729675574597004
957 );
958 }
959
960 #[test]
961 fn test_table_scan_as_of_time_after_rollback() {
962 for elapsed_ms in [0, 1000] {
963 let table = TableTestFixture::new().table;
964 let mut metadata = table.metadata().clone();
965 let original = metadata.history()[0].snapshot_id;
966 let previous = metadata.history()[1].clone();
967 let rollback_time = previous.timestamp_ms + elapsed_ms;
968 metadata.snapshot_log.push(crate::spec::SnapshotLog {
969 timestamp_ms: rollback_time,
970 snapshot_id: original,
971 });
972 metadata.current_snapshot_id = Some(original);
973 metadata.refs.get_mut(MAIN_BRANCH).unwrap().snapshot_id = original;
974 let table = table.with_metadata(Arc::new(metadata));
975 let scan = table.scan().as_of_time(rollback_time).build().unwrap();
976 let expected = if elapsed_ms == 0 {
978 previous.snapshot_id
979 } else {
980 original
981 };
982 assert_eq!(scan.snapshot().unwrap().snapshot_id(), expected);
983 }
984 }
985
986 #[test]
987 fn test_table_scan_as_of_time_rejects_empty_history() {
988 let table = TableTestFixture::new_empty().table;
989 assert!(table.scan().build().unwrap().snapshot().is_none());
990 let err = table.scan().as_of_time(0).build().unwrap_err();
991 assert_eq!(err.kind(), ErrorKind::DataInvalid);
992 assert!(err.message().contains("No snapshot history"));
993 }
994
995 #[test]
996 fn test_table_scan_as_of_time_rejects_before_history() {
997 let table = TableTestFixture::new().table;
998 let before = table.metadata().history()[0].timestamp_ms - 1;
999 let err = table.scan().as_of_time(before).build().unwrap_err();
1000 assert_eq!(err.kind(), ErrorKind::DataInvalid);
1001 assert!(err.message().contains(&before.to_string()));
1002 }
1003
1004 #[test]
1005 fn test_table_scan_as_of_time_rejects_all_qualifying_snapshots_expired() {
1006 let table = TableTestFixture::new_with_deep_history().table;
1007 let mut metadata = table.metadata().clone();
1008 let entry = metadata.history()[3].clone();
1009 let expired_ids: Vec<_> = metadata
1010 .history()
1011 .iter()
1012 .filter(|log| log.timestamp_ms <= entry.timestamp_ms)
1013 .map(|log| log.snapshot_id)
1014 .collect();
1015 assert_eq!(expired_ids.len(), 4);
1016 for snapshot_id in expired_ids {
1017 metadata.snapshots.remove(&snapshot_id);
1018 }
1019 let table = table.with_metadata(Arc::new(metadata));
1020 assert_eq!(
1022 snapshot_id_as_of_time(&table.metadata_ref(), entry.timestamp_ms).unwrap(),
1023 entry.snapshot_id
1024 );
1025 assert!(table.scan().build().unwrap().snapshot().is_some());
1026 let err = table
1027 .scan()
1028 .as_of_time(entry.timestamp_ms)
1029 .build()
1030 .unwrap_err();
1031 assert_eq!(err.kind(), ErrorKind::DataInvalid);
1032 assert_eq!(
1033 err.message(),
1034 format!(
1035 "Snapshot with id {} resolved from timestamp {} ms not found; it may have expired",
1036 entry.snapshot_id, entry.timestamp_ms
1037 )
1038 );
1039 }
1040
1041 #[test]
1042 fn test_table_scan_last_snapshot_selection_wins() {
1043 let table = TableTestFixture::new().table;
1044 let first = &table.metadata().history()[0];
1045 let last = &table.metadata().history()[1];
1046 let cases = [
1047 (
1048 table
1049 .scan()
1050 .snapshot_id(last.snapshot_id)
1051 .as_of_time(first.timestamp_ms),
1052 first.snapshot_id,
1053 ),
1054 (
1055 table
1056 .scan()
1057 .as_of_time(first.timestamp_ms)
1058 .snapshot_id(last.snapshot_id),
1059 last.snapshot_id,
1060 ),
1061 (
1063 table.scan().snapshot_id(-1).as_of_time(first.timestamp_ms),
1064 first.snapshot_id,
1065 ),
1066 (
1067 table
1068 .scan()
1069 .as_of_time(i64::MIN)
1070 .snapshot_id(last.snapshot_id),
1071 last.snapshot_id,
1072 ),
1073 (
1074 table
1075 .scan()
1076 .snapshot_id(first.snapshot_id)
1077 .snapshot_id(last.snapshot_id),
1078 last.snapshot_id,
1079 ),
1080 (
1081 table
1082 .scan()
1083 .as_of_time(i64::MAX)
1084 .as_of_time(first.timestamp_ms),
1085 first.snapshot_id,
1086 ),
1087 ];
1088 for (builder, expected) in cases {
1089 let scan = builder.build().unwrap();
1090 assert_eq!(scan.snapshot().unwrap().snapshot_id(), expected);
1091 }
1092 }
1093
1094 #[test]
1095 fn test_table_scan_invalid_last_snapshot_selection() {
1096 let table = TableTestFixture::new().table;
1097 let first = &table.metadata().history()[0];
1098 let cases = [
1099 (
1100 table
1101 .scan()
1102 .snapshot_id(first.snapshot_id)
1103 .as_of_time(first.timestamp_ms - 1),
1104 "No snapshot history",
1105 ),
1106 (
1107 table.scan().as_of_time(first.timestamp_ms).snapshot_id(-1),
1108 "Snapshot with id -1 not found",
1109 ),
1110 ];
1111 for (builder, expected) in cases {
1112 let err = builder.build().unwrap_err();
1113 assert_eq!(err.kind(), ErrorKind::DataInvalid);
1114 assert!(err.message().contains(expected));
1115 }
1116 }
1117
1118 #[test]
1119 fn test_table_scan_as_of_time_uses_snapshot_schema() {
1120 for select_current in [false, true] {
1121 let table = TableTestFixture::new().table;
1122 let mut metadata = table.metadata().clone();
1123 let entry = metadata.history()[usize::from(select_current)].clone();
1124 Arc::make_mut(metadata.snapshots.get_mut(&entry.snapshot_id).unwrap()).schema_id =
1127 Some(0);
1128 let table = table.with_metadata(Arc::new(metadata));
1129 let scan = table
1130 .scan()
1131 .as_of_time(entry.timestamp_ms)
1132 .select(["x"])
1133 .with_filter(Reference::new("x").greater_than(Datum::long(0)))
1134 .build()
1135 .unwrap();
1136 let context = scan.plan_context.as_ref().unwrap();
1137 assert_eq!(context.snapshot_schema.schema_id(), 0);
1138 assert!(context.snapshot_bound_predicate.is_some());
1139 assert!(
1140 table
1141 .scan()
1142 .as_of_time(entry.timestamp_ms)
1143 .select(["y"])
1144 .build()
1145 .is_err()
1146 );
1147 assert!(
1148 table
1149 .scan()
1150 .as_of_time(entry.timestamp_ms)
1151 .with_filter(Reference::new("y").greater_than(Datum::long(0)))
1152 .build()
1153 .is_err()
1154 );
1155 }
1156 }
1157
1158 #[test]
1159 fn test_table_scan_as_of_time_schema_compatibility() {
1160 let table = TableTestFixture::new().table;
1161 let entry = table.metadata().history()[0].clone();
1162 assert!(
1164 table
1165 .metadata()
1166 .snapshot_by_id(entry.snapshot_id)
1167 .unwrap()
1168 .schema_id()
1169 .is_none()
1170 );
1171 let scan = table.scan().as_of_time(entry.timestamp_ms).build().unwrap();
1172 assert_eq!(
1173 scan.plan_context.as_ref().unwrap().snapshot_schema,
1174 *table.metadata().current_schema()
1175 );
1176
1177 let mut metadata = table.metadata().clone();
1178 Arc::make_mut(metadata.snapshots.get_mut(&entry.snapshot_id).unwrap()).schema_id =
1179 Some(1234);
1180 let table = table.with_metadata(Arc::new(metadata));
1181 let err = table
1182 .scan()
1183 .as_of_time(entry.timestamp_ms)
1184 .build()
1185 .unwrap_err();
1186 assert_eq!(err.kind(), ErrorKind::DataInvalid);
1187 assert!(err.message().contains("Schema with id 1234 not found"));
1188 }
1189
1190 #[tokio::test]
1191 async fn test_table_scan_as_of_time_reads_historical_rows() {
1192 let mut fixture = TableTestFixture::new();
1193 fixture.setup_manifest_files().await;
1194 let entry = fixture.table.metadata().history()[0].clone();
1195
1196 let mut metadata = fixture.table.metadata().clone();
1198 Arc::make_mut(metadata.snapshots.get_mut(&entry.snapshot_id).unwrap()).schema_id = Some(0);
1199 fixture.table = fixture.table.with_metadata(Arc::new(metadata));
1200 let snapshot = fixture
1201 .table
1202 .metadata()
1203 .snapshot_by_id(entry.snapshot_id)
1204 .unwrap();
1205 let schema = snapshot.schema(fixture.table.metadata()).unwrap();
1206 let arrow_schema = Arc::new(crate::arrow::schema_to_arrow_schema(&schema).unwrap());
1207
1208 let path = format!("{}/historical.parquet", fixture.table_location);
1210 let batch = RecordBatch::try_new(arrow_schema.clone(), vec![Arc::new(Int64Array::from(
1211 vec![100],
1212 ))])
1213 .unwrap();
1214 let mut writer =
1215 ArrowWriter::try_new(File::create(&path).unwrap(), arrow_schema, None).unwrap();
1216 writer.write(&batch).unwrap();
1217 writer.close().unwrap();
1218
1219 let mut manifest_writer = ManifestWriterBuilder::new(
1220 fixture.next_manifest_file(),
1221 Some(snapshot.snapshot_id()),
1222 schema,
1223 fixture
1224 .table
1225 .metadata()
1226 .default_partition_spec()
1227 .as_ref()
1228 .clone(),
1229 )
1230 .build_v2_data();
1231 manifest_writer
1232 .add_entry(
1233 ManifestEntry::builder()
1234 .status(ManifestStatus::Added)
1235 .data_file(
1236 DataFileBuilder::default()
1237 .partition_spec_id(0)
1238 .content(DataContentType::Data)
1239 .file_format(DataFileFormat::Parquet)
1240 .file_size_in_bytes(fs::metadata(&path).unwrap().len())
1241 .file_path(path)
1242 .record_count(1)
1243 .partition(Struct::from_iter([Some(Literal::long(100))]))
1244 .build()
1245 .unwrap(),
1246 )
1247 .build(),
1248 )
1249 .unwrap();
1250 let manifest = manifest_writer.write_manifest_file().await.unwrap();
1251 let output = fixture
1252 .table
1253 .file_io()
1254 .new_output(snapshot.manifest_list())
1255 .unwrap()
1256 .writer()
1257 .await
1258 .unwrap();
1259 let mut list_writer = ManifestListWriter::v2(
1260 output,
1261 snapshot.snapshot_id(),
1262 snapshot.parent_snapshot_id(),
1263 snapshot.sequence_number(),
1264 );
1265 list_writer.add_manifests([manifest].into_iter()).unwrap();
1266 list_writer.close().await.unwrap();
1267
1268 let table = fixture.table;
1269 let [by_time, by_id] = [
1270 table.scan().as_of_time(entry.timestamp_ms),
1271 table.scan().snapshot_id(entry.snapshot_id),
1272 ]
1273 .map(|scan| {
1274 scan.with_filter(Reference::new("x").greater_than_or_equal_to(Datum::long(100)))
1275 .build()
1276 .unwrap()
1277 });
1278 let current = table.scan().build().unwrap();
1279 let mut planned_files = Vec::new();
1280 let mut rows = Vec::new();
1281 let mut schemas = Vec::new();
1282 for scan in [by_time, by_id, current] {
1283 let mut tasks: Vec<_> = scan
1284 .plan_files()
1285 .await
1286 .unwrap()
1287 .try_collect()
1288 .await
1289 .unwrap();
1290 tasks.sort_by_key(|task| task.data_file_path().to_string());
1291 planned_files.push(tasks);
1292 let batches: Vec<_> = scan.to_arrow().await.unwrap().try_collect().await.unwrap();
1293 assert!(!batches.is_empty());
1294 schemas.push(batches[0].schema());
1295 let mut values: Vec<i64> = batches
1296 .iter()
1297 .flat_map(|batch| {
1298 batch
1299 .column_by_name("x")
1300 .unwrap()
1301 .as_any()
1302 .downcast_ref::<Int64Array>()
1303 .unwrap()
1304 .values()
1305 .iter()
1306 .copied()
1307 })
1308 .collect();
1309 values.sort_unstable();
1310 rows.push(values);
1311 }
1312 assert!(!planned_files[0].is_empty());
1313 assert_eq!(planned_files[0], planned_files[1]);
1314 assert_eq!(rows[0], vec![100]);
1315 assert_eq!(rows[0], rows[1]);
1316 assert_eq!(rows[2], vec![1; 2 * ROWS_PER_FIXTURE_FILE]);
1318 assert_eq!(schemas[0], schemas[1]);
1319 assert_eq!(schemas[0].fields().len(), 1);
1320 assert_eq!(schemas[2].fields().len(), 8);
1321 }
1322
1323 #[test]
1326 fn test_scan_of_current_state_uses_current_schema() {
1327 let table = TableTestFixture::new_with_column_added_after_last_write().table;
1328
1329 let table_scan = table.scan().build().unwrap();
1330 assert_eq!(table_scan.snapshot(), table.metadata().current_snapshot());
1331 assert_eq!(
1332 table_scan.plan_context.as_ref().unwrap().snapshot_schema,
1333 *table.metadata().current_schema()
1334 );
1335 }
1336
1337 #[test]
1340 fn test_time_travel_scan_uses_snapshot_schema() {
1341 let table = TableTestFixture::new_with_column_added_after_last_write().table;
1342 let snapshot = table.metadata().current_snapshot().unwrap();
1343
1344 let table_scan = table
1345 .scan()
1346 .snapshot_id(snapshot.snapshot_id())
1347 .build()
1348 .unwrap();
1349 assert_eq!(table_scan.snapshot(), Some(snapshot));
1350 assert_eq!(
1351 table_scan.plan_context.as_ref().unwrap().snapshot_schema,
1352 snapshot.schema(table.metadata()).unwrap()
1353 );
1354 }
1355
1356 fn table_with_property(key: &str, value: &str) -> Table {
1357 let fixture = TableTestFixture::new();
1358 let mut metadata = fixture.table.metadata().clone();
1359 metadata
1360 .properties
1361 .insert(key.to_string(), value.to_string());
1362 Table::builder()
1363 .metadata(metadata)
1364 .identifier(fixture.table.identifier().clone())
1365 .file_io(fixture.table.file_io().clone())
1366 .metadata_location(fixture.table.metadata_location().unwrap().to_string())
1367 .runtime(test_runtime())
1368 .build()
1369 .unwrap()
1370 }
1371
1372 #[test]
1373 fn test_table_scan_without_name_mapping_property() {
1374 let table = TableTestFixture::new().table;
1375
1376 let table_scan = table.scan().build().unwrap();
1377 assert!(
1378 table_scan
1379 .plan_context
1380 .as_ref()
1381 .unwrap()
1382 .name_mapping
1383 .is_none()
1384 );
1385 }
1386
1387 #[test]
1388 fn test_table_scan_with_name_mapping_property() {
1389 let mapping_json = r#"[{"field-id":1,"names":["id","record_id"]}]"#;
1390 let table =
1391 table_with_property(TableProperties::PROPERTY_DEFAULT_NAME_MAPPING, mapping_json);
1392
1393 let table_scan = table.scan().build().unwrap();
1394 let mapping = table_scan
1395 .plan_context
1396 .as_ref()
1397 .unwrap()
1398 .name_mapping
1399 .as_ref()
1400 .expect("name_mapping should be parsed from the table property");
1401 let fields = mapping.fields();
1402 assert_eq!(fields.len(), 1);
1403 assert_eq!(fields[0].field_id(), Some(1));
1404 assert_eq!(fields[0].names(), &[
1405 "id".to_string(),
1406 "record_id".to_string()
1407 ]);
1408 }
1409
1410 #[test]
1411 fn test_table_scan_with_malformed_name_mapping_property() {
1412 let table = table_with_property(
1413 TableProperties::PROPERTY_DEFAULT_NAME_MAPPING,
1414 "{ not valid json",
1415 );
1416
1417 let err = table
1418 .scan()
1419 .build()
1420 .expect_err("malformed name mapping should fail to parse");
1421 assert_eq!(err.kind(), ErrorKind::DataInvalid);
1422 }
1423
1424 #[tokio::test]
1425 async fn test_plan_files_carries_name_mapping_into_file_scan_task() {
1426 let mut fixture = TableTestFixture::new();
1427 fixture.setup_manifest_files().await;
1428
1429 let mapping_json = r#"[{"field-id":1,"names":["id","record_id"]}]"#;
1430 let mut metadata = fixture.table.metadata().clone();
1431 metadata.properties.insert(
1432 TableProperties::PROPERTY_DEFAULT_NAME_MAPPING.to_string(),
1433 mapping_json.to_string(),
1434 );
1435 let table = Table::builder()
1436 .metadata(metadata)
1437 .identifier(fixture.table.identifier().clone())
1438 .file_io(fixture.table.file_io().clone())
1439 .metadata_location(fixture.table.metadata_location().unwrap().to_string())
1440 .runtime(test_runtime())
1441 .build()
1442 .unwrap();
1443
1444 let tasks: Vec<_> = table
1445 .scan()
1446 .build()
1447 .unwrap()
1448 .plan_files()
1449 .await
1450 .unwrap()
1451 .try_collect()
1452 .await
1453 .unwrap();
1454
1455 assert!(!tasks.is_empty(), "expected at least one FileScanTask");
1456 for task in &tasks {
1457 let mapping = task
1458 .name_mapping()
1459 .expect("name_mapping should reach the FileScanTask");
1460 assert_eq!(mapping.fields().len(), 1);
1461 assert_eq!(mapping.fields()[0].field_id(), Some(1));
1462 }
1463 }
1464
1465 #[tokio::test]
1466 async fn test_plan_files_carries_sort_order_into_file_scan_task() {
1467 let mut fixture = TableTestFixture::new();
1468
1469 let mut metadata = fixture.table.metadata().clone();
1473 metadata
1474 .sort_orders
1475 .insert(0, Arc::new(SortOrder::unsorted_order()));
1476 fixture.table = fixture.table.with_metadata(Arc::new(metadata));
1477
1478 let expected_sort_order = fixture
1479 .table
1480 .metadata()
1481 .sort_order_by_id(3)
1482 .unwrap()
1483 .clone();
1484
1485 fixture
1487 .setup_manifest_files_with_sort_order_ids([Some(3), None, Some(99), Some(0)])
1488 .await;
1489
1490 let tasks: Vec<_> = fixture
1491 .table
1492 .scan()
1493 .build()
1494 .unwrap()
1495 .plan_files()
1496 .await
1497 .unwrap()
1498 .try_collect()
1499 .await
1500 .unwrap();
1501
1502 assert_eq!(tasks.len(), 4, "expected all four FileScanTasks");
1503
1504 assert_eq!(
1507 tasks.iter().filter(|t| t.sort_order().is_some()).count(),
1508 1,
1509 "exactly one file resolves to a sort order"
1510 );
1511 assert_eq!(
1512 tasks.iter().filter(|t| t.sort_order_id().is_some()).count(),
1513 3,
1514 "three files carry a raw sort_order_id"
1515 );
1516
1517 let resolved = tasks
1518 .iter()
1519 .find(|t| t.data_file_path().ends_with("1.parquet"))
1520 .unwrap();
1521 assert_eq!(resolved.sort_order_id(), Some(3));
1522 assert_eq!(
1523 resolved.sort_order(),
1524 Some(&expected_sort_order),
1525 "sort_order_id 3 should resolve to the table's sort order at id 3"
1526 );
1527
1528 let missing = tasks
1529 .iter()
1530 .find(|t| t.data_file_path().ends_with("2.parquet"))
1531 .unwrap();
1532 assert_eq!(missing.sort_order_id(), None);
1533 assert!(
1534 missing.sort_order().is_none(),
1535 "a file with no sort_order_id carries no sort_order"
1536 );
1537
1538 let unresolvable = tasks
1539 .iter()
1540 .find(|t| t.data_file_path().ends_with("3.parquet"))
1541 .unwrap();
1542 assert_eq!(
1543 unresolvable.sort_order_id(),
1544 Some(99),
1545 "the raw id is preserved even when it does not resolve"
1546 );
1547 assert!(
1548 unresolvable.sort_order().is_none(),
1549 "an unresolvable sort_order_id resolves to no sort_order"
1550 );
1551
1552 let unsorted = tasks
1553 .iter()
1554 .find(|t| t.data_file_path().ends_with("4.parquet"))
1555 .unwrap();
1556 assert_eq!(unsorted.sort_order_id(), Some(0));
1557 assert!(
1558 unsorted.sort_order().is_none(),
1559 "the reserved unsorted order (id 0) resolves to no sort_order"
1560 );
1561 }
1562
1563 #[tokio::test]
1564 async fn test_plan_files_on_table_without_any_snapshots() {
1565 let table = TableTestFixture::new_empty().table;
1566 let batch_stream = table.scan().build().unwrap().to_arrow().await.unwrap();
1567 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
1568 assert!(batches.is_empty());
1569 }
1570
1571 #[tokio::test]
1572 async fn test_plan_files_no_deletions() {
1573 let mut fixture = TableTestFixture::new();
1574 fixture.setup_manifest_files().await;
1575
1576 let table_scan = fixture
1578 .table
1579 .scan()
1580 .with_row_selection_enabled(true)
1581 .build()
1582 .unwrap();
1583
1584 let mut tasks = table_scan
1585 .plan_files()
1586 .await
1587 .unwrap()
1588 .try_fold(vec![], |mut acc, task| async move {
1589 acc.push(task);
1590 Ok(acc)
1591 })
1592 .await
1593 .unwrap();
1594
1595 assert_eq!(tasks.len(), 2);
1596
1597 tasks.sort_by_key(|t| t.data_file_path().to_string());
1598
1599 assert_eq!(
1601 tasks[0].data_file_path(),
1602 format!("{}/1.parquet", &fixture.table_location)
1603 );
1604
1605 assert_eq!(
1607 tasks[1].data_file_path(),
1608 format!("{}/3.parquet", &fixture.table_location)
1609 );
1610 }
1611
1612 #[tokio::test]
1613 async fn test_scan_widens_old_partition_so_pruning_and_deletes_match() {
1614 let mut fixture = TableTestFixture::new();
1621 fixture.setup_promoted_partition_with_eq_delete(false).await;
1622
1623 let tasks: Vec<FileScanTask> = fixture
1624 .table
1625 .scan()
1626 .build()
1627 .unwrap()
1628 .plan_files()
1629 .await
1630 .unwrap()
1631 .try_collect()
1632 .await
1633 .unwrap();
1634 assert_eq!(tasks.len(), 1);
1635 assert_eq!(
1636 tasks[0].deletes().len(),
1637 1,
1638 "equality delete must attach after widening the old int partition to long"
1639 );
1640 assert_eq!(
1641 tasks[0].partition(),
1642 Some(&Struct::from_iter([Some(Literal::long(7))])),
1643 "the widened long(7) tuple must reach FileScanTask::partition()"
1644 );
1645
1646 let tasks: Vec<FileScanTask> = fixture
1647 .table
1648 .scan()
1649 .with_filter(Reference::new("x").equal_to(Datum::long(7)))
1650 .build()
1651 .unwrap()
1652 .plan_files()
1653 .await
1654 .unwrap()
1655 .try_collect()
1656 .await
1657 .unwrap();
1658 assert_eq!(
1659 tasks.len(),
1660 1,
1661 "filter on the promoted type must not prune the file"
1662 );
1663 assert_eq!(tasks[0].deletes().len(), 1);
1664
1665 let tasks: Vec<FileScanTask> = fixture
1666 .table
1667 .scan()
1668 .with_filter(Reference::new("x").equal_to(Datum::long(8)))
1669 .build()
1670 .unwrap()
1671 .plan_files()
1672 .await
1673 .unwrap()
1674 .try_collect()
1675 .await
1676 .unwrap();
1677 assert!(
1678 tasks.is_empty(),
1679 "a file in partition x=7 must be pruned by an x=8 filter"
1680 );
1681 }
1682
1683 #[tokio::test]
1684 async fn test_scan_widens_old_partition_with_pre_promotion_delete() {
1685 let mut fixture = TableTestFixture::new();
1689 fixture.setup_promoted_partition_with_eq_delete(true).await;
1690
1691 let tasks: Vec<FileScanTask> = fixture
1692 .table
1693 .scan()
1694 .build()
1695 .unwrap()
1696 .plan_files()
1697 .await
1698 .unwrap()
1699 .try_collect()
1700 .await
1701 .unwrap();
1702 assert_eq!(tasks.len(), 1);
1703 assert_eq!(tasks[0].deletes().len(), 1);
1704 }
1705
1706 #[tokio::test]
1707 async fn test_scan_widens_old_partition_so_deletion_vector_matches() {
1708 let mut fixture = TableTestFixture::new();
1712 fixture.setup_promoted_partition_with_dv().await;
1713
1714 let tasks: Vec<FileScanTask> = fixture
1715 .table
1716 .scan()
1717 .build()
1718 .unwrap()
1719 .plan_files()
1720 .await
1721 .unwrap()
1722 .try_collect()
1723 .await
1724 .unwrap();
1725 assert_eq!(tasks.len(), 1);
1726 assert_eq!(tasks[0].deletes().len(), 1);
1727 }
1728
1729 #[tokio::test]
1730 async fn test_scan_keeps_eq_delete_after_dropping_promoted_source() {
1731 let mut fixture = TableTestFixture::new();
1735 fixture.setup_promoted_partition_with_eq_delete(false).await;
1736 fixture.drop_partition_source(1);
1737 let table = fixture.table;
1738
1739 let tasks: Vec<FileScanTask> = table
1740 .scan()
1741 .build()
1742 .unwrap()
1743 .plan_files()
1744 .await
1745 .unwrap()
1746 .try_collect()
1747 .await
1748 .unwrap();
1749 assert_eq!(tasks.len(), 1);
1750 assert_eq!(tasks[0].deletes().len(), 1);
1751 }
1752
1753 #[tokio::test]
1754 async fn test_scan_keeps_deletion_vector_after_dropping_promoted_source() {
1755 let mut fixture = TableTestFixture::new();
1758 fixture.setup_promoted_partition_with_dv().await;
1759 fixture.drop_partition_source(1);
1760 let table = fixture.table;
1761
1762 let tasks: Vec<FileScanTask> = table
1763 .scan()
1764 .build()
1765 .unwrap()
1766 .plan_files()
1767 .await
1768 .unwrap()
1769 .try_collect()
1770 .await
1771 .unwrap();
1772 assert_eq!(tasks.len(), 1);
1773 assert_eq!(tasks[0].deletes().len(), 1);
1774 }
1775
1776 #[tokio::test]
1777 async fn test_scan_keeps_eq_delete_when_dropped_source_is_date() {
1778 let mut fixture = TableTestFixture::new();
1783 fixture
1784 .setup_promoted_partition_with_eq_delete_and_dropped_date()
1785 .await;
1786
1787 let tasks: Vec<FileScanTask> = fixture
1788 .table
1789 .scan()
1790 .build()
1791 .unwrap()
1792 .plan_files()
1793 .await
1794 .unwrap()
1795 .try_collect()
1796 .await
1797 .unwrap();
1798 assert_eq!(tasks.len(), 1);
1799 assert_eq!(tasks[0].deletes().len(), 1);
1800 }
1801
1802 #[tokio::test]
1803 async fn test_plan_files_carries_row_lineage_into_file_scan_task() {
1804 let mut fixture = TableTestFixture::new();
1805 fixture.setup_manifest_files().await;
1806
1807 let mut tasks: Vec<_> = fixture
1808 .table
1809 .scan()
1810 .build()
1811 .unwrap()
1812 .plan_files()
1813 .await
1814 .unwrap()
1815 .try_collect()
1816 .await
1817 .unwrap();
1818
1819 assert_eq!(tasks.len(), 2);
1820 tasks.sort_by_key(|task| task.data_file_path().to_string());
1821
1822 assert_eq!(
1825 tasks[0].data_file_path(),
1826 format!("{}/1.parquet", &fixture.table_location)
1827 );
1828 assert_eq!(tasks[0].data_sequence_number(), Some(1));
1829 assert_eq!(
1830 tasks[1].data_file_path(),
1831 format!("{}/3.parquet", &fixture.table_location)
1832 );
1833 assert_eq!(tasks[1].data_sequence_number(), Some(0));
1834
1835 assert!(tasks.iter().all(|task| task.first_row_id().is_none()));
1837 }
1838
1839 #[tokio::test]
1840 async fn test_plan_files_carries_row_lineage_from_v3_manifest() {
1841 let mut fixture = TableTestFixture::new();
1842 fixture.setup_v3_manifest_files().await;
1843
1844 let task = fixture
1845 .table
1846 .scan()
1847 .build()
1848 .unwrap()
1849 .plan_files()
1850 .await
1851 .unwrap()
1852 .try_collect::<Vec<_>>()
1853 .await
1854 .unwrap()
1855 .into_iter()
1856 .next()
1857 .expect("expected one FileScanTask");
1858
1859 assert_eq!(task.first_row_id(), Some(42));
1862 assert_eq!(task.data_sequence_number(), Some(1));
1864 }
1865
1866 #[tokio::test]
1867 async fn test_filtered_scan_with_dropped_partition_source_column() {
1868 let mut fixture = TableTestFixture::new();
1869 fixture.setup_manifest_files().await;
1870
1871 let baseline = scan_y_gte_5(&fixture.table).await;
1873 assert!(!baseline.is_empty());
1874 assert!(baseline.iter().all(|y| *y >= 5));
1875
1876 let current_schema = fixture.table.metadata().current_schema();
1880 let evolved_schema = Schema::builder()
1881 .with_fields(
1882 current_schema
1883 .as_struct()
1884 .fields()
1885 .iter()
1886 .filter(|field| field.id != 1)
1887 .cloned(),
1888 )
1889 .with_identifier_field_ids(vec![2])
1890 .build()
1891 .unwrap();
1892 let evolved =
1893 TableMetadataBuilder::new_from_metadata(fixture.table.metadata().clone(), None)
1894 .add_default_partition_spec(UnboundPartitionSpec::builder().build())
1895 .unwrap()
1896 .add_current_schema(evolved_schema)
1897 .unwrap()
1898 .build()
1899 .unwrap()
1900 .metadata;
1901
1902 let parent = evolved.current_snapshot().unwrap().clone();
1905 let snapshot = Snapshot::builder()
1906 .with_snapshot_id(parent.snapshot_id() + 1)
1907 .with_parent_snapshot_id(Some(parent.snapshot_id()))
1908 .with_sequence_number(evolved.last_sequence_number() + 1)
1909 .with_timestamp_ms(evolved.last_updated_ms + 1)
1910 .with_schema_id(evolved.current_schema_id())
1911 .with_manifest_list(parent.manifest_list())
1912 .with_summary(Summary {
1913 operation: Operation::Append,
1914 additional_properties: HashMap::new(),
1915 })
1916 .build();
1917 let metadata = TableMetadataBuilder::new_from_metadata(evolved, None)
1918 .set_branch_snapshot(snapshot, MAIN_BRANCH)
1919 .unwrap()
1920 .build()
1921 .unwrap()
1922 .metadata;
1923 let table = fixture.table.clone().with_metadata(Arc::new(metadata));
1924
1925 let evolved = scan_y_gte_5(&table).await;
1928 assert_eq!(evolved, baseline);
1929 }
1930
1931 async fn scan_y_gte_5(table: &Table) -> Vec<i64> {
1932 let table_scan = table
1933 .scan()
1934 .select(["y"])
1935 .with_filter(Reference::new("y").greater_than_or_equal_to(Datum::long(5)))
1936 .build()
1937 .unwrap();
1938 let batches: Vec<_> = table_scan
1939 .to_arrow()
1940 .await
1941 .unwrap()
1942 .try_collect()
1943 .await
1944 .unwrap();
1945
1946 let mut values: Vec<i64> = batches
1947 .iter()
1948 .flat_map(|batch| {
1949 let col = batch.column_by_name("y").unwrap();
1950 let arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
1951 (0..arr.len()).map(|i| arr.value(i)).collect::<Vec<_>>()
1952 })
1953 .collect();
1954 values.sort_unstable();
1955 values
1956 }
1957
1958 #[tokio::test]
1959 async fn test_open_parquet_no_deletions() {
1960 let mut fixture = TableTestFixture::new();
1961 fixture.setup_manifest_files().await;
1962
1963 let table_scan = fixture
1965 .table
1966 .scan()
1967 .with_row_selection_enabled(true)
1968 .build()
1969 .unwrap();
1970
1971 let batch_stream = table_scan.to_arrow().await.unwrap();
1972
1973 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
1974
1975 let col = batches[0].column_by_name("x").unwrap();
1976
1977 let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
1978 assert_eq!(int64_arr.value(0), 1);
1979 }
1980
1981 #[tokio::test]
1982 async fn test_open_parquet_no_deletions_by_separate_reader() {
1983 let mut fixture = TableTestFixture::new();
1984 fixture.setup_manifest_files().await;
1985
1986 let table_scan = fixture
1988 .table
1989 .scan()
1990 .with_row_selection_enabled(true)
1991 .build()
1992 .unwrap();
1993
1994 let mut plan_task: Vec<_> = table_scan
1995 .plan_files()
1996 .await
1997 .unwrap()
1998 .try_collect()
1999 .await
2000 .unwrap();
2001 assert_eq!(plan_task.len(), 2);
2002
2003 let reader = ArrowReaderBuilder::new(
2004 fixture.table.file_io().clone(),
2005 fixture.table.runtime().clone(),
2006 )
2007 .build();
2008 let batch_stream = reader
2009 .clone()
2010 .read(Box::pin(stream::iter(vec![Ok(plan_task.remove(0))])))
2011 .unwrap()
2012 .stream();
2013 let batch_1: Vec<_> = batch_stream.try_collect().await.unwrap();
2014
2015 let reader = ArrowReaderBuilder::new(
2016 fixture.table.file_io().clone(),
2017 fixture.table.runtime().clone(),
2018 )
2019 .build();
2020 let batch_stream = reader
2021 .read(Box::pin(stream::iter(vec![Ok(plan_task.remove(0))])))
2022 .unwrap()
2023 .stream();
2024 let batch_2: Vec<_> = batch_stream.try_collect().await.unwrap();
2025
2026 assert_eq!(batch_1, batch_2);
2027 }
2028
2029 #[tokio::test]
2030 async fn test_open_parquet_with_projection() {
2031 let mut fixture = TableTestFixture::new();
2032 fixture.setup_manifest_files().await;
2033
2034 let table_scan = fixture
2036 .table
2037 .scan()
2038 .select(["x", "z"])
2039 .with_row_selection_enabled(true)
2040 .build()
2041 .unwrap();
2042
2043 let batch_stream = table_scan.to_arrow().await.unwrap();
2044
2045 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2046
2047 assert_eq!(batches[0].num_columns(), 2);
2048
2049 let col1 = batches[0].column_by_name("x").unwrap();
2050 let int64_arr = col1.as_any().downcast_ref::<Int64Array>().unwrap();
2051 assert_eq!(int64_arr.value(0), 1);
2052
2053 let col2 = batches[0].column_by_name("z").unwrap();
2054 let int64_arr = col2.as_any().downcast_ref::<Int64Array>().unwrap();
2055 assert_eq!(int64_arr.value(0), 3);
2056
2057 let table_scan = fixture.table.scan().select_empty().build().unwrap();
2059 let batch_stream = table_scan.to_arrow().await.unwrap();
2060 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2061
2062 assert_eq!(batches[0].num_columns(), 0);
2063 assert_eq!(batches[0].num_rows(), 1024);
2064 }
2065
2066 #[tokio::test]
2067 async fn test_filter_on_arrow_lt() {
2068 let mut fixture = TableTestFixture::new();
2069 fixture.setup_manifest_files().await;
2070
2071 let mut builder = fixture.table.scan();
2073 let predicate = Reference::new("y").less_than(Datum::long(3));
2074 builder = builder
2075 .with_filter(predicate)
2076 .with_row_selection_enabled(true);
2077 let table_scan = builder.build().unwrap();
2078
2079 let batch_stream = table_scan.to_arrow().await.unwrap();
2080
2081 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2082
2083 assert_eq!(batches[0].num_rows(), 512);
2084
2085 let col = batches[0].column_by_name("x").unwrap();
2086 let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
2087 assert_eq!(int64_arr.value(0), 1);
2088
2089 let col = batches[0].column_by_name("y").unwrap();
2090 let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
2091 assert_eq!(int64_arr.value(0), 2);
2092 }
2093
2094 #[tokio::test]
2095 async fn test_filter_on_arrow_gt_eq() {
2096 let mut fixture = TableTestFixture::new();
2097 fixture.setup_manifest_files().await;
2098
2099 let mut builder = fixture.table.scan();
2101 let predicate = Reference::new("y").greater_than_or_equal_to(Datum::long(5));
2102 builder = builder
2103 .with_filter(predicate)
2104 .with_row_selection_enabled(true);
2105 let table_scan = builder.build().unwrap();
2106
2107 let batch_stream = table_scan.to_arrow().await.unwrap();
2108
2109 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2110
2111 assert_eq!(batches[0].num_rows(), 12);
2112
2113 let col = batches[0].column_by_name("x").unwrap();
2114 let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
2115 assert_eq!(int64_arr.value(0), 1);
2116
2117 let col = batches[0].column_by_name("y").unwrap();
2118 let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
2119 assert_eq!(int64_arr.value(0), 5);
2120 }
2121
2122 #[tokio::test]
2123 async fn test_filter_double_eq() {
2124 let mut fixture = TableTestFixture::new();
2125 fixture.setup_manifest_files().await;
2126
2127 let mut builder = fixture.table.scan();
2129 let predicate = Reference::new("dbl").equal_to(Datum::double(150.0f64));
2130 builder = builder
2131 .with_filter(predicate)
2132 .with_row_selection_enabled(true);
2133 let table_scan = builder.build().unwrap();
2134
2135 let batch_stream = table_scan.to_arrow().await.unwrap();
2136
2137 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2138
2139 assert_eq!(batches.len(), 2);
2140 assert_eq!(batches[0].num_rows(), 12);
2141
2142 let col = batches[0].column_by_name("dbl").unwrap();
2143 let f64_arr = col.as_any().downcast_ref::<Float64Array>().unwrap();
2144 assert_eq!(f64_arr.value(1), 150.0f64);
2145 }
2146
2147 #[tokio::test]
2148 async fn test_filter_int_eq() {
2149 let mut fixture = TableTestFixture::new();
2150 fixture.setup_manifest_files().await;
2151
2152 let mut builder = fixture.table.scan();
2154 let predicate = Reference::new("i32").equal_to(Datum::int(150i32));
2155 builder = builder
2156 .with_filter(predicate)
2157 .with_row_selection_enabled(true);
2158 let table_scan = builder.build().unwrap();
2159
2160 let batch_stream = table_scan.to_arrow().await.unwrap();
2161
2162 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2163
2164 assert_eq!(batches.len(), 2);
2165 assert_eq!(batches[0].num_rows(), 12);
2166
2167 let col = batches[0].column_by_name("i32").unwrap();
2168 let i32_arr = col.as_any().downcast_ref::<Int32Array>().unwrap();
2169 assert_eq!(i32_arr.value(1), 150i32);
2170 }
2171
2172 #[tokio::test]
2173 async fn test_filter_long_eq() {
2174 let mut fixture = TableTestFixture::new();
2175 fixture.setup_manifest_files().await;
2176
2177 let mut builder = fixture.table.scan();
2179 let predicate = Reference::new("i64").equal_to(Datum::long(150i64));
2180 builder = builder
2181 .with_filter(predicate)
2182 .with_row_selection_enabled(true);
2183 let table_scan = builder.build().unwrap();
2184
2185 let batch_stream = table_scan.to_arrow().await.unwrap();
2186
2187 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2188
2189 assert_eq!(batches.len(), 2);
2190 assert_eq!(batches[0].num_rows(), 12);
2191
2192 let col = batches[0].column_by_name("i64").unwrap();
2193 let i64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
2194 assert_eq!(i64_arr.value(1), 150i64);
2195 }
2196
2197 #[tokio::test]
2198 async fn test_filter_bool_eq() {
2199 let mut fixture = TableTestFixture::new();
2200 fixture.setup_manifest_files().await;
2201
2202 let mut builder = fixture.table.scan();
2204 let predicate = Reference::new("bool").equal_to(Datum::bool(true));
2205 builder = builder
2206 .with_filter(predicate)
2207 .with_row_selection_enabled(true);
2208 let table_scan = builder.build().unwrap();
2209
2210 let batch_stream = table_scan.to_arrow().await.unwrap();
2211
2212 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2213
2214 assert_eq!(batches.len(), 2);
2215 assert_eq!(batches[0].num_rows(), 512);
2216
2217 let col = batches[0].column_by_name("bool").unwrap();
2218 let bool_arr = col.as_any().downcast_ref::<BooleanArray>().unwrap();
2219 assert!(bool_arr.value(1));
2220 }
2221
2222 #[tokio::test]
2223 async fn test_filter_on_arrow_is_null() {
2224 let mut fixture = TableTestFixture::new();
2225 fixture.setup_manifest_files().await;
2226
2227 let mut builder = fixture.table.scan();
2229 let predicate = Reference::new("y").is_null();
2230 builder = builder
2231 .with_filter(predicate)
2232 .with_row_selection_enabled(true);
2233 let table_scan = builder.build().unwrap();
2234
2235 let batch_stream = table_scan.to_arrow().await.unwrap();
2236
2237 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2238 assert_eq!(batches.len(), 0);
2239 }
2240
2241 #[tokio::test]
2242 async fn test_filter_on_arrow_is_not_null() {
2243 let mut fixture = TableTestFixture::new();
2244 fixture.setup_manifest_files().await;
2245
2246 let mut builder = fixture.table.scan();
2248 let predicate = Reference::new("y").is_not_null();
2249 builder = builder
2250 .with_filter(predicate)
2251 .with_row_selection_enabled(true);
2252 let table_scan = builder.build().unwrap();
2253
2254 let batch_stream = table_scan.to_arrow().await.unwrap();
2255
2256 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2257 assert_eq!(batches[0].num_rows(), 1024);
2258 }
2259
2260 #[tokio::test]
2261 async fn test_filter_on_arrow_lt_and_gt() {
2262 let mut fixture = TableTestFixture::new();
2263 fixture.setup_manifest_files().await;
2264
2265 let mut builder = fixture.table.scan();
2267 let predicate = Reference::new("y")
2268 .less_than(Datum::long(5))
2269 .and(Reference::new("z").greater_than_or_equal_to(Datum::long(4)));
2270 builder = builder
2271 .with_filter(predicate)
2272 .with_row_selection_enabled(true);
2273 let table_scan = builder.build().unwrap();
2274
2275 let batch_stream = table_scan.to_arrow().await.unwrap();
2276
2277 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2278 assert_eq!(batches[0].num_rows(), 500);
2279
2280 let col = batches[0].column_by_name("x").unwrap();
2281 let expected_x = Arc::new(Int64Array::from_iter_values(vec![1; 500])) as ArrayRef;
2282 assert_eq!(col, &expected_x);
2283
2284 let col = batches[0].column_by_name("y").unwrap();
2285 let mut values = vec![];
2286 values.append(vec![3; 200].as_mut());
2287 values.append(vec![4; 300].as_mut());
2288 let expected_y = Arc::new(Int64Array::from_iter_values(values)) as ArrayRef;
2289 assert_eq!(col, &expected_y);
2290
2291 let col = batches[0].column_by_name("z").unwrap();
2292 let expected_z = Arc::new(Int64Array::from_iter_values(vec![4; 500])) as ArrayRef;
2293 assert_eq!(col, &expected_z);
2294 }
2295
2296 #[tokio::test]
2297 async fn test_filter_on_arrow_lt_or_gt() {
2298 let mut fixture = TableTestFixture::new();
2299 fixture.setup_manifest_files().await;
2300
2301 let mut builder = fixture.table.scan();
2303 let predicate = Reference::new("y")
2304 .less_than(Datum::long(5))
2305 .or(Reference::new("z").greater_than_or_equal_to(Datum::long(4)));
2306 builder = builder
2307 .with_filter(predicate)
2308 .with_row_selection_enabled(true);
2309 let table_scan = builder.build().unwrap();
2310
2311 let batch_stream = table_scan.to_arrow().await.unwrap();
2312
2313 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2314 assert_eq!(batches[0].num_rows(), 1024);
2315
2316 let col = batches[0].column_by_name("x").unwrap();
2317 let expected_x = Arc::new(Int64Array::from_iter_values(vec![1; 1024])) as ArrayRef;
2318 assert_eq!(col, &expected_x);
2319
2320 let col = batches[0].column_by_name("y").unwrap();
2321 let mut values = vec![2; 512];
2322 values.append(vec![3; 200].as_mut());
2323 values.append(vec![4; 300].as_mut());
2324 values.append(vec![5; 12].as_mut());
2325 let expected_y = Arc::new(Int64Array::from_iter_values(values)) as ArrayRef;
2326 assert_eq!(col, &expected_y);
2327
2328 let col = batches[0].column_by_name("z").unwrap();
2329 let mut values = vec![3; 512];
2330 values.append(vec![4; 512].as_mut());
2331 let expected_z = Arc::new(Int64Array::from_iter_values(values)) as ArrayRef;
2332 assert_eq!(col, &expected_z);
2333 }
2334
2335 #[tokio::test]
2336 async fn test_filter_on_arrow_startswith() {
2337 let mut fixture = TableTestFixture::new();
2338 fixture.setup_manifest_files().await;
2339
2340 let mut builder = fixture.table.scan();
2342 let predicate = Reference::new("a").starts_with(Datum::string("Ice"));
2343 builder = builder
2344 .with_filter(predicate)
2345 .with_row_selection_enabled(true);
2346 let table_scan = builder.build().unwrap();
2347
2348 let batch_stream = table_scan.to_arrow().await.unwrap();
2349
2350 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2351
2352 assert_eq!(batches[0].num_rows(), 512);
2353
2354 let col = batches[0].column_by_name("a").unwrap();
2355 let string_arr = col.as_any().downcast_ref::<StringArray>().unwrap();
2356 assert_eq!(string_arr.value(0), "Iceberg");
2357 }
2358
2359 #[tokio::test]
2360 async fn test_filter_on_arrow_not_startswith() {
2361 let mut fixture = TableTestFixture::new();
2362 fixture.setup_manifest_files().await;
2363
2364 let mut builder = fixture.table.scan();
2366 let predicate = Reference::new("a").not_starts_with(Datum::string("Ice"));
2367 builder = builder
2368 .with_filter(predicate)
2369 .with_row_selection_enabled(true);
2370 let table_scan = builder.build().unwrap();
2371
2372 let batch_stream = table_scan.to_arrow().await.unwrap();
2373
2374 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2375
2376 assert_eq!(batches[0].num_rows(), 512);
2377
2378 let col = batches[0].column_by_name("a").unwrap();
2379 let string_arr = col.as_any().downcast_ref::<StringArray>().unwrap();
2380 assert_eq!(string_arr.value(0), "Apache");
2381 }
2382
2383 #[tokio::test]
2384 async fn test_filter_on_arrow_in() {
2385 let mut fixture = TableTestFixture::new();
2386 fixture.setup_manifest_files().await;
2387
2388 let mut builder = fixture.table.scan();
2390 let predicate =
2391 Reference::new("a").is_in([Datum::string("Sioux"), Datum::string("Iceberg")]);
2392 builder = builder
2393 .with_filter(predicate)
2394 .with_row_selection_enabled(true);
2395 let table_scan = builder.build().unwrap();
2396
2397 let batch_stream = table_scan.to_arrow().await.unwrap();
2398
2399 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2400
2401 assert_eq!(batches[0].num_rows(), 512);
2402
2403 let col = batches[0].column_by_name("a").unwrap();
2404 let string_arr = col.as_any().downcast_ref::<StringArray>().unwrap();
2405 assert_eq!(string_arr.value(0), "Iceberg");
2406 }
2407
2408 #[tokio::test]
2409 async fn test_filter_on_arrow_not_in() {
2410 let mut fixture = TableTestFixture::new();
2411 fixture.setup_manifest_files().await;
2412
2413 let mut builder = fixture.table.scan();
2415 let predicate =
2416 Reference::new("a").is_not_in([Datum::string("Sioux"), Datum::string("Iceberg")]);
2417 builder = builder
2418 .with_filter(predicate)
2419 .with_row_selection_enabled(true);
2420 let table_scan = builder.build().unwrap();
2421
2422 let batch_stream = table_scan.to_arrow().await.unwrap();
2423
2424 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2425
2426 assert_eq!(batches[0].num_rows(), 512);
2427
2428 let col = batches[0].column_by_name("a").unwrap();
2429 let string_arr = col.as_any().downcast_ref::<StringArray>().unwrap();
2430 assert_eq!(string_arr.value(0), "Apache");
2431 }
2432
2433 fn file_scan_task_test_schema(primitive_type: PrimitiveType) -> Arc<Schema> {
2434 Arc::new(
2435 Schema::builder()
2436 .with_fields(vec![Arc::new(NestedField::required(
2437 1,
2438 "x",
2439 Type::Primitive(primitive_type),
2440 ))])
2441 .build()
2442 .unwrap(),
2443 )
2444 }
2445
2446 fn assert_file_scan_task_serde_round_trip(task: FileScanTask) {
2447 let serialized = serde_json::to_string(&task).unwrap();
2449 let deserialized: FileScanTask = serde_json::from_str(&serialized).unwrap();
2450
2451 assert_eq!(task, deserialized);
2452 }
2453
2454 fn file_scan_task_with_partition(
2455 primitive_type: PrimitiveType,
2456 transform: Transform,
2457 partition_value: Literal,
2458 ) -> FileScanTask {
2459 let schema = file_scan_task_test_schema(primitive_type);
2460 let partition_spec = Arc::new(
2461 PartitionSpec::builder(schema.clone())
2462 .add_partition_field("x", "x_partition", transform)
2463 .unwrap()
2464 .build()
2465 .unwrap(),
2466 );
2467 FileScanTask::builder()
2468 .with_data_file_path("data_file_path".to_string())
2469 .with_file_size_in_bytes(123)
2470 .with_start(10)
2471 .with_length(100)
2472 .with_project_field_ids(vec![1])
2473 .with_schema(schema)
2474 .with_data_file_format(DataFileFormat::Parquet)
2475 .with_partition(Some(Struct::from_iter([Some(partition_value)])))
2476 .with_partition_spec(Some(partition_spec))
2477 .with_case_sensitive(true)
2478 .build()
2479 .unwrap()
2480 }
2481
2482 #[test]
2483 fn test_file_scan_task_serde_without_predicate() {
2484 let task = FileScanTask::builder()
2485 .with_data_file_path("data_file_path".to_string())
2486 .with_file_size_in_bytes(0)
2487 .with_start(0)
2488 .with_length(100)
2489 .with_project_field_ids(vec![1, 2, 3])
2490 .with_schema(file_scan_task_test_schema(PrimitiveType::Binary))
2491 .with_record_count(Some(100))
2492 .with_first_row_id(Some(1000))
2493 .with_data_sequence_number(Some(5))
2494 .with_data_file_format(DataFileFormat::Parquet)
2495 .with_case_sensitive(false)
2496 .build()
2497 .unwrap();
2498 assert_file_scan_task_serde_round_trip(task);
2499 }
2500
2501 #[test]
2502 fn test_file_scan_task_serde_with_predicate() {
2503 let task = FileScanTask::builder()
2504 .with_data_file_path("data_file_path".to_string())
2505 .with_file_size_in_bytes(0)
2506 .with_start(0)
2507 .with_length(100)
2508 .with_project_field_ids(vec![1, 2, 3])
2509 .with_predicate(Some(BoundPredicate::AlwaysTrue))
2510 .with_schema(file_scan_task_test_schema(PrimitiveType::Binary))
2511 .with_data_file_format(DataFileFormat::Avro)
2512 .with_case_sensitive(false)
2513 .build()
2514 .unwrap();
2515
2516 let serialized = serde_json::to_value(&task).unwrap();
2517 assert!(serialized.get("record_count").is_none());
2518 assert_file_scan_task_serde_round_trip(task);
2519 }
2520
2521 #[test]
2522 fn test_unpartitioned_file_scan_task_serde() {
2523 let task = FileScanTask::builder()
2524 .with_data_file_path("data_file_path".to_string())
2525 .with_file_size_in_bytes(0)
2526 .with_start(0)
2527 .with_length(100)
2528 .with_project_field_ids(vec![1, 2, 3])
2529 .with_schema(file_scan_task_test_schema(PrimitiveType::Binary))
2530 .with_data_file_format(DataFileFormat::Parquet)
2531 .with_partition(Some(Struct::empty()))
2532 .with_case_sensitive(false)
2533 .build()
2534 .unwrap();
2535 assert_file_scan_task_serde_round_trip(task);
2536 }
2537
2538 #[test]
2539 fn test_file_scan_task_serde_with_all_optional_fields() {
2540 let schema = file_scan_task_test_schema(PrimitiveType::Long);
2541 let partition_spec = Arc::new(
2542 PartitionSpec::builder(schema.clone())
2543 .add_partition_field("x", "x", Transform::Identity)
2544 .unwrap()
2545 .build()
2546 .unwrap(),
2547 );
2548 let unified_partition_type = Arc::new(partition_spec.partition_type(&schema).unwrap());
2549 let sort_order = Arc::new(
2550 SortOrder::builder()
2551 .with_order_id(1)
2552 .with_sort_field(
2553 SortField::builder()
2554 .source_id(1)
2555 .transform(Transform::Identity)
2556 .direction(SortDirection::Ascending)
2557 .null_order(NullOrder::First)
2558 .build(),
2559 )
2560 .build(&schema)
2561 .unwrap(),
2562 );
2563 let task = FileScanTask::builder()
2564 .with_data_file_path("data_file_path".to_string())
2565 .with_file_size_in_bytes(123)
2566 .with_start(10)
2567 .with_length(100)
2568 .with_project_field_ids(vec![1])
2569 .with_schema(schema)
2570 .with_data_file_format(DataFileFormat::Parquet)
2571 .with_deletes(vec![
2572 FileScanTaskDeleteFile::builder()
2573 .with_file_path("delete_file_path".to_string())
2574 .with_file_size_in_bytes(23)
2575 .with_file_type(DataContentType::EqualityDeletes)
2576 .with_file_format(DataFileFormat::Parquet)
2577 .with_partition_spec_id(0)
2578 .with_equality_ids(Some(vec![1]))
2579 .with_referenced_data_file(Some("data_file_path".to_string()))
2580 .with_content_offset(Some(12))
2581 .with_content_size_in_bytes(Some(34))
2582 .with_record_count(Some(5))
2583 .with_key_metadata(Some(vec![4, 5, 6].into_boxed_slice()))
2584 .build()
2585 .unwrap(),
2586 ])
2587 .with_partition(Some(Struct::from_iter([Some(Literal::long(42))])))
2588 .with_partition_spec(Some(partition_spec))
2589 .with_name_mapping(Some(Arc::new(NameMapping::new(vec![MappedField::new(
2590 Some(1),
2591 vec!["x".to_string()],
2592 vec![],
2593 )]))))
2594 .with_unified_partition_type(Some(unified_partition_type))
2595 .with_sort_order_id(Some(1))
2596 .with_sort_order(Some(sort_order))
2597 .with_case_sensitive(true)
2598 .with_key_metadata(Some(vec![1, 2, 3].into_boxed_slice()))
2599 .build()
2600 .unwrap();
2601 assert_file_scan_task_serde_round_trip(task);
2602 }
2603
2604 #[test]
2605 fn test_file_scan_task_serde_with_date_partition() {
2606 let task = file_scan_task_with_partition(
2607 PrimitiveType::Date,
2608 Transform::Identity,
2609 Literal::date(19_000),
2610 );
2611 assert_file_scan_task_serde_round_trip(task);
2612 }
2613
2614 #[test]
2615 fn test_file_scan_task_serde_with_timestamp_ns_partition() {
2616 let task = file_scan_task_with_partition(
2617 PrimitiveType::TimestampNs,
2618 Transform::Identity,
2619 Literal::timestamp_nano(1_510_871_468_123_456_789),
2620 );
2621 assert_file_scan_task_serde_round_trip(task);
2622 }
2623
2624 #[test]
2625 fn test_file_scan_task_serde_with_timestamptz_ns_partition() {
2626 let task = file_scan_task_with_partition(
2627 PrimitiveType::TimestamptzNs,
2628 Transform::Identity,
2629 Literal::timestamptz_nano(1_510_871_468_123_456_789),
2630 );
2631 assert_file_scan_task_serde_round_trip(task);
2632 }
2633
2634 #[test]
2635 fn test_file_scan_task_serde_with_decimal_partition() {
2636 let task = file_scan_task_with_partition(
2637 PrimitiveType::Decimal {
2638 precision: 9,
2639 scale: 2,
2640 },
2641 Transform::Identity,
2642 Literal::decimal(12_345),
2643 );
2644 assert_file_scan_task_serde_round_trip(task);
2645 }
2646
2647 #[test]
2648 fn test_file_scan_task_serde_with_uuid_partition() {
2649 let task = file_scan_task_with_partition(
2650 PrimitiveType::Uuid,
2651 Transform::Identity,
2652 Literal::uuid(Uuid::from_u128(0x12345678_90ab_cdef_1234_567890abcdef)),
2653 );
2654 assert_file_scan_task_serde_round_trip(task);
2655 }
2656
2657 #[test]
2658 fn test_file_scan_task_serde_with_fixed_partition() {
2659 let task = file_scan_task_with_partition(
2660 PrimitiveType::Fixed(4),
2661 Transform::Identity,
2662 Literal::fixed([1, 2, 3, 4]),
2663 );
2664 assert_file_scan_task_serde_round_trip(task);
2665 }
2666
2667 #[test]
2668 fn test_file_scan_task_serde_with_bucket_partition() {
2669 let task = file_scan_task_with_partition(
2670 PrimitiveType::String,
2671 Transform::Bucket(4),
2672 Literal::int(2),
2673 );
2674 assert_file_scan_task_serde_round_trip(task);
2675 }
2676
2677 #[tokio::test]
2678 async fn test_select_with_file_column() {
2679 let mut fixture = TableTestFixture::new();
2680 fixture.setup_manifest_files().await;
2681
2682 let table_scan = fixture
2684 .table
2685 .scan()
2686 .select(["x", RESERVED_COL_NAME_FILE])
2687 .with_row_selection_enabled(true)
2688 .build()
2689 .unwrap();
2690
2691 let batch_stream = table_scan.to_arrow().await.unwrap();
2692 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2693
2694 assert_eq!(batches[0].num_columns(), 2);
2696
2697 let x_col = batches[0].column_by_name("x").unwrap();
2699 let x_arr = x_col.as_primitive::<arrow_array::types::Int64Type>();
2700 assert_eq!(x_arr.value(0), 1);
2701
2702 let file_col = batches[0].column_by_name(RESERVED_COL_NAME_FILE);
2704 assert!(
2705 file_col.is_some(),
2706 "_file column should be present in the batch"
2707 );
2708
2709 let file_col = file_col.unwrap();
2711 assert!(
2712 matches!(
2713 file_col.data_type(),
2714 arrow_schema::DataType::RunEndEncoded(_, _)
2715 ),
2716 "_file column should use RunEndEncoded type"
2717 );
2718
2719 let run_array = file_col
2721 .as_any()
2722 .downcast_ref::<RunArray<Int32Type>>()
2723 .expect("_file column should be a RunArray");
2724
2725 let values = run_array.values();
2726 let string_values = values.as_string::<i32>();
2727 assert_eq!(string_values.len(), 1, "Should have a single file path");
2728
2729 let file_path = string_values.value(0);
2730 assert!(
2731 file_path.ends_with(".parquet"),
2732 "File path should end with .parquet, got: {file_path}"
2733 );
2734 }
2735
2736 #[tokio::test]
2737 async fn test_select_file_column_position() {
2738 let mut fixture = TableTestFixture::new();
2739 fixture.setup_manifest_files().await;
2740
2741 let table_scan = fixture
2743 .table
2744 .scan()
2745 .select(["x", RESERVED_COL_NAME_FILE, "z"])
2746 .with_row_selection_enabled(true)
2747 .build()
2748 .unwrap();
2749
2750 let batch_stream = table_scan.to_arrow().await.unwrap();
2751 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2752
2753 assert_eq!(batches[0].num_columns(), 3);
2754
2755 let schema = batches[0].schema();
2757 assert_eq!(schema.field(0).name(), "x");
2758 assert_eq!(schema.field(1).name(), RESERVED_COL_NAME_FILE);
2759 assert_eq!(schema.field(2).name(), "z");
2760
2761 assert!(batches[0].column_by_name("x").is_some());
2763 assert!(batches[0].column_by_name(RESERVED_COL_NAME_FILE).is_some());
2764 assert!(batches[0].column_by_name("z").is_some());
2765 }
2766
2767 #[tokio::test]
2768 async fn test_select_file_column_only() {
2769 let mut fixture = TableTestFixture::new();
2770 fixture.setup_manifest_files().await;
2771
2772 let table_scan = fixture
2774 .table
2775 .scan()
2776 .select([RESERVED_COL_NAME_FILE])
2777 .with_row_selection_enabled(true)
2778 .build()
2779 .unwrap();
2780
2781 let batch_stream = table_scan.to_arrow().await.unwrap();
2782 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2783
2784 assert_eq!(batches[0].num_columns(), 1);
2786
2787 let schema = batches[0].schema();
2789 assert_eq!(schema.field(0).name(), RESERVED_COL_NAME_FILE);
2790
2791 let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
2795 assert_eq!(total_rows, 2048);
2796 }
2797
2798 #[tokio::test]
2799 async fn test_file_column_with_multiple_files() {
2800 use std::collections::HashSet;
2801
2802 let mut fixture = TableTestFixture::new();
2803 fixture.setup_manifest_files().await;
2804
2805 let table_scan = fixture
2807 .table
2808 .scan()
2809 .select(["x", RESERVED_COL_NAME_FILE])
2810 .with_row_selection_enabled(true)
2811 .build()
2812 .unwrap();
2813
2814 let batch_stream = table_scan.to_arrow().await.unwrap();
2815 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2816
2817 let mut file_paths = HashSet::new();
2819 for batch in &batches {
2820 let file_col = batch.column_by_name(RESERVED_COL_NAME_FILE).unwrap();
2821 let run_array = file_col
2822 .as_any()
2823 .downcast_ref::<RunArray<Int32Type>>()
2824 .expect("_file column should be a RunArray");
2825
2826 let values = run_array.values();
2827 let string_values = values.as_string::<i32>();
2828 for i in 0..string_values.len() {
2829 file_paths.insert(string_values.value(i).to_string());
2830 }
2831 }
2832
2833 assert!(!file_paths.is_empty(), "Should have at least one file path");
2835
2836 for path in &file_paths {
2838 assert!(
2839 path.ends_with(".parquet"),
2840 "All file paths should end with .parquet, got: {path}"
2841 );
2842 }
2843 }
2844
2845 #[tokio::test]
2846 async fn test_file_column_at_start() {
2847 let mut fixture = TableTestFixture::new();
2848 fixture.setup_manifest_files().await;
2849
2850 let table_scan = fixture
2852 .table
2853 .scan()
2854 .select([RESERVED_COL_NAME_FILE, "x", "y"])
2855 .with_row_selection_enabled(true)
2856 .build()
2857 .unwrap();
2858
2859 let batch_stream = table_scan.to_arrow().await.unwrap();
2860 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2861
2862 assert_eq!(batches[0].num_columns(), 3);
2863
2864 let schema = batches[0].schema();
2866 assert_eq!(schema.field(0).name(), RESERVED_COL_NAME_FILE);
2867 assert_eq!(schema.field(1).name(), "x");
2868 assert_eq!(schema.field(2).name(), "y");
2869 }
2870
2871 #[tokio::test]
2872 async fn test_file_column_at_end() {
2873 let mut fixture = TableTestFixture::new();
2874 fixture.setup_manifest_files().await;
2875
2876 let table_scan = fixture
2878 .table
2879 .scan()
2880 .select(["x", "y", RESERVED_COL_NAME_FILE])
2881 .with_row_selection_enabled(true)
2882 .build()
2883 .unwrap();
2884
2885 let batch_stream = table_scan.to_arrow().await.unwrap();
2886 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2887
2888 assert_eq!(batches[0].num_columns(), 3);
2889
2890 let schema = batches[0].schema();
2892 assert_eq!(schema.field(0).name(), "x");
2893 assert_eq!(schema.field(1).name(), "y");
2894 assert_eq!(schema.field(2).name(), RESERVED_COL_NAME_FILE);
2895 }
2896
2897 #[tokio::test]
2898 async fn test_select_with_repeated_column_names() {
2899 let mut fixture = TableTestFixture::new();
2900 fixture.setup_manifest_files().await;
2901
2902 let table_scan = fixture
2905 .table
2906 .scan()
2907 .select([
2908 "x",
2909 RESERVED_COL_NAME_FILE,
2910 "x", "y",
2912 RESERVED_COL_NAME_FILE, "y", ])
2915 .with_row_selection_enabled(true)
2916 .build()
2917 .unwrap();
2918
2919 let batch_stream = table_scan.to_arrow().await.unwrap();
2920 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2921
2922 assert_eq!(
2924 batches[0].num_columns(),
2925 6,
2926 "Should have exactly 6 columns with duplicates"
2927 );
2928
2929 let schema = batches[0].schema();
2930
2931 assert_eq!(schema.field(0).name(), "x", "Column 0 should be x");
2933 assert_eq!(
2934 schema.field(1).name(),
2935 RESERVED_COL_NAME_FILE,
2936 "Column 1 should be _file"
2937 );
2938 assert_eq!(
2939 schema.field(2).name(),
2940 "x",
2941 "Column 2 should be x (duplicate)"
2942 );
2943 assert_eq!(schema.field(3).name(), "y", "Column 3 should be y");
2944 assert_eq!(
2945 schema.field(4).name(),
2946 RESERVED_COL_NAME_FILE,
2947 "Column 4 should be _file (duplicate)"
2948 );
2949 assert_eq!(
2950 schema.field(5).name(),
2951 "y",
2952 "Column 5 should be y (duplicate)"
2953 );
2954
2955 assert!(
2957 matches!(schema.field(0).data_type(), arrow_schema::DataType::Int64),
2958 "Column x should be Int64"
2959 );
2960 assert!(
2961 matches!(schema.field(2).data_type(), arrow_schema::DataType::Int64),
2962 "Column x (duplicate) should be Int64"
2963 );
2964 assert!(
2965 matches!(schema.field(3).data_type(), arrow_schema::DataType::Int64),
2966 "Column y should be Int64"
2967 );
2968 assert!(
2969 matches!(schema.field(5).data_type(), arrow_schema::DataType::Int64),
2970 "Column y (duplicate) should be Int64"
2971 );
2972 assert!(
2973 matches!(
2974 schema.field(1).data_type(),
2975 arrow_schema::DataType::RunEndEncoded(_, _)
2976 ),
2977 "_file column should use RunEndEncoded type"
2978 );
2979 assert!(
2980 matches!(
2981 schema.field(4).data_type(),
2982 arrow_schema::DataType::RunEndEncoded(_, _)
2983 ),
2984 "_file column (duplicate) should use RunEndEncoded type"
2985 );
2986 }
2987
2988 fn table_with_data_column(column_name: &str) -> Table {
2994 use crate::spec::{
2995 FormatVersion, MAIN_BRANCH, Operation, Snapshot, SnapshotReference, SnapshotRetention,
2996 SortOrder, Summary, TableMetadataBuilder, UnboundPartitionSpec,
2997 };
2998
2999 let schema = Schema::builder()
3000 .with_schema_id(0)
3001 .with_fields(vec![
3002 NestedField::required(1, "id", Type::Primitive(PrimitiveType::Int)).into(),
3003 NestedField::required(2, column_name, Type::Primitive(PrimitiveType::Int)).into(),
3004 ])
3005 .build()
3006 .unwrap();
3007
3008 let snapshot = Snapshot::builder()
3009 .with_snapshot_id(1)
3010 .with_timestamp_ms(1)
3011 .with_sequence_number(0)
3012 .with_schema_id(0)
3013 .with_manifest_list("/snap-1.avro")
3014 .with_summary(Summary {
3015 operation: Operation::Append,
3016 additional_properties: HashMap::new(),
3017 })
3018 .build();
3019
3020 let metadata = TableMetadataBuilder::new(
3021 schema,
3022 UnboundPartitionSpec::builder().with_spec_id(0).build(),
3023 SortOrder::unsorted_order(),
3024 "s3://bucket/table".to_string(),
3025 FormatVersion::V2,
3026 HashMap::new(),
3027 )
3028 .unwrap()
3029 .add_snapshot(snapshot)
3030 .unwrap()
3031 .set_ref(MAIN_BRANCH, SnapshotReference {
3032 snapshot_id: 1,
3033 retention: SnapshotRetention::Branch {
3034 min_snapshots_to_keep: None,
3035 max_snapshot_age_ms: None,
3036 max_ref_age_ms: None,
3037 },
3038 })
3039 .unwrap()
3040 .build()
3041 .unwrap()
3042 .metadata;
3043
3044 Table::builder()
3045 .metadata(metadata)
3046 .identifier(TableIdent::from_strs(["db", "table1"]).unwrap())
3047 .file_io(FileIO::new_with_fs())
3048 .runtime(test_runtime())
3049 .build()
3050 .unwrap()
3051 }
3052
3053 #[test]
3057 fn test_scan_projects_data_column_named_like_delete_file_column() {
3058 for column_name in ["pos", "file_path"] {
3059 let table = table_with_data_column(column_name);
3060
3061 let table_scan = table
3064 .scan()
3065 .select([column_name])
3066 .build()
3067 .unwrap_or_else(|e| panic!("scan of data column `{column_name}` failed: {e}"));
3068
3069 assert_eq!(
3070 table_scan.plan_context.as_ref().unwrap().field_ids.as_ref(),
3071 &[2]
3072 );
3073
3074 let default_scan = table.scan().build().unwrap();
3077 assert_eq!(
3078 default_scan
3079 .plan_context
3080 .as_ref()
3081 .unwrap()
3082 .field_ids
3083 .as_ref(),
3084 &[1, 2]
3085 );
3086 }
3087 }
3088
3089 #[test]
3092 fn test_scan_rejects_unknown_column_named_like_delete_file_column() {
3093 let table = table_with_data_column("file_path");
3095
3096 let err = table
3097 .scan()
3098 .select(["pos"])
3099 .build()
3100 .expect_err("projecting an absent column should fail");
3101 assert_eq!(err.kind(), ErrorKind::DataInvalid);
3102 assert!(err.to_string().contains("not found"));
3103 }
3104
3105 #[tokio::test]
3106 async fn test_scan_deadlock() {
3107 let mut fixture = TableTestFixture::new();
3108 fixture.setup_deadlock_manifests().await;
3109
3110 let table_scan = fixture
3117 .table
3118 .scan()
3119 .with_concurrency_limit(1)
3120 .build()
3121 .unwrap();
3122
3123 let result = tokio::time::timeout(std::time::Duration::from_secs(5), async {
3126 table_scan
3127 .plan_files()
3128 .await
3129 .unwrap()
3130 .try_collect::<Vec<_>>()
3131 .await
3132 })
3133 .await;
3134
3135 assert!(result.is_ok(), "Scan timed out - deadlock detected");
3137 }
3138
3139 #[tokio::test]
3140 async fn test_select_with_spec_id_column() {
3141 let mut fixture = TableTestFixture::new();
3142 fixture.setup_manifest_files().await;
3143
3144 let table_scan = fixture
3146 .table
3147 .scan()
3148 .select(["x", RESERVED_COL_NAME_SPEC_ID, "z"])
3149 .with_row_selection_enabled(true)
3150 .build()
3151 .unwrap();
3152
3153 let batch_stream = table_scan.to_arrow().await.unwrap();
3154 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
3155
3156 assert_eq!(batches[0].num_columns(), 3);
3158
3159 let col1 = batches[0].column_by_name("x").unwrap();
3161 let int64_arr = col1.as_any().downcast_ref::<Int64Array>().unwrap();
3162 assert_eq!(int64_arr.value(0), 1);
3163
3164 let spec_id_col = batches[0].column_by_name(RESERVED_COL_NAME_SPEC_ID);
3166 assert!(
3167 spec_id_col.is_some(),
3168 "_spec_id column should be present in the batch"
3169 );
3170
3171 let spec_id_col = spec_id_col.unwrap();
3173 assert!(
3174 matches!(
3175 spec_id_col.data_type(),
3176 arrow_schema::DataType::RunEndEncoded(_, _)
3177 ),
3178 "_spec_id column should use RunEndEncoded type"
3179 );
3180
3181 let run_array = spec_id_col
3183 .as_any()
3184 .downcast_ref::<RunArray<Int32Type>>()
3185 .expect("_spec_id column should be a RunArray");
3186
3187 let values = run_array.values();
3188 let int_values = values.as_primitive::<Int32Type>();
3189 assert_eq!(int_values.len(), 1, "Should have a single _spec_id");
3190
3191 let spec_id = int_values.value(0);
3192 assert_eq!(spec_id, 0, "_spec_id should be 0, got: {spec_id}");
3193
3194 assert!(batches[0].column_by_name("z").is_some());
3196 }
3197
3198 #[tokio::test]
3199 async fn test_select_with_last_updated_sequence_number_column() {
3200 let mut fixture = TableTestFixture::new();
3205 fixture.setup_manifest_files().await;
3206
3207 let table_scan = fixture
3208 .table
3209 .scan()
3210 .select(["x", RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER])
3211 .with_row_selection_enabled(true)
3212 .build()
3213 .unwrap();
3214
3215 let batches: Vec<_> = table_scan
3216 .to_arrow()
3217 .await
3218 .unwrap()
3219 .try_collect()
3220 .await
3221 .unwrap();
3222
3223 assert_last_updated_seq_all(&batches, None);
3225 }
3226
3227 #[tokio::test]
3228 async fn test_select_with_last_updated_sequence_number_column_v3() {
3229 let mut fixture = TableTestFixture::new();
3235 fixture.setup_v3_manifest_files().await;
3236
3237 let expected_seq = fixture
3240 .table
3241 .metadata()
3242 .current_snapshot()
3243 .unwrap()
3244 .sequence_number();
3245
3246 let table_scan = fixture
3247 .table
3248 .scan()
3249 .select(["x", RESERVED_COL_NAME_LAST_UPDATED_SEQUENCE_NUMBER])
3250 .with_row_selection_enabled(true)
3251 .build()
3252 .unwrap();
3253
3254 let batches: Vec<_> = table_scan
3255 .to_arrow()
3256 .await
3257 .unwrap()
3258 .try_collect()
3259 .await
3260 .unwrap();
3261
3262 assert_last_updated_seq_all(&batches, Some(expected_seq));
3263 }
3264
3265 #[tokio::test]
3266 async fn test_select_with_spec_id_column_from_unpartitioned_table() {
3267 let mut fixture = TableTestFixture::new_unpartitioned();
3268 fixture.setup_unpartitioned_manifest_files().await;
3269
3270 let table_scan = fixture
3272 .table
3273 .scan()
3274 .select(["x", RESERVED_COL_NAME_SPEC_ID])
3275 .with_row_selection_enabled(true)
3276 .build()
3277 .unwrap();
3278
3279 let batch_stream = table_scan.to_arrow().await.unwrap();
3280 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
3281
3282 assert_eq!(batches[0].num_columns(), 2);
3284
3285 let spec_id_col = batches[0].column_by_name(RESERVED_COL_NAME_SPEC_ID);
3287 assert!(
3288 spec_id_col.is_some(),
3289 "_spec_id column should be present in the batch"
3290 );
3291
3292 let spec_id_col = spec_id_col.unwrap();
3294 assert!(
3295 matches!(
3296 spec_id_col.data_type(),
3297 arrow_schema::DataType::RunEndEncoded(_, _)
3298 ),
3299 "_spec_id column should use RunEndEncoded type"
3300 );
3301
3302 let run_array = spec_id_col
3304 .as_any()
3305 .downcast_ref::<RunArray<Int32Type>>()
3306 .expect("_spec_id column should be a RunArray");
3307
3308 let values = run_array.values();
3309 let int_values = values.as_primitive::<Int32Type>();
3310 assert_eq!(int_values.len(), 1, "Should have a single _spec_id");
3311
3312 let spec_id = int_values.value(0);
3313 assert_eq!(spec_id, 0, "_spec_id should be 0, got: {spec_id}");
3314 }
3315
3316 #[tokio::test]
3317 async fn test_select_with_spec_id_column_with_partition_evolution() {
3318 let mut fixture = TableTestFixture::new_with_partition_evolution();
3319 fixture
3320 .setup_manifest_files_with_partition_evolution()
3321 .await;
3322
3323 let table_scan = fixture
3325 .table
3326 .scan()
3327 .select(["x", RESERVED_COL_NAME_SPEC_ID, "z"])
3328 .with_row_selection_enabled(true)
3329 .build()
3330 .unwrap();
3331
3332 let batch_stream = table_scan.to_arrow().await.unwrap();
3333 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
3334
3335 let col1 = batches[0].column_by_name("x").unwrap();
3337 let int64_arr = col1.as_any().downcast_ref::<Int64Array>().unwrap();
3338 assert_eq!(int64_arr.value(0), 1);
3339
3340 let spec_id_col = batches[0].column_by_name(RESERVED_COL_NAME_SPEC_ID);
3342 assert!(
3343 spec_id_col.is_some(),
3344 "_spec_id column should be present in the batch"
3345 );
3346
3347 let spec_id_col = spec_id_col.unwrap();
3349 assert!(
3350 matches!(
3351 spec_id_col.data_type(),
3352 arrow_schema::DataType::RunEndEncoded(_, _)
3353 ),
3354 "_spec_id column should use RunEndEncoded type"
3355 );
3356
3357 let run_array = spec_id_col
3359 .as_any()
3360 .downcast_ref::<RunArray<Int32Type>>()
3361 .expect("_spec_id column should be a RunArray");
3362
3363 let values = run_array.values();
3364 let int_values = values.as_primitive::<Int32Type>();
3365 assert_eq!(int_values.len(), 1, "Should have a single _spec_id");
3366
3367 let spec_id = int_values.value(0);
3368 assert_eq!(spec_id, 2, "_spec_id should be 2, got: {spec_id}");
3369 }
3370
3371 #[tokio::test]
3372 async fn test_select_with_pos_and_file_columns() {
3373 use arrow_array::cast::AsArray;
3374
3375 let mut fixture = TableTestFixture::new();
3376 fixture.setup_manifest_files().await;
3377
3378 let table_scan = fixture
3380 .table
3381 .scan()
3382 .select(["x", RESERVED_COL_NAME_POS, RESERVED_COL_NAME_FILE])
3383 .with_row_selection_enabled(true)
3384 .build()
3385 .unwrap();
3386
3387 let batch_stream = table_scan.to_arrow().await.unwrap();
3388 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
3389 assert_eq!(batches.len(), 2);
3390
3391 for batch in batches.iter() {
3393 assert_eq!(batch.num_columns(), 3);
3395
3396 let x_col = batch.column_by_name("x").unwrap();
3398 let x_arr = x_col.as_primitive::<arrow_array::types::Int64Type>();
3399 assert_eq!(x_arr.value(0), 1);
3400
3401 let pos_col = batch.column(1);
3403 let pos_array: &Int64Array = pos_col
3404 .as_any()
3405 .downcast_ref::<Int64Array>()
3406 .expect("_pos column should be a Int64Array");
3407 assert_eq!(*pos_array, Int64Array::from_iter_values(0i64..1024));
3408
3409 let file_col = batch.column_by_name(RESERVED_COL_NAME_FILE);
3411 assert!(
3412 file_col.is_some(),
3413 "_file column should be present in the batch"
3414 );
3415 }
3416 }
3417
3418 #[tokio::test]
3419 async fn test_pos_column_at_start_with_filters() {
3420 let mut fixture = TableTestFixture::new();
3421 fixture.setup_manifest_files().await;
3422
3423 let predicate = Reference::new("y")
3425 .greater_than(Datum::long(4i64))
3426 .and(Reference::new("y").less_than_or_equal_to(Datum::long(5i64)));
3427 let table_scan = fixture
3429 .table
3430 .scan()
3431 .select([RESERVED_COL_NAME_POS, "x", "y"])
3432 .with_filter(predicate)
3433 .with_row_selection_enabled(true)
3434 .build()
3435 .unwrap();
3436
3437 let batch_stream = table_scan.to_arrow().await.unwrap();
3438 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
3439 assert_eq!(batches.len(), 2);
3440
3441 for batch in batches.iter() {
3443 assert_eq!(batch.num_columns(), 3);
3444 assert_eq!(batch.num_rows(), 12);
3445
3446 let schema = batch.schema();
3448 assert_eq!(schema.field(0).name(), RESERVED_COL_NAME_POS);
3449 assert_eq!(schema.field(1).name(), "x");
3450 assert_eq!(schema.field(2).name(), "y");
3451
3452 let pos_col = batch.column(0);
3453 let pos_array: &Int64Array = pos_col
3454 .as_any()
3455 .downcast_ref::<Int64Array>()
3456 .expect("_pos column should be a Int64Array");
3457 assert_eq!(*pos_array, Int64Array::from_iter_values(1012i64..1024));
3458 }
3459 }
3460
3461 #[tokio::test]
3462 async fn test_repeated_pos_column_with_filter() {
3463 let mut fixture = TableTestFixture::new();
3464 fixture.setup_manifest_files().await;
3465
3466 let predicate = Reference::new("a").not_starts_with(Datum::string("Apa"));
3468 let table_scan = fixture
3470 .table
3471 .scan()
3472 .select([RESERVED_COL_NAME_POS, "a", RESERVED_COL_NAME_POS, "x"])
3473 .with_row_selection_enabled(true)
3474 .with_filter(predicate)
3475 .build()
3476 .unwrap();
3477
3478 let batch_stream = table_scan.to_arrow().await.unwrap();
3479
3480 let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
3481 assert_eq!(batches.len(), 2);
3482
3483 for batch in batches.iter() {
3485 assert_eq!(batch.num_rows(), 512);
3486
3487 let pos_col = batch
3489 .column_by_name("_pos")
3490 .expect("_pos column should be present in the batch");
3491 let pos_array: &Int64Array = pos_col
3492 .as_any()
3493 .downcast_ref::<Int64Array>()
3494 .expect("_pos column should be a Int64Array");
3495 assert_eq!(*pos_array, Int64Array::from_iter_values(512i64..1024));
3496
3497 let pos_col = batch.column(2);
3499 let pos_array: &Int64Array = pos_col
3500 .as_any()
3501 .downcast_ref::<Int64Array>()
3502 .expect("_pos column should be a Int64Array");
3503 assert_eq!(*pos_array, Int64Array::from_iter_values(512i64..1024));
3504 }
3505 }
3506
3507 #[tokio::test]
3511 async fn test_pos_across_row_groups_via_table_scan() {
3512 let mut fixture = TableTestFixture::new();
3513 fixture.setup_multi_row_group_manifest(&[]).await;
3514
3515 let tasks: Vec<_> = fixture
3518 .table
3519 .scan()
3520 .select(["x", RESERVED_COL_NAME_POS])
3521 .build()
3522 .unwrap()
3523 .plan_files()
3524 .await
3525 .unwrap()
3526 .try_collect()
3527 .await
3528 .unwrap();
3529 assert_eq!(tasks.len(), 1, "expected a single FileScanTask");
3530 let task = &tasks[0];
3531 assert!(
3532 task.project_field_ids().contains(&RESERVED_FIELD_ID_POS),
3533 "_pos field id must be projected into the FileScanTask"
3534 );
3535 assert_eq!(task.start(), 0, "TableScan should plan whole-file tasks");
3536 assert_eq!(task.length(), task.file_size_in_bytes());
3537 assert!(task.deletes().is_empty());
3538
3539 let batches: Vec<_> = fixture
3541 .table
3542 .scan()
3543 .select(["x", RESERVED_COL_NAME_POS])
3544 .build()
3545 .unwrap()
3546 .to_arrow()
3547 .await
3548 .unwrap()
3549 .try_collect()
3550 .await
3551 .unwrap();
3552
3553 let pos: Vec<i64> = batches
3554 .iter()
3555 .flat_map(|b| {
3556 b.column_by_name(RESERVED_COL_NAME_POS)
3557 .expect("_pos column should be present")
3558 .as_any()
3559 .downcast_ref::<Int64Array>()
3560 .expect("_pos column should be a Int64Array")
3561 .values()
3562 .to_vec()
3563 })
3564 .collect();
3565 assert_eq!(pos, (0..300).collect::<Vec<i64>>());
3566
3567 let x: Vec<i64> = batches
3569 .iter()
3570 .flat_map(|b| {
3571 b.column_by_name("x")
3572 .unwrap()
3573 .as_primitive::<arrow_array::types::Int64Type>()
3574 .values()
3575 .to_vec()
3576 })
3577 .collect();
3578 assert_eq!(x, (1000..1300).collect::<Vec<i64>>());
3579 }
3580
3581 #[tokio::test]
3584 async fn test_pos_with_positional_deletes_via_table_scan() {
3585 let mut fixture = TableTestFixture::new();
3586 fixture.setup_multi_row_group_manifest(&[150, 299]).await;
3588
3589 let tasks: Vec<_> = fixture
3591 .table
3592 .scan()
3593 .select(["x", RESERVED_COL_NAME_POS])
3594 .build()
3595 .unwrap()
3596 .plan_files()
3597 .await
3598 .unwrap()
3599 .try_collect()
3600 .await
3601 .unwrap();
3602 assert_eq!(tasks.len(), 1);
3603 assert_eq!(
3604 tasks[0].deletes().len(),
3605 1,
3606 "positional delete file should be planned into the task"
3607 );
3608 assert_eq!(
3609 tasks[0].deletes()[0].file_type(),
3610 DataContentType::PositionDeletes
3611 );
3612
3613 let batches: Vec<_> = fixture
3615 .table
3616 .scan()
3617 .select(["x", RESERVED_COL_NAME_POS])
3618 .build()
3619 .unwrap()
3620 .to_arrow()
3621 .await
3622 .unwrap()
3623 .try_collect()
3624 .await
3625 .unwrap();
3626
3627 let pos: Vec<i64> = batches
3628 .iter()
3629 .flat_map(|b| {
3630 b.column_by_name(RESERVED_COL_NAME_POS)
3631 .expect("_pos column should be present")
3632 .as_any()
3633 .downcast_ref::<Int64Array>()
3634 .expect("_pos column should be a Int64Array")
3635 .values()
3636 .to_vec()
3637 })
3638 .collect();
3639
3640 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
3641 assert_eq!(
3642 total, 298,
3643 "two rows should be removed by positional deletes"
3644 );
3645 assert!(!pos.contains(&150) && !pos.contains(&299), "got {pos:?}");
3646 let expected: Vec<i64> = (0..150).chain(151..299).collect();
3647 assert_eq!(pos, expected);
3648 }
3649
3650 #[tokio::test]
3658 async fn test_pos_reads_only_middle_row_group_via_filter() {
3659 let mut fixture = TableTestFixture::new();
3660 fixture.setup_multi_row_group_manifest(&[]).await;
3661
3662 let predicate = Reference::new("y")
3664 .greater_than_or_equal_to(Datum::long(1100))
3665 .and(Reference::new("y").less_than(Datum::long(1200)));
3666
3667 let batches: Vec<_> = fixture
3668 .table
3669 .scan()
3670 .select(["y", RESERVED_COL_NAME_POS])
3671 .with_filter(predicate)
3672 .with_row_group_filtering_enabled(true)
3673 .build()
3674 .unwrap()
3675 .to_arrow()
3676 .await
3677 .unwrap()
3678 .try_collect()
3679 .await
3680 .unwrap();
3681
3682 let total: usize = batches.iter().map(|b| b.num_rows()).sum();
3683 assert_eq!(total, 100, "only the middle row group should be read");
3684
3685 let pos: Vec<i64> = batches
3686 .iter()
3687 .flat_map(|b| {
3688 b.column_by_name(RESERVED_COL_NAME_POS)
3689 .expect("_pos column should be present")
3690 .as_any()
3691 .downcast_ref::<Int64Array>()
3692 .expect("_pos column should be a Int64Array")
3693 .values()
3694 .to_vec()
3695 })
3696 .collect();
3697 assert_eq!(
3698 pos,
3699 (100..200).collect::<Vec<i64>>(),
3700 "_pos must be file-absolute for the middle row group"
3701 );
3702
3703 let y: Vec<i64> = batches
3705 .iter()
3706 .flat_map(|b| {
3707 b.column_by_name("y")
3708 .unwrap()
3709 .as_primitive::<arrow_array::types::Int64Type>()
3710 .values()
3711 .to_vec()
3712 })
3713 .collect();
3714 assert_eq!(y, (1100..1200).collect::<Vec<i64>>());
3715 }
3716}