use crate::dataframe::DataFrame;
use crate::error::{Error, Result};
use crate::optimized::OptimizedDataFrame;
use arrow::array::{ArrayRef, BooleanArray, Float64Array, Int64Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use parquet::arrow::arrow_writer::ArrowWriter;
use parquet::basic::Compression;
use parquet::file::properties::WriterProperties;
use std::fs::File;
use std::path::Path;
use std::sync::Arc;
use super::convert::record_batches_to_dataframe;
#[derive(Debug, Clone)]
pub struct ColumnStats {
pub name: String,
pub data_type: String,
pub null_count: Option<i64>,
pub distinct_count: Option<i64>,
pub min_value: Option<String>,
pub max_value: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParquetCompression {
None,
Snappy,
Gzip,
Lzo,
Brotli,
Lz4,
Zstd,
}
#[derive(Debug, Clone)]
pub struct ParquetMetadata {
pub num_rows: i64,
pub num_row_groups: usize,
pub schema: String,
pub file_size: Option<i64>,
pub compression: String,
pub created_by: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ParquetReadOptions {
pub columns: Option<Vec<String>>,
pub use_threads: bool,
pub use_memory_map: bool,
pub batch_size: Option<usize>,
pub row_groups: Option<Vec<usize>>,
}
impl Default for ParquetReadOptions {
fn default() -> Self {
Self {
columns: None,
use_threads: true,
use_memory_map: false,
batch_size: None,
row_groups: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ParquetWriteOptions {
pub compression: ParquetCompression,
pub row_group_size: Option<usize>,
pub page_size: Option<usize>,
pub enable_dictionary: bool,
pub use_threads: bool,
}
impl Default for ParquetWriteOptions {
fn default() -> Self {
Self {
compression: ParquetCompression::Snappy,
row_group_size: Some(50000),
page_size: Some(1024 * 1024),
enable_dictionary: true,
use_threads: true,
}
}
}
#[derive(Debug, Clone)]
pub struct RowGroupInfo {
pub index: usize,
pub num_rows: i64,
pub total_byte_size: i64,
pub num_columns: usize,
}
pub(super) fn validate_compression(compression: ParquetCompression) -> Result<()> {
match compression {
ParquetCompression::Zstd => Err(Error::NotImplemented(
"ParquetCompression::Zstd is unavailable: this build uses the Pure Rust `parquet` \
crate with the C-backed zstd codec feature disabled (COOLJAPAN Pure Rust policy). \
Use ParquetCompression::Snappy, Gzip, Brotli, or Lz4 instead."
.to_string(),
)),
ParquetCompression::Lzo => Err(Error::NotImplemented(
"ParquetCompression::Lzo is unavailable: the `parquet` crate does not implement an \
LZO codec (this is a gap in the upstream implementation, not a disabled build \
feature). Use ParquetCompression::Snappy, Gzip, Brotli, or Lz4 instead."
.to_string(),
)),
ParquetCompression::None
| ParquetCompression::Snappy
| ParquetCompression::Gzip
| ParquetCompression::Brotli
| ParquetCompression::Lz4 => Ok(()),
}
}
pub fn read_parquet(path: impl AsRef<Path>) -> Result<DataFrame> {
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 schema = builder.schema().clone();
let reader = builder
.build()
.map_err(|e| Error::IoError(format!("Failed to read Parquet file: {}", e)))?;
let mut all_batches = Vec::new();
for batch_result in reader {
let batch = batch_result
.map_err(|e| Error::IoError(format!("Failed to read record batch: {}", e)))?;
all_batches.push(batch);
}
if all_batches.is_empty() {
return Ok(DataFrame::new());
}
record_batches_to_dataframe(&all_batches, schema)
}
pub fn write_parquet(
df: &OptimizedDataFrame,
path: impl AsRef<Path>,
compression: Option<ParquetCompression>,
) -> Result<()> {
let schema_fields: Vec<Field> = df
.column_names()
.iter()
.filter_map(|col_name| {
if let Ok(col_view) = df.column(col_name) {
let data_type = match col_view.column_type() {
crate::column::ColumnType::Int64 => DataType::Int64,
crate::column::ColumnType::Float64 => DataType::Float64,
crate::column::ColumnType::Boolean => DataType::Boolean,
crate::column::ColumnType::String => DataType::Utf8,
};
Some(Field::new(col_name, data_type, true))
} else {
None
}
})
.collect();
let schema = Schema::new(schema_fields);
let schema_ref = Arc::new(schema);
let arrays: Vec<ArrayRef> = df
.column_names()
.iter()
.filter_map(|col_name| {
let col_view = match df.column(col_name) {
Ok(s) => s,
Err(_) => return None,
};
match col_view.column_type() {
crate::column::ColumnType::Int64 => {
if let Some(int_col) = col_view.as_int64() {
let mut values = Vec::with_capacity(df.row_count());
let mut validity = Vec::with_capacity(df.row_count());
for i in 0..df.row_count() {
match int_col.get(i) {
Ok(Some(val)) => {
values.push(val);
validity.push(true);
}
Ok(None) => {
values.push(0); validity.push(false);
}
Err(_) => {
values.push(0);
validity.push(false);
}
}
}
let array = Int64Array::new(values.into(), Some(validity.into()));
Some(Arc::new(array) as ArrayRef)
} else {
None
}
}
crate::column::ColumnType::Float64 => {
if let Some(float_col) = col_view.as_float64() {
let mut values = Vec::with_capacity(df.row_count());
let mut validity = Vec::with_capacity(df.row_count());
for i in 0..df.row_count() {
match float_col.get(i) {
Ok(Some(val)) => {
values.push(val);
validity.push(true);
}
Ok(None) => {
values.push(0.0); validity.push(false);
}
Err(_) => {
values.push(0.0);
validity.push(false);
}
}
}
let array = Float64Array::new(values.into(), Some(validity.into()));
Some(Arc::new(array) as ArrayRef)
} else {
None
}
}
crate::column::ColumnType::Boolean => {
if let Some(bool_col) = col_view.as_boolean() {
let mut values = Vec::with_capacity(df.row_count());
let mut validity = Vec::with_capacity(df.row_count());
for i in 0..df.row_count() {
match bool_col.get(i) {
Ok(Some(val)) => {
values.push(val);
validity.push(true);
}
Ok(None) => {
values.push(false); validity.push(false);
}
Err(_) => {
values.push(false);
validity.push(false);
}
}
}
let array = BooleanArray::new(values.into(), Some(validity.into()));
Some(Arc::new(array) as ArrayRef)
} else {
None
}
}
crate::column::ColumnType::String => {
if let Some(str_col) = col_view.as_string() {
let mut values = Vec::with_capacity(df.row_count());
let mut validity = Vec::with_capacity(df.row_count());
for i in 0..df.row_count() {
match str_col.get(i) {
Ok(Some(val)) => {
values.push(val.to_string());
validity.push(true);
}
Ok(None) => {
values.push(String::new()); validity.push(false);
}
Err(_) => {
values.push(String::new());
validity.push(false);
}
}
}
let string_values: Vec<Option<&str>> = values
.iter()
.zip(validity.iter())
.map(|(s, &is_valid)| if is_valid { Some(s.as_str()) } else { None })
.collect();
let array = StringArray::from(string_values);
Some(Arc::new(array) as ArrayRef)
} else {
None
}
}
}
})
.collect();
let batch = RecordBatch::try_new(schema_ref.clone(), arrays)
.map_err(|e| Error::Cast(format!("Failed to create record batch: {}", e)))?;
let compression_type = compression.unwrap_or(ParquetCompression::Snappy);
validate_compression(compression_type)?;
let props = WriterProperties::builder()
.set_compression(Compression::from(compression_type))
.build();
let file = File::create(path.as_ref())
.map_err(|e| Error::IoError(format!("Failed to create Parquet file: {}", e)))?;
let mut writer = ArrowWriter::try_new(file, schema_ref, Some(props))
.map_err(|e| Error::IoError(format!("Failed to create Parquet writer: {}", e)))?;
writer
.write(&batch)
.map_err(|e| Error::IoError(format!("Failed to write record batch: {}", e)))?;
writer
.close()
.map_err(|e| Error::IoError(format!("Failed to close Parquet file: {}", e)))?;
Ok(())
}
impl From<ParquetCompression> for Compression {
fn from(comp: ParquetCompression) -> Self {
match comp {
ParquetCompression::None => Compression::UNCOMPRESSED,
ParquetCompression::Snappy => Compression::SNAPPY,
ParquetCompression::Gzip => Compression::GZIP(Default::default()),
ParquetCompression::Lzo => Compression::LZO,
ParquetCompression::Brotli => Compression::BROTLI(Default::default()),
ParquetCompression::Lz4 => Compression::LZ4,
ParquetCompression::Zstd => Compression::ZSTD(Default::default()),
}
}
}