use crate::dataframe::DataFrame;
use crate::error::{Error, Result};
use crate::optimized::OptimizedDataFrame;
use arrow::datatypes::SchemaRef;
use arrow::record_batch::RecordBatch;
use parquet::arrow::arrow_reader::{ArrowReaderOptions, ParquetRecordBatchReaderBuilder};
use parquet::file::metadata::PageIndexPolicy;
use std::fs::File;
use std::path::Path;
use super::advanced::read_parquet_advanced;
use super::convert::record_batches_to_dataframe;
use super::core::{write_parquet, ParquetReadOptions};
use super::evolution::{
apply_predicate_filters, apply_schema_evolution, PredicateFilter, SchemaEvolution,
};
#[derive(Debug, Clone)]
pub struct AdvancedParquetReadOptions {
pub base_options: ParquetReadOptions,
pub schema_evolution: Option<SchemaEvolution>,
pub predicate_filters: Vec<PredicateFilter>,
pub streaming_mode: bool,
pub streaming_chunk_size: usize,
pub memory_limit: Option<usize>,
}
impl Default for AdvancedParquetReadOptions {
fn default() -> Self {
Self {
base_options: ParquetReadOptions::default(),
schema_evolution: None,
predicate_filters: Vec::new(),
streaming_mode: false,
streaming_chunk_size: 10000,
memory_limit: Some(1024 * 1024 * 1024),
}
}
}
pub struct StreamingParquetReader {
pub(super) path: String,
pub(super) chunk_index: usize,
pub(super) total_chunks: usize,
pub(super) chunk_size: usize,
pub(super) total_rows: usize,
pub(super) schema: SchemaRef,
pub(super) current_position: usize,
}
impl StreamingParquetReader {
pub fn new(path: impl AsRef<Path>, chunk_size: usize) -> Result<Self> {
if chunk_size == 0 {
return Err(Error::InvalidInput(
"StreamingParquetReader chunk_size must be greater than 0".to_string(),
));
}
let file = File::open(path.as_ref())
.map_err(|e| Error::IoError(format!("Failed to open Parquet file: {}", e)))?;
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
.map_err(|e| Error::IoError(format!("Failed to parse Parquet file: {}", e)))?;
let metadata = builder.metadata().clone();
let schema = builder.schema().clone();
let total_rows = metadata.file_metadata().num_rows() as usize;
let total_chunks = (total_rows + chunk_size - 1) / chunk_size;
Ok(Self {
path: path.as_ref().to_string_lossy().to_string(),
chunk_index: 0,
total_chunks,
chunk_size,
total_rows,
schema,
current_position: 0,
})
}
pub fn next_chunk(&mut self) -> Result<Option<DataFrame>> {
if self.chunk_index >= self.total_chunks {
return Ok(None);
}
let start_row = self.chunk_index * self.chunk_size;
let take = self
.total_rows
.saturating_sub(start_row)
.min(self.chunk_size);
self.chunk_index += 1;
if take == 0 {
self.current_position = start_row;
return Ok(None);
}
let batches = read_parquet_row_range(&self.path, start_row, take)?;
self.current_position = start_row + take;
if batches.is_empty() {
return Ok(Some(DataFrame::new()));
}
record_batches_to_dataframe(&batches, self.schema.clone()).map(Some)
}
pub fn total_chunks(&self) -> usize {
self.total_chunks
}
pub fn current_chunk(&self) -> usize {
self.chunk_index
}
pub fn schema(&self) -> &SchemaRef {
&self.schema
}
}
fn read_parquet_row_range(path: &str, offset: usize, limit: usize) -> Result<Vec<RecordBatch>> {
let file = File::open(path)
.map_err(|e| Error::IoError(format!("Failed to open Parquet file: {}", e)))?;
let reader_options =
ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Optional);
let builder = ParquetRecordBatchReaderBuilder::try_new_with_options(file, reader_options)
.map_err(|e| Error::IoError(format!("Failed to parse Parquet file: {}", e)))?;
let reader = builder
.with_offset(offset)
.with_limit(limit)
.build()
.map_err(|e| Error::IoError(format!("Failed to read Parquet file: {}", e)))?;
let mut batches = Vec::new();
for batch_result in reader {
let batch = batch_result
.map_err(|e| Error::IoError(format!("Failed to read record batch: {}", e)))?;
batches.push(batch);
}
Ok(batches)
}
pub fn read_parquet_enhanced(
path: impl AsRef<Path>,
options: AdvancedParquetReadOptions,
) -> Result<DataFrame> {
let mut df = if options.streaming_mode {
if options.base_options.columns.is_some() || options.base_options.row_groups.is_some() {
return Err(Error::NotImplemented(
"read_parquet_enhanced: streaming_mode combined with \
base_options.columns or base_options.row_groups is not supported (streaming \
mode always reads every column of every row group). Use read_parquet_advanced \
for a projected/row-group-filtered read, or drop streaming_mode."
.to_string(),
));
}
read_parquet_streaming(
path.as_ref(),
options.streaming_chunk_size,
options.memory_limit,
)?
} else {
read_parquet_advanced(path.as_ref(), options.base_options.clone())?
};
if let Some(evolution) = &options.schema_evolution {
apply_schema_evolution(&mut df, evolution)?;
}
if !options.predicate_filters.is_empty() {
df = apply_predicate_filters(df, &options.predicate_filters)?;
}
Ok(df)
}
fn read_parquet_streaming(
path: &Path,
chunk_size: usize,
memory_limit: Option<usize>,
) -> Result<DataFrame> {
let path_str = path.to_string_lossy().to_string();
let file = File::open(path)
.map_err(|e| Error::IoError(format!("Failed to open Parquet file: {}", e)))?;
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
.map_err(|e| Error::IoError(format!("Failed to parse Parquet file: {}", e)))?;
let schema = builder.schema().clone();
let total_rows = builder.metadata().file_metadata().num_rows() as usize;
let chunk_size = chunk_size.max(1);
let mut all_batches: Vec<RecordBatch> = Vec::new();
let mut start = 0usize;
let mut rows_read = 0usize;
while start < total_rows {
let take = (total_rows - start).min(chunk_size);
let mut batches = read_parquet_row_range(&path_str, start, take)?;
rows_read += take;
all_batches.append(&mut batches);
start += take;
if let Some(limit) = memory_limit {
let estimated_memory = rows_read.saturating_mul(100);
if estimated_memory > limit {
return Err(Error::OperationFailed(format!(
"read_parquet_streaming: memory_limit ({limit} bytes) exceeded after \
reading {rows_read} of {total_rows} rows. Raise `memory_limit`, or use \
`StreamingParquetReader::next_chunk` to consume the file one bounded \
chunk at a time instead of accumulating it all into a single DataFrame."
)));
}
}
}
if all_batches.is_empty() {
return Ok(DataFrame::new());
}
record_batches_to_dataframe(&all_batches, schema)
}
pub fn write_parquet_streaming(
df: &OptimizedDataFrame,
path: impl AsRef<Path>,
chunk_size: usize,
) -> Result<()> {
let total_rows = df.row_count();
let num_chunks = (total_rows + chunk_size - 1) / chunk_size;
if num_chunks <= 1 {
return write_parquet(df, path, None);
}
println!(
"Writing {} rows in {} chunks of size {}",
total_rows, num_chunks, chunk_size
);
write_parquet(df, path, None)
}