use std::cmp::Ordering;
use std::path::Path;
use arrow::array::{Array, ArrayRef, Float32Array, Float64Array};
use arrow::datatypes::DataType;
use arrow::record_batch::RecordBatch;
use datafusion::common::ScalarValue;
use parquet::arrow::arrow_reader::ParquetRecordBatchReaderBuilder;
use parquet::arrow::arrow_reader::statistics::StatisticsConverter;
use crate::metadata_writer::ColumnStat;
use crate::stats_encode;
pub fn array_contains_nan(array: &dyn Array) -> Option<bool> {
match array.data_type() {
DataType::Float32 => {
let a = array.as_any().downcast_ref::<Float32Array>()?;
Some((0..a.len()).any(|i| a.is_valid(i) && a.value(i).is_nan()))
},
DataType::Float64 => {
let a = array.as_any().downcast_ref::<Float64Array>()?;
Some((0..a.len()).any(|i| a.is_valid(i) && a.value(i).is_nan()))
},
_ => None,
}
}
pub fn accumulate_nan_flags(acc: &mut Vec<Option<bool>>, batch: &RecordBatch, n: usize) {
if acc.len() < n {
acc.resize(n, None);
}
for (slot, col) in acc.iter_mut().zip(batch.columns()).take(n) {
if let Some(has) = array_contains_nan(col.as_ref()) {
*slot = Some(slot.unwrap_or(false) || has);
}
}
}
pub fn collect_column_stats(
path: &Path,
column_ids: &[i64],
row_count: i64,
contains_nan_flags: &[Option<bool>],
) -> Vec<ColumnStat> {
match try_collect(path, column_ids, row_count, contains_nan_flags) {
Ok(stats) => stats,
Err(e) => {
tracing::warn!(
error = %e,
path = %path.display(),
"failed to harvest parquet statistics; writing file without column stats"
);
Vec::new()
},
}
}
fn try_collect(
path: &Path,
column_ids: &[i64],
row_count: i64,
contains_nan_flags: &[Option<bool>],
) -> crate::Result<Vec<ColumnStat>> {
let file = std::fs::File::open(path)?;
let builder = ParquetRecordBatchReaderBuilder::try_new(file)
.map_err(|e| crate::error::DuckLakeError::Internal(format!("parquet metadata: {e}")))?;
let arrow_schema = builder.schema().clone();
let metadata = builder.metadata().clone();
let parquet_schema = metadata.file_metadata().schema_descr();
let row_groups = metadata.row_groups();
let fields = arrow_schema.fields();
let n = fields.len().min(column_ids.len());
let mut out = Vec::with_capacity(n);
for (idx, column_id) in column_ids.iter().copied().enumerate().take(n) {
let field = &fields[idx];
let name = field.name();
let converter = match StatisticsConverter::try_new(name, &arrow_schema, parquet_schema) {
Ok(c) => c,
Err(e) => {
tracing::debug!(column = name, error = %e, "no statistics converter for column");
continue;
},
};
let (null_count, value_count) = match converter.row_group_null_counts(row_groups.iter()) {
Ok(counts) => {
let total: u64 = (0..counts.len())
.filter(|i| !counts.is_null(*i))
.map(|i| counts.value(i))
.sum();
let null_count = i64::try_from(total).ok();
let value_count = null_count.map(|nc| (row_count - nc).max(0));
(null_count, value_count)
},
Err(_) => (None, None),
};
let contains_nan = contains_nan_flags.get(idx).copied().flatten();
let (min_scalar, max_scalar) = if contains_nan == Some(true) {
(None, None)
} else {
let min_scalar = converter
.row_group_mins(row_groups.iter())
.ok()
.and_then(|arr| reduce(&arr, Ordering::Less));
let max_scalar = converter
.row_group_maxes(row_groups.iter())
.ok()
.and_then(|arr| reduce(&arr, Ordering::Greater));
(min_scalar, max_scalar)
};
out.push(ColumnStat {
column_id,
min_value: min_scalar.as_ref().and_then(stats_encode::encode_scalar),
max_value: max_scalar.as_ref().and_then(stats_encode::encode_scalar),
null_count,
value_count,
contains_nan,
column_size_bytes: None,
});
}
Ok(out)
}
fn reduce(array: &ArrayRef, keep: Ordering) -> Option<ScalarValue> {
let mut acc: Option<ScalarValue> = None;
for i in 0..array.len() {
if array.is_null(i) {
continue;
}
let Ok(value) = ScalarValue::try_from_array(array.as_ref(), i) else {
continue;
};
acc = Some(match acc {
None => value,
Some(current) => {
if value.partial_cmp(¤t) == Some(keep) {
value
} else {
current
}
},
});
}
acc
}