linliu-code opened a new issue, #11958:
URL: https://github.com/apache/arrow-rs/issues/11958

   **Describe the bug**
   
   When a reader schema is supplied, a writer union with more than two branches 
whose branches reference named types by name (for example `["null", "Bw", 
"Sw"]`, where `Bw` and `Sw` are records defined earlier in the schema) is 
resolved incorrectly: every by-name branch is mapped to reader branch 0. If 
branch 0 is `null`, a non-null value of that union is decoded as null, its 
bytes are not consumed, and the rest of the record (and every following record) 
is decoded from the wrong offset. Decoding then fails with an error that 
depends on the bytes that follow, e.g. `Parser error: bad varint` or `Parser 
error: Union branch index 61 not resolvable by reader schema`.
   
   This happens even when the reader schema is identical to the writer schema. 
Decoding the same bytes without a reader schema is correct.
   
   We found this while reading Apache Hudi metadata-table files, whose 
column-statistics record has `minValue` defined as a union of inline wrapper 
records and `maxValue` as a union referencing the same records by name.
   
   **To Reproduce**
   
   Writer schema (also used as the reader schema):
   
   ```json
   {"type": "record", "name": "Root", "fields": [
     {"name": "minValue", "type": ["null",
       {"type": "record", "name": "Bw", "fields": [{"name": "value", "type": 
"bytes"}]},
       {"type": "record", "name": "Sw", "fields": [{"name": "value", "type": 
"string"}]}]},
     {"name": "maxValue", "type": ["null", "Bw", "Sw"]},
     {"name": "tail", "type": "long"}
   ]}
   ```
   
   Dependencies: `arrow-avro = "60.0.0"`, `apache-avro = "0.22"`.
   
   ```rust
   use std::io::Cursor;
   use apache_avro::{types::Value, Schema as ApacheSchema, Writer};
   use arrow_avro::reader::ReaderBuilder;
   use arrow_avro::schema::AvroSchema;
   
   fn main() {
       let schema_json = r#"{"type": "record", "name": "Root", "fields": [
         {"name": "minValue", "type": ["null",
           {"type": "record", "name": "Bw", "fields": [{"name": "value", 
"type": "bytes"}]},
           {"type": "record", "name": "Sw", "fields": [{"name": "value", 
"type": "string"}]}]},
         {"name": "maxValue", "type": ["null", "Bw", "Sw"]},
         {"name": "tail", "type": "long"}]}"#;
       let schema = ApacheSchema::parse_str(schema_json).unwrap();
       let bw = |v: &[u8]| Value::Union(1, 
Box::new(Value::Record(vec![("value".into(), Value::Bytes(v.to_vec()))])));
       let mut bytes = Vec::new();
       {
           let mut w = Writer::new(&schema, &mut bytes).unwrap();
           for tail in [1i64, 2] {
               w.append_value_ref(&Value::Record(vec![
                   ("minValue".into(), bw(b"a")),
                   ("maxValue".into(), bw(b"zz")),
                   ("tail".into(), Value::Long(tail)),
               ])).unwrap();
           }
           w.flush().unwrap();
       }
   
       // Writer schema only: decodes correctly.
       let mut r = 
ReaderBuilder::new().build(Cursor::new(bytes.clone())).unwrap();
       println!("{:?}", r.next().unwrap().map(|b| b.num_rows()));
   
       // Reader schema == writer schema: fails.
       let mut r = ReaderBuilder::new()
           .with_reader_schema(AvroSchema::new(schema_json.to_string()))
           .build(Cursor::new(bytes))
           .unwrap();
       println!("{:?}", r.next().unwrap().map(|b| b.num_rows()));
   }
   ```
   
   **Expected behavior**
   
   Both reads return the two rows, with `maxValue` decoded as the `Bw` branch 
holding `"zz"` and `tail` = 1, 2.
   
   **Actual behavior**
   
   ```text
   Ok(2)
   Err(AvroError("Parser error: Union branch index 61 not resolvable by reader 
schema"))
   ```
   
   The writer-only read succeeds. The read with the reader schema fails: 
`maxValue`'s `Bw` branch is mapped to the `null` branch, so the 3 bytes 
encoding `"zz"` (length prefix 0x04, then `z`, `z`) are not consumed as 
`maxValue`; `tail` is read from the length prefix (0x04 decodes to 2), and the 
next record starts at the first `z` (`Union branch index 61` is the zig-zag 
decoding of `z`, 0x7a). With other data the error is `Parser error: bad varint`.
   
   Reproduced on `main` (5db01b1fa) and on the 58.4.1 and 60.0.0 releases.
   
   **Root cause**
   
   `Maker::resolve_type` in `arrow-avro/src/codec.rs` has two arms that accept 
a named-type reference against any schema on the other side, without comparing 
names:
   
   
https://github.com/apache/arrow-rs/blob/0b94195639ccbae0d615596cdb11445cde1441b6/arrow-avro/src/codec.rs#L1965-L1966
   
   ```rust
   (Schema::TypeName(TypeName::Ref(_)), _) => self.parse_type(reader_schema, 
namespace),
   (_, Schema::TypeName(TypeName::Ref(_))) => self.parse_type(reader_schema, 
namespace),
   ```
   
   For a union that is not a two-branch nullable union, `resolve_unions` calls 
`find_best_union_match` for each writer branch, which returns the first reader 
branch whose resolution is an exact match:
   
   
https://github.com/apache/arrow-rs/blob/0b94195639ccbae0d615596cdb11445cde1441b6/arrow-avro/src/codec.rs#L1973
   
   For a by-name writer branch, the first reader branch tried (`null`) hits the 
first arm, parses successfully with no resolution info, and is taken as an 
exact match. Branches defined inline are not affected because they go through 
`resolve_records` / `resolve_enums` / `resolve_fixed`, which check names.
   
   A reference should only resolve against a reference or a named definition 
with the same full name (or a matching alias).
   
   **Additional context**
   
   A related case that the name check alone does not address: for a two-branch 
nullable union `["null", "T"]`, `resolve_type` resolves the non-null branches 
directly, and a writer-side reference resolves to the *reader's* definition of 
`T` via `parse_type`, without resolving it against the writer's definition. If 
the writer and reader define the named type `T` differently (schema evolution 
of `T`), values read through the reference are decoded with the reader's layout 
rather than the writer's, which can mis-decode. Fixing that requires access to 
the writer's definition of the referenced type at resolution time, which the 
resolver currently stores only as a parsed `AvroDataType`.
   


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

Reply via email to