|
20 | 20 | // TODO: potentially move this to arrow-rs: https://github.com/apache/arrow-rs/issues/4328 |
21 | 21 |
|
22 | 22 | use arrow::{array::ArrayRef, datatypes::DataType}; |
23 | | -use arrow_array::new_empty_array; |
24 | | -use arrow_schema::{FieldRef, Schema}; |
25 | | -use datafusion_common::{Result, ScalarValue}; |
| 23 | +use arrow_array::{new_empty_array, new_null_array, UInt64Array}; |
| 24 | +use arrow_schema::{Field, FieldRef, Schema}; |
| 25 | +use datafusion_common::{ |
| 26 | + internal_datafusion_err, internal_err, plan_err, Result, ScalarValue, |
| 27 | +}; |
| 28 | +use parquet::file::metadata::ParquetMetaData; |
26 | 29 | use parquet::file::statistics::Statistics as ParquetStatistics; |
27 | 30 | use parquet::schema::types::SchemaDescriptor; |
| 31 | +use std::sync::Arc; |
28 | 32 |
|
29 | 33 | // Convert the bytes array to i128. |
30 | 34 | // The endian of the input bytes array must be big-endian. |
@@ -210,6 +214,152 @@ fn collect_scalars<I: Iterator<Item = Option<ScalarValue>>>( |
210 | 214 | } |
211 | 215 | } |
212 | 216 |
|
| 217 | +/// What type of statistics should be extracted? |
| 218 | +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] |
| 219 | +pub enum RequestedStatistics { |
| 220 | + /// Minimum Value |
| 221 | + Min, |
| 222 | + /// Maximum Value |
| 223 | + Max, |
| 224 | + /// Null Count, returned as a [`UInt64Array`]) |
| 225 | + NullCount, |
| 226 | +} |
| 227 | + |
| 228 | +/// Extracts Parquet statistics as Arrow arrays |
| 229 | +/// |
| 230 | +/// This is used to convert Parquet statistics to Arrow arrays, with proper type |
| 231 | +/// conversions. This information can be used for pruning parquet files or row |
| 232 | +/// groups based on the statistics embedded in parquet files |
| 233 | +/// |
| 234 | +/// # Schemas |
| 235 | +/// |
| 236 | +/// The schema of the parquet file and the arrow schema are used to convert the |
| 237 | +/// underlying statistics value (stored as a parquet value) into the |
| 238 | +/// corresponding Arrow value. For example, Decimals are stored as binary in |
| 239 | +/// parquet files. |
| 240 | +/// |
| 241 | +/// The parquet_schema and arrow _schema do not have to be identical (for |
| 242 | +/// example, the columns may be in different orders and one or the other schemas |
| 243 | +/// may have additional columns). The function [`parquet_column`] is used to |
| 244 | +/// match the column in the parquet file to the column in the arrow schema. |
| 245 | +/// |
| 246 | +/// # Multiple parquet files |
| 247 | +/// |
| 248 | +/// This API is designed to support efficiently extracting statistics from |
| 249 | +/// multiple parquet files (hence why the parquet schema is passed in as an |
| 250 | +/// argument). This is useful when building an index for a directory of parquet |
| 251 | +/// files. |
| 252 | +/// |
| 253 | +#[derive(Debug)] |
| 254 | +pub struct StatisticsConverter<'a> { |
| 255 | + /// The name of the column to extract statistics for |
| 256 | + column_name: &'a str, |
| 257 | + /// The type of statistics to extract |
| 258 | + statistics_type: RequestedStatistics, |
| 259 | + /// The arrow schema of the query |
| 260 | + arrow_schema: &'a Schema, |
| 261 | + /// The field (with data type) of the column in the arrow schema |
| 262 | + arrow_field: &'a Field, |
| 263 | +} |
| 264 | + |
| 265 | +impl<'a> StatisticsConverter<'a> { |
| 266 | + /// Returns a [`UInt64Array`] with counts for each row group |
| 267 | + /// |
| 268 | + /// The returned array has no nulls, and has one value for each row group. |
| 269 | + /// Each value is the number of rows in the row group. |
| 270 | + pub fn row_counts(metadata: &ParquetMetaData) -> Result<UInt64Array> { |
| 271 | + let row_groups = metadata.row_groups(); |
| 272 | + let mut builder = UInt64Array::builder(row_groups.len()); |
| 273 | + for row_group in row_groups { |
| 274 | + let row_count = row_group.num_rows(); |
| 275 | + let row_count: u64 = row_count.try_into().map_err(|e| { |
| 276 | + internal_datafusion_err!( |
| 277 | + "Parquet row count {row_count} too large to convert to u64: {e}" |
| 278 | + ) |
| 279 | + })?; |
| 280 | + builder.append_value(row_count); |
| 281 | + } |
| 282 | + Ok(builder.finish()) |
| 283 | + } |
| 284 | + |
| 285 | + /// create an new statistics converter |
| 286 | + pub fn try_new( |
| 287 | + column_name: &'a str, |
| 288 | + statistics_type: RequestedStatistics, |
| 289 | + arrow_schema: &'a Schema, |
| 290 | + ) -> Result<Self> { |
| 291 | + // ensure the requested column is in the arrow schema |
| 292 | + let Some((_idx, arrow_field)) = arrow_schema.column_with_name(column_name) else { |
| 293 | + return plan_err!( |
| 294 | + "Column '{}' not found in schema for statistics conversion", |
| 295 | + column_name |
| 296 | + ); |
| 297 | + }; |
| 298 | + |
| 299 | + Ok(Self { |
| 300 | + column_name, |
| 301 | + statistics_type, |
| 302 | + arrow_schema, |
| 303 | + arrow_field, |
| 304 | + }) |
| 305 | + } |
| 306 | + |
| 307 | + /// extract the statistics from a parquet file, given the parquet file's metadata |
| 308 | + /// |
| 309 | + /// The returned array contains 1 value for each row group in the parquet |
| 310 | + /// file in order |
| 311 | + /// |
| 312 | + /// Each value is either |
| 313 | + /// * the requested statistics type for the column |
| 314 | + /// * a null value, if the statistics can not be extracted |
| 315 | + /// |
| 316 | + /// Note that a null value does NOT mean the min or max value was actually |
| 317 | + /// `null` it means it the requested statistic is unknown |
| 318 | + /// |
| 319 | + /// Reasons for not being able to extract the statistics include: |
| 320 | + /// * the column is not present in the parquet file |
| 321 | + /// * statistics for the column are not present in the row group |
| 322 | + /// * the stored statistic value can not be converted to the requested type |
| 323 | + pub fn extract(&self, metadata: &ParquetMetaData) -> Result<ArrayRef> { |
| 324 | + let data_type = self.arrow_field.data_type(); |
| 325 | + let num_row_groups = metadata.row_groups().len(); |
| 326 | + |
| 327 | + let parquet_schema = metadata.file_metadata().schema_descr(); |
| 328 | + let row_groups = metadata.row_groups(); |
| 329 | + |
| 330 | + // find the column in the parquet schema, if not, return a null array |
| 331 | + let Some((parquet_idx, matched_field)) = |
| 332 | + parquet_column(parquet_schema, self.arrow_schema, self.column_name) |
| 333 | + else { |
| 334 | + // column was in the arrow schema but not in the parquet schema, so return a null array |
| 335 | + return Ok(new_null_array(data_type, num_row_groups)); |
| 336 | + }; |
| 337 | + |
| 338 | + // sanity check that matching field matches the arrow field |
| 339 | + if matched_field.as_ref() != self.arrow_field { |
| 340 | + return internal_err!( |
| 341 | + "Matched column '{:?}' does not match original matched column '{:?}'", |
| 342 | + matched_field, |
| 343 | + self.arrow_field |
| 344 | + ); |
| 345 | + } |
| 346 | + |
| 347 | + // Get an iterator over the column statistics |
| 348 | + let iter = row_groups |
| 349 | + .iter() |
| 350 | + .map(|x| x.column(parquet_idx).statistics()); |
| 351 | + |
| 352 | + match self.statistics_type { |
| 353 | + RequestedStatistics::Min => min_statistics(data_type, iter), |
| 354 | + RequestedStatistics::Max => max_statistics(data_type, iter), |
| 355 | + RequestedStatistics::NullCount => { |
| 356 | + let null_counts = iter.map(|stats| stats.map(|s| s.null_count())); |
| 357 | + Ok(Arc::new(UInt64Array::from_iter(null_counts))) |
| 358 | + } |
| 359 | + } |
| 360 | + } |
| 361 | +} |
| 362 | + |
213 | 363 | #[cfg(test)] |
214 | 364 | mod test { |
215 | 365 | use super::*; |
|
0 commit comments