use std::collections::HashMap;
use std::fmt::Display;
use std::str::FromStr;
use strum_macros::{AsRefStr, EnumIter, IntoStaticStr};
use crate::config::Result;
use crate::config::error::ConfigError;
use crate::config::error::ConfigError::{InvalidValue, NotFound, ParseBool, ParseInt};
use crate::config::{ConfigParser, HudiConfigValue};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, AsRefStr)]
pub enum QueryType {
#[default]
#[strum(serialize = "snapshot")]
Snapshot,
#[strum(serialize = "incremental")]
Incremental,
}
impl Display for QueryType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_ref())
}
}
impl FromStr for QueryType {
type Err = ConfigError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"snapshot" => Ok(Self::Snapshot),
"incremental" => Ok(Self::Incremental),
v => Err(InvalidValue(v.to_string())),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, EnumIter, IntoStaticStr)]
pub enum HudiReadConfig {
QueryType,
AsOfTimestamp,
StartTimestamp,
EndTimestamp,
InputPartitions,
UseReadOptimizedMode,
FileGroupReaderVersion,
StreamBatchSize,
FileSliceReadConcurrency,
ScanMaxMemorySize,
MergeUseRecordPositions,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ReadConfigScope {
TableOrRead,
ReadOnly,
}
impl HudiReadConfig {
pub const fn scope(&self) -> ReadConfigScope {
match self {
Self::QueryType | Self::AsOfTimestamp | Self::StartTimestamp | Self::EndTimestamp => {
ReadConfigScope::ReadOnly
}
Self::InputPartitions
| Self::UseReadOptimizedMode
| Self::StreamBatchSize
| Self::FileSliceReadConcurrency
| Self::ScanMaxMemorySize
| Self::FileGroupReaderVersion
| Self::MergeUseRecordPositions => ReadConfigScope::TableOrRead,
}
}
pub fn scope_of_key(key: &str) -> Option<ReadConfigScope> {
use strum::IntoEnumIterator;
Self::iter()
.find(|config| config.as_ref() == key)
.map(|config| config.scope())
}
pub const fn key_str(&self) -> &'static str {
match self {
Self::QueryType => "hoodie.read.query.type",
Self::AsOfTimestamp => "hoodie.read.as.of.timestamp",
Self::StartTimestamp => "hoodie.read.start.timestamp",
Self::EndTimestamp => "hoodie.read.end.timestamp",
Self::InputPartitions => "hoodie.read.input.partitions",
Self::UseReadOptimizedMode => "hoodie.read.use.read_optimized.mode",
Self::FileGroupReaderVersion => "hoodie.read.file.group.reader.version",
Self::StreamBatchSize => "hoodie.read.stream.batch_size",
Self::FileSliceReadConcurrency => "hoodie.read.file.slice.read.concurrency",
Self::ScanMaxMemorySize => "hoodie.read.scan.max.memory.size",
Self::MergeUseRecordPositions => "hoodie.merge.use.record.positions",
}
}
}
impl AsRef<str> for HudiReadConfig {
fn as_ref(&self) -> &str {
self.key_str()
}
}
impl Display for HudiReadConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_ref())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum FileGroupReaderVersion {
One,
#[default]
Two,
}
impl FileGroupReaderVersion {
pub fn as_usize(&self) -> usize {
match self {
Self::One => 1,
Self::Two => 2,
}
}
}
impl TryFrom<usize> for FileGroupReaderVersion {
type Error = ConfigError;
fn try_from(value: usize) -> std::result::Result<Self, Self::Error> {
match value {
1 => Ok(Self::One),
2 => Ok(Self::Two),
v => Err(InvalidValue(v.to_string())),
}
}
}
impl FromStr for FileGroupReaderVersion {
type Err = ConfigError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.trim() {
"1" => Ok(Self::One),
"2" => Ok(Self::Two),
v => Err(InvalidValue(v.to_string())),
}
}
}
impl ConfigParser for HudiReadConfig {
type Output = HudiConfigValue;
fn default_value(&self) -> Option<HudiConfigValue> {
match self {
HudiReadConfig::QueryType => Some(HudiConfigValue::String(
QueryType::default().as_ref().to_string(),
)),
HudiReadConfig::InputPartitions => Some(HudiConfigValue::UInteger(0usize)),
HudiReadConfig::UseReadOptimizedMode => Some(HudiConfigValue::Boolean(false)),
HudiReadConfig::FileGroupReaderVersion => Some(HudiConfigValue::UInteger(
FileGroupReaderVersion::default().as_usize(),
)),
HudiReadConfig::MergeUseRecordPositions => Some(HudiConfigValue::Boolean(false)),
HudiReadConfig::StreamBatchSize => Some(HudiConfigValue::UInteger(1024usize)),
HudiReadConfig::FileSliceReadConcurrency => Some(HudiConfigValue::UInteger(4usize)),
_ => None,
}
}
fn parse_value(&self, configs: &HashMap<String, String>) -> Result<Self::Output> {
let get_result = configs
.get(self.as_ref())
.map(|v| v.as_str())
.ok_or(NotFound(self.key()));
match self {
Self::QueryType => get_result
.and_then(QueryType::from_str)
.map(|v| HudiConfigValue::String(v.as_ref().to_string())),
Self::AsOfTimestamp => get_result.map(|v| HudiConfigValue::String(v.to_string())),
Self::StartTimestamp => get_result.map(|v| HudiConfigValue::String(v.to_string())),
Self::EndTimestamp => get_result.map(|v| HudiConfigValue::String(v.to_string())),
Self::InputPartitions => get_result
.and_then(|v| {
usize::from_str(v).map_err(|e| ParseInt(self.key(), v.to_string(), e))
})
.map(HudiConfigValue::UInteger),
Self::FileGroupReaderVersion => get_result
.and_then(FileGroupReaderVersion::from_str)
.map(|v| HudiConfigValue::UInteger(v.as_usize())),
Self::UseReadOptimizedMode | Self::MergeUseRecordPositions => get_result
.and_then(|v| {
bool::from_str(v).map_err(|e| ParseBool(self.key(), v.to_string(), e))
})
.map(HudiConfigValue::Boolean),
Self::StreamBatchSize => get_result
.and_then(|v| {
let key = self.key();
let parsed =
usize::from_str(v).map_err(|e| ParseInt(key.clone(), v.to_string(), e))?;
if parsed == 0 {
return Err(InvalidValue(format!("{key}=0 (must be > 0)")));
}
Ok(parsed)
})
.map(HudiConfigValue::UInteger),
Self::ScanMaxMemorySize => get_result
.and_then(|v| {
let key = self.key();
let parsed =
u64::from_str(v).map_err(|e| ParseInt(key.clone(), v.to_string(), e))?;
if parsed == 0 {
return Err(InvalidValue(format!("{key}=0 (must be > 0)")));
}
Ok(parsed as usize)
})
.map(HudiConfigValue::UInteger),
Self::FileSliceReadConcurrency => get_result
.and_then(|v| {
let key = self.key();
let parsed =
usize::from_str(v).map_err(|e| ParseInt(key.clone(), v.to_string(), e))?;
if parsed == 0 {
return Err(InvalidValue(format!("{key}=0 (must be > 0)")));
}
Ok(parsed)
})
.map(HudiConfigValue::UInteger),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::read::HudiReadConfig::{
AsOfTimestamp, EndTimestamp, FileSliceReadConcurrency, InputPartitions,
QueryType as QueryTypeKey, StartTimestamp, StreamBatchSize, UseReadOptimizedMode,
};
#[test]
fn parse_valid_config_value() {
let options = HashMap::from([
(QueryTypeKey.as_ref().to_string(), "Incremental".to_string()),
(AsOfTimestamp.as_ref().to_string(), "20240101".to_string()),
(StartTimestamp.as_ref().to_string(), "20240102".to_string()),
(EndTimestamp.as_ref().to_string(), "20240103".to_string()),
(InputPartitions.as_ref().to_string(), "100".to_string()),
(
UseReadOptimizedMode.as_ref().to_string(),
"true".to_string(),
),
(StreamBatchSize.as_ref().to_string(), "2048".to_string()),
(
FileSliceReadConcurrency.as_ref().to_string(),
"8".to_string(),
),
]);
let actual: String = QueryTypeKey.parse_value(&options).unwrap().into();
assert_eq!(actual, "incremental");
let actual: String = AsOfTimestamp.parse_value(&options).unwrap().into();
assert_eq!(actual, "20240101");
let actual: String = StartTimestamp.parse_value(&options).unwrap().into();
assert_eq!(actual, "20240102");
let actual: String = EndTimestamp.parse_value(&options).unwrap().into();
assert_eq!(actual, "20240103");
let actual: usize = InputPartitions.parse_value(&options).unwrap().into();
assert_eq!(actual, 100);
let actual: bool = UseReadOptimizedMode.parse_value(&options).unwrap().into();
assert!(actual);
let actual: usize = StreamBatchSize.parse_value(&options).unwrap().into();
assert_eq!(actual, 2048);
let actual: usize = FileSliceReadConcurrency
.parse_value(&options)
.unwrap()
.into();
assert_eq!(actual, 8);
}
#[test]
fn parse_invalid_config_value() {
let options = HashMap::from([
(QueryTypeKey.as_ref().to_string(), "bogus".to_string()),
(InputPartitions.as_ref().to_string(), "foo".to_string()),
(UseReadOptimizedMode.as_ref().to_string(), "1".to_string()),
(StreamBatchSize.as_ref().to_string(), "abc".to_string()),
(
FileSliceReadConcurrency.as_ref().to_string(),
"abc".to_string(),
),
]);
assert!(matches!(
QueryTypeKey.parse_value(&options).unwrap_err(),
InvalidValue(_)
));
let actual: String = QueryTypeKey.parse_value_or_default(&options).into();
assert_eq!(actual, "snapshot");
assert!(matches!(
InputPartitions.parse_value(&options).unwrap_err(),
ParseInt(_, _, _)
));
let actual: usize = InputPartitions.parse_value_or_default(&options).into();
assert_eq!(actual, 0);
assert!(matches!(
UseReadOptimizedMode.parse_value(&options).unwrap_err(),
ParseBool(_, _, _)
));
let actual: bool = UseReadOptimizedMode.parse_value_or_default(&options).into();
assert!(!actual);
assert!(matches!(
StreamBatchSize.parse_value(&options).unwrap_err(),
ParseInt(_, _, _)
));
let actual: usize = StreamBatchSize.parse_value_or_default(&options).into();
assert_eq!(actual, 1024);
assert!(matches!(
FileSliceReadConcurrency.parse_value(&options).unwrap_err(),
ParseInt(_, _, _)
));
let actual: usize = FileSliceReadConcurrency
.parse_value_or_default(&options)
.into();
assert_eq!(actual, 4);
let zero = HashMap::from([(
FileSliceReadConcurrency.as_ref().to_string(),
"0".to_string(),
)]);
assert!(matches!(
FileSliceReadConcurrency.parse_value(&zero).unwrap_err(),
InvalidValue(_)
));
}
#[test]
fn timestamp_keys_have_no_default_value() {
assert!(AsOfTimestamp.default_value().is_none());
assert!(StartTimestamp.default_value().is_none());
assert!(EndTimestamp.default_value().is_none());
}
#[test]
fn file_group_reader_version_try_from_usize_accepts_1_and_2_and_rejects_others() {
assert_eq!(
FileGroupReaderVersion::try_from(1).unwrap(),
FileGroupReaderVersion::One
);
assert_eq!(
FileGroupReaderVersion::try_from(2).unwrap(),
FileGroupReaderVersion::Two
);
assert!(FileGroupReaderVersion::try_from(3).is_err());
}
#[test]
fn query_type_from_str_accepts_case_insensitive_and_rejects_invalid() {
assert_eq!(
QueryType::from_str("snapshot").unwrap(),
QueryType::Snapshot
);
assert_eq!(
QueryType::from_str("SNAPSHOT").unwrap(),
QueryType::Snapshot
);
assert_eq!(
QueryType::from_str("Incremental").unwrap(),
QueryType::Incremental
);
assert!(matches!(
QueryType::from_str("bogus").unwrap_err(),
InvalidValue(_)
));
}
#[test]
fn display_impls_match_canonical_keys() {
assert_eq!(format!("{}", QueryType::Snapshot), "snapshot");
assert_eq!(format!("{}", QueryType::Incremental), "incremental");
assert_eq!(
format!("{}", HudiReadConfig::StreamBatchSize),
"hoodie.read.stream.batch_size"
);
assert_eq!(
format!("{}", HudiReadConfig::QueryType),
"hoodie.read.query.type"
);
}
}