use anyhow::{anyhow, Context, Result};
use paste::paste;
use std::collections::HashMap;
use std::env;
use std::hash::Hash;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use strum::IntoEnumIterator;
use crate::config::internal::HudiInternalConfig::SkipConfigValidation;
use crate::config::read::HudiReadConfig;
use crate::config::table::HudiTableConfig::{DropsPartitionFields, TableType, TableVersion};
use crate::config::table::TableTypeValue::CopyOnWrite;
use crate::config::table::{HudiTableConfig, TableTypeValue};
use crate::config::utils::{parse_data_for_options, split_hudi_options_from_others};
use crate::config::{HudiConfigs, HUDI_CONF_DIR};
use crate::storage::Storage;
use crate::table::fs_view::FileSystemView;
use crate::table::timeline::Timeline;
use crate::table::Table;
#[derive(Debug, Clone)]
pub struct TableBuilder {
base_uri: String,
hudi_options: HashMap<String, String>,
storage_options: HashMap<String, String>,
options: HashMap<String, String>,
}
macro_rules! impl_with_options {
($struct_name:ident, $($field:ident, $singular:ident),+) => {
impl $struct_name {
$(
paste! {
#[doc = "Add " $singular " to the builder."]
#[doc = "Subsequent calls overwrite the previous values if the key already exists."]
pub fn [<with_ $singular>]<K, V>(mut self, k: K, v: V) -> Self
where
K: AsRef<str>,
V: Into<String>,
{
self.$field.insert(k.as_ref().to_string(), v.into());
self
}
#[doc = "Add " $field " to the builder."]
#[doc = "Subsequent calls overwrite the previous values if the key already exists."]
pub fn [<with_ $field>]<I, K, V>(mut self, options: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: AsRef<str>,
V: Into<String>,
{
self.$field.extend(options.into_iter().map(|(k, v)| (k.as_ref().to_string(), v.into())));
self
}
}
)+
}
};
}
impl_with_options!(
TableBuilder,
hudi_options,
hudi_option,
storage_options,
storage_option,
options,
option
);
impl TableBuilder {
pub fn from_base_uri(base_uri: &str) -> Self {
TableBuilder {
base_uri: base_uri.to_string(),
storage_options: HashMap::new(),
hudi_options: HashMap::new(),
options: HashMap::new(),
}
}
pub async fn build(&mut self) -> Result<Table> {
self.resolve_options().await?;
let hudi_configs = HudiConfigs::new(self.hudi_options.iter());
Self::validate_configs(&hudi_configs).expect("Hudi configs are not valid.");
let hudi_configs = Arc::from(hudi_configs);
let storage_options = Arc::from(self.storage_options.clone());
let timeline = Timeline::new(hudi_configs.clone(), storage_options.clone())
.await
.context("Failed to load timeline")?;
let file_system_view = FileSystemView::new(hudi_configs.clone(), storage_options.clone())
.await
.context("Failed to load file system view")?;
Ok(Table {
hudi_configs,
storage_options,
timeline,
file_system_view,
})
}
async fn resolve_options(&mut self) -> Result<()> {
self.resolve_user_provided_options();
self.resolve_cloud_env_vars();
self.resolve_hudi_options().await
}
fn resolve_user_provided_options(&mut self) {
self.hudi_options.insert(
HudiTableConfig::BasePath.as_ref().to_string(),
self.base_uri.clone(),
);
let (generic_hudi_opts, generic_other_opts) =
split_hudi_options_from_others(self.options.iter());
Self::extend_if_absent(&mut self.hudi_options, &generic_hudi_opts);
Self::extend_if_absent(&mut self.storage_options, &generic_other_opts)
}
fn resolve_cloud_env_vars(&mut self) {
for (key, value) in env::vars() {
if Storage::CLOUD_STORAGE_PREFIXES
.iter()
.any(|prefix| key.starts_with(prefix))
&& !self.storage_options.contains_key(&key.to_ascii_lowercase())
{
self.storage_options.insert(key.to_ascii_lowercase(), value);
}
}
}
async fn resolve_hudi_options(&mut self) -> Result<()> {
let storage = Storage::new(
Arc::new(self.storage_options.clone()),
Arc::new(HudiConfigs::new(self.hudi_options.iter())),
)?;
let hudi_options = &mut self.hudi_options;
Self::imbue_table_properties(hudi_options, storage.clone()).await?;
Self::imbue_global_hudi_configs_if_absent(hudi_options, storage.clone()).await
}
async fn imbue_table_properties(
options: &mut HashMap<String, String>,
storage: Arc<Storage>,
) -> Result<()> {
let bytes = storage.get_file_data(".hoodie/hoodie.properties").await?;
let table_properties = parse_data_for_options(&bytes, "=")?;
for (k, v) in table_properties {
options.insert(k.to_string(), v.to_string());
}
Ok(())
}
async fn imbue_global_hudi_configs_if_absent(
options: &mut HashMap<String, String>,
storage: Arc<Storage>,
) -> Result<()> {
let global_config_path = env::var(HUDI_CONF_DIR)
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/etc/hudi/conf"))
.join("hudi-defaults.conf");
if let Ok(bytes) = storage
.get_file_data_from_absolute_path(global_config_path.to_str().unwrap())
.await
{
if let Ok(global_configs) = parse_data_for_options(&bytes, " \t=") {
for (key, value) in global_configs {
if key.starts_with("hoodie.") && !options.contains_key(&key) {
options.insert(key.to_string(), value.to_string());
}
}
}
}
Ok(())
}
fn validate_configs(hudi_configs: &HudiConfigs) -> Result<()> {
if hudi_configs
.get_or_default(SkipConfigValidation)
.to::<bool>()
{
return Ok(());
}
for conf in HudiTableConfig::iter() {
hudi_configs.validate(conf)?
}
for conf in HudiReadConfig::iter() {
hudi_configs.validate(conf)?
}
let table_type = hudi_configs.get(TableType)?.to::<String>();
if TableTypeValue::from_str(&table_type)? != CopyOnWrite {
return Err(anyhow!("Only support copy-on-write table."));
}
let table_version = hudi_configs.get(TableVersion)?.to::<isize>();
if !(5..=6).contains(&table_version) {
return Err(anyhow!("Only support table version 5 and 6."));
}
let drops_partition_cols = hudi_configs
.get_or_default(DropsPartitionFields)
.to::<bool>();
if drops_partition_cols {
return Err(anyhow!(
"Only support when `{}` is disabled",
DropsPartitionFields.as_ref()
));
}
Ok(())
}
fn extend_if_absent<K, V>(target: &mut HashMap<K, V>, source: &HashMap<K, V>)
where
K: Eq + Hash + Clone,
V: Clone,
{
for (key, value) in source {
target.entry(key.clone()).or_insert_with(|| value.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_table_builder() -> TableBuilder {
TableBuilder {
base_uri: "test_uri".to_string(),
hudi_options: HashMap::new(),
storage_options: HashMap::new(),
options: HashMap::new(),
}
}
#[test]
fn test_with_hudi_option() {
let builder = create_table_builder();
let updated = builder.with_hudi_option("key", "value");
assert_eq!(updated.hudi_options["key"], "value")
}
#[test]
fn test_with_hudi_options() {
let builder = create_table_builder();
let options = [("key1", "value1"), ("key2", "value2")];
let updated = builder.with_hudi_options(options);
assert_eq!(updated.hudi_options["key1"], "value1");
assert_eq!(updated.hudi_options["key2"], "value2")
}
#[test]
fn test_with_storage_option() {
let builder = create_table_builder();
let updated = builder.with_storage_option("key", "value");
assert_eq!(updated.storage_options["key"], "value")
}
#[test]
fn test_with_storage_options() {
let builder = create_table_builder();
let options = [("key1", "value1"), ("key2", "value2")];
let updated = builder.with_storage_options(options);
assert_eq!(updated.storage_options["key1"], "value1");
assert_eq!(updated.storage_options["key2"], "value2");
}
#[test]
fn test_with_option() {
let builder = create_table_builder();
let updated = builder.with_option("key", "value");
assert_eq!(updated.options["key"], "value")
}
#[test]
fn test_with_options() {
let builder = create_table_builder();
let options = [("key1", "value1"), ("key2", "value2")];
let updated = builder.with_options(options);
assert_eq!(updated.options["key1"], "value1");
assert_eq!(updated.options["key2"], "value2")
}
#[test]
fn test_builder_resolve_user_provided_options_should_apply_precedence_order() {
let mut builder = TableBuilder::from_base_uri("/tmp/hudi_data")
.with_hudi_option("hoodie.option1", "value1")
.with_option("hoodie.option2", "'value2")
.with_hudi_options([
("hoodie.option1", "value1-1"),
("hoodie.option3", "value3"),
("hoodie.option1", "value1-2"),
])
.with_storage_option("AWS_REGION", "us-east-2")
.with_storage_options([
("AWS_REGION", "us-east-1"),
("AWS_ENDPOINT", "s3.us-east-1.amazonaws.com"),
])
.with_option("AWS_REGION", "us-west-1")
.with_options([
("hoodie.option3", "value3-1"),
("hoodie.option2", "value2-1"),
]);
builder.resolve_user_provided_options();
assert_eq!(builder.hudi_options.len(), 4);
assert_eq!(builder.hudi_options["hoodie.base.path"], "/tmp/hudi_data");
assert_eq!(builder.hudi_options["hoodie.option1"], "value1-2");
assert_eq!(builder.hudi_options["hoodie.option2"], "value2-1");
assert_eq!(builder.hudi_options["hoodie.option3"], "value3");
assert_eq!(builder.storage_options.len(), 2);
assert_eq!(builder.storage_options["AWS_REGION"], "us-east-1");
assert_eq!(
builder.storage_options["AWS_ENDPOINT"],
"s3.us-east-1.amazonaws.com"
);
}
}