This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 7dd674b5 fix: reject overlapping partial-update sequence groups (#1127)
7dd674b5 is described below
commit 7dd674b5725af220bc8e4279cbfa689b615ffe6e
Author: reswqa <[email protected]>
AuthorDate: Fri Oct 16 11:39:30 2026 +0800
fix: reject overlapping partial-update sequence groups (#1127)
---
crates/paimon/src/spec/partial_update.rs | 58 ++++++++++++++++++++++++++++++++
1 file changed, 58 insertions(+)
diff --git a/crates/paimon/src/spec/partial_update.rs
b/crates/paimon/src/spec/partial_update.rs
index 93d2a31a..88e253bf 100644
--- a/crates/paimon/src/spec/partial_update.rs
+++ b/crates/paimon/src/spec/partial_update.rs
@@ -252,6 +252,7 @@ impl<'a> PartialUpdateConfig<'a> {
let groups = self.sequence_groups()?;
let field_names: HashSet<&str> =
fields.iter().map(DataField::name).collect();
let primary_keys: HashSet<&str> =
primary_keys.iter().map(String::as_str).collect();
+ let mut sequence_field_owners: HashMap<String, String> =
HashMap::new();
let mut protected_field_owners: HashMap<String, String> =
HashMap::new();
for group in &groups {
@@ -279,6 +280,22 @@ impl<'a> PartialUpdateConfig<'a> {
});
}
}
+ for field in &group.sequence_fields {
+ if let Some(previous_option) =
+ sequence_field_owners.insert(field.clone(),
option_key.clone())
+ {
+ if previous_option == option_key {
+ continue;
+ }
+ return Err(crate::Error::ConfigInvalid {
+ message: format!(
+ "Sequence field '{field}' is shared by sequence
groups \
+ '{previous_option}' and '{option_key}'. \
+ Ordering fields must not overlap between sequence
groups."
+ ),
+ });
+ }
+ }
for field in &group.protected_fields {
if let Some(previous_option) =
protected_field_owners.insert(field.clone(),
option_key.clone())
@@ -820,6 +837,47 @@ mod tests {
);
}
+ #[test]
+ fn test_validate_sequence_groups_rejects_overlapping_ordering_fields() {
+ let fields = ["id", "seq_a", "seq_b", "seq_c", "left", "right"]
+ .into_iter()
+ .enumerate()
+ .map(|(index, name)| {
+ DataField::new(
+ index as i32,
+ name.to_string(),
+ DataType::Int(IntType::new()),
+ )
+ })
+ .collect::<Vec<_>>();
+
+ for (first, second, shared) in [
+ ("seq_a", "seq_b,seq_a", "seq_a"),
+ ("seq_a,seq_b", "seq_b,seq_c", "seq_b"),
+ ("seq_a,seq_b", "seq_b,seq_a", "seq_b"),
+ ] {
+ let first_option = format!("fields.{first}.sequence-group");
+ let second_option = format!("fields.{second}.sequence-group");
+ let options = partial_update_options(&[
+ (first_option.as_str(), "left"),
+ (second_option.as_str(), "right"),
+ ]);
+ let config = PartialUpdateConfig::new(&options);
+ let error = config
+ .validated_sequence_groups(&fields, &["id".to_string()])
+ .unwrap_err();
+
+ assert!(
+ matches!(error, crate::Error::ConfigInvalid { ref message }
+ if message.contains(&format!("Sequence field '{shared}'"))
+ && message.contains(&first_option)
+ && message.contains(&second_option)
+ && message.contains("Ordering fields must not
overlap")),
+ "expected overlapping ordering fields to be rejected, got
{error:?}"
+ );
+ }
+ }
+
#[test]
fn test_required_sequence_fields_for_projection() {
let options = partial_update_options(&[