Skip to main content

iceberg/scan/
mod.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Table scan api.
19
20mod cache;
21use cache::*;
22mod context;
23use context::*;
24mod task;
25
26use std::sync::Arc;
27
28use arrow_array::RecordBatch;
29use futures::channel::mpsc::{Sender, channel};
30use futures::stream::BoxStream;
31use futures::{SinkExt, StreamExt, TryStreamExt};
32pub use task::*;
33
34use crate::arrow::ArrowReaderBuilder;
35pub use crate::arrow::{ScanMetrics, ScanResult};
36use crate::delete_file_index::DeleteFileIndex;
37use crate::expr::visitors::inclusive_metrics_evaluator::InclusiveMetricsEvaluator;
38use crate::expr::{Bind, BoundPredicate, Predicate};
39use crate::io::FileIO;
40use crate::metadata_columns::{get_metadata_field_id, is_metadata_column_name};
41use crate::runtime::Runtime;
42use crate::spec::{DEFAULT_SCHEMA_NAME_MAPPING, DataContentType, NameMapping, SnapshotRef};
43use crate::table::Table;
44use crate::util::available_parallelism;
45use crate::{Error, ErrorKind, Result};
46
47/// A stream of arrow [`RecordBatch`]es.
48pub type ArrowRecordBatchStream = BoxStream<'static, Result<RecordBatch>>;
49
50/// Builder to create table scan.
51pub struct TableScanBuilder<'a> {
52    table: &'a Table,
53    // Defaults to none which means select all columns
54    column_names: Option<Vec<String>>,
55    snapshot_id: Option<i64>,
56    batch_size: Option<usize>,
57    case_sensitive: bool,
58    filter: Option<Predicate>,
59    concurrency_limit_data_files: usize,
60    concurrency_limit_manifest_entries: usize,
61    concurrency_limit_manifest_files: usize,
62    row_group_filtering_enabled: bool,
63    row_selection_enabled: bool,
64}
65
66impl<'a> TableScanBuilder<'a> {
67    pub(crate) fn new(table: &'a Table) -> Self {
68        let num_cpus = available_parallelism().get();
69
70        Self {
71            table,
72            column_names: None,
73            snapshot_id: None,
74            batch_size: None,
75            case_sensitive: true,
76            filter: None,
77            concurrency_limit_data_files: num_cpus,
78            concurrency_limit_manifest_entries: num_cpus,
79            concurrency_limit_manifest_files: num_cpus,
80            row_group_filtering_enabled: true,
81            row_selection_enabled: false,
82        }
83    }
84
85    /// Sets the desired size of batches in the response
86    /// to something other than the default
87    pub fn with_batch_size(mut self, batch_size: Option<usize>) -> Self {
88        self.batch_size = batch_size;
89        self
90    }
91
92    /// Sets the scan's case sensitivity
93    pub fn with_case_sensitive(mut self, case_sensitive: bool) -> Self {
94        self.case_sensitive = case_sensitive;
95        self
96    }
97
98    /// Specifies a predicate to use as a filter
99    pub fn with_filter(mut self, predicate: Predicate) -> Self {
100        // calls rewrite_not to remove Not nodes, which must be absent
101        // when applying the manifest evaluator
102        self.filter = Some(predicate.rewrite_not());
103        self
104    }
105
106    /// Select all columns.
107    pub fn select_all(mut self) -> Self {
108        self.column_names = None;
109        self
110    }
111
112    /// Select empty columns.
113    pub fn select_empty(mut self) -> Self {
114        self.column_names = Some(vec![]);
115        self
116    }
117
118    /// Select some columns of the table.
119    pub fn select(mut self, column_names: impl IntoIterator<Item = impl ToString>) -> Self {
120        self.column_names = Some(
121            column_names
122                .into_iter()
123                .map(|item| item.to_string())
124                .collect(),
125        );
126        self
127    }
128
129    /// Set the snapshot to scan. When not set, it uses current snapshot.
130    pub fn snapshot_id(mut self, snapshot_id: i64) -> Self {
131        self.snapshot_id = Some(snapshot_id);
132        self
133    }
134
135    /// Sets the concurrency limit for both manifest files and manifest
136    /// entries for this scan
137    pub fn with_concurrency_limit(mut self, limit: usize) -> Self {
138        self.concurrency_limit_manifest_files = limit;
139        self.concurrency_limit_manifest_entries = limit;
140        self.concurrency_limit_data_files = limit;
141        self
142    }
143
144    /// Sets the data file concurrency limit for this scan
145    pub fn with_data_file_concurrency_limit(mut self, limit: usize) -> Self {
146        self.concurrency_limit_data_files = limit;
147        self
148    }
149
150    /// Sets the manifest entry concurrency limit for this scan
151    pub fn with_manifest_entry_concurrency_limit(mut self, limit: usize) -> Self {
152        self.concurrency_limit_manifest_entries = limit;
153        self
154    }
155
156    /// Determines whether to enable row group filtering.
157    /// When enabled, if a read is performed with a filter predicate,
158    /// then the metadata for each row group in the parquet file is
159    /// evaluated against the filter predicate and row groups
160    /// that cant contain matching rows will be skipped entirely.
161    ///
162    /// Defaults to enabled, as it generally improves performance or
163    /// keeps it the same, with performance degradation unlikely.
164    pub fn with_row_group_filtering_enabled(mut self, row_group_filtering_enabled: bool) -> Self {
165        self.row_group_filtering_enabled = row_group_filtering_enabled;
166        self
167    }
168
169    /// Determines whether to enable row selection.
170    /// When enabled, if a read is performed with a filter predicate,
171    /// then (for row groups that have not been skipped) the page index
172    /// for each row group in a parquet file is parsed and evaluated
173    /// against the filter predicate to determine if ranges of rows
174    /// within a row group can be skipped, based upon the page-level
175    /// statistics for each column.
176    ///
177    /// Defaults to being disabled. Enabling requires parsing the parquet page
178    /// index, which can be slow enough that parsing the page index outweighs any
179    /// gains from the reduced number of rows that need scanning.
180    /// It is recommended to experiment with partitioning, sorting, row group size,
181    /// page size, and page row limit Iceberg settings on the table being scanned in
182    /// order to get the best performance from using row selection.
183    pub fn with_row_selection_enabled(mut self, row_selection_enabled: bool) -> Self {
184        self.row_selection_enabled = row_selection_enabled;
185        self
186    }
187
188    /// Build the table scan.
189    pub fn build(self) -> Result<TableScan> {
190        let snapshot = match self.snapshot_id {
191            Some(snapshot_id) => self
192                .table
193                .metadata()
194                .snapshot_by_id(snapshot_id)
195                .ok_or_else(|| {
196                    Error::new(
197                        ErrorKind::DataInvalid,
198                        format!("Snapshot with id {snapshot_id} not found"),
199                    )
200                })?
201                .clone(),
202            None => {
203                let Some(current_snapshot_id) = self.table.metadata().current_snapshot() else {
204                    return Ok(TableScan {
205                        batch_size: self.batch_size,
206                        column_names: self.column_names,
207                        file_io: self.table.file_io().clone(),
208                        plan_context: None,
209                        concurrency_limit_data_files: self.concurrency_limit_data_files,
210                        concurrency_limit_manifest_entries: self.concurrency_limit_manifest_entries,
211                        concurrency_limit_manifest_files: self.concurrency_limit_manifest_files,
212                        row_group_filtering_enabled: self.row_group_filtering_enabled,
213                        row_selection_enabled: self.row_selection_enabled,
214                        runtime: self.table.runtime().clone(),
215                    });
216                };
217                current_snapshot_id.clone()
218            }
219        };
220
221        let schema = snapshot.schema(self.table.metadata())?;
222
223        // Check that all column names exist in the schema (skip reserved columns).
224        if let Some(column_names) = self.column_names.as_ref() {
225            for column_name in column_names {
226                // Skip reserved columns that don't exist in the schema
227                if is_metadata_column_name(column_name) {
228                    continue;
229                }
230                if schema.field_by_name(column_name).is_none() {
231                    return Err(Error::new(
232                        ErrorKind::DataInvalid,
233                        format!("Column {column_name} not found in table. Schema: {schema}"),
234                    ));
235                }
236            }
237        }
238
239        let mut field_ids = vec![];
240        let column_names = self.column_names.clone().unwrap_or_else(|| {
241            schema
242                .as_struct()
243                .fields()
244                .iter()
245                .map(|f| f.name.clone())
246                .collect()
247        });
248
249        for column_name in column_names.iter() {
250            // Handle metadata columns (like "_file")
251            if is_metadata_column_name(column_name) {
252                field_ids.push(get_metadata_field_id(column_name)?);
253                continue;
254            }
255
256            let field_id = schema.field_id_by_name(column_name).ok_or_else(|| {
257                Error::new(
258                    ErrorKind::DataInvalid,
259                    format!("Column {column_name} not found in table. Schema: {schema}"),
260                )
261            })?;
262
263            schema
264                .as_struct()
265                .field_by_id(field_id)
266                .ok_or_else(|| {
267                    Error::new(
268                        ErrorKind::FeatureUnsupported,
269                        format!(
270                        "Column {column_name} is not a direct child of schema but a nested field, which is not supported now. Schema: {schema}"
271                    ),
272                )
273            })?;
274
275            field_ids.push(field_id);
276        }
277
278        let snapshot_bound_predicate = if let Some(ref predicates) = self.filter {
279            Some(predicates.bind(schema.clone(), true)?)
280        } else {
281            None
282        };
283
284        let name_mapping = self
285            .table
286            .metadata()
287            .properties()
288            .get(DEFAULT_SCHEMA_NAME_MAPPING)
289            .map(|raw| {
290                serde_json::from_str::<NameMapping>(raw).map_err(|e| {
291                    Error::new(
292                        ErrorKind::DataInvalid,
293                        format!(
294                            "Failed to parse table property {DEFAULT_SCHEMA_NAME_MAPPING} as a NameMapping"
295                        ),
296                    )
297                    .with_source(e)
298                })
299            })
300            .transpose()?
301            .map(Arc::new);
302
303        let plan_context = PlanContext {
304            snapshot,
305            table_metadata: self.table.metadata_ref(),
306            snapshot_schema: schema,
307            case_sensitive: self.case_sensitive,
308            predicate: self.filter.map(Arc::new),
309            snapshot_bound_predicate: snapshot_bound_predicate.map(Arc::new),
310            object_cache: self.table.object_cache(),
311            field_ids: Arc::new(field_ids),
312            name_mapping,
313            partition_filter_cache: Arc::new(PartitionFilterCache::new()),
314            manifest_evaluator_cache: Arc::new(ManifestEvaluatorCache::new()),
315            expression_evaluator_cache: Arc::new(ExpressionEvaluatorCache::new()),
316        };
317
318        Ok(TableScan {
319            batch_size: self.batch_size,
320            column_names: self.column_names,
321            file_io: self.table.file_io().clone(),
322            plan_context: Some(plan_context),
323            concurrency_limit_data_files: self.concurrency_limit_data_files,
324            concurrency_limit_manifest_entries: self.concurrency_limit_manifest_entries,
325            concurrency_limit_manifest_files: self.concurrency_limit_manifest_files,
326            row_group_filtering_enabled: self.row_group_filtering_enabled,
327            row_selection_enabled: self.row_selection_enabled,
328            runtime: self.table.runtime().clone(),
329        })
330    }
331}
332
333/// Table scan.
334#[derive(Debug)]
335pub struct TableScan {
336    /// A [PlanContext], if this table has at least one snapshot, otherwise None.
337    ///
338    /// If this is None, then the scan contains no rows.
339    plan_context: Option<PlanContext>,
340    batch_size: Option<usize>,
341    file_io: FileIO,
342    column_names: Option<Vec<String>>,
343    /// The maximum number of manifest files that will be
344    /// retrieved from [`FileIO`] concurrently
345    concurrency_limit_manifest_files: usize,
346
347    /// The maximum number of [`ManifestEntry`]s that will
348    /// be processed in parallel
349    concurrency_limit_manifest_entries: usize,
350
351    /// The maximum number of [`ManifestEntry`]s that will
352    /// be processed in parallel
353    concurrency_limit_data_files: usize,
354
355    row_group_filtering_enabled: bool,
356    row_selection_enabled: bool,
357
358    runtime: Runtime,
359}
360
361impl TableScan {
362    /// Returns a stream of [`FileScanTask`]s.
363    pub async fn plan_files(&self) -> Result<FileScanTaskStream> {
364        let Some(plan_context) = self.plan_context.as_ref() else {
365            return Ok(Box::pin(futures::stream::empty()));
366        };
367
368        let concurrency_limit_manifest_files = self.concurrency_limit_manifest_files;
369        let concurrency_limit_manifest_entries = self.concurrency_limit_manifest_entries;
370
371        // used to stream ManifestEntryContexts between stages of the file plan operation
372        let (manifest_entry_data_ctx_tx, manifest_entry_data_ctx_rx) =
373            channel(concurrency_limit_manifest_files);
374        let (manifest_entry_delete_ctx_tx, manifest_entry_delete_ctx_rx) =
375            channel(concurrency_limit_manifest_files);
376
377        // used to stream the results back to the caller
378        let (file_scan_task_tx, file_scan_task_rx) = channel(concurrency_limit_manifest_entries);
379
380        let (delete_file_idx, delete_file_tx) = DeleteFileIndex::new(self.runtime.clone());
381
382        let manifest_list = plan_context.get_manifest_list().await?;
383
384        // get the [`ManifestFile`]s from the [`ManifestList`], filtering out any
385        // whose partitions cannot match this
386        // scan's filter
387        let manifest_file_contexts = plan_context.build_manifest_file_contexts(
388            manifest_list,
389            manifest_entry_data_ctx_tx,
390            delete_file_idx.clone(),
391            manifest_entry_delete_ctx_tx,
392        )?;
393
394        let mut channel_for_manifest_error = file_scan_task_tx.clone();
395        let mut channel_for_data_manifest_entry_error = file_scan_task_tx.clone();
396        let mut channel_for_delete_manifest_entry_error = file_scan_task_tx.clone();
397
398        let rt = self.runtime.clone();
399
400        // Concurrently load all [`Manifest`]s and stream their [`ManifestEntry`]s
401        rt.io().spawn(async move {
402            let result = futures::stream::iter(manifest_file_contexts)
403                .try_for_each_concurrent(concurrency_limit_manifest_files, |ctx| async move {
404                    ctx.fetch_manifest_and_stream_manifest_entries().await
405                })
406                .await;
407
408            if let Err(error) = result {
409                let _ = channel_for_manifest_error.send(Err(error)).await;
410            }
411        });
412
413        // Process the delete file [`ManifestEntry`] stream in parallel
414        {
415            let rt = rt.clone();
416            let rt_inner = rt.clone();
417            rt.cpu().spawn(async move {
418                let result = manifest_entry_delete_ctx_rx
419                    .map(|me_ctx| Ok((me_ctx, delete_file_tx.clone())))
420                    .try_for_each_concurrent(
421                        concurrency_limit_manifest_entries,
422                        |(manifest_entry_context, tx)| {
423                            let rt_inner = rt_inner.clone();
424                            async move {
425                                rt_inner
426                                    .cpu()
427                                    .spawn(async move {
428                                        Self::process_delete_manifest_entry(
429                                            manifest_entry_context,
430                                            tx,
431                                        )
432                                        .await
433                                    })
434                                    .await?
435                            }
436                        },
437                    )
438                    .await;
439
440                if let Err(error) = result {
441                    let _ = channel_for_delete_manifest_entry_error
442                        .send(Err(error))
443                        .await;
444                }
445            });
446        }
447
448        // Process the data file [`ManifestEntry`] stream in parallel
449        {
450            let rt_inner = rt.clone();
451            rt.cpu().spawn(async move {
452                let result = manifest_entry_data_ctx_rx
453                    .map(|me_ctx| Ok((me_ctx, file_scan_task_tx.clone())))
454                    .try_for_each_concurrent(
455                        concurrency_limit_manifest_entries,
456                        |(manifest_entry_context, tx)| {
457                            let rt_inner = rt_inner.clone();
458                            async move {
459                                rt_inner
460                                    .cpu()
461                                    .spawn(async move {
462                                        Self::process_data_manifest_entry(
463                                            manifest_entry_context,
464                                            tx,
465                                        )
466                                        .await
467                                    })
468                                    .await?
469                            }
470                        },
471                    )
472                    .await;
473
474                if let Err(error) = result {
475                    let _ = channel_for_data_manifest_entry_error.send(Err(error)).await;
476                }
477            });
478        }
479
480        Ok(file_scan_task_rx.boxed())
481    }
482
483    /// Returns an [`ArrowRecordBatchStream`].
484    pub async fn to_arrow(&self) -> Result<ArrowRecordBatchStream> {
485        let mut arrow_reader_builder =
486            ArrowReaderBuilder::new(self.file_io.clone(), self.runtime.clone())
487                .with_data_file_concurrency_limit(self.concurrency_limit_data_files)
488                .with_row_group_filtering_enabled(self.row_group_filtering_enabled)
489                .with_row_selection_enabled(self.row_selection_enabled);
490
491        if let Some(batch_size) = self.batch_size {
492            arrow_reader_builder = arrow_reader_builder.with_batch_size(batch_size);
493        }
494
495        arrow_reader_builder
496            .build()
497            .read(self.plan_files().await?)
498            .map(|result| result.stream())
499    }
500
501    /// Returns a reference to the column names of the table scan.
502    pub fn column_names(&self) -> Option<&[String]> {
503        self.column_names.as_deref()
504    }
505
506    /// Returns a reference to the snapshot of the table scan.
507    pub fn snapshot(&self) -> Option<&SnapshotRef> {
508        self.plan_context.as_ref().map(|x| &x.snapshot)
509    }
510
511    async fn process_data_manifest_entry(
512        manifest_entry_context: ManifestEntryContext,
513        mut file_scan_task_tx: Sender<Result<FileScanTask>>,
514    ) -> Result<()> {
515        // skip processing this manifest entry if it has been marked as deleted
516        if !manifest_entry_context.manifest_entry.is_alive() {
517            return Ok(());
518        }
519
520        // abort the plan if we encounter a manifest entry for a delete file
521        if manifest_entry_context.manifest_entry.content_type() != DataContentType::Data {
522            return Err(Error::new(
523                ErrorKind::FeatureUnsupported,
524                "Encountered an entry for a delete file in a data file manifest",
525            ));
526        }
527
528        if let Some(ref bound_predicates) = manifest_entry_context.bound_predicates {
529            let BoundPredicates {
530                snapshot_bound_predicate,
531                partition_bound_predicate,
532            } = bound_predicates.as_ref();
533
534            let expression_evaluator_cache =
535                manifest_entry_context.expression_evaluator_cache.as_ref();
536
537            let expression_evaluator = expression_evaluator_cache.get(
538                manifest_entry_context.partition_spec_id,
539                partition_bound_predicate,
540            )?;
541
542            // skip any data file whose partition data indicates that it can't contain
543            // any data that matches this scan's filter
544            if !expression_evaluator.eval(manifest_entry_context.manifest_entry.data_file())? {
545                return Ok(());
546            }
547
548            // skip any data file whose metrics don't match this scan's filter
549            if !InclusiveMetricsEvaluator::eval(
550                snapshot_bound_predicate,
551                manifest_entry_context.manifest_entry.data_file(),
552                false,
553            )? {
554                return Ok(());
555            }
556        }
557
558        // congratulations! the manifest entry has made its way through the
559        // entire plan without getting filtered out. Create a corresponding
560        // FileScanTask and push it to the result stream
561        file_scan_task_tx
562            .send(Ok(manifest_entry_context.into_file_scan_task().await?))
563            .await?;
564
565        Ok(())
566    }
567
568    async fn process_delete_manifest_entry(
569        manifest_entry_context: ManifestEntryContext,
570        mut delete_file_ctx_tx: Sender<DeleteFileContext>,
571    ) -> Result<()> {
572        // skip processing this manifest entry if it has been marked as deleted
573        if !manifest_entry_context.manifest_entry.is_alive() {
574            return Ok(());
575        }
576
577        // abort the plan if we encounter a manifest entry that is not for a delete file
578        if manifest_entry_context.manifest_entry.content_type() == DataContentType::Data {
579            return Err(Error::new(
580                ErrorKind::FeatureUnsupported,
581                "Encountered an entry for a data file in a delete manifest",
582            ));
583        }
584
585        if let Some(ref bound_predicates) = manifest_entry_context.bound_predicates {
586            let expression_evaluator_cache =
587                manifest_entry_context.expression_evaluator_cache.as_ref();
588
589            let expression_evaluator = expression_evaluator_cache.get(
590                manifest_entry_context.partition_spec_id,
591                &bound_predicates.partition_bound_predicate,
592            )?;
593
594            // skip any data file whose partition data indicates that it can't contain
595            // any data that matches this scan's filter
596            if !expression_evaluator.eval(manifest_entry_context.manifest_entry.data_file())? {
597                return Ok(());
598            }
599        }
600
601        delete_file_ctx_tx
602            .send(DeleteFileContext {
603                manifest_entry: manifest_entry_context.manifest_entry.clone(),
604                partition_spec_id: manifest_entry_context.partition_spec_id,
605            })
606            .await?;
607
608        Ok(())
609    }
610}
611
612pub(crate) struct BoundPredicates {
613    partition_bound_predicate: BoundPredicate,
614    snapshot_bound_predicate: BoundPredicate,
615}
616
617#[cfg(test)]
618pub mod tests {
619    //! shared tests for the table scan API
620    #![allow(missing_docs)]
621
622    use std::collections::HashMap;
623    use std::fs;
624    use std::fs::File;
625    use std::sync::Arc;
626
627    use arrow_array::cast::AsArray;
628    use arrow_array::types::Int32Type;
629    use arrow_array::{
630        Array, ArrayRef, BooleanArray, Float64Array, Int32Array, Int64Array, RecordBatch, RunArray,
631        StringArray,
632    };
633    use futures::{TryStreamExt, stream};
634    use minijinja::value::Value;
635    use minijinja::{AutoEscape, Environment, context};
636    use parquet::arrow::{ArrowWriter, PARQUET_FIELD_ID_META_KEY};
637    use parquet::basic::Compression;
638    use parquet::file::properties::WriterProperties;
639    use tempfile::TempDir;
640    use uuid::Uuid;
641
642    use crate::arrow::ArrowReaderBuilder;
643    use crate::expr::{BoundPredicate, Reference};
644    use crate::io::{FileIO, OutputFile};
645    use crate::metadata_columns::{
646        RESERVED_COL_NAME_DELETE_FILE_PATH, RESERVED_COL_NAME_DELETE_FILE_POS,
647        RESERVED_COL_NAME_FILE, RESERVED_COL_NAME_POS, RESERVED_COL_NAME_SPEC_ID,
648        RESERVED_FIELD_ID_DELETE_FILE_PATH, RESERVED_FIELD_ID_DELETE_FILE_POS,
649        RESERVED_FIELD_ID_POS,
650    };
651    use crate::scan::FileScanTask;
652    use crate::spec::{
653        DEFAULT_SCHEMA_NAME_MAPPING, DataContentType, DataFileBuilder, DataFileFormat, Datum,
654        Literal, ManifestEntry, ManifestListWriter, ManifestStatus, ManifestWriterBuilder,
655        NestedField, PartitionSpec, PrimitiveType, Schema, Struct, StructType, TableMetadata, Type,
656    };
657    use crate::table::Table;
658    use crate::test_utils::test_runtime;
659    use crate::{ErrorKind, TableIdent};
660
661    fn render_template(template: &str, ctx: Value) -> String {
662        let mut env = Environment::new();
663        env.set_auto_escape_callback(|_| AutoEscape::None);
664        env.render_str(template, ctx).unwrap()
665    }
666
667    pub struct TableTestFixture {
668        pub table_location: String,
669        pub table: Table,
670    }
671
672    impl TableTestFixture {
673        #[allow(clippy::new_without_default)]
674        pub fn new() -> Self {
675            let tmp_dir = TempDir::new().unwrap();
676            let table_location = tmp_dir.path().join("table1");
677            let manifest_list1_location = table_location.join("metadata/manifests_list_1.avro");
678            let manifest_list2_location = table_location.join("metadata/manifests_list_2.avro");
679            let table_metadata1_location = table_location.join("metadata/v1.json");
680
681            let file_io = FileIO::new_with_fs();
682
683            let table_metadata = {
684                let template_json_str = fs::read_to_string(format!(
685                    "{}/testdata/example_table_metadata_v2.json",
686                    env!("CARGO_MANIFEST_DIR")
687                ))
688                .unwrap();
689                let metadata_json = render_template(&template_json_str, context! {
690                    table_location => &table_location,
691                    manifest_list_1_location => &manifest_list1_location,
692                    manifest_list_2_location => &manifest_list2_location,
693                    table_metadata_1_location => &table_metadata1_location,
694                });
695                serde_json::from_str::<TableMetadata>(&metadata_json).unwrap()
696            };
697
698            let table = Table::builder()
699                .metadata(table_metadata)
700                .identifier(TableIdent::from_strs(["db", "table1"]).unwrap())
701                .file_io(file_io.clone())
702                .metadata_location(table_metadata1_location.as_os_str().to_str().unwrap())
703                .runtime(test_runtime())
704                .build()
705                .unwrap();
706
707            Self {
708                table_location: table_location.to_str().unwrap().to_string(),
709                table,
710            }
711        }
712
713        #[allow(clippy::new_without_default)]
714        pub fn new_empty() -> Self {
715            let tmp_dir = TempDir::new().unwrap();
716            let table_location = tmp_dir.path().join("table1");
717            let table_metadata1_location = table_location.join("metadata/v1.json");
718
719            let file_io = FileIO::new_with_fs();
720
721            let table_metadata = {
722                let template_json_str = fs::read_to_string(format!(
723                    "{}/testdata/example_empty_table_metadata_v2.json",
724                    env!("CARGO_MANIFEST_DIR")
725                ))
726                .unwrap();
727                let metadata_json = render_template(&template_json_str, context! {
728                    table_location => &table_location,
729                    table_metadata_1_location => &table_metadata1_location,
730                });
731                serde_json::from_str::<TableMetadata>(&metadata_json).unwrap()
732            };
733
734            let table = Table::builder()
735                .metadata(table_metadata)
736                .identifier(TableIdent::from_strs(["db", "table1"]).unwrap())
737                .file_io(file_io.clone())
738                .metadata_location(table_metadata1_location.as_os_str().to_str().unwrap())
739                .runtime(test_runtime())
740                .build()
741                .unwrap();
742
743            Self {
744                table_location: table_location.to_str().unwrap().to_string(),
745                table,
746            }
747        }
748
749        /// Creates a fixture with 5 snapshots chained as:
750        ///   S1 (root) -> S2 -> S3 -> S4 -> S5 (current)
751        /// Useful for testing snapshot history traversal.
752        pub fn new_with_deep_history() -> Self {
753            let tmp_dir = TempDir::new().unwrap();
754            let table_location = tmp_dir.path().join("table1");
755            let table_metadata1_location = table_location.join("metadata/v1.json");
756
757            let file_io = FileIO::new_with_fs();
758
759            let table_metadata = {
760                let json_str = fs::read_to_string(format!(
761                    "{}/testdata/example_table_metadata_v2_deep_history.json",
762                    env!("CARGO_MANIFEST_DIR")
763                ))
764                .unwrap();
765                serde_json::from_str::<TableMetadata>(&json_str).unwrap()
766            };
767
768            let table = Table::builder()
769                .metadata(table_metadata)
770                .identifier(TableIdent::from_strs(["db", "table1"]).unwrap())
771                .file_io(file_io.clone())
772                .metadata_location(table_metadata1_location.as_os_str().to_str().unwrap())
773                .runtime(test_runtime())
774                .build()
775                .unwrap();
776
777            Self {
778                table_location: table_location.to_str().unwrap().to_string(),
779                table,
780            }
781        }
782
783        pub fn new_unpartitioned() -> Self {
784            let tmp_dir = TempDir::new().unwrap();
785            let table_location = tmp_dir.path().join("table1");
786            let manifest_list1_location = table_location.join("metadata/manifests_list_1.avro");
787            let manifest_list2_location = table_location.join("metadata/manifests_list_2.avro");
788            let table_metadata1_location = table_location.join("metadata/v1.json");
789
790            let file_io = FileIO::new_with_fs();
791
792            let mut table_metadata = {
793                let template_json_str = fs::read_to_string(format!(
794                    "{}/testdata/example_table_metadata_v2.json",
795                    env!("CARGO_MANIFEST_DIR")
796                ))
797                .unwrap();
798                let metadata_json = render_template(&template_json_str, context! {
799                    table_location => &table_location,
800                    manifest_list_1_location => &manifest_list1_location,
801                    manifest_list_2_location => &manifest_list2_location,
802                    table_metadata_1_location => &table_metadata1_location,
803                });
804                serde_json::from_str::<TableMetadata>(&metadata_json).unwrap()
805            };
806
807            table_metadata.default_spec = Arc::new(PartitionSpec::unpartition_spec());
808            table_metadata.partition_specs.clear();
809            table_metadata.default_partition_type = StructType::new(vec![]);
810            table_metadata
811                .partition_specs
812                .insert(0, table_metadata.default_spec.clone());
813
814            let table = Table::builder()
815                .metadata(table_metadata)
816                .identifier(TableIdent::from_strs(["db", "table1"]).unwrap())
817                .file_io(file_io.clone())
818                .metadata_location(table_metadata1_location.to_str().unwrap())
819                .runtime(test_runtime())
820                .build()
821                .unwrap();
822
823            Self {
824                table_location: table_location.to_str().unwrap().to_string(),
825                table,
826            }
827        }
828
829        pub fn new_with_partition_evolution() -> Self {
830            let table = Self::new().table;
831            let table_location = table.metadata().location.clone();
832
833            let manifest_list1_location =
834                format!("{}/metadata/manifests_list_1.avro", table_location);
835            let manifest_list2_location =
836                format!("{}/metadata/manifests_list_2.avro", table_location);
837            let manifest_list3_location =
838                format!("{}/metadata/manifests_list_3.avro", table_location);
839            let table_metadata1_location = format!("{}/metadata/v1.json", table_location);
840
841            let new_table_metadata = {
842                let template_json_str = fs::read_to_string(format!(
843                    "{}/testdata/example_table_metadata_v2_partition_evolution.json",
844                    env!("CARGO_MANIFEST_DIR")
845                ))
846                .unwrap();
847                let metadata_json = render_template(&template_json_str, context! {
848                    table_location => &table_location,
849                    manifest_list_1_location => &manifest_list1_location,
850                    manifest_list_2_location => &manifest_list2_location,
851                    manifest_list_3_location => &manifest_list3_location,
852                    table_metadata_1_location => &table_metadata1_location,
853                });
854                Arc::new(serde_json::from_str::<TableMetadata>(&metadata_json).unwrap())
855            };
856
857            Self {
858                table_location,
859                table: table.with_metadata(new_table_metadata),
860            }
861        }
862
863        fn next_manifest_file(&self) -> OutputFile {
864            self.table
865                .file_io()
866                .new_output(format!(
867                    "{}/metadata/manifest_{}.avro",
868                    self.table_location,
869                    Uuid::new_v4()
870                ))
871                .unwrap()
872        }
873
874        pub async fn setup_manifest_files(&mut self) {
875            let current_snapshot = self.table.metadata().current_snapshot().unwrap();
876            let parent_snapshot = current_snapshot
877                .parent_snapshot(self.table.metadata())
878                .unwrap();
879            let current_schema = current_snapshot.schema(self.table.metadata()).unwrap();
880            let current_partition_spec = self.table.metadata().default_partition_spec();
881
882            // Write the data files first, then use the file size in the manifest entries
883            let parquet_file_size = self.write_parquet_data_files();
884
885            let mut writer = ManifestWriterBuilder::new(
886                self.next_manifest_file(),
887                Some(current_snapshot.snapshot_id()),
888                current_schema.clone(),
889                current_partition_spec.as_ref().clone(),
890            )
891            .build_v2_data();
892            writer
893                .add_entry(
894                    ManifestEntry::builder()
895                        .status(ManifestStatus::Added)
896                        .data_file(
897                            DataFileBuilder::default()
898                                .partition_spec_id(0)
899                                .content(DataContentType::Data)
900                                .file_path(format!("{}/1.parquet", &self.table_location))
901                                .file_format(DataFileFormat::Parquet)
902                                .file_size_in_bytes(parquet_file_size)
903                                .record_count(1)
904                                .partition(Struct::from_iter([Some(Literal::long(100))]))
905                                .key_metadata(None)
906                                .build()
907                                .unwrap(),
908                        )
909                        .build(),
910                )
911                .unwrap();
912            writer
913                .add_delete_entry(
914                    ManifestEntry::builder()
915                        .status(ManifestStatus::Deleted)
916                        .snapshot_id(parent_snapshot.snapshot_id())
917                        .sequence_number(parent_snapshot.sequence_number())
918                        .file_sequence_number(parent_snapshot.sequence_number())
919                        .data_file(
920                            DataFileBuilder::default()
921                                .partition_spec_id(0)
922                                .content(DataContentType::Data)
923                                .file_path(format!("{}/2.parquet", &self.table_location))
924                                .file_format(DataFileFormat::Parquet)
925                                .file_size_in_bytes(parquet_file_size)
926                                .record_count(1)
927                                .partition(Struct::from_iter([Some(Literal::long(200))]))
928                                .build()
929                                .unwrap(),
930                        )
931                        .build(),
932                )
933                .unwrap();
934            writer
935                .add_existing_entry(
936                    ManifestEntry::builder()
937                        .status(ManifestStatus::Existing)
938                        .snapshot_id(parent_snapshot.snapshot_id())
939                        .sequence_number(parent_snapshot.sequence_number())
940                        .file_sequence_number(parent_snapshot.sequence_number())
941                        .data_file(
942                            DataFileBuilder::default()
943                                .partition_spec_id(0)
944                                .content(DataContentType::Data)
945                                .file_path(format!("{}/3.parquet", &self.table_location))
946                                .file_format(DataFileFormat::Parquet)
947                                .file_size_in_bytes(parquet_file_size)
948                                .record_count(1)
949                                .partition(Struct::from_iter([Some(Literal::long(300))]))
950                                .build()
951                                .unwrap(),
952                        )
953                        .build(),
954                )
955                .unwrap();
956            let data_file_manifest = writer.write_manifest_file().await.unwrap();
957
958            // Write to manifest list
959            let manifest_list_writer = self
960                .table
961                .file_io()
962                .new_output(current_snapshot.manifest_list())
963                .unwrap()
964                .writer()
965                .await
966                .unwrap();
967            let mut manifest_list_write = ManifestListWriter::v2(
968                manifest_list_writer,
969                current_snapshot.snapshot_id(),
970                current_snapshot.parent_snapshot_id(),
971                current_snapshot.sequence_number(),
972            );
973            manifest_list_write
974                .add_manifests(vec![data_file_manifest].into_iter())
975                .unwrap();
976            manifest_list_write.close().await.unwrap();
977        }
978
979        pub async fn setup_manifest_files_with_partition_evolution(&mut self) {
980            let current_snapshot = self.table.metadata().current_snapshot().unwrap();
981            let parent_snapshot = current_snapshot
982                .parent_snapshot(self.table.metadata())
983                .unwrap();
984            let current_schema = current_snapshot.schema(self.table.metadata()).unwrap();
985            let current_partition_spec = self.table.metadata().default_partition_spec();
986
987            // Write the data files first, then use the file size in the manifest entries
988            let parquet_file_size = self.write_parquet_data_files();
989
990            let mut writer = ManifestWriterBuilder::new(
991                self.next_manifest_file(),
992                Some(current_snapshot.snapshot_id()),
993                current_schema.clone(),
994                current_partition_spec.as_ref().clone(),
995            )
996            .build_v2_data();
997            writer
998                .add_entry(
999                    ManifestEntry::builder()
1000                        .status(ManifestStatus::Added)
1001                        .data_file(
1002                            DataFileBuilder::default()
1003                                .partition_spec_id(1)
1004                                .content(DataContentType::Data)
1005                                .file_path(format!("{}/1.parquet", &self.table_location))
1006                                .file_format(DataFileFormat::Parquet)
1007                                .file_size_in_bytes(parquet_file_size)
1008                                .record_count(1)
1009                                .partition(Struct::from_iter([
1010                                    Some(Literal::long(100)),
1011                                    Some(Literal::string("apa")),
1012                                    Some(Literal::int(27)),
1013                                ]))
1014                                .key_metadata(None)
1015                                .build()
1016                                .unwrap(),
1017                        )
1018                        .build(),
1019                )
1020                .unwrap();
1021            writer
1022                .add_delete_entry(
1023                    ManifestEntry::builder()
1024                        .status(ManifestStatus::Deleted)
1025                        .snapshot_id(parent_snapshot.snapshot_id())
1026                        .sequence_number(parent_snapshot.sequence_number())
1027                        .file_sequence_number(parent_snapshot.sequence_number())
1028                        .data_file(
1029                            DataFileBuilder::default()
1030                                .partition_spec_id(1)
1031                                .content(DataContentType::Data)
1032                                .file_path(format!("{}/2.parquet", &self.table_location))
1033                                .file_format(DataFileFormat::Parquet)
1034                                .file_size_in_bytes(parquet_file_size)
1035                                .record_count(1)
1036                                .partition(Struct::from_iter([
1037                                    Some(Literal::long(200)),
1038                                    Some(Literal::string("ice")),
1039                                    Some(Literal::int(5)),
1040                                ]))
1041                                .build()
1042                                .unwrap(),
1043                        )
1044                        .build(),
1045                )
1046                .unwrap();
1047            writer
1048                .add_existing_entry(
1049                    ManifestEntry::builder()
1050                        .status(ManifestStatus::Existing)
1051                        .snapshot_id(parent_snapshot.snapshot_id())
1052                        .sequence_number(parent_snapshot.sequence_number())
1053                        .file_sequence_number(parent_snapshot.sequence_number())
1054                        .data_file(
1055                            DataFileBuilder::default()
1056                                .partition_spec_id(1)
1057                                .content(DataContentType::Data)
1058                                .file_path(format!("{}/3.parquet", &self.table_location))
1059                                .file_format(DataFileFormat::Parquet)
1060                                .file_size_in_bytes(parquet_file_size)
1061                                .record_count(1)
1062                                .partition(Struct::from_iter([
1063                                    Some(Literal::long(300)),
1064                                    Some(Literal::string("apa")),
1065                                    Some(Literal::int(19)),
1066                                ]))
1067                                .build()
1068                                .unwrap(),
1069                        )
1070                        .build(),
1071                )
1072                .unwrap();
1073            let data_file_manifest = writer.write_manifest_file().await.unwrap();
1074
1075            // Write to manifest list
1076            let manifest_list_writer = self
1077                .table
1078                .file_io()
1079                .new_output(current_snapshot.manifest_list())
1080                .unwrap()
1081                .writer()
1082                .await
1083                .unwrap();
1084            let mut manifest_list_write = ManifestListWriter::v2(
1085                manifest_list_writer,
1086                current_snapshot.snapshot_id(),
1087                current_snapshot.parent_snapshot_id(),
1088                current_snapshot.sequence_number(),
1089            );
1090            manifest_list_write
1091                .add_manifests(vec![data_file_manifest].into_iter())
1092                .unwrap();
1093            manifest_list_write.close().await.unwrap();
1094        }
1095
1096        /// Writes identical Parquet data files (1.parquet, 2.parquet, 3.parquet)
1097        /// and returns the file size in bytes.
1098        fn write_parquet_data_files(&self) -> u64 {
1099            fs::create_dir_all(&self.table_location).unwrap();
1100
1101            let schema = {
1102                let fields = vec![
1103                    arrow_schema::Field::new("x", arrow_schema::DataType::Int64, false)
1104                        .with_metadata(HashMap::from([(
1105                            PARQUET_FIELD_ID_META_KEY.to_string(),
1106                            "1".to_string(),
1107                        )])),
1108                    arrow_schema::Field::new("y", arrow_schema::DataType::Int64, false)
1109                        .with_metadata(HashMap::from([(
1110                            PARQUET_FIELD_ID_META_KEY.to_string(),
1111                            "2".to_string(),
1112                        )])),
1113                    arrow_schema::Field::new("z", arrow_schema::DataType::Int64, false)
1114                        .with_metadata(HashMap::from([(
1115                            PARQUET_FIELD_ID_META_KEY.to_string(),
1116                            "3".to_string(),
1117                        )])),
1118                    arrow_schema::Field::new("a", arrow_schema::DataType::Utf8, false)
1119                        .with_metadata(HashMap::from([(
1120                            PARQUET_FIELD_ID_META_KEY.to_string(),
1121                            "4".to_string(),
1122                        )])),
1123                    arrow_schema::Field::new("dbl", arrow_schema::DataType::Float64, false)
1124                        .with_metadata(HashMap::from([(
1125                            PARQUET_FIELD_ID_META_KEY.to_string(),
1126                            "5".to_string(),
1127                        )])),
1128                    arrow_schema::Field::new("i32", arrow_schema::DataType::Int32, false)
1129                        .with_metadata(HashMap::from([(
1130                            PARQUET_FIELD_ID_META_KEY.to_string(),
1131                            "6".to_string(),
1132                        )])),
1133                    arrow_schema::Field::new("i64", arrow_schema::DataType::Int64, false)
1134                        .with_metadata(HashMap::from([(
1135                            PARQUET_FIELD_ID_META_KEY.to_string(),
1136                            "7".to_string(),
1137                        )])),
1138                    arrow_schema::Field::new("bool", arrow_schema::DataType::Boolean, false)
1139                        .with_metadata(HashMap::from([(
1140                            PARQUET_FIELD_ID_META_KEY.to_string(),
1141                            "8".to_string(),
1142                        )])),
1143                ];
1144                Arc::new(arrow_schema::Schema::new(fields))
1145            };
1146            // x: [1, 1, 1, 1, ...]
1147            let col1 = Arc::new(Int64Array::from_iter_values(vec![1; 1024])) as ArrayRef;
1148
1149            let mut values = vec![2; 512];
1150            values.append(vec![3; 200].as_mut());
1151            values.append(vec![4; 300].as_mut());
1152            values.append(vec![5; 12].as_mut());
1153
1154            // y: [2, 2, 2, 2, ..., 3, 3, 3, 3, ..., 4, 4, 4, 4, ..., 5, 5, 5, 5]
1155            let col2 = Arc::new(Int64Array::from_iter_values(values)) as ArrayRef;
1156
1157            let mut values = vec![3; 512];
1158            values.append(vec![4; 512].as_mut());
1159
1160            // z: [3, 3, 3, 3, ..., 4, 4, 4, 4]
1161            let col3 = Arc::new(Int64Array::from_iter_values(values)) as ArrayRef;
1162
1163            // a: ["Apache", "Apache", "Apache", ..., "Iceberg", "Iceberg", "Iceberg"]
1164            let mut values = vec!["Apache"; 512];
1165            values.append(vec!["Iceberg"; 512].as_mut());
1166            let col4 = Arc::new(StringArray::from_iter_values(values)) as ArrayRef;
1167
1168            // dbl:
1169            let mut values = vec![100.0f64; 512];
1170            values.append(vec![150.0f64; 12].as_mut());
1171            values.append(vec![200.0f64; 500].as_mut());
1172            let col5 = Arc::new(Float64Array::from_iter_values(values)) as ArrayRef;
1173
1174            // i32:
1175            let mut values = vec![100i32; 512];
1176            values.append(vec![150i32; 12].as_mut());
1177            values.append(vec![200i32; 500].as_mut());
1178            let col6 = Arc::new(Int32Array::from_iter_values(values)) as ArrayRef;
1179
1180            // i64:
1181            let mut values = vec![100i64; 512];
1182            values.append(vec![150i64; 12].as_mut());
1183            values.append(vec![200i64; 500].as_mut());
1184            let col7 = Arc::new(Int64Array::from_iter_values(values)) as ArrayRef;
1185
1186            // bool:
1187            let mut values = vec![false; 512];
1188            values.append(vec![true; 512].as_mut());
1189            let values: BooleanArray = values.into();
1190            let col8 = Arc::new(values) as ArrayRef;
1191
1192            let to_write = RecordBatch::try_new(schema.clone(), vec![
1193                col1, col2, col3, col4, col5, col6, col7, col8,
1194            ])
1195            .unwrap();
1196
1197            // Write the Parquet files
1198            let props = WriterProperties::builder()
1199                .set_compression(Compression::SNAPPY)
1200                .build();
1201
1202            for n in 1..=3 {
1203                let file = File::create(format!("{}/{}.parquet", &self.table_location, n)).unwrap();
1204                let mut writer =
1205                    ArrowWriter::try_new(file, to_write.schema(), Some(props.clone())).unwrap();
1206
1207                writer.write(&to_write).expect("Writing batch");
1208
1209                // writer must be closed to write footer
1210                writer.close().unwrap();
1211            }
1212
1213            fs::metadata(format!("{}/1.parquet", &self.table_location))
1214                .unwrap()
1215                .len()
1216        }
1217
1218        pub async fn setup_unpartitioned_manifest_files(&mut self) {
1219            let current_snapshot = self.table.metadata().current_snapshot().unwrap();
1220            let parent_snapshot = current_snapshot
1221                .parent_snapshot(self.table.metadata())
1222                .unwrap();
1223            let current_schema = current_snapshot.schema(self.table.metadata()).unwrap();
1224            let current_partition_spec = Arc::new(PartitionSpec::unpartition_spec());
1225
1226            // Write the data files first, then use the file size in the manifest entries
1227            let parquet_file_size = self.write_parquet_data_files();
1228
1229            // Write data files using an empty partition for unpartitioned tables.
1230            let mut writer = ManifestWriterBuilder::new(
1231                self.next_manifest_file(),
1232                Some(current_snapshot.snapshot_id()),
1233                current_schema.clone(),
1234                current_partition_spec.as_ref().clone(),
1235            )
1236            .build_v2_data();
1237
1238            // Create an empty partition value.
1239            let empty_partition = Struct::empty();
1240
1241            writer
1242                .add_entry(
1243                    ManifestEntry::builder()
1244                        .status(ManifestStatus::Added)
1245                        .data_file(
1246                            DataFileBuilder::default()
1247                                .partition_spec_id(0)
1248                                .content(DataContentType::Data)
1249                                .file_path(format!("{}/1.parquet", &self.table_location))
1250                                .file_format(DataFileFormat::Parquet)
1251                                .file_size_in_bytes(parquet_file_size)
1252                                .record_count(1)
1253                                .partition(empty_partition.clone())
1254                                .key_metadata(None)
1255                                .build()
1256                                .unwrap(),
1257                        )
1258                        .build(),
1259                )
1260                .unwrap();
1261
1262            writer
1263                .add_delete_entry(
1264                    ManifestEntry::builder()
1265                        .status(ManifestStatus::Deleted)
1266                        .snapshot_id(parent_snapshot.snapshot_id())
1267                        .sequence_number(parent_snapshot.sequence_number())
1268                        .file_sequence_number(parent_snapshot.sequence_number())
1269                        .data_file(
1270                            DataFileBuilder::default()
1271                                .partition_spec_id(0)
1272                                .content(DataContentType::Data)
1273                                .file_path(format!("{}/2.parquet", &self.table_location))
1274                                .file_format(DataFileFormat::Parquet)
1275                                .file_size_in_bytes(parquet_file_size)
1276                                .record_count(1)
1277                                .partition(empty_partition.clone())
1278                                .build()
1279                                .unwrap(),
1280                        )
1281                        .build(),
1282                )
1283                .unwrap();
1284
1285            writer
1286                .add_existing_entry(
1287                    ManifestEntry::builder()
1288                        .status(ManifestStatus::Existing)
1289                        .snapshot_id(parent_snapshot.snapshot_id())
1290                        .sequence_number(parent_snapshot.sequence_number())
1291                        .file_sequence_number(parent_snapshot.sequence_number())
1292                        .data_file(
1293                            DataFileBuilder::default()
1294                                .partition_spec_id(0)
1295                                .content(DataContentType::Data)
1296                                .file_path(format!("{}/3.parquet", &self.table_location))
1297                                .file_format(DataFileFormat::Parquet)
1298                                .file_size_in_bytes(parquet_file_size)
1299                                .record_count(1)
1300                                .partition(empty_partition.clone())
1301                                .build()
1302                                .unwrap(),
1303                        )
1304                        .build(),
1305                )
1306                .unwrap();
1307
1308            let data_file_manifest = writer.write_manifest_file().await.unwrap();
1309
1310            // Write to manifest list
1311            let manifest_list_writer = self
1312                .table
1313                .file_io()
1314                .new_output(current_snapshot.manifest_list())
1315                .unwrap()
1316                .writer()
1317                .await
1318                .unwrap();
1319            let mut manifest_list_write = ManifestListWriter::v2(
1320                manifest_list_writer,
1321                current_snapshot.snapshot_id(),
1322                current_snapshot.parent_snapshot_id(),
1323                current_snapshot.sequence_number(),
1324            );
1325            manifest_list_write
1326                .add_manifests(vec![data_file_manifest].into_iter())
1327                .unwrap();
1328            manifest_list_write.close().await.unwrap();
1329        }
1330
1331        pub async fn setup_deadlock_manifests(&mut self) {
1332            let current_snapshot = self.table.metadata().current_snapshot().unwrap();
1333            let _parent_snapshot = current_snapshot
1334                .parent_snapshot(self.table.metadata())
1335                .unwrap();
1336            let current_schema = current_snapshot.schema(self.table.metadata()).unwrap();
1337            let current_partition_spec = self.table.metadata().default_partition_spec();
1338
1339            // 1. Write DATA manifest with MULTIPLE entries to fill buffer
1340            let mut writer = ManifestWriterBuilder::new(
1341                self.next_manifest_file(),
1342                Some(current_snapshot.snapshot_id()),
1343                current_schema.clone(),
1344                current_partition_spec.as_ref().clone(),
1345            )
1346            .build_v2_data();
1347
1348            // Add 10 data entries
1349            for i in 0..10 {
1350                writer
1351                    .add_entry(
1352                        ManifestEntry::builder()
1353                            .status(ManifestStatus::Added)
1354                            .data_file(
1355                                DataFileBuilder::default()
1356                                    .partition_spec_id(0)
1357                                    .content(DataContentType::Data)
1358                                    .file_path(format!("{}/{}.parquet", &self.table_location, i))
1359                                    .file_format(DataFileFormat::Parquet)
1360                                    .file_size_in_bytes(100)
1361                                    .record_count(1)
1362                                    .partition(Struct::from_iter([Some(Literal::long(100))]))
1363                                    .key_metadata(None)
1364                                    .build()
1365                                    .unwrap(),
1366                            )
1367                            .build(),
1368                    )
1369                    .unwrap();
1370            }
1371            let data_manifest = writer.write_manifest_file().await.unwrap();
1372
1373            // 2. Write DELETE manifest
1374            let mut writer = ManifestWriterBuilder::new(
1375                self.next_manifest_file(),
1376                Some(current_snapshot.snapshot_id()),
1377                current_schema.clone(),
1378                current_partition_spec.as_ref().clone(),
1379            )
1380            .build_v2_deletes();
1381
1382            writer
1383                .add_entry(
1384                    ManifestEntry::builder()
1385                        .status(ManifestStatus::Added)
1386                        .data_file(
1387                            DataFileBuilder::default()
1388                                .partition_spec_id(0)
1389                                .content(DataContentType::PositionDeletes)
1390                                .file_path(format!("{}/del.parquet", &self.table_location))
1391                                .file_format(DataFileFormat::Parquet)
1392                                .file_size_in_bytes(100)
1393                                .record_count(1)
1394                                .partition(Struct::from_iter([Some(Literal::long(100))]))
1395                                .build()
1396                                .unwrap(),
1397                        )
1398                        .build(),
1399                )
1400                .unwrap();
1401            let delete_manifest = writer.write_manifest_file().await.unwrap();
1402
1403            // Write to manifest list - DATA FIRST then DELETE
1404            // This order is crucial for reproduction
1405            let manifest_list_writer = self
1406                .table
1407                .file_io()
1408                .new_output(current_snapshot.manifest_list())
1409                .unwrap()
1410                .writer()
1411                .await
1412                .unwrap();
1413            let mut manifest_list_write = ManifestListWriter::v2(
1414                manifest_list_writer,
1415                current_snapshot.snapshot_id(),
1416                current_snapshot.parent_snapshot_id(),
1417                current_snapshot.sequence_number(),
1418            );
1419            manifest_list_write
1420                .add_manifests(vec![data_manifest, delete_manifest].into_iter())
1421                .unwrap();
1422            manifest_list_write.close().await.unwrap();
1423        }
1424
1425        /// Sets up a single data file `mrg.parquet` with three 100-row row groups
1426        /// (column `x` = 1000..1300, so row position `p` carries `x = 1000 + p`) and
1427        /// registers it in the current snapshot. When `delete_positions` is non-empty,
1428        /// also writes a positional delete file targeting those file-absolute positions
1429        /// and registers it in a delete manifest.
1430        ///
1431        /// Used to exercise the `_pos` metadata column through the real `TableScan`
1432        /// planning path across row-group boundaries and (optionally) positional deletes.
1433        pub async fn setup_multi_row_group_manifest(&mut self, delete_positions: &[i64]) {
1434            let current_snapshot = self.table.metadata().current_snapshot().unwrap();
1435            let current_schema = current_snapshot.schema(self.table.metadata()).unwrap();
1436            let current_partition_spec = self.table.metadata().default_partition_spec();
1437
1438            // The table's spec 0 is identity on `x`, so give the data and delete files a
1439            // fixed partition value. Filter tests deliberately filter on `y` (a
1440            // non-partition column) so pruning is driven by Parquet row-group statistics
1441            // rather than partition values.
1442            let partition = Struct::from_iter([Some(Literal::long(1000))]);
1443
1444            let (data_file_path, data_file_size) = self.write_multi_row_group_data_file();
1445
1446            let mut data_writer = ManifestWriterBuilder::new(
1447                self.next_manifest_file(),
1448                Some(current_snapshot.snapshot_id()),
1449                current_schema.clone(),
1450                current_partition_spec.as_ref().clone(),
1451            )
1452            .build_v2_data();
1453            data_writer
1454                .add_entry(
1455                    ManifestEntry::builder()
1456                        .status(ManifestStatus::Added)
1457                        .data_file(
1458                            DataFileBuilder::default()
1459                                .partition_spec_id(0)
1460                                .content(DataContentType::Data)
1461                                .file_path(data_file_path.clone())
1462                                .file_format(DataFileFormat::Parquet)
1463                                .file_size_in_bytes(data_file_size)
1464                                .record_count(300)
1465                                .partition(partition.clone())
1466                                .key_metadata(None)
1467                                .build()
1468                                .unwrap(),
1469                        )
1470                        .build(),
1471                )
1472                .unwrap();
1473            let data_manifest = data_writer.write_manifest_file().await.unwrap();
1474
1475            let mut manifests = vec![data_manifest];
1476
1477            if !delete_positions.is_empty() {
1478                let (del_path, del_size) =
1479                    self.write_positional_delete_file(&data_file_path, delete_positions);
1480
1481                let mut delete_writer = ManifestWriterBuilder::new(
1482                    self.next_manifest_file(),
1483                    Some(current_snapshot.snapshot_id()),
1484                    current_schema.clone(),
1485                    current_partition_spec.as_ref().clone(),
1486                )
1487                .build_v2_deletes();
1488                delete_writer
1489                    .add_entry(
1490                        ManifestEntry::builder()
1491                            .status(ManifestStatus::Added)
1492                            .data_file(
1493                                DataFileBuilder::default()
1494                                    .partition_spec_id(0)
1495                                    .content(DataContentType::PositionDeletes)
1496                                    .file_path(del_path)
1497                                    .file_format(DataFileFormat::Parquet)
1498                                    .file_size_in_bytes(del_size)
1499                                    .record_count(delete_positions.len() as u64)
1500                                    .partition(partition.clone())
1501                                    .build()
1502                                    .unwrap(),
1503                            )
1504                            .build(),
1505                    )
1506                    .unwrap();
1507                manifests.push(delete_writer.write_manifest_file().await.unwrap());
1508            }
1509
1510            let manifest_list_writer = self
1511                .table
1512                .file_io()
1513                .new_output(current_snapshot.manifest_list())
1514                .unwrap()
1515                .writer()
1516                .await
1517                .unwrap();
1518            let mut manifest_list_write = ManifestListWriter::v2(
1519                manifest_list_writer,
1520                current_snapshot.snapshot_id(),
1521                current_snapshot.parent_snapshot_id(),
1522                current_snapshot.sequence_number(),
1523            );
1524            manifest_list_write
1525                .add_manifests(manifests.into_iter())
1526                .unwrap();
1527            manifest_list_write.close().await.unwrap();
1528        }
1529
1530        /// Writes `mrg.parquet` with three 100-row row groups. Columns `x` (field
1531        /// id `1`) and `y` (field id `2`) both run 1000..1300, so row position `p`
1532        /// carries `x = y = 1000 + p`. Returns `(path, file_size_in_bytes)`.
1533        fn write_multi_row_group_data_file(&self) -> (String, u64) {
1534            fs::create_dir_all(&self.table_location).unwrap();
1535
1536            let arrow_schema = Arc::new(arrow_schema::Schema::new(vec![
1537                arrow_schema::Field::new("x", arrow_schema::DataType::Int64, false).with_metadata(
1538                    HashMap::from([(PARQUET_FIELD_ID_META_KEY.to_string(), "1".to_string())]),
1539                ),
1540                arrow_schema::Field::new("y", arrow_schema::DataType::Int64, false).with_metadata(
1541                    HashMap::from([(PARQUET_FIELD_ID_META_KEY.to_string(), "2".to_string())]),
1542                ),
1543            ]));
1544
1545            let path = format!("{}/mrg.parquet", &self.table_location);
1546            let max_row_group_row_count = 100;
1547            let props = WriterProperties::builder()
1548                .set_compression(Compression::SNAPPY)
1549                .set_max_row_group_row_count(Some(max_row_group_row_count))
1550                .build();
1551
1552            let file = File::create(&path).unwrap();
1553            let mut writer = ArrowWriter::try_new(file, arrow_schema.clone(), Some(props)).unwrap();
1554            for group in 0..3i64 {
1555                let base = 1000 + group * max_row_group_row_count as i64;
1556                let col = Arc::new(Int64Array::from_iter_values(
1557                    base..base + max_row_group_row_count as i64,
1558                )) as ArrayRef;
1559                let batch =
1560                    RecordBatch::try_new(arrow_schema.clone(), vec![col.clone(), col]).unwrap();
1561                writer.write(&batch).unwrap();
1562            }
1563            writer.close().unwrap();
1564
1565            let size = fs::metadata(&path).unwrap().len();
1566            (path, size)
1567        }
1568
1569        /// Writes a positional delete file targeting `positions` in `data_path`.
1570        /// Returns `(path, file_size_in_bytes)`.
1571        fn write_positional_delete_file(
1572            &self,
1573            data_path: &str,
1574            positions: &[i64],
1575        ) -> (String, u64) {
1576            let del_schema = Arc::new(arrow_schema::Schema::new(vec![
1577                arrow_schema::Field::new(
1578                    RESERVED_COL_NAME_DELETE_FILE_PATH,
1579                    arrow_schema::DataType::Utf8,
1580                    false,
1581                )
1582                .with_metadata(HashMap::from([(
1583                    PARQUET_FIELD_ID_META_KEY.to_string(),
1584                    RESERVED_FIELD_ID_DELETE_FILE_PATH.to_string(), // 2147483546
1585                )])),
1586                arrow_schema::Field::new(
1587                    RESERVED_COL_NAME_DELETE_FILE_POS,
1588                    arrow_schema::DataType::Int64,
1589                    false,
1590                )
1591                .with_metadata(HashMap::from([(
1592                    PARQUET_FIELD_ID_META_KEY.to_string(),
1593                    RESERVED_FIELD_ID_DELETE_FILE_POS.to_string(), // 2147483545
1594                )])),
1595            ]));
1596
1597            let batch = RecordBatch::try_new(del_schema.clone(), vec![
1598                Arc::new(StringArray::from_iter_values(std::iter::repeat_n(
1599                    data_path.to_string(),
1600                    positions.len(),
1601                ))) as ArrayRef,
1602                Arc::new(Int64Array::from_iter_values(positions.iter().copied())) as ArrayRef,
1603            ])
1604            .unwrap();
1605
1606            let path = format!("{}/pos-del.parquet", &self.table_location);
1607            let props = WriterProperties::builder()
1608                .set_compression(Compression::SNAPPY)
1609                .build();
1610            let file = File::create(&path).unwrap();
1611            let mut writer = ArrowWriter::try_new(file, del_schema, Some(props)).unwrap();
1612            writer.write(&batch).unwrap();
1613            writer.close().unwrap();
1614
1615            let size = fs::metadata(&path).unwrap().len();
1616            (path, size)
1617        }
1618    }
1619
1620    #[tokio::test]
1621    async fn test_table_scan_columns() {
1622        let table = TableTestFixture::new().table;
1623
1624        let table_scan = table.scan().select(["x", "y"]).build().unwrap();
1625        assert_eq!(
1626            Some(vec!["x".to_string(), "y".to_string()]),
1627            table_scan.column_names
1628        );
1629
1630        let table_scan = table
1631            .scan()
1632            .select(["x", "y"])
1633            .select(["z"])
1634            .build()
1635            .unwrap();
1636        assert_eq!(Some(vec!["z".to_string()]), table_scan.column_names);
1637    }
1638
1639    #[tokio::test]
1640    async fn test_select_all() {
1641        let table = TableTestFixture::new().table;
1642
1643        let table_scan = table.scan().select_all().build().unwrap();
1644        assert!(table_scan.column_names.is_none());
1645    }
1646
1647    #[test]
1648    fn test_select_no_exist_column() {
1649        let table = TableTestFixture::new().table;
1650
1651        let table_scan = table.scan().select(["x", "y", "z", "a", "b"]).build();
1652        assert!(table_scan.is_err());
1653    }
1654
1655    #[tokio::test]
1656    async fn test_table_scan_default_snapshot_id() {
1657        let table = TableTestFixture::new().table;
1658
1659        let table_scan = table.scan().build().unwrap();
1660        assert_eq!(
1661            table.metadata().current_snapshot().unwrap().snapshot_id(),
1662            table_scan.snapshot().unwrap().snapshot_id()
1663        );
1664    }
1665
1666    #[test]
1667    fn test_table_scan_non_exist_snapshot_id() {
1668        let table = TableTestFixture::new().table;
1669
1670        let table_scan = table.scan().snapshot_id(1024).build();
1671        assert!(table_scan.is_err());
1672    }
1673
1674    #[tokio::test]
1675    async fn test_table_scan_with_snapshot_id() {
1676        let table = TableTestFixture::new().table;
1677
1678        let table_scan = table
1679            .scan()
1680            .snapshot_id(3051729675574597004)
1681            .with_row_selection_enabled(true)
1682            .build()
1683            .unwrap();
1684        assert_eq!(
1685            table_scan.snapshot().unwrap().snapshot_id(),
1686            3051729675574597004
1687        );
1688    }
1689
1690    fn table_with_property(key: &str, value: &str) -> Table {
1691        let fixture = TableTestFixture::new();
1692        let mut metadata = fixture.table.metadata().clone();
1693        metadata
1694            .properties
1695            .insert(key.to_string(), value.to_string());
1696        Table::builder()
1697            .metadata(metadata)
1698            .identifier(fixture.table.identifier().clone())
1699            .file_io(fixture.table.file_io().clone())
1700            .metadata_location(fixture.table.metadata_location().unwrap().to_string())
1701            .runtime(test_runtime())
1702            .build()
1703            .unwrap()
1704    }
1705
1706    #[test]
1707    fn test_table_scan_without_name_mapping_property() {
1708        let table = TableTestFixture::new().table;
1709
1710        let table_scan = table.scan().build().unwrap();
1711        assert!(
1712            table_scan
1713                .plan_context
1714                .as_ref()
1715                .unwrap()
1716                .name_mapping
1717                .is_none()
1718        );
1719    }
1720
1721    #[test]
1722    fn test_table_scan_with_name_mapping_property() {
1723        let mapping_json = r#"[{"field-id":1,"names":["id","record_id"]}]"#;
1724        let table = table_with_property(DEFAULT_SCHEMA_NAME_MAPPING, mapping_json);
1725
1726        let table_scan = table.scan().build().unwrap();
1727        let mapping = table_scan
1728            .plan_context
1729            .as_ref()
1730            .unwrap()
1731            .name_mapping
1732            .as_ref()
1733            .expect("name_mapping should be parsed from the table property");
1734        let fields = mapping.fields();
1735        assert_eq!(fields.len(), 1);
1736        assert_eq!(fields[0].field_id(), Some(1));
1737        assert_eq!(fields[0].names(), &[
1738            "id".to_string(),
1739            "record_id".to_string()
1740        ]);
1741    }
1742
1743    #[test]
1744    fn test_table_scan_with_malformed_name_mapping_property() {
1745        let table = table_with_property(DEFAULT_SCHEMA_NAME_MAPPING, "{ not valid json");
1746
1747        let err = table
1748            .scan()
1749            .build()
1750            .expect_err("malformed name mapping should fail to parse");
1751        assert_eq!(err.kind(), ErrorKind::DataInvalid);
1752    }
1753
1754    #[tokio::test]
1755    async fn test_plan_files_carries_name_mapping_into_file_scan_task() {
1756        let mut fixture = TableTestFixture::new();
1757        fixture.setup_manifest_files().await;
1758
1759        let mapping_json = r#"[{"field-id":1,"names":["id","record_id"]}]"#;
1760        let mut metadata = fixture.table.metadata().clone();
1761        metadata.properties.insert(
1762            DEFAULT_SCHEMA_NAME_MAPPING.to_string(),
1763            mapping_json.to_string(),
1764        );
1765        let table = Table::builder()
1766            .metadata(metadata)
1767            .identifier(fixture.table.identifier().clone())
1768            .file_io(fixture.table.file_io().clone())
1769            .metadata_location(fixture.table.metadata_location().unwrap().to_string())
1770            .runtime(test_runtime())
1771            .build()
1772            .unwrap();
1773
1774        let tasks: Vec<_> = table
1775            .scan()
1776            .build()
1777            .unwrap()
1778            .plan_files()
1779            .await
1780            .unwrap()
1781            .try_collect()
1782            .await
1783            .unwrap();
1784
1785        assert!(!tasks.is_empty(), "expected at least one FileScanTask");
1786        for task in &tasks {
1787            let mapping = task
1788                .name_mapping
1789                .as_ref()
1790                .expect("name_mapping should reach the FileScanTask");
1791            assert_eq!(mapping.fields().len(), 1);
1792            assert_eq!(mapping.fields()[0].field_id(), Some(1));
1793        }
1794    }
1795
1796    #[tokio::test]
1797    async fn test_plan_files_on_table_without_any_snapshots() {
1798        let table = TableTestFixture::new_empty().table;
1799        let batch_stream = table.scan().build().unwrap().to_arrow().await.unwrap();
1800        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
1801        assert!(batches.is_empty());
1802    }
1803
1804    #[tokio::test]
1805    async fn test_plan_files_no_deletions() {
1806        let mut fixture = TableTestFixture::new();
1807        fixture.setup_manifest_files().await;
1808
1809        // Create table scan for current snapshot and plan files
1810        let table_scan = fixture
1811            .table
1812            .scan()
1813            .with_row_selection_enabled(true)
1814            .build()
1815            .unwrap();
1816
1817        let mut tasks = table_scan
1818            .plan_files()
1819            .await
1820            .unwrap()
1821            .try_fold(vec![], |mut acc, task| async move {
1822                acc.push(task);
1823                Ok(acc)
1824            })
1825            .await
1826            .unwrap();
1827
1828        assert_eq!(tasks.len(), 2);
1829
1830        tasks.sort_by_key(|t| t.data_file_path.to_string());
1831
1832        // Check first task is added data file
1833        assert_eq!(
1834            tasks[0].data_file_path,
1835            format!("{}/1.parquet", &fixture.table_location)
1836        );
1837
1838        // Check second task is existing data file
1839        assert_eq!(
1840            tasks[1].data_file_path,
1841            format!("{}/3.parquet", &fixture.table_location)
1842        );
1843    }
1844
1845    #[tokio::test]
1846    async fn test_open_parquet_no_deletions() {
1847        let mut fixture = TableTestFixture::new();
1848        fixture.setup_manifest_files().await;
1849
1850        // Create table scan for current snapshot and plan files
1851        let table_scan = fixture
1852            .table
1853            .scan()
1854            .with_row_selection_enabled(true)
1855            .build()
1856            .unwrap();
1857
1858        let batch_stream = table_scan.to_arrow().await.unwrap();
1859
1860        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
1861
1862        let col = batches[0].column_by_name("x").unwrap();
1863
1864        let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
1865        assert_eq!(int64_arr.value(0), 1);
1866    }
1867
1868    #[tokio::test]
1869    async fn test_open_parquet_no_deletions_by_separate_reader() {
1870        let mut fixture = TableTestFixture::new();
1871        fixture.setup_manifest_files().await;
1872
1873        // Create table scan for current snapshot and plan files
1874        let table_scan = fixture
1875            .table
1876            .scan()
1877            .with_row_selection_enabled(true)
1878            .build()
1879            .unwrap();
1880
1881        let mut plan_task: Vec<_> = table_scan
1882            .plan_files()
1883            .await
1884            .unwrap()
1885            .try_collect()
1886            .await
1887            .unwrap();
1888        assert_eq!(plan_task.len(), 2);
1889
1890        let reader = ArrowReaderBuilder::new(
1891            fixture.table.file_io().clone(),
1892            fixture.table.runtime().clone(),
1893        )
1894        .build();
1895        let batch_stream = reader
1896            .clone()
1897            .read(Box::pin(stream::iter(vec![Ok(plan_task.remove(0))])))
1898            .unwrap()
1899            .stream();
1900        let batch_1: Vec<_> = batch_stream.try_collect().await.unwrap();
1901
1902        let reader = ArrowReaderBuilder::new(
1903            fixture.table.file_io().clone(),
1904            fixture.table.runtime().clone(),
1905        )
1906        .build();
1907        let batch_stream = reader
1908            .read(Box::pin(stream::iter(vec![Ok(plan_task.remove(0))])))
1909            .unwrap()
1910            .stream();
1911        let batch_2: Vec<_> = batch_stream.try_collect().await.unwrap();
1912
1913        assert_eq!(batch_1, batch_2);
1914    }
1915
1916    #[tokio::test]
1917    async fn test_open_parquet_with_projection() {
1918        let mut fixture = TableTestFixture::new();
1919        fixture.setup_manifest_files().await;
1920
1921        // Create table scan for current snapshot and plan files
1922        let table_scan = fixture
1923            .table
1924            .scan()
1925            .select(["x", "z"])
1926            .with_row_selection_enabled(true)
1927            .build()
1928            .unwrap();
1929
1930        let batch_stream = table_scan.to_arrow().await.unwrap();
1931
1932        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
1933
1934        assert_eq!(batches[0].num_columns(), 2);
1935
1936        let col1 = batches[0].column_by_name("x").unwrap();
1937        let int64_arr = col1.as_any().downcast_ref::<Int64Array>().unwrap();
1938        assert_eq!(int64_arr.value(0), 1);
1939
1940        let col2 = batches[0].column_by_name("z").unwrap();
1941        let int64_arr = col2.as_any().downcast_ref::<Int64Array>().unwrap();
1942        assert_eq!(int64_arr.value(0), 3);
1943
1944        // test empty scan
1945        let table_scan = fixture.table.scan().select_empty().build().unwrap();
1946        let batch_stream = table_scan.to_arrow().await.unwrap();
1947        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
1948
1949        assert_eq!(batches[0].num_columns(), 0);
1950        assert_eq!(batches[0].num_rows(), 1024);
1951    }
1952
1953    #[tokio::test]
1954    async fn test_filter_on_arrow_lt() {
1955        let mut fixture = TableTestFixture::new();
1956        fixture.setup_manifest_files().await;
1957
1958        // Filter: y < 3
1959        let mut builder = fixture.table.scan();
1960        let predicate = Reference::new("y").less_than(Datum::long(3));
1961        builder = builder
1962            .with_filter(predicate)
1963            .with_row_selection_enabled(true);
1964        let table_scan = builder.build().unwrap();
1965
1966        let batch_stream = table_scan.to_arrow().await.unwrap();
1967
1968        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
1969
1970        assert_eq!(batches[0].num_rows(), 512);
1971
1972        let col = batches[0].column_by_name("x").unwrap();
1973        let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
1974        assert_eq!(int64_arr.value(0), 1);
1975
1976        let col = batches[0].column_by_name("y").unwrap();
1977        let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
1978        assert_eq!(int64_arr.value(0), 2);
1979    }
1980
1981    #[tokio::test]
1982    async fn test_filter_on_arrow_gt_eq() {
1983        let mut fixture = TableTestFixture::new();
1984        fixture.setup_manifest_files().await;
1985
1986        // Filter: y >= 5
1987        let mut builder = fixture.table.scan();
1988        let predicate = Reference::new("y").greater_than_or_equal_to(Datum::long(5));
1989        builder = builder
1990            .with_filter(predicate)
1991            .with_row_selection_enabled(true);
1992        let table_scan = builder.build().unwrap();
1993
1994        let batch_stream = table_scan.to_arrow().await.unwrap();
1995
1996        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
1997
1998        assert_eq!(batches[0].num_rows(), 12);
1999
2000        let col = batches[0].column_by_name("x").unwrap();
2001        let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
2002        assert_eq!(int64_arr.value(0), 1);
2003
2004        let col = batches[0].column_by_name("y").unwrap();
2005        let int64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
2006        assert_eq!(int64_arr.value(0), 5);
2007    }
2008
2009    #[tokio::test]
2010    async fn test_filter_double_eq() {
2011        let mut fixture = TableTestFixture::new();
2012        fixture.setup_manifest_files().await;
2013
2014        // Filter: dbl == 150.0
2015        let mut builder = fixture.table.scan();
2016        let predicate = Reference::new("dbl").equal_to(Datum::double(150.0f64));
2017        builder = builder
2018            .with_filter(predicate)
2019            .with_row_selection_enabled(true);
2020        let table_scan = builder.build().unwrap();
2021
2022        let batch_stream = table_scan.to_arrow().await.unwrap();
2023
2024        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2025
2026        assert_eq!(batches.len(), 2);
2027        assert_eq!(batches[0].num_rows(), 12);
2028
2029        let col = batches[0].column_by_name("dbl").unwrap();
2030        let f64_arr = col.as_any().downcast_ref::<Float64Array>().unwrap();
2031        assert_eq!(f64_arr.value(1), 150.0f64);
2032    }
2033
2034    #[tokio::test]
2035    async fn test_filter_int_eq() {
2036        let mut fixture = TableTestFixture::new();
2037        fixture.setup_manifest_files().await;
2038
2039        // Filter: i32 == 150
2040        let mut builder = fixture.table.scan();
2041        let predicate = Reference::new("i32").equal_to(Datum::int(150i32));
2042        builder = builder
2043            .with_filter(predicate)
2044            .with_row_selection_enabled(true);
2045        let table_scan = builder.build().unwrap();
2046
2047        let batch_stream = table_scan.to_arrow().await.unwrap();
2048
2049        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2050
2051        assert_eq!(batches.len(), 2);
2052        assert_eq!(batches[0].num_rows(), 12);
2053
2054        let col = batches[0].column_by_name("i32").unwrap();
2055        let i32_arr = col.as_any().downcast_ref::<Int32Array>().unwrap();
2056        assert_eq!(i32_arr.value(1), 150i32);
2057    }
2058
2059    #[tokio::test]
2060    async fn test_filter_long_eq() {
2061        let mut fixture = TableTestFixture::new();
2062        fixture.setup_manifest_files().await;
2063
2064        // Filter: i64 == 150
2065        let mut builder = fixture.table.scan();
2066        let predicate = Reference::new("i64").equal_to(Datum::long(150i64));
2067        builder = builder
2068            .with_filter(predicate)
2069            .with_row_selection_enabled(true);
2070        let table_scan = builder.build().unwrap();
2071
2072        let batch_stream = table_scan.to_arrow().await.unwrap();
2073
2074        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2075
2076        assert_eq!(batches.len(), 2);
2077        assert_eq!(batches[0].num_rows(), 12);
2078
2079        let col = batches[0].column_by_name("i64").unwrap();
2080        let i64_arr = col.as_any().downcast_ref::<Int64Array>().unwrap();
2081        assert_eq!(i64_arr.value(1), 150i64);
2082    }
2083
2084    #[tokio::test]
2085    async fn test_filter_bool_eq() {
2086        let mut fixture = TableTestFixture::new();
2087        fixture.setup_manifest_files().await;
2088
2089        // Filter: bool == true
2090        let mut builder = fixture.table.scan();
2091        let predicate = Reference::new("bool").equal_to(Datum::bool(true));
2092        builder = builder
2093            .with_filter(predicate)
2094            .with_row_selection_enabled(true);
2095        let table_scan = builder.build().unwrap();
2096
2097        let batch_stream = table_scan.to_arrow().await.unwrap();
2098
2099        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2100
2101        assert_eq!(batches.len(), 2);
2102        assert_eq!(batches[0].num_rows(), 512);
2103
2104        let col = batches[0].column_by_name("bool").unwrap();
2105        let bool_arr = col.as_any().downcast_ref::<BooleanArray>().unwrap();
2106        assert!(bool_arr.value(1));
2107    }
2108
2109    #[tokio::test]
2110    async fn test_filter_on_arrow_is_null() {
2111        let mut fixture = TableTestFixture::new();
2112        fixture.setup_manifest_files().await;
2113
2114        // Filter: y is null
2115        let mut builder = fixture.table.scan();
2116        let predicate = Reference::new("y").is_null();
2117        builder = builder
2118            .with_filter(predicate)
2119            .with_row_selection_enabled(true);
2120        let table_scan = builder.build().unwrap();
2121
2122        let batch_stream = table_scan.to_arrow().await.unwrap();
2123
2124        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2125        assert_eq!(batches.len(), 0);
2126    }
2127
2128    #[tokio::test]
2129    async fn test_filter_on_arrow_is_not_null() {
2130        let mut fixture = TableTestFixture::new();
2131        fixture.setup_manifest_files().await;
2132
2133        // Filter: y is not null
2134        let mut builder = fixture.table.scan();
2135        let predicate = Reference::new("y").is_not_null();
2136        builder = builder
2137            .with_filter(predicate)
2138            .with_row_selection_enabled(true);
2139        let table_scan = builder.build().unwrap();
2140
2141        let batch_stream = table_scan.to_arrow().await.unwrap();
2142
2143        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2144        assert_eq!(batches[0].num_rows(), 1024);
2145    }
2146
2147    #[tokio::test]
2148    async fn test_filter_on_arrow_lt_and_gt() {
2149        let mut fixture = TableTestFixture::new();
2150        fixture.setup_manifest_files().await;
2151
2152        // Filter: y < 5 AND z >= 4
2153        let mut builder = fixture.table.scan();
2154        let predicate = Reference::new("y")
2155            .less_than(Datum::long(5))
2156            .and(Reference::new("z").greater_than_or_equal_to(Datum::long(4)));
2157        builder = builder
2158            .with_filter(predicate)
2159            .with_row_selection_enabled(true);
2160        let table_scan = builder.build().unwrap();
2161
2162        let batch_stream = table_scan.to_arrow().await.unwrap();
2163
2164        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2165        assert_eq!(batches[0].num_rows(), 500);
2166
2167        let col = batches[0].column_by_name("x").unwrap();
2168        let expected_x = Arc::new(Int64Array::from_iter_values(vec![1; 500])) as ArrayRef;
2169        assert_eq!(col, &expected_x);
2170
2171        let col = batches[0].column_by_name("y").unwrap();
2172        let mut values = vec![];
2173        values.append(vec![3; 200].as_mut());
2174        values.append(vec![4; 300].as_mut());
2175        let expected_y = Arc::new(Int64Array::from_iter_values(values)) as ArrayRef;
2176        assert_eq!(col, &expected_y);
2177
2178        let col = batches[0].column_by_name("z").unwrap();
2179        let expected_z = Arc::new(Int64Array::from_iter_values(vec![4; 500])) as ArrayRef;
2180        assert_eq!(col, &expected_z);
2181    }
2182
2183    #[tokio::test]
2184    async fn test_filter_on_arrow_lt_or_gt() {
2185        let mut fixture = TableTestFixture::new();
2186        fixture.setup_manifest_files().await;
2187
2188        // Filter: y < 5 AND z >= 4
2189        let mut builder = fixture.table.scan();
2190        let predicate = Reference::new("y")
2191            .less_than(Datum::long(5))
2192            .or(Reference::new("z").greater_than_or_equal_to(Datum::long(4)));
2193        builder = builder
2194            .with_filter(predicate)
2195            .with_row_selection_enabled(true);
2196        let table_scan = builder.build().unwrap();
2197
2198        let batch_stream = table_scan.to_arrow().await.unwrap();
2199
2200        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2201        assert_eq!(batches[0].num_rows(), 1024);
2202
2203        let col = batches[0].column_by_name("x").unwrap();
2204        let expected_x = Arc::new(Int64Array::from_iter_values(vec![1; 1024])) as ArrayRef;
2205        assert_eq!(col, &expected_x);
2206
2207        let col = batches[0].column_by_name("y").unwrap();
2208        let mut values = vec![2; 512];
2209        values.append(vec![3; 200].as_mut());
2210        values.append(vec![4; 300].as_mut());
2211        values.append(vec![5; 12].as_mut());
2212        let expected_y = Arc::new(Int64Array::from_iter_values(values)) as ArrayRef;
2213        assert_eq!(col, &expected_y);
2214
2215        let col = batches[0].column_by_name("z").unwrap();
2216        let mut values = vec![3; 512];
2217        values.append(vec![4; 512].as_mut());
2218        let expected_z = Arc::new(Int64Array::from_iter_values(values)) as ArrayRef;
2219        assert_eq!(col, &expected_z);
2220    }
2221
2222    #[tokio::test]
2223    async fn test_filter_on_arrow_startswith() {
2224        let mut fixture = TableTestFixture::new();
2225        fixture.setup_manifest_files().await;
2226
2227        // Filter: a STARTSWITH "Ice"
2228        let mut builder = fixture.table.scan();
2229        let predicate = Reference::new("a").starts_with(Datum::string("Ice"));
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
2239        assert_eq!(batches[0].num_rows(), 512);
2240
2241        let col = batches[0].column_by_name("a").unwrap();
2242        let string_arr = col.as_any().downcast_ref::<StringArray>().unwrap();
2243        assert_eq!(string_arr.value(0), "Iceberg");
2244    }
2245
2246    #[tokio::test]
2247    async fn test_filter_on_arrow_not_startswith() {
2248        let mut fixture = TableTestFixture::new();
2249        fixture.setup_manifest_files().await;
2250
2251        // Filter: a NOT STARTSWITH "Ice"
2252        let mut builder = fixture.table.scan();
2253        let predicate = Reference::new("a").not_starts_with(Datum::string("Ice"));
2254        builder = builder
2255            .with_filter(predicate)
2256            .with_row_selection_enabled(true);
2257        let table_scan = builder.build().unwrap();
2258
2259        let batch_stream = table_scan.to_arrow().await.unwrap();
2260
2261        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2262
2263        assert_eq!(batches[0].num_rows(), 512);
2264
2265        let col = batches[0].column_by_name("a").unwrap();
2266        let string_arr = col.as_any().downcast_ref::<StringArray>().unwrap();
2267        assert_eq!(string_arr.value(0), "Apache");
2268    }
2269
2270    #[tokio::test]
2271    async fn test_filter_on_arrow_in() {
2272        let mut fixture = TableTestFixture::new();
2273        fixture.setup_manifest_files().await;
2274
2275        // Filter: a IN ("Sioux", "Iceberg")
2276        let mut builder = fixture.table.scan();
2277        let predicate =
2278            Reference::new("a").is_in([Datum::string("Sioux"), Datum::string("Iceberg")]);
2279        builder = builder
2280            .with_filter(predicate)
2281            .with_row_selection_enabled(true);
2282        let table_scan = builder.build().unwrap();
2283
2284        let batch_stream = table_scan.to_arrow().await.unwrap();
2285
2286        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2287
2288        assert_eq!(batches[0].num_rows(), 512);
2289
2290        let col = batches[0].column_by_name("a").unwrap();
2291        let string_arr = col.as_any().downcast_ref::<StringArray>().unwrap();
2292        assert_eq!(string_arr.value(0), "Iceberg");
2293    }
2294
2295    #[tokio::test]
2296    async fn test_filter_on_arrow_not_in() {
2297        let mut fixture = TableTestFixture::new();
2298        fixture.setup_manifest_files().await;
2299
2300        // Filter: a NOT IN ("Sioux", "Iceberg")
2301        let mut builder = fixture.table.scan();
2302        let predicate =
2303            Reference::new("a").is_not_in([Datum::string("Sioux"), Datum::string("Iceberg")]);
2304        builder = builder
2305            .with_filter(predicate)
2306            .with_row_selection_enabled(true);
2307        let table_scan = builder.build().unwrap();
2308
2309        let batch_stream = table_scan.to_arrow().await.unwrap();
2310
2311        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2312
2313        assert_eq!(batches[0].num_rows(), 512);
2314
2315        let col = batches[0].column_by_name("a").unwrap();
2316        let string_arr = col.as_any().downcast_ref::<StringArray>().unwrap();
2317        assert_eq!(string_arr.value(0), "Apache");
2318    }
2319
2320    #[test]
2321    fn test_file_scan_task_serialize_deserialize() {
2322        let test_fn = |task: FileScanTask| {
2323            let serialized = serde_json::to_string(&task).unwrap();
2324            let deserialized: FileScanTask = serde_json::from_str(&serialized).unwrap();
2325
2326            assert_eq!(task.data_file_path, deserialized.data_file_path);
2327            assert_eq!(task.start, deserialized.start);
2328            assert_eq!(task.length, deserialized.length);
2329            assert_eq!(task.project_field_ids, deserialized.project_field_ids);
2330            assert_eq!(task.predicate, deserialized.predicate);
2331            assert_eq!(task.schema, deserialized.schema);
2332        };
2333
2334        // without predicate
2335        let schema = Arc::new(
2336            Schema::builder()
2337                .with_fields(vec![Arc::new(NestedField::required(
2338                    1,
2339                    "x",
2340                    Type::Primitive(PrimitiveType::Binary),
2341                ))])
2342                .build()
2343                .unwrap(),
2344        );
2345        let task = FileScanTask::builder()
2346            .with_data_file_path("data_file_path".to_string())
2347            .with_file_size_in_bytes(0)
2348            .with_start(0)
2349            .with_length(100)
2350            .with_project_field_ids(vec![1, 2, 3])
2351            .with_schema(schema.clone())
2352            .with_record_count(Some(100))
2353            .with_data_file_format(DataFileFormat::Parquet)
2354            .with_case_sensitive(false)
2355            .build();
2356        test_fn(task);
2357
2358        // with predicate
2359        let task = FileScanTask::builder()
2360            .with_data_file_path("data_file_path".to_string())
2361            .with_file_size_in_bytes(0)
2362            .with_start(0)
2363            .with_length(100)
2364            .with_project_field_ids(vec![1, 2, 3])
2365            .with_predicate(Some(BoundPredicate::AlwaysTrue))
2366            .with_schema(schema)
2367            .with_data_file_format(DataFileFormat::Avro)
2368            .with_case_sensitive(false)
2369            .build();
2370        test_fn(task);
2371    }
2372
2373    #[tokio::test]
2374    async fn test_select_with_file_column() {
2375        let mut fixture = TableTestFixture::new();
2376        fixture.setup_manifest_files().await;
2377
2378        // Select regular columns plus the _file column
2379        let table_scan = fixture
2380            .table
2381            .scan()
2382            .select(["x", RESERVED_COL_NAME_FILE])
2383            .with_row_selection_enabled(true)
2384            .build()
2385            .unwrap();
2386
2387        let batch_stream = table_scan.to_arrow().await.unwrap();
2388        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2389
2390        // Verify we have 2 columns: x and _file
2391        assert_eq!(batches[0].num_columns(), 2);
2392
2393        // Verify the x column exists and has correct data
2394        let x_col = batches[0].column_by_name("x").unwrap();
2395        let x_arr = x_col.as_primitive::<arrow_array::types::Int64Type>();
2396        assert_eq!(x_arr.value(0), 1);
2397
2398        // Verify the _file column exists
2399        let file_col = batches[0].column_by_name(RESERVED_COL_NAME_FILE);
2400        assert!(
2401            file_col.is_some(),
2402            "_file column should be present in the batch"
2403        );
2404
2405        // Verify the _file column contains a file path
2406        let file_col = file_col.unwrap();
2407        assert!(
2408            matches!(
2409                file_col.data_type(),
2410                arrow_schema::DataType::RunEndEncoded(_, _)
2411            ),
2412            "_file column should use RunEndEncoded type"
2413        );
2414
2415        // Decode the RunArray to verify it contains the file path
2416        let run_array = file_col
2417            .as_any()
2418            .downcast_ref::<RunArray<Int32Type>>()
2419            .expect("_file column should be a RunArray");
2420
2421        let values = run_array.values();
2422        let string_values = values.as_string::<i32>();
2423        assert_eq!(string_values.len(), 1, "Should have a single file path");
2424
2425        let file_path = string_values.value(0);
2426        assert!(
2427            file_path.ends_with(".parquet"),
2428            "File path should end with .parquet, got: {file_path}"
2429        );
2430    }
2431
2432    #[tokio::test]
2433    async fn test_select_file_column_position() {
2434        let mut fixture = TableTestFixture::new();
2435        fixture.setup_manifest_files().await;
2436
2437        // Select columns in specific order: x, _file, z
2438        let table_scan = fixture
2439            .table
2440            .scan()
2441            .select(["x", RESERVED_COL_NAME_FILE, "z"])
2442            .with_row_selection_enabled(true)
2443            .build()
2444            .unwrap();
2445
2446        let batch_stream = table_scan.to_arrow().await.unwrap();
2447        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2448
2449        assert_eq!(batches[0].num_columns(), 3);
2450
2451        // Verify column order: x at position 0, _file at position 1, z at position 2
2452        let schema = batches[0].schema();
2453        assert_eq!(schema.field(0).name(), "x");
2454        assert_eq!(schema.field(1).name(), RESERVED_COL_NAME_FILE);
2455        assert_eq!(schema.field(2).name(), "z");
2456
2457        // Verify columns by name also works
2458        assert!(batches[0].column_by_name("x").is_some());
2459        assert!(batches[0].column_by_name(RESERVED_COL_NAME_FILE).is_some());
2460        assert!(batches[0].column_by_name("z").is_some());
2461    }
2462
2463    #[tokio::test]
2464    async fn test_select_file_column_only() {
2465        let mut fixture = TableTestFixture::new();
2466        fixture.setup_manifest_files().await;
2467
2468        // Select only the _file column
2469        let table_scan = fixture
2470            .table
2471            .scan()
2472            .select([RESERVED_COL_NAME_FILE])
2473            .with_row_selection_enabled(true)
2474            .build()
2475            .unwrap();
2476
2477        let batch_stream = table_scan.to_arrow().await.unwrap();
2478        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2479
2480        // Should have exactly 1 column
2481        assert_eq!(batches[0].num_columns(), 1);
2482
2483        // Verify it's the _file column
2484        let schema = batches[0].schema();
2485        assert_eq!(schema.field(0).name(), RESERVED_COL_NAME_FILE);
2486
2487        // Verify the batch has the correct number of rows
2488        // The scan reads files 1.parquet and 3.parquet (2.parquet is deleted)
2489        // Each file has 1024 rows, so total is 2048 rows
2490        let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum();
2491        assert_eq!(total_rows, 2048);
2492    }
2493
2494    #[tokio::test]
2495    async fn test_file_column_with_multiple_files() {
2496        use std::collections::HashSet;
2497
2498        let mut fixture = TableTestFixture::new();
2499        fixture.setup_manifest_files().await;
2500
2501        // Select x and _file columns
2502        let table_scan = fixture
2503            .table
2504            .scan()
2505            .select(["x", RESERVED_COL_NAME_FILE])
2506            .with_row_selection_enabled(true)
2507            .build()
2508            .unwrap();
2509
2510        let batch_stream = table_scan.to_arrow().await.unwrap();
2511        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2512
2513        // Collect all unique file paths from the batches
2514        let mut file_paths = HashSet::new();
2515        for batch in &batches {
2516            let file_col = batch.column_by_name(RESERVED_COL_NAME_FILE).unwrap();
2517            let run_array = file_col
2518                .as_any()
2519                .downcast_ref::<RunArray<Int32Type>>()
2520                .expect("_file column should be a RunArray");
2521
2522            let values = run_array.values();
2523            let string_values = values.as_string::<i32>();
2524            for i in 0..string_values.len() {
2525                file_paths.insert(string_values.value(i).to_string());
2526            }
2527        }
2528
2529        // We should have multiple files (the test creates 1.parquet and 3.parquet)
2530        assert!(!file_paths.is_empty(), "Should have at least one file path");
2531
2532        // All paths should end with .parquet
2533        for path in &file_paths {
2534            assert!(
2535                path.ends_with(".parquet"),
2536                "All file paths should end with .parquet, got: {path}"
2537            );
2538        }
2539    }
2540
2541    #[tokio::test]
2542    async fn test_file_column_at_start() {
2543        let mut fixture = TableTestFixture::new();
2544        fixture.setup_manifest_files().await;
2545
2546        // Select _file at the start
2547        let table_scan = fixture
2548            .table
2549            .scan()
2550            .select([RESERVED_COL_NAME_FILE, "x", "y"])
2551            .with_row_selection_enabled(true)
2552            .build()
2553            .unwrap();
2554
2555        let batch_stream = table_scan.to_arrow().await.unwrap();
2556        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2557
2558        assert_eq!(batches[0].num_columns(), 3);
2559
2560        // Verify _file is at position 0
2561        let schema = batches[0].schema();
2562        assert_eq!(schema.field(0).name(), RESERVED_COL_NAME_FILE);
2563        assert_eq!(schema.field(1).name(), "x");
2564        assert_eq!(schema.field(2).name(), "y");
2565    }
2566
2567    #[tokio::test]
2568    async fn test_file_column_at_end() {
2569        let mut fixture = TableTestFixture::new();
2570        fixture.setup_manifest_files().await;
2571
2572        // Select _file at the end
2573        let table_scan = fixture
2574            .table
2575            .scan()
2576            .select(["x", "y", RESERVED_COL_NAME_FILE])
2577            .with_row_selection_enabled(true)
2578            .build()
2579            .unwrap();
2580
2581        let batch_stream = table_scan.to_arrow().await.unwrap();
2582        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2583
2584        assert_eq!(batches[0].num_columns(), 3);
2585
2586        // Verify _file is at position 2 (the end)
2587        let schema = batches[0].schema();
2588        assert_eq!(schema.field(0).name(), "x");
2589        assert_eq!(schema.field(1).name(), "y");
2590        assert_eq!(schema.field(2).name(), RESERVED_COL_NAME_FILE);
2591    }
2592
2593    #[tokio::test]
2594    async fn test_select_with_repeated_column_names() {
2595        let mut fixture = TableTestFixture::new();
2596        fixture.setup_manifest_files().await;
2597
2598        // Select with repeated column names - both regular columns and virtual columns
2599        // Repeated columns should appear multiple times in the result (duplicates are allowed)
2600        let table_scan = fixture
2601            .table
2602            .scan()
2603            .select([
2604                "x",
2605                RESERVED_COL_NAME_FILE,
2606                "x", // x repeated
2607                "y",
2608                RESERVED_COL_NAME_FILE, // _file repeated
2609                "y",                    // y repeated
2610            ])
2611            .with_row_selection_enabled(true)
2612            .build()
2613            .unwrap();
2614
2615        let batch_stream = table_scan.to_arrow().await.unwrap();
2616        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2617
2618        // Verify we have exactly 6 columns (duplicates are allowed and preserved)
2619        assert_eq!(
2620            batches[0].num_columns(),
2621            6,
2622            "Should have exactly 6 columns with duplicates"
2623        );
2624
2625        let schema = batches[0].schema();
2626
2627        // Verify columns appear in the exact order requested: x, _file, x, y, _file, y
2628        assert_eq!(schema.field(0).name(), "x", "Column 0 should be x");
2629        assert_eq!(
2630            schema.field(1).name(),
2631            RESERVED_COL_NAME_FILE,
2632            "Column 1 should be _file"
2633        );
2634        assert_eq!(
2635            schema.field(2).name(),
2636            "x",
2637            "Column 2 should be x (duplicate)"
2638        );
2639        assert_eq!(schema.field(3).name(), "y", "Column 3 should be y");
2640        assert_eq!(
2641            schema.field(4).name(),
2642            RESERVED_COL_NAME_FILE,
2643            "Column 4 should be _file (duplicate)"
2644        );
2645        assert_eq!(
2646            schema.field(5).name(),
2647            "y",
2648            "Column 5 should be y (duplicate)"
2649        );
2650
2651        // Verify all columns have correct data types
2652        assert!(
2653            matches!(schema.field(0).data_type(), arrow_schema::DataType::Int64),
2654            "Column x should be Int64"
2655        );
2656        assert!(
2657            matches!(schema.field(2).data_type(), arrow_schema::DataType::Int64),
2658            "Column x (duplicate) should be Int64"
2659        );
2660        assert!(
2661            matches!(schema.field(3).data_type(), arrow_schema::DataType::Int64),
2662            "Column y should be Int64"
2663        );
2664        assert!(
2665            matches!(schema.field(5).data_type(), arrow_schema::DataType::Int64),
2666            "Column y (duplicate) should be Int64"
2667        );
2668        assert!(
2669            matches!(
2670                schema.field(1).data_type(),
2671                arrow_schema::DataType::RunEndEncoded(_, _)
2672            ),
2673            "_file column should use RunEndEncoded type"
2674        );
2675        assert!(
2676            matches!(
2677                schema.field(4).data_type(),
2678                arrow_schema::DataType::RunEndEncoded(_, _)
2679            ),
2680            "_file column (duplicate) should use RunEndEncoded type"
2681        );
2682    }
2683
2684    #[tokio::test]
2685    async fn test_scan_deadlock() {
2686        let mut fixture = TableTestFixture::new();
2687        fixture.setup_deadlock_manifests().await;
2688
2689        // Create table scan with concurrency limit 1
2690        // This sets channel size to 1.
2691        // Data manifest has 10 entries -> will block producer.
2692        // Delete manifest is 2nd in list -> won't be processed.
2693        // Consumer 2 (Data) not started -> blocked.
2694        // Consumer 1 (Delete) waiting -> blocked.
2695        let table_scan = fixture
2696            .table
2697            .scan()
2698            .with_concurrency_limit(1)
2699            .build()
2700            .unwrap();
2701
2702        // This should timeout/hang if deadlock exists
2703        // We can use tokio::time::timeout
2704        let result = tokio::time::timeout(std::time::Duration::from_secs(5), async {
2705            table_scan
2706                .plan_files()
2707                .await
2708                .unwrap()
2709                .try_collect::<Vec<_>>()
2710                .await
2711        })
2712        .await;
2713
2714        // Assert it finished (didn't timeout)
2715        assert!(result.is_ok(), "Scan timed out - deadlock detected");
2716    }
2717
2718    #[tokio::test]
2719    async fn test_select_with_spec_id_column() {
2720        let mut fixture = TableTestFixture::new();
2721        fixture.setup_manifest_files().await;
2722
2723        // Select regular columns plus the _spec_id column
2724        let table_scan = fixture
2725            .table
2726            .scan()
2727            .select(["x", RESERVED_COL_NAME_SPEC_ID, "z"])
2728            .with_row_selection_enabled(true)
2729            .build()
2730            .unwrap();
2731
2732        let batch_stream = table_scan.to_arrow().await.unwrap();
2733        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2734
2735        // Verify we have 3 columns: x, _spec_id, and z
2736        assert_eq!(batches[0].num_columns(), 3);
2737
2738        // Verify the x column exists and has correct data
2739        let col1 = batches[0].column_by_name("x").unwrap();
2740        let int64_arr = col1.as_any().downcast_ref::<Int64Array>().unwrap();
2741        assert_eq!(int64_arr.value(0), 1);
2742
2743        // Verify the _spec_id column exists
2744        let spec_id_col = batches[0].column_by_name(RESERVED_COL_NAME_SPEC_ID);
2745        assert!(
2746            spec_id_col.is_some(),
2747            "_spec_id column should be present in the batch"
2748        );
2749
2750        // Verify the _spec_id data type
2751        let spec_id_col = spec_id_col.unwrap();
2752        assert!(
2753            matches!(
2754                spec_id_col.data_type(),
2755                arrow_schema::DataType::RunEndEncoded(_, _)
2756            ),
2757            "_spec_id column should use RunEndEncoded type"
2758        );
2759
2760        // Decode the RunArray to verify it contains the spec id
2761        let run_array = spec_id_col
2762            .as_any()
2763            .downcast_ref::<RunArray<Int32Type>>()
2764            .expect("_spec_id column should be a RunArray");
2765
2766        let values = run_array.values();
2767        let int_values = values.as_primitive::<Int32Type>();
2768        assert_eq!(int_values.len(), 1, "Should have a single _spec_id");
2769
2770        let spec_id = int_values.value(0);
2771        assert_eq!(spec_id, 0, "_spec_id should be 0, got: {spec_id}");
2772
2773        // Verify 'z' column exists
2774        assert!(batches[0].column_by_name("z").is_some());
2775    }
2776
2777    #[tokio::test]
2778    async fn test_select_with_spec_id_column_from_unpartitioned_table() {
2779        let mut fixture = TableTestFixture::new_unpartitioned();
2780        fixture.setup_unpartitioned_manifest_files().await;
2781
2782        // Select regular columns plus the _spec_id column
2783        let table_scan = fixture
2784            .table
2785            .scan()
2786            .select(["x", RESERVED_COL_NAME_SPEC_ID])
2787            .with_row_selection_enabled(true)
2788            .build()
2789            .unwrap();
2790
2791        let batch_stream = table_scan.to_arrow().await.unwrap();
2792        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2793
2794        // Verify we have 2 columns: x and _spec_id
2795        assert_eq!(batches[0].num_columns(), 2);
2796
2797        // Verify the _spec_id column exists
2798        let spec_id_col = batches[0].column_by_name(RESERVED_COL_NAME_SPEC_ID);
2799        assert!(
2800            spec_id_col.is_some(),
2801            "_spec_id column should be present in the batch"
2802        );
2803
2804        // Verify the _spec_id data type
2805        let spec_id_col = spec_id_col.unwrap();
2806        assert!(
2807            matches!(
2808                spec_id_col.data_type(),
2809                arrow_schema::DataType::RunEndEncoded(_, _)
2810            ),
2811            "_spec_id column should use RunEndEncoded type"
2812        );
2813
2814        // Decode the RunArray to verify it contains the spec id
2815        let run_array = spec_id_col
2816            .as_any()
2817            .downcast_ref::<RunArray<Int32Type>>()
2818            .expect("_spec_id column should be a RunArray");
2819
2820        let values = run_array.values();
2821        let int_values = values.as_primitive::<Int32Type>();
2822        assert_eq!(int_values.len(), 1, "Should have a single _spec_id");
2823
2824        let spec_id = int_values.value(0);
2825        assert_eq!(spec_id, 0, "_spec_id should be 0, got: {spec_id}");
2826    }
2827
2828    #[tokio::test]
2829    async fn test_select_with_spec_id_column_with_partition_evolution() {
2830        let mut fixture = TableTestFixture::new_with_partition_evolution();
2831        fixture
2832            .setup_manifest_files_with_partition_evolution()
2833            .await;
2834
2835        // Select regular columns plus the _spec_id column
2836        let table_scan = fixture
2837            .table
2838            .scan()
2839            .select(["x", RESERVED_COL_NAME_SPEC_ID, "z"])
2840            .with_row_selection_enabled(true)
2841            .build()
2842            .unwrap();
2843
2844        let batch_stream = table_scan.to_arrow().await.unwrap();
2845        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2846
2847        // Verify the x column exists and has correct data
2848        let col1 = batches[0].column_by_name("x").unwrap();
2849        let int64_arr = col1.as_any().downcast_ref::<Int64Array>().unwrap();
2850        assert_eq!(int64_arr.value(0), 1);
2851
2852        // Verify the _spec_id column exists
2853        let spec_id_col = batches[0].column_by_name(RESERVED_COL_NAME_SPEC_ID);
2854        assert!(
2855            spec_id_col.is_some(),
2856            "_spec_id column should be present in the batch"
2857        );
2858
2859        // Verify the _spec_id data type
2860        let spec_id_col = spec_id_col.unwrap();
2861        assert!(
2862            matches!(
2863                spec_id_col.data_type(),
2864                arrow_schema::DataType::RunEndEncoded(_, _)
2865            ),
2866            "_spec_id column should use RunEndEncoded type"
2867        );
2868
2869        // Decode the RunArray to verify it contains the spec id
2870        let run_array = spec_id_col
2871            .as_any()
2872            .downcast_ref::<RunArray<Int32Type>>()
2873            .expect("_spec_id column should be a RunArray");
2874
2875        let values = run_array.values();
2876        let int_values = values.as_primitive::<Int32Type>();
2877        assert_eq!(int_values.len(), 1, "Should have a single _spec_id");
2878
2879        let spec_id = int_values.value(0);
2880        assert_eq!(spec_id, 2, "_spec_id should be 2, got: {spec_id}");
2881    }
2882
2883    #[tokio::test]
2884    async fn test_select_with_pos_and_file_columns() {
2885        use arrow_array::cast::AsArray;
2886
2887        let mut fixture = TableTestFixture::new();
2888        fixture.setup_manifest_files().await;
2889
2890        // Select regular columns plus the _pos column
2891        let table_scan = fixture
2892            .table
2893            .scan()
2894            .select(["x", RESERVED_COL_NAME_POS, RESERVED_COL_NAME_FILE])
2895            .with_row_selection_enabled(true)
2896            .build()
2897            .unwrap();
2898
2899        let batch_stream = table_scan.to_arrow().await.unwrap();
2900        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2901        assert_eq!(batches.len(), 2);
2902
2903        // Examine batches are 1.paruqet and 3.parquet, 2.parquet is deleted.
2904        for batch in batches.iter() {
2905            // Verify we have 3 columns: x, _pos and _file
2906            assert_eq!(batch.num_columns(), 3);
2907
2908            // Verify the x column exists and has correct data
2909            let x_col = batch.column_by_name("x").unwrap();
2910            let x_arr = x_col.as_primitive::<arrow_array::types::Int64Type>();
2911            assert_eq!(x_arr.value(0), 1);
2912
2913            // The _pos column exists and verify it is Int64Array with the expected values
2914            let pos_col = batch.column(1);
2915            let pos_array: &Int64Array = pos_col
2916                .as_any()
2917                .downcast_ref::<Int64Array>()
2918                .expect("_pos column should be a Int64Array");
2919            assert_eq!(*pos_array, Int64Array::from_iter_values(0i64..1024));
2920
2921            // Verify the _file column exists
2922            let file_col = batch.column_by_name(RESERVED_COL_NAME_FILE);
2923            assert!(
2924                file_col.is_some(),
2925                "_file column should be present in the batch"
2926            );
2927        }
2928    }
2929
2930    #[tokio::test]
2931    async fn test_pos_column_at_start_with_filters() {
2932        let mut fixture = TableTestFixture::new();
2933        fixture.setup_manifest_files().await;
2934
2935        // y is in [4, 5)
2936        let predicate = Reference::new("y")
2937            .greater_than(Datum::long(4i64))
2938            .and(Reference::new("y").less_than_or_equal_to(Datum::long(5i64)));
2939        // Select _pos at the start
2940        let table_scan = fixture
2941            .table
2942            .scan()
2943            .select([RESERVED_COL_NAME_POS, "x", "y"])
2944            .with_filter(predicate)
2945            .with_row_selection_enabled(true)
2946            .build()
2947            .unwrap();
2948
2949        let batch_stream = table_scan.to_arrow().await.unwrap();
2950        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2951        assert_eq!(batches.len(), 2);
2952
2953        // Examine batches are 1.paruqet and 3.parquet, 2.parquet is deleted.
2954        for batch in batches.iter() {
2955            assert_eq!(batch.num_columns(), 3);
2956            assert_eq!(batch.num_rows(), 12);
2957
2958            // Verify _pos is at position 0
2959            let schema = batch.schema();
2960            assert_eq!(schema.field(0).name(), RESERVED_COL_NAME_POS);
2961            assert_eq!(schema.field(1).name(), "x");
2962            assert_eq!(schema.field(2).name(), "y");
2963
2964            let pos_col = batch.column(0);
2965            let pos_array: &Int64Array = pos_col
2966                .as_any()
2967                .downcast_ref::<Int64Array>()
2968                .expect("_pos column should be a Int64Array");
2969            assert_eq!(*pos_array, Int64Array::from_iter_values(1012i64..1024));
2970        }
2971    }
2972
2973    #[tokio::test]
2974    async fn test_repeated_pos_column_with_filter() {
2975        let mut fixture = TableTestFixture::new();
2976        fixture.setup_manifest_files().await;
2977
2978        // a NOT STARTSWITH "Apa"
2979        let predicate = Reference::new("a").not_starts_with(Datum::string("Apa"));
2980        // Select '_pos' columns twice
2981        let table_scan = fixture
2982            .table
2983            .scan()
2984            .select([RESERVED_COL_NAME_POS, "a", RESERVED_COL_NAME_POS, "x"])
2985            .with_row_selection_enabled(true)
2986            .with_filter(predicate)
2987            .build()
2988            .unwrap();
2989
2990        let batch_stream = table_scan.to_arrow().await.unwrap();
2991
2992        let batches: Vec<_> = batch_stream.try_collect().await.unwrap();
2993        assert_eq!(batches.len(), 2);
2994
2995        // Examine batches are 1.paruqet and 3.parquet, 2.parquet is deleted.
2996        for batch in batches.iter() {
2997            assert_eq!(batch.num_rows(), 512);
2998
2999            // fetch the 1st _pos column by name and verify it is Int64Array with the expected values
3000            let pos_col = batch
3001                .column_by_name("_pos")
3002                .expect("_pos column should be present in the batch");
3003            let pos_array: &Int64Array = pos_col
3004                .as_any()
3005                .downcast_ref::<Int64Array>()
3006                .expect("_pos column should be a Int64Array");
3007            assert_eq!(*pos_array, Int64Array::from_iter_values(512i64..1024));
3008
3009            // fetch the 2nd _pos column by index and verify it is Int64Array with the expected values
3010            let pos_col = batch.column(2);
3011            let pos_array: &Int64Array = pos_col
3012                .as_any()
3013                .downcast_ref::<Int64Array>()
3014                .expect("_pos column should be a Int64Array");
3015            assert_eq!(*pos_array, Int64Array::from_iter_values(512i64..1024));
3016        }
3017    }
3018
3019    /// End-to-end through `TableScan`: a data file with three row groups planned
3020    /// as a single whole-file `FileScanTask` must yield contiguous, file-absolute
3021    /// `_pos` values (0..300) across the row-group boundaries.
3022    #[tokio::test]
3023    async fn test_pos_across_row_groups_via_table_scan() {
3024        let mut fixture = TableTestFixture::new();
3025        fixture.setup_multi_row_group_manifest(&[]).await;
3026
3027        // Planning must produce exactly one whole-file task with _pos projected and
3028        // no delete files, confirming TableScan does not sub-split the file.
3029        let tasks: Vec<_> = fixture
3030            .table
3031            .scan()
3032            .select(["x", RESERVED_COL_NAME_POS])
3033            .build()
3034            .unwrap()
3035            .plan_files()
3036            .await
3037            .unwrap()
3038            .try_collect()
3039            .await
3040            .unwrap();
3041        assert_eq!(tasks.len(), 1, "expected a single FileScanTask");
3042        let task = &tasks[0];
3043        assert!(
3044            task.project_field_ids.contains(&RESERVED_FIELD_ID_POS),
3045            "_pos field id must be projected into the FileScanTask"
3046        );
3047        assert_eq!(task.start, 0, "TableScan should plan whole-file tasks");
3048        assert_eq!(task.length, task.file_size_in_bytes);
3049        assert!(task.deletes.is_empty());
3050
3051        // Reading that task yields absolute _pos 0..300 in order.
3052        let batches: Vec<_> = fixture
3053            .table
3054            .scan()
3055            .select(["x", RESERVED_COL_NAME_POS])
3056            .build()
3057            .unwrap()
3058            .to_arrow()
3059            .await
3060            .unwrap()
3061            .try_collect()
3062            .await
3063            .unwrap();
3064
3065        let pos: Vec<i64> = batches
3066            .iter()
3067            .flat_map(|b| {
3068                b.column_by_name(RESERVED_COL_NAME_POS)
3069                    .expect("_pos column should be present")
3070                    .as_any()
3071                    .downcast_ref::<Int64Array>()
3072                    .expect("_pos column should be a Int64Array")
3073                    .values()
3074                    .to_vec()
3075            })
3076            .collect();
3077        assert_eq!(pos, (0..300).collect::<Vec<i64>>());
3078
3079        // Sanity: x == 1000 + _pos, proving _pos aligns with the actual rows read.
3080        let x: Vec<i64> = batches
3081            .iter()
3082            .flat_map(|b| {
3083                b.column_by_name("x")
3084                    .unwrap()
3085                    .as_primitive::<arrow_array::types::Int64Type>()
3086                    .values()
3087                    .to_vec()
3088            })
3089            .collect();
3090        assert_eq!(x, (1000..1300).collect::<Vec<i64>>());
3091    }
3092
3093    /// A positional delete file registered in the manifest must be attached to the planned
3094    /// `FileScanTask` and applied on read, while surviving `_pos` values stay file-absolute.
3095    #[tokio::test]
3096    async fn test_pos_with_positional_deletes_via_table_scan() {
3097        let mut fixture = TableTestFixture::new();
3098        // Delete file-absolute positions 150 (middle row group) and 299 (last row).
3099        fixture.setup_multi_row_group_manifest(&[150, 299]).await;
3100
3101        // Planning must attach the positional delete file to the task.
3102        let tasks: Vec<_> = fixture
3103            .table
3104            .scan()
3105            .select(["x", RESERVED_COL_NAME_POS])
3106            .build()
3107            .unwrap()
3108            .plan_files()
3109            .await
3110            .unwrap()
3111            .try_collect()
3112            .await
3113            .unwrap();
3114        assert_eq!(tasks.len(), 1);
3115        assert_eq!(
3116            tasks[0].deletes.len(),
3117            1,
3118            "positional delete file should be planned into the task"
3119        );
3120        assert_eq!(
3121            tasks[0].deletes[0].file_type,
3122            DataContentType::PositionDeletes
3123        );
3124
3125        // Reading applies the deletes; _pos must skip 150 and 299 and stay absolute.
3126        let batches: Vec<_> = fixture
3127            .table
3128            .scan()
3129            .select(["x", RESERVED_COL_NAME_POS])
3130            .build()
3131            .unwrap()
3132            .to_arrow()
3133            .await
3134            .unwrap()
3135            .try_collect()
3136            .await
3137            .unwrap();
3138
3139        let pos: Vec<i64> = batches
3140            .iter()
3141            .flat_map(|b| {
3142                b.column_by_name(RESERVED_COL_NAME_POS)
3143                    .expect("_pos column should be present")
3144                    .as_any()
3145                    .downcast_ref::<Int64Array>()
3146                    .expect("_pos column should be a Int64Array")
3147                    .values()
3148                    .to_vec()
3149            })
3150            .collect();
3151
3152        let total: usize = batches.iter().map(|b| b.num_rows()).sum();
3153        assert_eq!(
3154            total, 298,
3155            "two rows should be removed by positional deletes"
3156        );
3157        assert!(!pos.contains(&150) && !pos.contains(&299), "got {pos:?}");
3158        let expected: Vec<i64> = (0..150).chain(151..299).collect();
3159        assert_eq!(pos, expected);
3160    }
3161
3162    /// A filter that only matches the middle row group (`y` in [1100, 1200)) and
3163    /// prunes the other two row groups by statistics, so only the middle row group
3164    /// is read. `_pos` must report the file-absolute positions 100..200 for those
3165    /// rows, not values reset to 0..100.
3166    ///
3167    /// `y` is a non-partition column, so pruning here is driven purely by Parquet
3168    /// row-group statistics (the TableTestFixture's partition column is `x`).
3169    #[tokio::test]
3170    async fn test_pos_reads_only_middle_row_group_via_filter() {
3171        let mut fixture = TableTestFixture::new();
3172        fixture.setup_multi_row_group_manifest(&[]).await;
3173
3174        // Middle row group holds y = 1100..1200 at file positions 100..200.
3175        let predicate = Reference::new("y")
3176            .greater_than_or_equal_to(Datum::long(1100))
3177            .and(Reference::new("y").less_than(Datum::long(1200)));
3178
3179        let batches: Vec<_> = fixture
3180            .table
3181            .scan()
3182            .select(["y", RESERVED_COL_NAME_POS])
3183            .with_filter(predicate)
3184            .with_row_group_filtering_enabled(true)
3185            .build()
3186            .unwrap()
3187            .to_arrow()
3188            .await
3189            .unwrap()
3190            .try_collect()
3191            .await
3192            .unwrap();
3193
3194        let total: usize = batches.iter().map(|b| b.num_rows()).sum();
3195        assert_eq!(total, 100, "only the middle row group should be read");
3196
3197        let pos: Vec<i64> = batches
3198            .iter()
3199            .flat_map(|b| {
3200                b.column_by_name(RESERVED_COL_NAME_POS)
3201                    .expect("_pos column should be present")
3202                    .as_any()
3203                    .downcast_ref::<Int64Array>()
3204                    .expect("_pos column should be a Int64Array")
3205                    .values()
3206                    .to_vec()
3207            })
3208            .collect();
3209        assert_eq!(
3210            pos,
3211            (100..200).collect::<Vec<i64>>(),
3212            "_pos must be file-absolute for the middle row group"
3213        );
3214
3215        // Cross-check: y == 1000 + _pos for every surviving row.
3216        let y: Vec<i64> = batches
3217            .iter()
3218            .flat_map(|b| {
3219                b.column_by_name("y")
3220                    .unwrap()
3221                    .as_primitive::<arrow_array::types::Int64Type>()
3222                    .values()
3223                    .to_vec()
3224            })
3225            .collect();
3226        assert_eq!(y, (1100..1200).collect::<Vec<i64>>());
3227    }
3228}