use crate::file_group::log_file::LogFile;
use std::str::FromStr;
#[derive(Debug, Clone)]
pub struct InputSplit {
pub base_file_path: Option<String>,
pub base_file_commit_time: Option<String>,
pub log_file_paths: Vec<String>,
pub partition_path: String,
}
const CDC_LOGFILE_SUFFIX: &str = ".cdc";
impl InputSplit {
pub fn new(
base_file_path: Option<String>,
base_file_commit_time: Option<String>,
log_file_paths: Vec<String>,
partition_path: String,
) -> Self {
let log_file_paths = Self::filter_cdc_log_files(log_file_paths);
let log_file_paths = Self::sort_log_file_paths(log_file_paths);
Self {
base_file_path,
base_file_commit_time,
log_file_paths,
partition_path,
}
}
fn filter_cdc_log_files(paths: Vec<String>) -> Vec<String> {
paths
.into_iter()
.filter(|p| {
let name = p.rsplit('/').next().unwrap_or(p);
!name.ends_with(CDC_LOGFILE_SUFFIX)
})
.collect()
}
fn sort_log_file_paths(mut paths: Vec<String>) -> Vec<String> {
if paths.len() <= 1 {
return paths;
}
paths.sort_by(|a, b| {
let name_a = a.rsplit('/').next().unwrap_or(a);
let name_b = b.rsplit('/').next().unwrap_or(b);
match (LogFile::from_str(name_a), LogFile::from_str(name_b)) {
(Ok(lf_a), Ok(lf_b)) => lf_a.cmp(&lf_b),
_ => a.cmp(b), }
});
paths
}
pub fn has_log_files(&self) -> bool {
!self.log_file_paths.is_empty()
}
pub fn is_base_only(&self) -> bool {
!self.has_log_files()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_file_paths_sorted_ascending() {
let log_files = vec![
".72d44234-0_20260408194651210.log.1_0-272-505".to_string(), ".72d44234-0_20260408194649548.log.1_0-254-473".to_string(), ];
let split = InputSplit::new(
Some("base.parquet".to_string()),
None,
log_files,
String::new(),
);
assert_eq!(split.log_file_paths.len(), 2);
assert!(
split.log_file_paths[0].contains("20260408194649548"),
"First log file should be the OLDER commit, got: {}",
split.log_file_paths[0]
);
assert!(
split.log_file_paths[1].contains("20260408194651210"),
"Second log file should be the NEWER commit, got: {}",
split.log_file_paths[1]
);
}
#[test]
fn test_log_file_paths_sorted_by_version() {
let log_files = vec![
".fileId-0_20260408194649548.log.3_0-100-200".to_string(), ".fileId-0_20260408194649548.log.1_0-100-200".to_string(), ".fileId-0_20260408194649548.log.2_0-100-200".to_string(), ];
let split = InputSplit::new(None, None, log_files, String::new());
assert!(split.log_file_paths[0].contains(".log.1_"));
assert!(split.log_file_paths[1].contains(".log.2_"));
assert!(split.log_file_paths[2].contains(".log.3_"));
}
#[test]
fn test_log_file_paths_sorted_with_partition_prefix() {
let log_files = vec![
"year=2024/month=01/.fileId-0_20260408194651210.log.1_0-272-505".to_string(),
"year=2024/month=01/.fileId-0_20260408194649548.log.1_0-254-473".to_string(),
];
let split = InputSplit::new(None, None, log_files, "year=2024/month=01".to_string());
assert!(split.log_file_paths[0].contains("20260408194649548"));
assert!(split.log_file_paths[1].contains("20260408194651210"));
}
#[test]
fn test_cdc_log_files_filtered_out() {
let log_files = vec![
".fileId-0_20260408194649548.log.1_0-100-200".to_string(), ".fileId-0_20260408194651210.log.2_0-101-201.cdc".to_string(), ".fileId-0_20260408194652000.log.3_0-102-202".to_string(), ];
let split = InputSplit::new(None, None, log_files, String::new());
assert_eq!(
split.log_file_paths.len(),
2,
"CDC log file should be excluded, got: {:?}",
split.log_file_paths
);
assert!(
split.log_file_paths.iter().all(|p| !p.ends_with(".cdc")),
"no remaining log file should be a .cdc file, got: {:?}",
split.log_file_paths
);
assert!(split.log_file_paths[0].contains("20260408194649548"));
assert!(split.log_file_paths[1].contains("20260408194652000"));
}
#[test]
fn test_cdc_log_files_filtered_with_partition_prefix() {
let log_files = vec![
"year=2024/.fileId-0_20260408194649548.log.1_0-100-200".to_string(),
"year=2024/.fileId-0_20260408194651210.log.2_0-101-201.cdc".to_string(),
];
let split = InputSplit::new(None, None, log_files, "year=2024".to_string());
assert_eq!(split.log_file_paths.len(), 1);
assert!(split.log_file_paths[0].contains("20260408194649548"));
}
#[test]
fn test_log_file_paths_empty_and_single() {
let split_empty = InputSplit::new(None, None, vec![], String::new());
assert!(split_empty.log_file_paths.is_empty());
let split_single = InputSplit::new(
None,
None,
vec![".fileId-0_20260408194649548.log.1_0-254-473".to_string()],
String::new(),
);
assert_eq!(split_single.log_file_paths.len(), 1);
}
}