use std::sync::Arc;
use arrow::compute::concat_batches;
use arrow::record_batch::RecordBatch;
use arrow_schema::SchemaRef;
use futures::StreamExt;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use crate::config::table::BaseFileFormatValue;
use crate::statistics::StatisticsContainer;
use crate::storage::error::Result;
use crate::storage::file_metadata::FileMetadata;
use crate::storage::{RowFilterBuilder, RowGroupSelector, Storage};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum KeyPredicate {
Keys(Vec<String>),
Prefixes(Vec<String>),
}
impl KeyPredicate {
pub fn matcher(&self) -> KeyMatcher<'_> {
match self {
Self::Keys(keys) => KeyMatcher::Keys(keys.iter().map(String::as_str).collect()),
Self::Prefixes(prefixes) => KeyMatcher::Prefixes(prefixes),
}
}
pub fn admits(&self, key: &str) -> bool {
match self {
Self::Keys(keys) => keys.iter().any(|k| k == key),
Self::Prefixes(prefixes) => prefixes.iter().any(|p| key.starts_with(p.as_str())),
}
}
}
pub enum KeyMatcher<'a> {
Keys(std::collections::HashSet<&'a str>),
Prefixes(&'a [String]),
}
impl KeyMatcher<'_> {
pub fn admits(&self, key: &str) -> bool {
match self {
Self::Keys(keys) => keys.contains(key),
Self::Prefixes(prefixes) => prefixes.iter().any(|p| key.starts_with(p.as_str())),
}
}
}
#[derive(Clone, Default)]
pub struct BaseFileReadOptions {
pub batch_size: Option<usize>,
pub projection: Option<Vec<String>>,
pub known_file_size: Option<u64>,
pub row_filter: Option<RowFilterBuilder>,
pub key_predicate: Option<KeyPredicate>,
pub row_index_column: Option<String>,
pub row_group_selector: Option<RowGroupSelector>,
}
impl std::fmt::Debug for BaseFileReadOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BaseFileReadOptions")
.field("batch_size", &self.batch_size)
.field("projection", &self.projection)
.field("known_file_size", &self.known_file_size)
.field("key_predicate", &self.key_predicate)
.field("row_filter", &self.row_filter.is_some())
.field("row_index_column", &self.row_index_column)
.field("row_group_selector", &self.row_group_selector.is_some())
.finish()
}
}
impl BaseFileReadOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_row_filter(mut self, row_filter: RowFilterBuilder) -> Self {
self.row_filter = Some(row_filter);
self
}
pub fn with_row_group_selector(mut self, selector: RowGroupSelector) -> Self {
self.row_group_selector = Some(selector);
self
}
pub fn with_key_predicate(mut self, predicate: KeyPredicate) -> Self {
self.key_predicate = Some(predicate);
self
}
pub fn with_row_index_column(mut self, name: impl Into<String>) -> Self {
self.row_index_column = Some(name.into());
self
}
pub fn with_batch_size(mut self, batch_size: usize) -> Self {
self.batch_size = Some(batch_size);
self
}
pub fn with_known_file_size(mut self, size: u64) -> Self {
self.known_file_size = Some(size);
self
}
pub fn with_projection<I, S>(mut self, columns: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.projection = Some(columns.into_iter().map(|s| s.into()).collect());
self
}
}
pub struct BaseFileStream {
schema: SchemaRef,
stream: BoxStream<'static, Result<RecordBatch>>,
}
impl BaseFileStream {
pub fn new(schema: SchemaRef, stream: BoxStream<'static, Result<RecordBatch>>) -> Self {
Self { schema, stream }
}
pub fn schema(&self) -> &SchemaRef {
&self.schema
}
pub fn into_stream(self) -> BoxStream<'static, Result<RecordBatch>> {
self.stream
}
}
impl futures::Stream for BaseFileStream {
type Item = Result<RecordBatch>;
fn poll_next(
mut self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
self.stream.as_mut().poll_next(cx)
}
}
pub trait BaseFileReader: Send + Sync {
fn read_data<'a>(
&'a self,
relative_path: &'a str,
options: BaseFileReadOptions,
) -> BoxFuture<'a, Result<RecordBatch>> {
Box::pin(async move {
let base_stream = self.read_stream(relative_path, options).await?;
let schema = base_stream.schema().clone();
let mut stream = base_stream.into_stream();
let mut batches = Vec::new();
while let Some(batch) = stream.next().await {
batches.push(batch?);
}
if batches.is_empty() {
return Ok(RecordBatch::new_empty(schema));
}
Ok(concat_batches(&schema, &batches)?)
})
}
fn read_schema<'a>(
&'a self,
relative_path: &'a str,
) -> BoxFuture<'a, Result<arrow_schema::SchemaRef>> {
Box::pin(async move {
let stream = self
.read_stream(relative_path, BaseFileReadOptions::new())
.await?;
Ok(stream.schema().clone())
})
}
fn read_stream<'a>(
&'a self,
relative_path: &'a str,
options: BaseFileReadOptions,
) -> BoxFuture<'a, Result<BaseFileStream>>;
fn get_metadata_and_stats<'a>(
&'a self,
relative_path: &'a str,
table_schema: &'a arrow_schema::Schema,
) -> BoxFuture<'a, Result<(FileMetadata, StatisticsContainer)>>;
}
pub fn create_base_file_reader(
storage: &Arc<Storage>,
format: &BaseFileFormatValue,
) -> Result<Arc<dyn BaseFileReader>> {
match format {
BaseFileFormatValue::Parquet => Ok(Arc::new(super::parquet::ParquetBaseFileReader::new(
storage.clone(),
))),
BaseFileFormatValue::HFile => Ok(Arc::new(super::hfile::HFileBaseFileReader::new(
storage.clone(),
))),
BaseFileFormatValue::Lance => Ok(Arc::new(super::lance::LanceBaseFileReader::new(
storage.clone(),
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::canonicalize;
use std::path::Path;
use url::Url;
fn test_storage() -> Arc<Storage> {
let base_url =
Url::from_directory_path(canonicalize(Path::new("tests/data")).unwrap()).unwrap();
Storage::new_with_base_url(base_url).unwrap()
}
#[test]
fn test_create_base_file_reader_parquet() {
let storage = test_storage();
let reader = create_base_file_reader(&storage, &BaseFileFormatValue::Parquet);
assert!(reader.is_ok());
}
#[test]
fn create_base_file_reader_builds_an_hfile_reader() {
let storage = test_storage();
create_base_file_reader(&storage, &BaseFileFormatValue::HFile)
.expect("the factory serves HFile like any other format");
}
#[test]
fn test_create_base_file_reader_lance() {
let storage = test_storage();
let result = create_base_file_reader(&storage, &BaseFileFormatValue::Lance);
assert!(result.is_ok());
}
}