laskoviymishka commented on code in PR #3303:
URL: https://github.com/apache/iceberg-rust/pull/3303#discussion_r4232814866


##########
crates/iceberg/src/arrow/schema.rs:
##########
@@ -1320,6 +1310,17 @@ impl ArrowSchemaVisitor for MetadataStripVisitor {
 /// including nested struct, list, and map fields. This is useful for schema 
comparison
 /// where metadata differences should be ignored.
 ///
+/// Two other differences are normalized away too, so that schemas that differ 
only in
+/// them compare equal once stripped:
+/// - A map's entries field is renamed to [`DEFAULT_MAP_FIELD_NAME`] and made 
non-nullable,
+///   as [`schema_to_arrow_schema`] builds it.
+/// - A dictionary-encoded field becomes a field of the dictionary's value 
type.
+///
+/// All other field names are kept, including a map's key and value fields and 
a list's

Review Comment:
   The normalization is asymmetric in a way that'll surprise callers: we rename 
the map entries field to `key_value` but leave the list element name alone, so 
a `List<item>` from DataFusion or arrow-rs still won't match a table's 
`List<element>`. Same story for map key/value — arrow-rs `MapBuilder` emits 
`keys`/`values` — and the key's required-ness isn't normalized either. The 
motivating datafusion-iceberg INSERT still gets a spurious schema mismatch on 
any list column; it just fails later than the crash used to.
   
   I'd pick one rule: either normalize the list element and map key/value names 
(and the key nullability) to the `schema_to_arrow_schema` set, or keep this 
metadata-only and align names in the caller. The half-and-half is the confusing 
part. If the list-element case is deliberately out of scope for #3297, let's 
say so in the PR so it's tracked rather than reading like an oversight.



##########
crates/iceberg/src/arrow/schema.rs:
##########
@@ -2600,4 +2601,135 @@ mod tests {
         pretty_assertions::assert_eq!(schema, expected);
         assert_eq!(schema.highest_field_id(), 17);
     }
+
+    #[test]
+    fn test_strip_metadata_from_nested_schema() {
+        // The same schema, built with and without metadata on the schema and 
every field.
+        let schema = |with_metadata: bool| {
+            let metadata = |value: &str| {
+                if with_metadata {
+                    HashMap::from([("k".to_string(), value.to_string())])
+                } else {
+                    HashMap::new()
+                }
+            };
+            let field = |name: &str, ty: DataType, nullable: bool| {
+                Field::new(name, ty, nullable).with_metadata(metadata(name))
+            };
+            let element = || Arc::new(field("element", DataType::Int32, 
false));
+            let map = |sorted| {
+                let entries = Fields::from(vec![
+                    field("key", DataType::Utf8, false),
+                    field("value", DataType::List(element()), true),
+                ]);
+                let entries = field(DEFAULT_MAP_FIELD_NAME, 
DataType::Struct(entries), false);
+                DataType::Map(Arc::new(entries), sorted)
+            };
+            let nested = Fields::from(vec![field("m", map(false), true)]);
+            ArrowSchema::new(vec![

Review Comment:
   The fixture's solid, but the original bug was an off-by-one in paired 
push/pop hooks, so the cases most likely to catch a regression are containers 
nested inside another container's push window — `map<string, struct<..>>` (the 
map-value push interleaving with the struct's children) and `list<list<..>>`. 
Both are cheap with the builder closures already here. A negative case would 
help too: the doc claims schemas differing only in `item` vs `element` stay 
unequal, but nothing asserts that a genuine mismatch stays unequal.



##########
crates/iceberg/src/arrow/schema.rs:
##########
@@ -1320,6 +1310,17 @@ impl ArrowSchemaVisitor for MetadataStripVisitor {
 /// including nested struct, list, and map fields. This is useful for schema 
comparison
 /// where metadata differences should be ignored.
 ///
+/// Two other differences are normalized away too, so that schemas that differ 
only in
+/// them compare equal once stripped:
+/// - A map's entries field is renamed to [`DEFAULT_MAP_FIELD_NAME`] and made 
non-nullable,
+///   as [`schema_to_arrow_schema`] builds it.
+/// - A dictionary-encoded field becomes a field of the dictionary's value 
type.

Review Comment:
   While we're listing what gets normalized — stripping also drops 
`ARROW:extension:name`, so a variant column (`arrow.parquet.variant`) collapses 
to a plain `struct<metadata, value>` once stripped. Harmless for the datafusion 
comparison since both sides go through the same strip, but it's now reachable 
for variants nested in lists/maps and nothing here documents or tests it. I'd 
add a bullet noting extension-type metadata is dropped and a variant-in-list 
case to pin it — or preserve `ARROW:extension:*` if we want variants to stay 
distinct from plain structs.



##########
crates/iceberg/src/arrow/schema.rs:
##########
@@ -1190,63 +1194,74 @@ pub(crate) fn 
primitive_type_to_arrow_type_with_ree(primitive_type: &PrimitiveTy
 /// including nested struct, list, and map fields. This is useful for schema 
comparison
 /// where metadata differences should be ignored.
 struct MetadataStripVisitor {
-    /// Stack to track field information during traversal
-    field_stack: Vec<Field>,
+    /// Fields whose types are still being visited, innermost last
+    pending_fields: Vec<PendingField>,
+}
+
+/// What `MetadataStripVisitor` keeps of a field until its type is visited
+struct PendingField {
+    name: String,
+    nullable: bool,
 }
 
 impl MetadataStripVisitor {
     fn new() -> Self {
         Self {
-            field_stack: Vec::new(),
+            pending_fields: Vec::new(),
         }
     }
+
+    /// Records a field whose type is about to be visited.
+    fn start_field(&mut self, field: &FieldRef) -> Result<()> {
+        self.pending_fields.push(PendingField {
+            name: field.name().clone(),
+            nullable: field.is_nullable(),
+        });
+        Ok(())
+    }
+
+    /// Builds the innermost pending field with `data_type` and no metadata.
+    /// `kind` names the type in the error for an unbalanced stack.
+    fn finish_field(&mut self, data_type: DataType, kind: &str) -> 
Result<Field> {

Review Comment:
   `finish_field` catches an underflow, but the opposite — a field left on 
`pending_fields` after traversal — is silent today. A cheap 
`pending_fields.is_empty()` check at the end of `schema()` (returning an error, 
not a panic) would catch leaks as well as underflows and make the 
one-pop-per-node invariant enforce itself. Optional, but it's the kind of thing 
that turns the next regression into a clear error instead of a quietly wrong 
schema.



-- 
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