use std::sync::Arc;
use object_store::path::Path;
use parquet::{
arrow::{
ParquetRecordBatchStreamBuilder, arrow_reader::RowSelection, async_reader::AsyncFileReader,
},
file::metadata::{PageIndexPolicy, ParquetMetaData},
};
use crate::{
data_file::usize_as_u64,
error::{Error, Result},
};
#[derive(Debug, Clone, Copy)]
pub(crate) enum ScopedRows<'a> {
All,
At(&'a RowPositions),
RowGroup {
group: usize,
from: u64,
},
}
impl ScopedRows<'_> {
pub(super) fn is_empty(self) -> bool {
matches!(self, Self::At(wanted) if wanted.is_empty())
}
pub(super) fn page_index_policy(self) -> PageIndexPolicy {
match self {
Self::All => PageIndexPolicy::Skip,
Self::At(_) | Self::RowGroup { .. } => PageIndexPolicy::Optional,
}
}
}
pub(super) struct RowScope {
groups: Option<Vec<usize>>,
rows: Option<RowSelection>,
}
impl RowScope {
pub(super) fn narrow<T: AsyncFileReader>(
self,
mut builder: ParquetRecordBatchStreamBuilder<T>,
) -> ParquetRecordBatchStreamBuilder<T> {
if let Some(groups) = self.groups {
builder = builder.with_row_groups(groups);
}
if let Some(rows) = self.rows {
builder = builder.with_row_selection(rows);
}
builder
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct RowPositions(Arc<[u64]>);
impl RowPositions {
pub(crate) fn from_unsorted(mut positions: Vec<u64>) -> Self {
positions.sort_unstable();
positions.dedup();
Self(positions.into())
}
pub(crate) fn as_slice(&self) -> &[u64] {
&self.0
}
pub(crate) fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub(crate) fn contains(&self, position: u64) -> bool {
self.0.binary_search(&position).is_ok()
}
}
impl FromIterator<u64> for RowPositions {
fn from_iter<T: IntoIterator<Item = u64>>(iter: T) -> Self {
Self::from_unsorted(iter.into_iter().collect())
}
}
#[derive(Debug, Clone, Copy)]
pub(super) enum Ordinals<'a> {
Dense,
Offset(u64),
Selected(&'a [u64]),
}
impl Ordinals<'_> {
pub(super) fn at(self, nth: usize) -> Result<u64> {
match self {
Self::Dense => Ok(usize_as_u64(nth)),
Self::Offset(start) => Ok(start.saturating_add(usize_as_u64(nth))),
Self::Selected(positions) => positions.get(nth).copied().ok_or_else(|| {
Error::Corruption(
"scoped read: the reader emitted more rows than the selection named".to_owned(),
)
}),
}
}
}
pub(super) enum OwnedOrdinals {
Dense,
Offset(u64),
Selected(RowPositions),
}
impl OwnedOrdinals {
pub(super) fn borrowed(&self) -> Ordinals<'_> {
match self {
Self::Dense => Ordinals::Dense,
Self::Offset(start) => Ordinals::Offset(*start),
Self::Selected(positions) => Ordinals::Selected(positions.as_slice()),
}
}
}
pub(super) fn scoped_selection(
rows: ScopedRows<'_>,
metadata: &ParquetMetaData,
path: &Path,
) -> Result<(RowScope, OwnedOrdinals)> {
let total = total_rows(metadata, path)?;
let whole_file = |rows| RowScope { groups: None, rows };
match rows {
ScopedRows::All => Ok((whole_file(None), OwnedOrdinals::Dense)),
ScopedRows::At(positions) => Ok((
whole_file(Some(row_selection(positions.as_slice(), total)?)),
OwnedOrdinals::Selected(positions.clone()),
)),
ScopedRows::RowGroup { group, from } => {
let counts = row_group_row_counts(metadata, path)?;
let group_rows = counts.get(group).copied().ok_or_else(|| {
Error::Corruption(format!("scoped read: {path} has no row group {group}"))
})?;
let group_start: u64 = counts[..group].iter().sum();
let offset = from
.checked_sub(group_start)
.and_then(|offset| usize::try_from(offset).ok())
.filter(|offset| usize_as_u64(*offset) <= group_rows)
.ok_or_else(|| {
Error::Corruption(format!(
"scoped read: resume row {from} is outside row group {group} of {path}"
))
})?;
let group_rows = usize::try_from(group_rows).map_err(|_| {
Error::Corruption(format!(
"scoped read: row group {group} of {path} is larger than memory"
))
})?;
Ok((
RowScope {
groups: Some(vec![group]),
rows: Some(RowSelection::from_consecutive_ranges(
std::iter::once(offset..group_rows),
group_rows,
)),
},
OwnedOrdinals::Offset(from),
))
}
}
}
pub(super) fn row_group_row_counts(metadata: &ParquetMetaData, path: &Path) -> Result<Vec<u64>> {
metadata
.row_groups()
.iter()
.map(|group| {
u64::try_from(group.num_rows()).map_err(|_| {
Error::Corruption(format!("scoped read: {path} reports a negative row count"))
})
})
.collect()
}
fn row_selection(positions: &[u64], total_rows: usize) -> Result<RowSelection> {
let ranges = positions
.iter()
.map(|&position| {
usize::try_from(position)
.ok()
.filter(|start| *start < total_rows)
.map(|start| start..start.saturating_add(1))
.ok_or_else(|| {
Error::Corruption(format!(
"scoped read: selected row {position} is past the file's {total_rows} rows"
))
})
})
.collect::<Result<Vec<_>>>()?;
Ok(RowSelection::from_consecutive_ranges(
ranges.into_iter(),
total_rows,
))
}
pub(super) fn total_rows(metadata: &ParquetMetaData, path: &Path) -> Result<usize> {
usize::try_from(metadata.file_metadata().num_rows())
.map_err(|_| Error::Corruption(format!("scoped read: {path} reports a negative row count")))
}