use super::*;
pub(crate) fn ensure_writable_table_version(table: &Table) -> Result<()> {
let version: isize = table
.hudi_configs
.get_or_default(crate::config::table::HudiTableConfig::TableVersion)
.into();
if !(8..=9).contains(&version) {
return Err(CoreError::Unsupported(format!(
"writing to table version {version} is not supported; supported versions are 8 and 9"
)));
}
Ok(())
}
pub(super) fn ensure_set_columns_updatable(table: &Table, updates: &RecordBatch) -> Result<()> {
let mut identity = configured_record_key_fields(&table.hudi_configs)?;
let partition_fields: Vec<String> = table
.hudi_configs
.get_or_default(crate::config::table::HudiTableConfig::PartitionFields)
.into();
identity.extend(
partition_fields
.into_iter()
.map(|field| match field.split_once(':') {
Some((name, _)) => name.to_string(),
None => field,
}),
);
for field in updates.schema().fields() {
if identity.iter().any(|name| name == field.name()) {
return Err(CoreError::Write(format!(
"update cannot SET record-key or partition column '{}'",
field.name()
)));
}
}
Ok(())
}
pub(super) async fn validate_write_input(
table: &Table,
batches: &[RecordBatch],
verb: &str,
) -> Result<Vec<RecordBatch>> {
let rows: usize = batches.iter().map(RecordBatch::num_rows).sum();
if rows == 0 {
return Err(CoreError::Write(format!(
"{verb} requires at least one row"
)));
}
let first = batches[0].schema();
crate::write::append::arrow_schema_to_avro_json(
&crate::write::append::strip_meta_fields_from_schema(&first),
)?;
for batch in &batches[1..] {
if crate::write::align_batch_to_schema(batch, &first).is_none() {
return Err(CoreError::Schema(format!(
"all {verb} batches must share one schema"
)));
}
}
if table
.timeline
.get_latest_commit_timestamp_as_option()
.is_some()
{
let table_schema = std::sync::Arc::new(table.get_schema().await?);
for batch in batches {
if crate::write::align_batch_to_schema(batch, &table_schema).is_none() {
return Err(CoreError::Schema(format!(
"{verb} batch schema does not match the current table schema"
)));
}
}
return crate::write::widen_to_table_nullability(batches, &table_schema);
}
Ok(batches.to_vec())
}
pub(super) fn ensure_rewrite_supported(table: &Table) -> Result<()> {
ensure_copy_on_write(table)?;
ensure_supported_merge_configs(table)
}
pub(super) fn ensure_mor_merge_supported(table: &Table) -> Result<()> {
ensure_supported_merge_configs(table)?;
let populates_meta_fields: bool = table
.hudi_configs
.get_or_default(crate::config::table::HudiTableConfig::PopulatesMetaFields)
.into();
if !populates_meta_fields {
return Err(CoreError::Unsupported(
"MERGE_ON_READ upsert and delete require hoodie.populate.meta.fields=true".to_string(),
));
}
let strategy: String = table
.hudi_configs
.get_or_default(RecordMergeStrategy)
.into();
if RecordMergeStrategyValue::from_str(&strategy)?
!= RecordMergeStrategyValue::OverwriteWithLatest
{
return Err(CoreError::Unsupported(
"MERGE_ON_READ upsert and delete require an upsertable merge mode".to_string(),
));
}
Ok(())
}
pub(super) fn ensure_supported_merge_configs(table: &Table) -> Result<()> {
let options = table.hudi_configs.as_options();
if let Some(mode) = options.get("hoodie.record.merge.mode") {
let normalized = mode.to_ascii_uppercase();
match normalized.as_str() {
"COMMIT_TIME_ORDERING" | "EVENT_TIME_ORDERING" => {}
"CUSTOM" => {
return Err(CoreError::Unsupported(
"writes do not support hoodie.record.merge.mode=CUSTOM".to_string(),
));
}
other => {
return Err(CoreError::Unsupported(format!(
"writes only support COMMIT_TIME_ORDERING or EVENT_TIME_ORDERING merge modes, got '{other}'"
)));
}
}
}
if let Some(merger) = options.get("hoodie.record.merger.impls")
&& !merger.trim().is_empty()
{
return Err(CoreError::Unsupported(
"writes do not support custom hoodie.record.merger.impls".to_string(),
));
}
Ok(())
}
pub(super) fn uses_event_time_merge(hudi_configs: &crate::config::HudiConfigs) -> Result<bool> {
use crate::file_group::reader_v2::reader_context::MergeMode;
if let Ok(mode) = crate::file_group::reader_v2::resolver::resolve_merge_mode(hudi_configs)
&& mode == MergeMode::CommitTimeOrdering
{
return Ok(false);
}
let strategy: String = hudi_configs.get_or_default(RecordMergeStrategy).into();
let strategy = RecordMergeStrategyValue::from_str(&strategy)?;
Ok(strategy == RecordMergeStrategyValue::OverwriteWithLatest
&& hudi_configs
.try_get(OrderingFields)?
.map(|v| -> Vec<String> { v.into() })
.is_some_and(|fields| fields.iter().any(|f| !f.trim().is_empty())))
}
pub(super) fn event_time_ordering_fields(
hudi_configs: &crate::config::HudiConfigs,
) -> Result<Vec<String>> {
if !uses_event_time_merge(hudi_configs)? {
return Ok(Vec::new());
}
Ok(hudi_configs
.try_get(OrderingFields)?
.map(Into::into)
.unwrap_or_default())
}
pub(super) fn upsert_ordering_fields(
hudi_configs: &crate::config::HudiConfigs,
update_columns: Option<&[String]>,
) -> Result<Vec<String>> {
let fields = event_time_ordering_fields(hudi_configs)?;
if update_columns.is_some_and(|columns| fields.iter().any(|field| !columns.contains(field))) {
return Ok(Vec::new());
}
Ok(fields)
}