blob: 5387093a205b9a01ae7f8313b7df7c783228b09b [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
use crate::arrow::paimon_type_to_arrow;
use crate::arrow::schema_evolution::{create_index_mapping, NULL_FIELD_INDEX};
use crate::deletion_vector::{DeletionVector, DeletionVectorFactory};
use crate::io::{FileIO, FileRead, FileStatus};
use crate::spec::{DataField, DataFileMeta};
use crate::table::schema_manager::SchemaManager;
use crate::table::ArrowRecordBatchStream;
use crate::{DataSplit, Error};
use arrow_array::RecordBatch;
use arrow_cast::cast;
use arrow_schema::{Field as ArrowField, Schema as ArrowSchema};
use async_stream::try_stream;
use bytes::Bytes;
use futures::future::BoxFuture;
use futures::{StreamExt, TryFutureExt};
use parquet::arrow::arrow_reader::{ArrowReaderOptions, RowSelection, RowSelector};
use parquet::arrow::async_reader::{AsyncFileReader, MetadataFetch};
use parquet::arrow::{ParquetRecordBatchStreamBuilder, ProjectionMask};
use parquet::file::metadata::ParquetMetaDataReader;
use parquet::file::metadata::{ParquetMetaData, RowGroupMetaData};
use std::collections::HashMap;
use std::ops::Range;
use std::sync::Arc;
use tokio::try_join;
/// Builder to create ArrowReader
pub struct ArrowReaderBuilder {
batch_size: Option<usize>,
file_io: FileIO,
schema_manager: SchemaManager,
table_schema_id: i64,
}
impl ArrowReaderBuilder {
/// Create a new ArrowReaderBuilder
pub(crate) fn new(
file_io: FileIO,
schema_manager: SchemaManager,
table_schema_id: i64,
) -> Self {
ArrowReaderBuilder {
batch_size: None,
file_io,
schema_manager,
table_schema_id,
}
}
/// Build the ArrowReader with the given read type (logical row type or projected subset).
/// Used to clip Parquet schema to requested columns only.
pub fn build(self, read_type: Vec<DataField>) -> ArrowReader {
ArrowReader {
batch_size: self.batch_size,
file_io: self.file_io,
schema_manager: self.schema_manager,
table_schema_id: self.table_schema_id,
read_type,
}
}
}
/// Reads data from Parquet files
#[derive(Clone)]
pub struct ArrowReader {
batch_size: Option<usize>,
file_io: FileIO,
schema_manager: SchemaManager,
table_schema_id: i64,
read_type: Vec<DataField>,
}
impl ArrowReader {
/// Take a stream of DataSplits and read every data file in each split.
/// Returns a stream of Arrow RecordBatches from all files.
///
/// Uses SchemaManager to load the data file's schema (via `DataFileMeta.schema_id`)
/// and computes field-ID-based index mapping for schema evolution (added columns,
/// type promotion, column reordering).
///
/// Matches [RawFileSplitRead.createReader](https://github.com/apache/paimon/blob/master/paimon-core/src/main/java/org/apache/paimon/operation/RawFileSplitRead.java).
pub fn read(self, data_splits: &[DataSplit]) -> crate::Result<ArrowRecordBatchStream> {
let file_io = self.file_io.clone();
let batch_size = self.batch_size;
let splits: Vec<DataSplit> = data_splits.to_vec();
let read_type = self.read_type;
let schema_manager = self.schema_manager;
let table_schema_id = self.table_schema_id;
Ok(try_stream! {
for split in splits {
// Create DV factory for this split only.
let dv_factory = if split
.data_deletion_files()
.is_some_and(|files| files.iter().any(Option::is_some))
{
Some(
DeletionVectorFactory::new(
&file_io,
split.data_files(),
split.data_deletion_files(),
)
.await?,
)
} else {
None
};
for file_meta in split.data_files().to_vec() {
let dv = dv_factory
.as_ref()
.and_then(|factory| factory.get_deletion_vector(&file_meta.file_name))
.cloned();
// Load data file's schema if it differs from the table schema.
let data_fields: Option<Vec<DataField>> = if file_meta.schema_id != table_schema_id {
let data_schema = schema_manager.schema(file_meta.schema_id).await?;
Some(data_schema.fields().to_vec())
} else {
None
};
let mut stream = read_single_file_stream(
file_io.clone(),
split.clone(),
file_meta,
read_type.clone(),
data_fields,
batch_size,
dv,
)?;
while let Some(batch) = stream.next().await {
yield batch?;
}
}
}
}
.boxed())
}
/// Read data files in data evolution mode, merging columns from files that share the same row ID range.
///
/// Each DataSplit contains files grouped by `first_row_id`. Files within a split may contain
/// different columns for the same logical rows. This method reads each file and merges them
/// column-wise, respecting `max_sequence_number` for conflict resolution.
///
/// `table_fields` is the full table schema fields, used to determine which columns each file
/// provides when `write_cols` is not set.
pub fn read_data_evolution(
self,
data_splits: &[DataSplit],
table_fields: &[DataField],
) -> crate::Result<ArrowRecordBatchStream> {
let file_io = self.file_io.clone();
let batch_size = self.batch_size;
let splits: Vec<DataSplit> = data_splits.to_vec();
let read_type = self.read_type;
let table_fields: Vec<DataField> = table_fields.to_vec();
let schema_manager = self.schema_manager;
let table_schema_id = self.table_schema_id;
Ok(try_stream! {
for split in splits {
if split.raw_convertible() || split.data_files().len() == 1 {
for file_meta in split.data_files().to_vec() {
let data_fields: Option<Vec<DataField>> = if file_meta.schema_id != table_schema_id {
let data_schema = schema_manager.schema(file_meta.schema_id).await?;
Some(data_schema.fields().to_vec())
} else {
None
};
let mut stream = read_single_file_stream(
file_io.clone(), split.clone(), file_meta, read_type.clone(),
data_fields, batch_size, None,
)?;
while let Some(batch) = stream.next().await {
yield batch?;
}
}
} else {
// Multiple files need column-wise merge.
let mut merge_stream = merge_files_by_columns(
&file_io,
&split,
&read_type,
&table_fields,
schema_manager.clone(),
table_schema_id,
batch_size,
)?;
while let Some(batch) = merge_stream.next().await {
yield batch?;
}
}
}
}
.boxed())
}
}
/// Build the target Arrow schema from the read type (Paimon DataFields).
fn build_target_arrow_schema(read_type: &[DataField]) -> crate::Result<Arc<ArrowSchema>> {
let fields: Vec<ArrowField> = read_type
.iter()
.map(|f| {
let arrow_type = paimon_type_to_arrow(f.data_type())?;
Ok(ArrowField::new(
f.name(),
arrow_type,
f.data_type().is_nullable(),
))
})
.collect::<crate::Result<Vec<_>>>()?;
Ok(Arc::new(ArrowSchema::new(fields)))
}
/// Read a single parquet file from a split, returning a lazy stream of batches.
/// Optionally applies a deletion vector.
///
/// Handles schema evolution using field-ID-based index mapping:
/// - `data_fields`: if `Some`, the fields from the data file's schema (loaded via SchemaManager).
/// Used to compute index mapping between `read_type` and data fields by field ID.
/// - Columns missing from the file are filled with null arrays.
/// - Columns whose Arrow type differs from the target type are cast (type promotion).
///
/// Reference: [RawFileSplitRead.createFileReader](https://github.com/apache/paimon/blob/release-1.3/paimon-core/src/main/java/org/apache/paimon/operation/RawFileSplitRead.java)
fn read_single_file_stream(
file_io: FileIO,
split: DataSplit,
file_meta: DataFileMeta,
read_type: Vec<DataField>,
data_fields: Option<Vec<DataField>>,
batch_size: Option<usize>,
dv: Option<Arc<DeletionVector>>,
) -> crate::Result<ArrowRecordBatchStream> {
let target_schema = build_target_arrow_schema(&read_type)?;
// Compute index mapping and determine which columns to read from the parquet file.
// If data_fields is provided, use field-ID-based mapping; otherwise use read_type names directly.
let (parquet_read_fields, index_mapping) = if let Some(ref df) = data_fields {
let mapping = create_index_mapping(&read_type, df);
match mapping {
Some(ref idx_map) => {
// Only read data fields that are referenced by the index mapping.
// Dedup by data field index to avoid duplicate parquet column projections.
let mut seen = std::collections::HashSet::new();
let fields_to_read: Vec<DataField> = idx_map
.iter()
.filter(|&&idx| idx != NULL_FIELD_INDEX && seen.insert(idx))
.map(|&idx| df[idx as usize].clone())
.collect();
(fields_to_read, Some(idx_map.clone()))
}
None => {
// Identity mapping — read data fields in order.
(df.clone(), None)
}
}
} else {
// No schema evolution — read by read_type names.
(read_type.clone(), None)
};
let parquet_column_names: Vec<String> = parquet_read_fields
.iter()
.map(|f| f.name().to_string())
.collect();
Ok(try_stream! {
let path_to_read = split.data_file_path(&file_meta);
if !path_to_read.to_ascii_lowercase().ends_with(".parquet") {
Err(Error::Unsupported {
message: format!(
"unsupported file format: only .parquet is supported, got: {path_to_read}"
),
})?
}
let parquet_file = file_io.new_input(&path_to_read)?;
let (parquet_metadata, parquet_reader) =
try_join!(parquet_file.metadata(), parquet_file.reader())?;
let arrow_file_reader = ArrowFileReader::new(parquet_metadata, parquet_reader);
let mut batch_stream_builder = ParquetRecordBatchStreamBuilder::new(arrow_file_reader).await?;
// Only project columns that exist in this file.
let parquet_schema = batch_stream_builder.parquet_schema().clone();
let file_column_names: Vec<&str> = parquet_schema.columns().iter().map(|c| c.name()).collect();
let available_columns: Vec<&str> = parquet_column_names
.iter()
.filter(|name| file_column_names.contains(&name.as_str()))
.map(String::as_str)
.collect();
let mask = ProjectionMask::columns(&parquet_schema, available_columns.iter().copied());
batch_stream_builder = batch_stream_builder.with_projection(mask);
if let Some(ref dv) = dv {
if !dv.is_empty() {
let row_selection =
build_deletes_row_selection(batch_stream_builder.metadata().row_groups(), dv)?;
batch_stream_builder = batch_stream_builder.with_row_selection(row_selection);
}
}
if let Some(size) = batch_size {
batch_stream_builder = batch_stream_builder.with_batch_size(size);
}
let mut batch_stream = batch_stream_builder.build()?;
while let Some(batch) = batch_stream.next().await {
let batch = batch?;
let num_rows = batch.num_rows();
let batch_schema = batch.schema();
// Build output columns using index mapping (field-ID-based) or by name.
let mut columns: Vec<Arc<dyn arrow_array::Array>> = Vec::with_capacity(target_schema.fields().len());
for (i, target_field) in target_schema.fields().iter().enumerate() {
let source_col = if let Some(ref idx_map) = index_mapping {
let data_idx = idx_map[i];
if data_idx == NULL_FIELD_INDEX {
None
} else {
// Find the column in the batch by the data field's name.
let data_field = &data_fields.as_ref().unwrap()[data_idx as usize];
batch_schema
.index_of(data_field.name())
.ok()
.map(|col_idx| batch.column(col_idx))
}
} else if let Some(ref df) = data_fields {
// Identity mapping with data_fields present (e.g. renamed column).
// Use data field name (old name in parquet) at the same position.
batch_schema
.index_of(df[i].name())
.ok()
.map(|col_idx| batch.column(col_idx))
} else {
// No schema evolution — look up by target field name.
batch_schema
.index_of(target_field.name())
.ok()
.map(|col_idx| batch.column(col_idx))
};
match source_col {
Some(col) => {
if col.data_type() == target_field.data_type() {
columns.push(col.clone());
} else {
// Type promotion: cast to target type.
let casted = cast(col, target_field.data_type()).map_err(|e| {
Error::UnexpectedError {
message: format!(
"Failed to cast column '{}' from {:?} to {:?}: {e}",
target_field.name(),
col.data_type(),
target_field.data_type()
),
source: Some(Box::new(e)),
}
})?;
columns.push(casted);
}
}
None => {
// Column missing from file: fill with nulls.
let null_array = arrow_array::new_null_array(target_field.data_type(), num_rows);
columns.push(null_array);
}
}
}
let result = if columns.is_empty() {
RecordBatch::try_new_with_options(
target_schema.clone(),
columns,
&arrow_array::RecordBatchOptions::new().with_row_count(Some(num_rows)),
)
} else {
RecordBatch::try_new(target_schema.clone(), columns)
}
.map_err(|e| {
Error::UnexpectedError {
message: format!("Failed to build schema-evolved RecordBatch: {e}"),
source: Some(Box::new(e)),
}
})?;
yield result;
}
}
.boxed())
}
/// Merge multiple files column-wise for data evolution, streaming with bounded memory.
///
/// Uses field IDs (not column names) to resolve which file provides which column,
/// ensuring correctness across schema evolution (column rename, add, drop).
///
/// Opens all file readers simultaneously and maintains a cursor (current batch + offset)
/// per file. Each poll slices up to `batch_size` rows from each file's current batch,
/// assembles columns from the winning files, and yields the merged batch. When a file's
/// current batch is exhausted, the next batch is read from its stream on demand.
fn merge_files_by_columns(
file_io: &FileIO,
split: &DataSplit,
read_type: &[DataField],
table_fields: &[DataField],
schema_manager: SchemaManager,
table_schema_id: i64,
batch_size: Option<usize>,
) -> crate::Result<ArrowRecordBatchStream> {
let data_files = split.data_files();
if data_files.is_empty() {
return Ok(futures::stream::empty().boxed());
}
// Build owned data for the stream closure.
let file_io = file_io.clone();
let split = split.clone();
let data_files: Vec<DataFileMeta> = data_files.to_vec();
let read_type = read_type.to_vec();
let table_fields = table_fields.to_vec();
let output_batch_size = batch_size.unwrap_or(1024);
let target_schema = build_target_arrow_schema(&read_type)?;
Ok(try_stream! {
// Pre-load schemas and collect field IDs + data_fields per file.
// file_idx -> (field_ids, Option<Vec<DataField>>)
let mut file_info: HashMap<usize, (Vec<i32>, Option<Vec<DataField>>)> = HashMap::new();
for (file_idx, file_meta) in data_files.iter().enumerate() {
let (field_ids, data_fields) = if file_meta.schema_id != table_schema_id {
let file_schema = schema_manager.schema(file_meta.schema_id).await?;
let file_fields = file_schema.fields();
let ids: Vec<i32> = if let Some(ref wc) = file_meta.write_cols {
// write_cols names are from the file's schema at write time.
wc.iter()
.filter_map(|name| file_fields.iter().find(|f| f.name() == name).map(|f| f.id()))
.collect()
} else {
file_fields.iter().map(|f| f.id()).collect()
};
(ids, Some(file_fields.to_vec()))
} else {
let ids: Vec<i32> = if let Some(ref wc) = file_meta.write_cols {
// write_cols names are from the current table schema.
wc.iter()
.filter_map(|name| table_fields.iter().find(|f| f.name() == name).map(|f| f.id()))
.collect()
} else {
table_fields.iter().map(|f| f.id()).collect()
};
(ids, None)
};
file_info.insert(file_idx, (field_ids, data_fields));
}
// Determine which file provides each field ID, resolving conflicts by max_sequence_number.
// field_id -> (file_index, max_sequence_number)
let mut field_id_source: HashMap<i32, (usize, i64)> = HashMap::new();
for (file_idx, file_meta) in data_files.iter().enumerate() {
let (ref field_ids, _) = file_info[&file_idx];
for &fid in field_ids {
let entry = field_id_source
.entry(fid)
.or_insert((file_idx, i64::MIN));
if file_meta.max_sequence_number > entry.1 {
*entry = (file_idx, file_meta.max_sequence_number);
}
}
}
// For each projected field, determine which file provides it (by field ID).
// file_index -> Vec<column_name> (target column names)
let mut file_read_columns: HashMap<usize, Vec<String>> = HashMap::new();
for field in &read_type {
if let Some(&(file_idx, _)) = field_id_source.get(&field.id()) {
file_read_columns
.entry(file_idx)
.or_default()
.push(field.name().to_string());
}
}
// For each projected field, record (file_index, target_column_name) for assembly.
let column_plan: Vec<(Option<usize>, String)> = read_type
.iter()
.map(|field| {
let file_idx = field_id_source.get(&field.id()).map(|&(idx, _)| idx);
(file_idx, field.name().to_string())
})
.collect();
// Collect which file indices we need to open streams for.
let active_file_indices: Vec<usize> = file_read_columns.keys().copied().collect();
// Open a stream for each active file.
// Build per-file read_type: only the DataFields this file is responsible for.
let mut file_streams: HashMap<usize, ArrowRecordBatchStream> = HashMap::new();
for &file_idx in &active_file_indices {
let file_cols = file_read_columns.get(&file_idx).cloned().unwrap_or_default();
let file_read_type: Vec<DataField> = file_cols
.iter()
.filter_map(|col_name| read_type.iter().find(|f| f.name() == col_name).cloned())
.collect();
let (_, ref data_fields) = file_info[&file_idx];
let stream = read_single_file_stream(
file_io.clone(),
split.clone(),
data_files[file_idx].clone(),
file_read_type,
data_fields.clone(),
batch_size,
None,
)?;
file_streams.insert(file_idx, stream);
}
// Per-file cursor: current batch + offset within it.
let mut file_cursors: HashMap<usize, (RecordBatch, usize)> = HashMap::new();
loop {
// Ensure each active file has a current batch. If a file's cursor is exhausted
// or not yet initialized, read the next batch from its stream.
for &file_idx in &active_file_indices {
let needs_next = match file_cursors.get(&file_idx) {
None => true,
Some((batch, offset)) => *offset >= batch.num_rows(),
};
if needs_next {
file_cursors.remove(&file_idx);
if let Some(stream) = file_streams.get_mut(&file_idx) {
if let Some(batch_result) = stream.next().await {
let batch = batch_result?;
if batch.num_rows() > 0 {
file_cursors.insert(file_idx, (batch, 0));
}
}
}
}
}
// All active files must have a cursor to assemble a valid row.
// If any file has no cursor (stream exhausted), we're done.
if active_file_indices.iter().any(|idx| !file_cursors.contains_key(idx)) {
break;
}
// Determine how many rows we can emit: min of remaining rows across all files.
let remaining: usize = active_file_indices
.iter()
.map(|idx| {
let (batch, offset) = file_cursors.get(idx).unwrap();
batch.num_rows() - offset
})
.min()
.unwrap_or(0);
if remaining == 0 {
break;
}
let rows_to_emit = remaining.min(output_batch_size);
// Slice each file's current batch and assemble columns.
// Use the target schema so that missing columns are null-filled.
let mut columns: Vec<Arc<dyn arrow_array::Array>> =
Vec::with_capacity(column_plan.len());
for (i, (file_idx_opt, col_name)) in column_plan.iter().enumerate() {
let target_field = &target_schema.fields()[i];
let col = file_idx_opt
.and_then(|file_idx| file_cursors.get(&file_idx))
.and_then(|(batch, offset)| {
batch
.schema()
.index_of(col_name)
.ok()
.map(|col_idx| batch.column(col_idx).slice(*offset, rows_to_emit))
});
columns.push(col.unwrap_or_else(|| {
arrow_array::new_null_array(target_field.data_type(), rows_to_emit)
}));
}
// Advance all cursors.
for &file_idx in &active_file_indices {
if let Some((_, ref mut offset)) = file_cursors.get_mut(&file_idx) {
*offset += rows_to_emit;
}
}
let merged = RecordBatch::try_new(target_schema.clone(), columns).map_err(|e| Error::UnexpectedError {
message: format!("Failed to build merged RecordBatch: {e}"),
source: Some(Box::new(e)),
})?;
yield merged;
}
}
.boxed())
}
/// Builds a Parquet [RowSelection] from deletion vector.
/// Only rows not in the deletion vector are selected; deleted rows are skipped at read time.
/// todo: Uses [DeletionVectorIterator] with [advance_to](DeletionVectorIterator::advance_to) when skipping row groups similar to iceberg-rust
fn build_deletes_row_selection(
row_group_metadata_list: &[RowGroupMetaData],
deletion_vector: &DeletionVector,
) -> crate::Result<RowSelection> {
let mut delete_iter = deletion_vector.iter();
let mut results: Vec<RowSelector> = Vec::new();
let mut current_row_group_base_idx: u64 = 0;
let mut next_deleted_row_idx_opt = delete_iter.next();
for row_group_metadata in row_group_metadata_list {
let row_group_num_rows = row_group_metadata.num_rows() as u64;
let next_row_group_base_idx = current_row_group_base_idx + row_group_num_rows;
let mut next_deleted_row_idx = match next_deleted_row_idx_opt {
Some(next_deleted_row_idx) => {
if next_deleted_row_idx >= next_row_group_base_idx {
results.push(RowSelector::select(row_group_num_rows as usize));
current_row_group_base_idx += row_group_num_rows;
continue;
}
next_deleted_row_idx
}
None => {
results.push(RowSelector::select(row_group_num_rows as usize));
current_row_group_base_idx += row_group_num_rows;
continue;
}
};
let mut current_idx = current_row_group_base_idx;
'chunks: while next_deleted_row_idx < next_row_group_base_idx {
if current_idx < next_deleted_row_idx {
let run_length = next_deleted_row_idx - current_idx;
results.push(RowSelector::select(run_length as usize));
current_idx += run_length;
}
let mut run_length = 0u64;
while next_deleted_row_idx == current_idx
&& next_deleted_row_idx < next_row_group_base_idx
{
run_length += 1;
current_idx += 1;
next_deleted_row_idx_opt = delete_iter.next();
next_deleted_row_idx = match next_deleted_row_idx_opt {
Some(v) => v,
None => {
results.push(RowSelector::skip(run_length as usize));
break 'chunks;
}
};
}
if run_length > 0 {
results.push(RowSelector::skip(run_length as usize));
}
}
if current_idx < next_row_group_base_idx {
results.push(RowSelector::select(
(next_row_group_base_idx - current_idx) as usize,
));
}
current_row_group_base_idx += row_group_num_rows;
}
Ok(results.into())
}
/// ArrowFileReader is a wrapper around a FileRead that impls parquets AsyncFileReader.
///
/// # TODO
///
/// [ParquetObjectReader](https://docs.rs/parquet/latest/src/parquet/arrow/async_reader/store.rs.html#64)
/// contains the following hints to speed up metadata loading, similar to iceberg, we can consider adding them to this struct:
///
/// - `metadata_size_hint`: Provide a hint as to the size of the parquet file's footer.
/// - `preload_column_index`: Load the Column Index as part of [`Self::get_metadata`].
/// - `preload_offset_index`: Load the Offset Index as part of [`Self::get_metadata`].
struct ArrowFileReader<R: FileRead> {
meta: FileStatus,
r: R,
}
impl<R: FileRead> ArrowFileReader<R> {
/// Create a new ArrowFileReader
fn new(meta: FileStatus, r: R) -> Self {
Self { meta, r }
}
fn read_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, parquet::errors::Result<Bytes>> {
Box::pin(self.r.read(range.start..range.end).map_err(|err| {
let err_msg = format!("{err}");
parquet::errors::ParquetError::External(err_msg.into())
}))
}
}
impl<R: FileRead> MetadataFetch for ArrowFileReader<R> {
fn fetch(&mut self, range: Range<u64>) -> BoxFuture<'_, parquet::errors::Result<Bytes>> {
self.read_bytes(range)
}
}
impl<R: FileRead> AsyncFileReader for ArrowFileReader<R> {
fn get_bytes(&mut self, range: Range<u64>) -> BoxFuture<'_, parquet::errors::Result<Bytes>> {
self.read_bytes(range)
}
fn get_metadata(
&mut self,
options: Option<&ArrowReaderOptions>,
) -> BoxFuture<'_, parquet::errors::Result<Arc<ParquetMetaData>>> {
let metadata_opts = options.map(|o| o.metadata_options().clone());
Box::pin(async move {
let file_size = self.meta.size;
let metadata = ParquetMetaDataReader::new()
.with_metadata_options(metadata_opts)
.load_and_finish(self, file_size)
.await?;
Ok(Arc::new(metadata))
})
}
}