This is an automated email from the ASF dual-hosted git repository. github-merge-queue[bot] pushed a commit to branch gh-readonly-queue/main/pr-11730-ef22fc44f3a92b142c1583261418326e565811a9 in repository https://gitbox.apache.org/repos/asf/arrow-rs.git
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));
