This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new 0b94195639 fix(arrow-avro): resolve named types declared with a full 
name (#11730)
0b94195639 is described below

commit 0b94195639ccbae0d615596cdb11445cde1441b6
Author: Sebastian Turner <[email protected]>
AuthorDate: Tue Oct 27 08:54:40 2026 +0000

    fix(arrow-avro): resolve named types declared with a full name (#11730)
    
    # Which issue does this PR close?
    
    - Closes #NNN
    
    # Rationale for this change
    
    Per the Avro spec, a name containing a dot is a full name. Its namespace
    attribute is ignored and nested types inherit the namespace taken from
    the name. The named type resolver in `codec.rs` did not apply this rule
    when registering types, so a type declared as `ns.Status` could not be
    referenced as `ns.Status`, and a type nested in a record named
    `com.example.Event` could not be referenced as `com.example.Kind`. Both
    fail with `Failed to resolve ...`.
    
    # What changes are included in this PR?
    
    - `Resolver::register` splits a dotted name into namespace and name, the
    same way `Resolver::resolve` already does.
    - A small `named_type_namespace` helper returns the namespace of a named
    type, taking it from the name when the name is dotted. It replaces the
    `x.namespace.or(namespace)` expressions for records, enums and fixed
    types in both `parse_type` and the schema resolution paths, so nested
    types inherit the right namespace.
    
    # Are these changes tested?
    
    Yes. `test_named_type_reuse_with_full_name` covers both a dotted enum
    name referenced by its full name and a nested record referenced through
    the namespace of a dotted enclosing record name. It fails on `main` with
    `Failed to resolve enum_ns.Status`.
    
    # Are there any significant user-facing changes?
    
    No API changes. Schemas that use dotted names now parse instead of
    returning an error. For a named type declared with a dotted name, the
    `avro.namespace` field metadata now holds the namespace from that name
    rather than the enclosing one.
    
    Fixes #11729
    
    AI tools were used to find this bug and to write the fix, the test and
    this description. The tests were run locally before and after the
    change.
    
    Signed-off-by: Sebastian Turner <[email protected]>
    Co-authored-by: Jeffrey Vo <[email protected]>
---
 arrow-avro/src/codec.rs | 88 +++++++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 81 insertions(+), 7 deletions(-)

diff --git a/arrow-avro/src/codec.rs b/arrow-avro/src/codec.rs
index 34d5ea9437..97e82cd42c 100644
--- a/arrow-avro/src/codec.rs
+++ b/arrow-avro/src/codec.rs
@@ -1236,7 +1236,10 @@ struct Resolver<'a> {
 
 impl<'a> Resolver<'a> {
     fn register(&mut self, name: &'a str, namespace: Option<&'a str>, schema: 
AvroDataType) {
-        self.map.insert((namespace.unwrap_or(""), name), schema);
+        let (namespace, name) = name
+            .rsplit_once('.')
+            .unwrap_or_else(|| (namespace.unwrap_or(""), name));
+        self.map.insert((namespace, name), schema);
     }
 
     fn resolve(&self, name: &str, namespace: Option<&'a str>) -> 
Result<AvroDataType, ArrowError> {
@@ -1250,6 +1253,23 @@ impl<'a> Resolver<'a> {
     }
 }
 
+/// Returns the namespace of a named type.
+///
+/// A name containing a dot is a full name, so its namespace is taken from the 
name and both
+/// the `namespace` attribute and the enclosing namespace are ignored.
+///
+/// See <https://avro.apache.org/docs/1.11.1/specification/#names>
+fn named_type_namespace<'a>(
+    name: &'a str,
+    namespace: Option<&'a str>,
+    enclosing_ns: Option<&'a str>,
+) -> Option<&'a str> {
+    match name.rsplit_once('.') {
+        Some((ns, _)) => Some(ns),
+        None => namespace.or(enclosing_ns),
+    }
+}
+
 fn full_name_set(name: &str, ns: Option<&str>, aliases: &[&str]) -> 
HashSet<String> {
     let mut out = HashSet::with_capacity(1 + aliases.len());
     let (full, _) = make_full_name(name, ns, None);
@@ -1502,7 +1522,7 @@ impl<'a> Maker<'a> {
             }
             Schema::Complex(c) => match c {
                 ComplexType::Record(r) => {
-                    let namespace = r.namespace.or(namespace);
+                    let namespace = named_type_namespace(r.name, r.namespace, 
namespace);
                     let mut metadata = r.attributes.field_metadata();
                     let fields = r
                         .fields
@@ -1540,7 +1560,7 @@ impl<'a> Maker<'a> {
                     let size = f.size.try_into().map_err(|e| {
                         ArrowError::ParseError(format!("Overflow converting 
size to i32: {e}"))
                     })?;
-                    let namespace = f.namespace.or(namespace);
+                    let namespace = named_type_namespace(f.name, f.namespace, 
namespace);
                     let mut metadata = f.attributes.field_metadata();
                     metadata.insert(AVRO_NAME_METADATA_KEY.to_string(), 
f.name.to_string());
                     if let Some(ns) = namespace {
@@ -1630,7 +1650,7 @@ impl<'a> Maker<'a> {
                     Ok(field)
                 }
                 ComplexType::Enum(e) => {
-                    let namespace = e.namespace.or(namespace);
+                    let namespace = named_type_namespace(e.name, e.namespace, 
namespace);
                     let symbols = e
                         .symbols
                         .iter()
@@ -2224,7 +2244,7 @@ impl<'a> Maker<'a> {
             mapping: Arc::from(mapping),
             default_index,
         }));
-        let reader_ns = reader_enum.namespace.or(namespace);
+        let reader_ns = named_type_namespace(reader_enum.name, 
reader_enum.namespace, namespace);
         self.resolver
             .register(reader_enum.name, reader_ns, dt.clone());
         Ok(dt)
@@ -2272,8 +2292,10 @@ impl<'a> Maker<'a> {
             reader_record.namespace,
             &reader_record.aliases,
         )?;
-        let writer_ns = writer_record.namespace.or(namespace);
-        let reader_ns = reader_record.namespace.or(namespace);
+        let writer_ns =
+            named_type_namespace(writer_record.name, writer_record.namespace, 
namespace);
+        let reader_ns =
+            named_type_namespace(reader_record.name, reader_record.namespace, 
namespace);
         let mut reader_md = reader_record.attributes.field_metadata();
         reader_md.insert(
             AVRO_NAME_METADATA_KEY.to_string(),
@@ -3284,6 +3306,58 @@ mod tests {
         }
     }
 
+    #[test]
+    fn test_named_type_reuse_with_full_name() {
+        // A name containing a dot is a full name, and types nested in a record
+        // with a full name inherit the namespace from that name.
+        let schema_str = r#"
+        {
+          "type": "record",
+          "name": "record_ns.Record",
+          "fields": [
+            {
+              "name": "status",
+              "type": {
+                "type": "enum",
+                "name": "enum_ns.Status",
+                "symbols": ["ACTIVE", "INACTIVE"]
+              }
+            },
+            { "name": "backupStatus", "type": "enum_ns.Status" },
+            {
+              "name": "nested",
+              "type": {
+                "type": "record",
+                "name": "Nested",
+                "fields": [{ "name": "nested_int", "type": "int" }]
+              }
+            },
+            { "name": "nestedAgain", "type": "record_ns.Nested" }
+          ]
+        }
+        "#;
+
+        let schema: Schema = serde_json::from_str(schema_str).unwrap();
+
+        let mut maker = Maker::new(false, false, Tz::default());
+        let avro_data_type = maker.make_data_type(&schema, None, 
None).unwrap();
+
+        let Codec::Struct(fields) = avro_data_type.codec() else {
+            panic!("Top-level schema is not a struct");
+        };
+        assert_eq!(fields.len(), 4);
+        assert!(matches!(fields[1].data_type().codec(), Codec::Enum(_)));
+        assert_eq!(
+            fields[1].data_type().codec().data_type(),
+            fields[0].data_type().codec().data_type()
+        );
+        assert!(matches!(fields[3].data_type().codec(), Codec::Struct(_)));
+        assert_eq!(
+            fields[3].data_type().codec().data_type(),
+            fields[2].data_type().codec().data_type()
+        );
+    }
+
     #[test]
     fn 
test_resolve_from_writer_and_reader_defaults_root_name_for_non_record_reader() {
         let writer_schema = 
Schema::TypeName(TypeName::Primitive(PrimitiveType::String));

Reply via email to