ewoodbury commented on code in PR #3255:
URL: https://github.com/apache/iceberg-rust/pull/3255#discussion_r4078460073


##########
crates/iceberg/src/arrow/record_batch_transformer.rs:
##########
@@ -192,6 +194,293 @@ pub(crate) enum ColumnSource {
     // post-processing step by using the projection mask.
 }
 
+/// Reconciles a source array to the target type by matching nested fields on
+/// field id rather than position, mirroring iceberg-java's nested readers.
+///
+/// The plan is resolved once per file, when the `BatchTransform` is built, so
+/// applying it per batch is just index lookups and array assembly.
+#[derive(Debug)]
+pub(crate) enum PromotePlan {
+    PassThrough,
+    Cast(DataType),
+    Struct {
+        fields: Fields,
+        children: Vec<ChildPlan>,
+    },
+    List {
+        field: FieldRef,
+        element: Box<PromotePlan>,
+    },
+    LargeList {
+        field: FieldRef,
+        element: Box<PromotePlan>,
+    },
+    Map {
+        field: FieldRef,
+        entry_fields: Fields,
+        entries: Vec<ChildPlan>,
+        sorted: bool,
+    },
+}
+
+#[derive(Debug)]
+pub(crate) enum ChildPlan {
+    FromSource {
+        source_index: usize,
+        plan: PromotePlan,
+    },
+    Null {
+        target_type: DataType,
+    },
+}
+
+impl PromotePlan {
+    fn build(
+        source: &DataType,
+        target: &DataType,
+        snapshot_schema: &IcebergSchema,
+    ) -> Result<Self> {
+        if source == target {
+            return Ok(PromotePlan::PassThrough);
+        }
+        match (source, target) {
+            (DataType::Struct(source_fields), DataType::Struct(target_fields)) 
=> {
+                Ok(PromotePlan::Struct {
+                    children: Self::build_struct_children(
+                        source_fields,
+                        target_fields,
+                        snapshot_schema,
+                    )?,
+                    fields: target_fields.clone(),
+                })
+            }
+            (DataType::List(source_field), DataType::List(target_field)) => 
Ok(PromotePlan::List {
+                element: Box::new(Self::build(
+                    source_field.data_type(),
+                    target_field.data_type(),
+                    snapshot_schema,
+                )?),
+                field: target_field.clone(),
+            }),
+            (DataType::LargeList(source_field), 
DataType::LargeList(target_field)) => {
+                Ok(PromotePlan::LargeList {
+                    element: Box::new(Self::build(
+                        source_field.data_type(),
+                        target_field.data_type(),
+                        snapshot_schema,
+                    )?),
+                    field: target_field.clone(),
+                })
+            }
+            (DataType::Map(source_entries, _), DataType::Map(target_entries, 
sorted)) => {
+                match (source_entries.data_type(), target_entries.data_type()) 
{
+                    (DataType::Struct(source_fields), 
DataType::Struct(target_fields)) => {
+                        Ok(PromotePlan::Map {
+                            entries: Self::build_struct_children(
+                                source_fields,
+                                target_fields,
+                                snapshot_schema,
+                            )?,
+                            entry_fields: target_fields.clone(),
+                            field: target_entries.clone(),
+                            sorted: *sorted,
+                        })
+                    }
+                    _ => Err(Error::new(
+                        ErrorKind::Unexpected,
+                        format!(
+                            "expected struct-typed map entries, got 
{source_entries:?} and {target_entries:?}"
+                        ),
+                    )),
+                }
+            }
+            (_, DataType::Struct(_) | DataType::List(_) | 
DataType::LargeList(_))
+            | (_, DataType::Map(_, _)) => Err(Error::new(
+                ErrorKind::DataInvalid,
+                format!("cannot promote {source:?} to {target:?}"),
+            )),
+            _ => Ok(PromotePlan::Cast(target.clone())),
+        }
+    }
+
+    fn build_struct_children(
+        source_fields: &Fields,
+        target_fields: &Fields,
+        snapshot_schema: &IcebergSchema,
+    ) -> Result<Vec<ChildPlan>> {
+        let mut source_by_id = HashMap::with_capacity(source_fields.len());
+        for (idx, field) in source_fields.iter().enumerate() {
+            if let Some(id) = try_get_field_id_from_metadata(field)? {
+                source_by_id.insert(id, idx);
+            }
+        }
+        // Name mapping only assigns top-level ids. Fully id-less children 
match by
+        // position when types line up. A same-type reorder cannot be detected 
without
+        // ids and stays positional. Any missing id errors instead of nulling 
that child.
+        if !source_fields.is_empty() && source_by_id.len() != 
source_fields.len() {
+            if source_by_id.is_empty()
+                && source_fields.len() == target_fields.len()
+                && source_fields
+                    .iter()
+                    .zip(target_fields.iter())
+                    .all(|(s, t)| s.data_type().equals_datatype(t.data_type()))
+            {

Review Comment:
   Done in 749e98b- The id-less path requires the same child count, and `build` 
checks each pair, so an id-less `struct<x: int>` promotes to `long`. 
`promote_idless_struct_promotes_child_int_to_long` sets that through 
`transform_top_level`. A different count still errors (same for ids on only 
some children).
   
   The description now says that, and the out-of-scope name-mapping line points 
at #1845



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to