use crate::Result;
use crate::error::CoreError;
use crate::storage::file_metadata::FileMetadata;
use crate::timeline::completion_time::CompletionTimeView;
use std::cmp::Ordering;
use std::fmt::Display;
use std::str::FromStr;
pub(crate) mod avro;
pub(crate) mod content;
pub mod log_block;
pub mod log_format;
pub mod reader;
pub mod scanner;
#[derive(Clone, Debug)]
pub struct LogFile {
pub file_id: String,
pub timestamp: String,
pub completion_timestamp: Option<String>,
pub extension: String,
pub version: u32,
pub write_token: String,
pub file_metadata: Option<FileMetadata>,
}
const LOG_FILE_PREFIX: char = '.';
impl LogFile {
pub fn is_log_file_name(file_name: &str) -> bool {
file_name.starts_with(LOG_FILE_PREFIX)
}
}
impl LogFile {
fn parse_file_name(file_name: &str) -> Result<(String, String, String, u32, String)> {
let err_msg = format!("Failed to parse file name '{file_name}' for log file.");
if !file_name.starts_with(LOG_FILE_PREFIX) {
return Err(CoreError::FileGroup(err_msg));
}
let file_name = &file_name[LOG_FILE_PREFIX.len_utf8()..];
let (file_id, rest) = file_name
.split_once('_')
.ok_or_else(|| CoreError::FileGroup(err_msg.clone()))?;
let (middle, file_write_token) = rest
.rsplit_once('_')
.ok_or_else(|| CoreError::FileGroup(err_msg.clone()))?;
let parts: Vec<&str> = middle.split('.').collect();
if parts.len() != 3 {
return Err(CoreError::FileGroup(err_msg.clone()));
}
let timestamp = parts[0];
let log_file_extension = parts[1];
let log_file_version_str = parts[2];
if file_id.is_empty()
|| timestamp.is_empty()
|| log_file_extension.is_empty()
|| log_file_version_str.is_empty()
|| file_write_token.is_empty()
{
return Err(CoreError::FileGroup(err_msg.clone()));
}
let log_file_version = log_file_version_str
.parse::<u32>()
.map_err(|_| CoreError::FileGroup(err_msg.clone()))?;
Ok((
file_id.to_string(),
timestamp.to_string(),
log_file_extension.to_string(),
log_file_version,
file_write_token.to_string(),
))
}
#[inline]
pub fn file_name(&self) -> String {
format!(
"{prefix}{file_id}_{timestamp}.{extension}.{version}_{write_token}",
prefix = LOG_FILE_PREFIX,
file_id = self.file_id,
timestamp = self.timestamp,
extension = self.extension,
version = self.version,
write_token = self.write_token
)
}
#[inline]
pub fn is_completed(&self) -> bool {
self.completion_timestamp.is_some()
}
pub fn set_completion_time<V: CompletionTimeView>(&mut self, view: &V) {
self.completion_timestamp = view
.get_completion_time(&self.timestamp)
.map(|s| s.to_string());
}
}
impl FromStr for LogFile {
type Err = CoreError;
fn from_str(file_name: &str) -> Result<Self, Self::Err> {
let (file_id, timestamp, extension, version, write_token) =
Self::parse_file_name(file_name)?;
Ok(LogFile {
file_id,
timestamp,
completion_timestamp: None,
extension,
version,
write_token,
file_metadata: None,
})
}
}
impl TryFrom<FileMetadata> for LogFile {
type Error = CoreError;
fn try_from(metadata: FileMetadata) -> Result<Self> {
let file_name = metadata.name.as_str();
let (file_id, timestamp, extension, version, write_token) =
Self::parse_file_name(file_name)?;
Ok(LogFile {
file_id,
timestamp,
completion_timestamp: None,
extension,
version,
write_token,
file_metadata: Some(metadata),
})
}
}
impl Display for LogFile {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "LogFile: {}", self.file_name())
}
}
impl PartialEq for LogFile {
fn eq(&self, other: &Self) -> bool {
self.file_name() == other.file_name()
}
}
impl Eq for LogFile {}
impl PartialOrd for LogFile {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for LogFile {
fn cmp(&self, other: &Self) -> Ordering {
match (&self.completion_timestamp, &other.completion_timestamp) {
(Some(ct1), Some(ct2)) => {
ct1.cmp(ct2)
.then(self.version.cmp(&other.version))
.then(self.write_token.cmp(&other.write_token))
}
(Some(_), None) => {
Ordering::Less
}
(None, Some(_)) => {
Ordering::Greater
}
(None, None) => {
self.timestamp
.cmp(&other.timestamp)
.then(self.version.cmp(&other.version))
.then(self.write_token.cmp(&other.write_token))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_log_file_name_in_formatted_str() {
let filename = ".54e9a5e9-ee5d-4ed2-acee-720b5810d380-0_20250109233025121.log.1_0-51-115";
let log_file = LogFile::from_str(filename).unwrap();
assert!(format!("{log_file}").contains(filename));
}
#[test]
fn test_valid_filename_parsing() {
let filename = ".54e9a5e9-ee5d-4ed2-acee-720b5810d380-0_20250109233025121.log.1_0-51-115";
let log_file = LogFile::from_str(filename).unwrap();
assert_eq!(log_file.file_id, "54e9a5e9-ee5d-4ed2-acee-720b5810d380-0");
assert_eq!(log_file.timestamp, "20250109233025121");
assert_eq!(log_file.extension, "log");
assert_eq!(log_file.version, 1);
assert_eq!(log_file.write_token, "0-51-115");
assert_eq!(log_file.completion_timestamp, None);
assert!(!log_file.is_completed());
}
#[test]
fn test_filename_reconstruction() {
let original = ".54e9a5e9-ee5d-4ed2-acee-720b5810d380-0_20250109233025121.log.1_0-51-115";
let log_file = LogFile::from_str(original).unwrap();
assert_eq!(log_file.file_name(), original);
}
#[test]
fn test_missing_dot_prefix() {
let filename = "myfile_20250109233025121.log.v1_abc123";
assert!(matches!(
LogFile::from_str(filename),
Err(CoreError::FileGroup(_))
));
}
#[test]
fn test_missing_first_underscore() {
let filename = ".myfile20250109233025121.log.v1_abc123";
assert!(matches!(
LogFile::from_str(filename),
Err(CoreError::FileGroup(_))
));
}
#[test]
fn test_missing_last_underscore() {
let filename = ".myfile_20250109233025121.log.v1abc123";
assert!(matches!(
LogFile::from_str(filename),
Err(CoreError::FileGroup(_))
));
}
#[test]
fn test_incorrect_dot_parts() {
let filename = ".myfile_20250109233025121.log.v1.extra_abc123";
assert!(matches!(
LogFile::from_str(filename),
Err(CoreError::FileGroup(_))
));
}
#[test]
fn test_empty_components() {
let filenames = vec![
"._20250109233025121.log.v1_abc123", ".myfile_.log.v1_abc123", ".myfile_20250109233025121..v1_abc123", ".myfile_20250109233025121.log._abc123", ".myfile_20250109233025121.log.v1_", ];
for filename in filenames {
assert!(matches!(
LogFile::from_str(filename),
Err(CoreError::FileGroup(_))
));
}
}
#[test]
fn test_log_file_ordering_no_completion_timestamp() {
let log1 = LogFile {
file_id: "ee2ace10-7667-40f5-9848-0a144b5ea064-0".to_string(),
timestamp: "20250113230302428".to_string(),
completion_timestamp: None,
extension: "log".to_string(),
version: 1,
write_token: "0-188-387".to_string(),
file_metadata: None,
};
let log2 = LogFile {
file_id: "ee2ace10-7667-40f5-9848-0a144b5ea064-0".to_string(),
timestamp: "20250113230302428".to_string(),
completion_timestamp: None,
extension: "log".to_string(),
version: 2,
write_token: "0-188-387".to_string(),
file_metadata: None,
};
let log3 = LogFile {
file_id: "ee2ace10-7667-40f5-9848-0a144b5ea064-0".to_string(),
timestamp: "20250113230424191".to_string(),
completion_timestamp: None,
extension: "log".to_string(),
version: 1,
write_token: "0-188-387".to_string(),
file_metadata: None,
};
let log4 = LogFile {
file_id: "ee2ace10-7667-40f5-9848-0a144b5ea064-0".to_string(),
timestamp: "20250113230302428".to_string(),
completion_timestamp: None,
extension: "log".to_string(),
version: 1,
write_token: "1-188-387".to_string(),
file_metadata: None,
};
assert!(log1 < log2, "version ordering failed");
assert!(log1 < log3, "timestamp ordering failed");
assert!(log2 < log3, "timestamp ordering failed");
assert!(log1 < log4, "write token ordering failed");
let mut logs = vec![log3.clone(), log4.clone(), log1.clone(), log2.clone()];
logs.sort();
assert_eq!(logs, vec![log1, log4, log2, log3]);
let log_v2 = LogFile {
file_id: "file-0".to_string(),
timestamp: "20250113230302428".to_string(),
completion_timestamp: None,
extension: "log".to_string(),
version: 2,
write_token: "0-188-387".to_string(),
file_metadata: None,
};
let log_v10 = LogFile {
file_id: "file-0".to_string(),
timestamp: "20250113230302428".to_string(),
completion_timestamp: None,
extension: "log".to_string(),
version: 10,
write_token: "0-188-387".to_string(),
file_metadata: None,
};
assert!(
log_v2 < log_v10,
"version 2 should be less than version 10 (integer ordering)"
);
}
#[test]
fn test_log_file_ordering_by_completion_time() {
let log1 = LogFile {
file_id: "file-0".to_string(),
timestamp: "20250113230302428".to_string(),
completion_timestamp: Some("20250113230310000".to_string()),
extension: "log".to_string(),
version: 1,
write_token: "0-188-387".to_string(),
file_metadata: None,
};
let log2 = LogFile {
file_id: "file-0".to_string(),
timestamp: "20250113230300000".to_string(), completion_timestamp: Some("20250113230320000".to_string()), extension: "log".to_string(),
version: 1,
write_token: "0-188-387".to_string(),
file_metadata: None,
};
let log3 = LogFile {
file_id: "file-0".to_string(),
timestamp: "20250113230305000".to_string(),
completion_timestamp: None,
extension: "log".to_string(),
version: 1,
write_token: "0-188-387".to_string(),
file_metadata: None,
};
assert!(
log1 < log2,
"log1 should be before log2 (earlier completion)"
);
assert!(
log1 < log3,
"completed log1 should be before uncommitted log3"
);
assert!(
log2 < log3,
"completed log2 should be before uncommitted log3"
);
let mut logs = vec![log3.clone(), log1.clone(), log2.clone()];
logs.sort();
assert_eq!(logs, vec![log1, log2, log3]);
}
}
#[cfg(test)]
mod memory_bench;