mod auxiliary_cache;
mod columns;
mod data_store;
mod delete_file;
mod entries;
mod inline_batch;
mod metrics;
mod reader;
mod row_location;
mod row_set;
mod selection;
mod values;
#[cfg(test)]
mod auxiliary_cache_tests;
#[cfg(test)]
mod row_set_tests;
#[cfg(test)]
mod tests;
use std::{sync::Arc, time::Instant};
use bytes::Bytes;
use futures::{
StreamExt, TryStreamExt,
stream::{self, BoxStream},
};
use object_store::path::Path;
use parquet::{
arrow::{arrow_reader::ArrowReaderOptions, async_reader::ParquetRecordBatchStreamBuilder},
file::metadata::PageIndexPolicy,
};
pub use crate::data_file::data_store::DataStore;
#[cfg(test)]
pub(crate) use crate::data_file::inline_batch::{
inline_batch_decode_count, inline_schema_decode_count,
};
pub(crate) use crate::data_file::{
auxiliary_cache::{
install as install_auxiliary, occupancy as auxiliary_occupancy, row_summary_occupancy,
},
delete_file::delete_file_positions,
inline_batch::{decode_inline_schema, inline_batch_entries, inline_batch_index_entries},
metrics::{DataStoreCounters, ScopedReadMetrics, ScopedReadTally, run_bounded_index_encoding},
row_location::{FileSummary, file_summary},
selection::{RowPositions, ScopedRows},
};
use crate::{
data_file::{
columns::{
embedded_row_id_position, index_positions, projection, remap_index_projections,
resolve_row_id_source,
},
entries::{record_batch_entries, record_batch_index_entries},
metrics::INDEX_ENCODING_CONCURRENCY,
reader::ObjectStoreReader,
selection::{scoped_selection, total_rows},
},
error::{Error, Result},
store::index_encoding::{Direction, IndexKeyValue, NullOrder},
};
pub(crate) const ROW_ID_FIELD_ID: u64 = 2_147_483_540;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct ScopedReadEntry {
pub(crate) ordinal: u64,
pub(crate) row_id: u64,
pub(crate) values: Vec<Option<IndexKeyValue>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct IndexProjection {
pub(crate) index_id: u64,
pub(crate) unique: bool,
pub(crate) positions: Vec<usize>,
pub(crate) directions: Vec<Direction>,
pub(crate) nulls: Vec<NullOrder>,
}
pub(crate) struct ScopedIndexEntry {
pub(crate) index: usize,
pub(crate) row_id: u64,
pub(crate) key: Bytes,
pub(crate) unique: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RowIdSource {
Resolve {
row_id_start: Option<u64>,
},
Ordinal,
}
fn corrupt<E: std::fmt::Display>(what: &'static str) -> impl Fn(E) -> Error {
move |err| Error::Corruption(format!("{what}: {err}"))
}
fn usize_as_u64(value: usize) -> u64 {
#[allow(clippy::expect_used)]
u64::try_from(value).expect("usize fits in u64 on supported targets")
}
pub(crate) async fn scoped_read_recorded_entries(
file: ParquetFile,
indexed_positions: &[usize],
rows: ScopedRows<'_>,
row_id_source: RowIdSource,
) -> Result<Vec<ScopedReadEntry>> {
scoped_read_entry_batches(file, indexed_positions, rows, row_id_source)
.await?
.try_fold(Vec::new(), |mut entries, batch| async move {
entries.extend(batch);
Ok(entries)
})
.await
}
#[derive(Clone)]
pub(crate) struct ParquetFile {
store: DataStore,
path: Path,
file_size: u64,
footer_size: u64,
metrics: Arc<ScopedReadMetrics>,
}
impl ParquetFile {
pub(crate) fn new(store: DataStore, path: Path, file_size: u64, footer_size: u64) -> Self {
Self {
store,
path,
file_size,
footer_size,
metrics: Arc::new(ScopedReadMetrics::default()),
}
}
pub(crate) fn with_metrics(mut self, metrics: Arc<ScopedReadMetrics>) -> Self {
self.metrics = metrics;
self
}
}
pub(crate) async fn carries_embedded_row_ids(file: ParquetFile) -> Result<bool> {
let reader = ObjectStoreReader::new(&file, PageIndexPolicy::Skip);
let options = ArrowReaderOptions::new().with_page_index_policy(PageIndexPolicy::Skip);
let builder = ParquetRecordBatchStreamBuilder::new_with_options(reader, options)
.await
.map_err(corrupt("row-id probe"))?;
Ok(embedded_row_id_position(builder.parquet_schema()).is_some())
}
const BUILD_READ_BATCH_ROWS: usize = 8_192;
pub(crate) async fn scoped_read_index_entry_batches(
file: ParquetFile,
projections: Vec<IndexProjection>,
rows: ScopedRows<'_>,
row_id_source: RowIdSource,
excluded_ordinals: Option<&RowPositions>,
) -> Result<BoxStream<'static, Result<Vec<ScopedIndexEntry>>>> {
if rows.is_empty() || projections.is_empty() {
return Ok(stream::empty().boxed());
}
file.metrics.parquet_file();
let source_positions = index_positions(&projections);
let excluded_ordinals = excluded_ordinals.cloned();
let reader = ObjectStoreReader::new(&file, rows.page_index_policy());
let options = ArrowReaderOptions::new().with_page_index_policy(rows.page_index_policy());
let builder = ParquetRecordBatchStreamBuilder::new_with_options(reader, options)
.await
.map_err(corrupt("scoped read"))?;
let (row_id_position, row_id_start) =
resolve_row_id_source(builder.parquet_schema(), row_id_source, &file.path)?;
let (mask, indexed_output, row_id_output) =
projection(builder.parquet_schema(), &source_positions, row_id_position)?;
let projections = Arc::new(remap_index_projections(
projections,
&source_positions,
&indexed_output,
)?);
let total = total_rows(builder.metadata(), &file.path)?;
let (selection, ordinals) = scoped_selection(rows, total)?;
let ordinals = Arc::new(ordinals);
let mut builder = builder
.with_projection(mask)
.with_batch_size(BUILD_READ_BATCH_ROWS);
if let Some(selection) = selection {
builder = builder.with_row_selection(selection);
}
let arrow_reader = builder.build().map_err(corrupt("scoped read"))?;
let mut emitted = 0usize;
let metrics = Arc::clone(&file.metrics);
Ok(arrow_reader
.map(move |batch| {
let batch = batch.map_err(corrupt("scoped read"))?;
file.metrics.arrow_batch();
let batch_start = emitted;
emitted = emitted.saturating_add(batch.num_rows());
Result::Ok((batch, batch_start))
})
.map(move |prepared| {
let projections = Arc::clone(&projections);
let ordinals = Arc::clone(&ordinals);
let excluded_ordinals = excluded_ordinals.clone();
let metrics = Arc::clone(&metrics);
async move {
let (batch, batch_start) = prepared?;
run_bounded_index_encoding(move || {
let started = Instant::now();
let result = record_batch_index_entries(
&batch,
&projections,
row_id_output,
row_id_start,
ordinals.borrowed(),
batch_start,
excluded_ordinals.as_ref(),
None,
);
metrics.encoded(started.elapsed());
result
})
.await
}
})
.buffered(INDEX_ENCODING_CONCURRENCY)
.boxed())
}
pub(crate) async fn scoped_read_entry_batches(
file: ParquetFile,
indexed_positions: &[usize],
rows: ScopedRows<'_>,
row_id_source: RowIdSource,
) -> Result<BoxStream<'static, Result<Vec<ScopedReadEntry>>>> {
if rows.is_empty() {
return Ok(stream::empty().boxed());
}
let reader = ObjectStoreReader::new(&file, rows.page_index_policy());
let options = ArrowReaderOptions::new().with_page_index_policy(rows.page_index_policy());
let builder = ParquetRecordBatchStreamBuilder::new_with_options(reader, options)
.await
.map_err(corrupt("scoped read"))?;
let total = total_rows(builder.metadata(), &file.path)?;
let (selection, ordinals) = scoped_selection(rows, total)?;
let (row_id_position, row_id_start) =
resolve_row_id_source(builder.parquet_schema(), row_id_source, &file.path)?;
let (mask, indexed_output, row_id_output) =
projection(builder.parquet_schema(), indexed_positions, row_id_position)?;
let mut builder = builder
.with_projection(mask)
.with_batch_size(BUILD_READ_BATCH_ROWS);
if let Some(selection) = selection {
builder = builder.with_row_selection(selection);
}
let arrow_reader = builder.build().map_err(corrupt("scoped read"))?;
let mut emitted = 0usize;
let selection_stream = arrow_reader.map(move |batch| {
let batch = batch.map_err(corrupt("scoped read"))?;
let batch_start = emitted;
emitted = emitted.saturating_add(batch.num_rows());
record_batch_entries(
&batch,
&indexed_output,
row_id_output,
row_id_start,
ordinals.borrowed(),
batch_start,
)
});
Ok(selection_stream.boxed())
}