use arrow::array::{Array, StringArray};
use arrow::datatypes::{DataType, Field, Schema};
use arrow::record_batch::RecordBatch;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use parquet::arrow::ArrowWriter;
use parquet::file::properties::WriterProperties;
use std::collections::HashMap;
use std::fs::File;
use std::sync::Arc;
pub fn read_string_column(path: &str, column_name: &str) -> Result<Vec<String>, String> {
let file =
File::open(path).map_err(|e| format!("Failed to open Parquet file '{}': {}", path, e))?;
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
.map_err(|e| format!("Failed to read Parquet file '{}': {}", path, e))?;
let schema = builder.schema();
let (column_index, field) = schema.column_with_name(column_name).ok_or_else(|| {
let available: Vec<_> = schema.fields().iter().map(|f| f.name().as_str()).collect();
format!(
"Column '{}' not found in Parquet file. Available columns: {}",
column_name,
available.join(", ")
)
})?;
let data_type = field.data_type().clone();
match &data_type {
DataType::Utf8 | DataType::LargeUtf8 => {}
other => {
return Err(format!(
"Column '{}' has type {:?}, but must be Utf8 or LargeUtf8 (string)",
column_name, other
));
}
}
let reader = builder
.build()
.map_err(|e| format!("Failed to create Parquet reader: {}", e))?;
let mut strings = Vec::new();
for batch_result in reader {
let batch = batch_result.map_err(|e| format!("Failed to read record batch: {}", e))?;
let column = batch.column(column_index);
let strings_in_batch: Vec<String> = match &data_type {
DataType::Utf8 => {
let string_array = column
.as_any()
.downcast_ref::<StringArray>()
.ok_or_else(|| "Failed to downcast column to StringArray".to_string())?;
(0..string_array.len())
.filter_map(|i| {
if !string_array.is_null(i) {
Some(string_array.value(i).to_string())
} else {
None
}
})
.collect()
}
DataType::LargeUtf8 => {
use arrow::array::LargeStringArray;
let string_array = column
.as_any()
.downcast_ref::<LargeStringArray>()
.ok_or_else(|| "Failed to downcast column to LargeStringArray".to_string())?;
(0..string_array.len())
.filter_map(|i| {
if !string_array.is_null(i) {
Some(string_array.value(i).to_string())
} else {
None
}
})
.collect()
}
_ => unreachable!("Type already validated"),
};
strings.extend(strings_in_batch);
}
Ok(strings)
}
pub fn write_string_column(
path: &str,
column_name: &str,
strings: Vec<String>,
metadata: Option<HashMap<String, String>>,
) -> Result<(), String> {
let total_bytes: usize = strings.iter().map(|s| s.len()).sum();
let use_large = total_bytes > i32::MAX as usize || strings.len() > i32::MAX as usize;
let data_type = if use_large {
DataType::LargeUtf8
} else {
DataType::Utf8
};
let field = Field::new(column_name, data_type.clone(), true);
let schema = if let Some(meta) = metadata {
Schema::new_with_metadata(vec![field], meta)
} else {
Schema::new(vec![field])
};
let schema_ref = Arc::new(schema);
let array: Arc<dyn Array> = if use_large {
use arrow::array::LargeStringArray;
Arc::new(LargeStringArray::from(strings))
} else {
Arc::new(StringArray::from(strings))
};
let batch = RecordBatch::try_new(schema_ref.clone(), vec![array])
.map_err(|e| format!("Failed to create RecordBatch: {}", e))?;
let file = File::create(path)
.map_err(|e| format!("Failed to create output file '{}': {}", path, e))?;
let props = WriterProperties::builder().build();
let mut writer = ArrowWriter::try_new(file, schema_ref, Some(props))
.map_err(|e| format!("Failed to create Parquet writer: {}", e))?;
writer
.write(&batch)
.map_err(|e| format!("Failed to write RecordBatch: {}", e))?;
writer
.close()
.map_err(|e| format!("Failed to close Parquet writer: {}", e))?;
Ok(())
}
pub fn read_parquet_metadata(path: &str) -> Result<HashMap<String, String>, String> {
let file =
File::open(path).map_err(|e| format!("Failed to open Parquet file '{}': {}", path, e))?;
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
.map_err(|e| format!("Failed to read Parquet file '{}': {}", path, e))?;
let metadata = builder.schema().metadata().clone();
Ok(metadata)
}
#[cfg(test)]
mod tests {
include!("tests/parquet.rs");
}