use crate::Result;
use crate::config::HudiConfigs;
use crate::config::read::HudiReadConfig;
use crate::error::CoreError;
use crate::file_group::reader_v2::MAX_INSTANT_TIME;
use crate::file_group::reader_v2::engine::HoodieFileGroupReader;
use crate::file_group::reader_v2::input_split::InputSplit;
use crate::file_group::reader_v2::reader_context::CompletionGateInputs;
use crate::file_group::reader_v2::reader_parameters::ReaderParameters;
use crate::file_group::reader_v2::resolver::resolve_reader_context;
use crate::storage::Storage;
use arrow_array::RecordBatch;
use arrow_schema::SchemaRef;
use std::sync::Arc;
pub(crate) async fn read_file_slice(
hudi_configs: Arc<HudiConfigs>,
storage: Arc<Storage>,
base_file_path: &str,
log_file_paths: Vec<String>,
partition_path: String,
data_schema: Option<SchemaRef>,
completion_gate_inputs: Option<Arc<CompletionGateInputs>>,
) -> Result<RecordBatch> {
let mut reader = build_reader(
hudi_configs,
storage,
base_file_path,
log_file_paths,
partition_path,
data_schema,
completion_gate_inputs,
)?;
reader.read().await
}
pub(crate) async fn read_file_slice_stream(
hudi_configs: Arc<HudiConfigs>,
storage: Arc<Storage>,
base_file_path: &str,
log_file_paths: Vec<String>,
partition_path: String,
data_schema: Option<SchemaRef>,
completion_gate_inputs: Option<Arc<CompletionGateInputs>>,
) -> Result<futures::stream::BoxStream<'static, Result<RecordBatch>>> {
let mut reader = build_reader(
hudi_configs,
storage,
base_file_path,
log_file_paths,
partition_path,
data_schema,
completion_gate_inputs,
)?;
reader.open_stream().await
}
fn build_reader(
hudi_configs: Arc<HudiConfigs>,
storage: Arc<Storage>,
base_file_path: &str,
log_file_paths: Vec<String>,
partition_path: String,
data_schema: Option<SchemaRef>,
completion_gate_inputs: Option<Arc<CompletionGateInputs>>,
) -> Result<HoodieFileGroupReader> {
let has_log_files = !log_file_paths.is_empty();
let hudi_configs = with_unbounded_end_timestamp(hudi_configs);
let base_file_path = (!base_file_path.is_empty()).then_some(base_file_path);
let mut context = resolve_reader_context(&hudi_configs, has_log_files, base_file_path)?;
context.completion_gate_inputs = completion_gate_inputs;
context.rebuild_record_context(partition_path.clone());
let base_file_commit_time = base_file_path.and_then(base_file_commit_time);
let base_file_path = base_file_path.map(str::to_string);
let input_split = InputSplit::new(
base_file_path,
base_file_commit_time,
log_file_paths,
partition_path,
);
let reader_parameters = ReaderParameters {
use_record_position: context.should_merge_use_record_position,
..ReaderParameters::default()
};
HoodieFileGroupReader::new(
Arc::new(context),
storage,
input_split,
reader_parameters,
data_schema,
None,
)
}
fn base_file_commit_time(base_file_path: &str) -> Option<String> {
let file_name = base_file_path.rsplit('/').next().unwrap_or(base_file_path);
file_name
.parse::<crate::file_group::base_file::BaseFile>()
.ok()
.map(|base_file| base_file.commit_timestamp)
}
fn with_unbounded_end_timestamp(hudi_configs: Arc<HudiConfigs>) -> Arc<HudiConfigs> {
let key = HudiReadConfig::EndTimestamp.as_ref();
let mut options = hudi_configs.as_options();
if options.contains_key(key) {
return hudi_configs;
}
options.insert(key.to_string(), MAX_INSTANT_TIME.to_string());
Arc::new(HudiConfigs::new(options))
}
pub(crate) fn refuse_reason(
is_metadata_table: bool,
data_schema: Option<&SchemaRef>,
) -> Option<CoreError> {
if is_metadata_table {
return Some(CoreError::Unsupported(
"A metadata table is read through its own reader, not this one. Not for want of \
HFile support -- the engine reads HFile base files and log blocks on an ordinary \
table -- but because the metadata table's record key, merge rule and partition \
come from its own configuration rather than the table's."
.to_string(),
));
}
if data_schema.is_none() {
return Some(CoreError::Unsupported(
"The merge-on-read reader needs the table schema up front, and none was resolved \
for this read."
.to_string(),
));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::table::HudiTableConfig;
fn schema() -> SchemaRef {
Arc::new(arrow_schema::Schema::new(vec![arrow_schema::Field::new(
"id",
arrow_schema::DataType::Int32,
true,
)]))
}
#[test]
fn refuses_a_metadata_table() {
let reason = refuse_reason(true, Some(&schema())).expect("should refuse");
assert!(
reason.to_string().contains("metadata table"),
"reason should name the metadata table, got: {reason}"
);
}
#[test]
fn refuses_when_no_schema_was_resolved() {
let reason = refuse_reason(false, None).expect("should refuse");
assert!(
reason.to_string().contains("schema"),
"reason should name the missing schema, got: {reason}"
);
}
#[test]
fn accepts_a_regular_table_with_a_schema() {
assert!(refuse_reason(false, Some(&schema())).is_none());
}
#[test]
fn reads_the_base_file_instant_out_of_its_name() {
assert_eq!(
base_file_commit_time(
"city=sf/fee86b18-67b1-4479-b517-075683aeb2d1-0_0-13-33_20260408053032350.parquet"
)
.as_deref(),
Some("20260408053032350")
);
}
#[test]
fn declines_an_unparseable_base_file_name() {
assert_eq!(
base_file_commit_time("city=sf/not-a-hudi-name.parquet"),
None
);
}
#[test]
fn resolves_a_context_for_a_base_only_slice() {
let configs = HudiConfigs::new([
(HudiTableConfig::BasePath.as_ref(), "file:///tmp/t"),
("hoodie.read.end.timestamp", "20240101000000000"),
(HudiTableConfig::OrderingFields.as_ref(), "ts"),
]);
let context = resolve_reader_context(&configs, false, None).unwrap();
assert!(!context.has_log_files);
}
}