use crate::Error;
use serde_json::Value;
#[derive(Debug)]
pub struct WriterSchema {
pub fields: Vec<WriterField>,
pub is_union_wrapped: bool,
}
#[derive(Debug)]
pub struct WriterField {
pub name: String,
pub schema: FieldSchema,
pub nullable: bool,
}
#[derive(Debug)]
pub enum FieldSchema {
Null,
Boolean,
Int,
Long,
Float,
Double,
Bytes,
String,
Fixed(usize),
Enum { symbols_count: usize },
Union(Vec<FieldSchema>),
Array(Box<FieldSchema>),
Map(Box<FieldSchema>),
Record(WriterSchema),
}
impl WriterSchema {
pub fn parse(json: &str) -> crate::Result<Self> {
let value: Value = serde_json::from_str(json).map_err(|e| Error::UnexpectedError {
message: format!("avro schema: invalid JSON: {e}"),
source: None,
})?;
parse_record_from_value(&value)
}
}
fn parse_record_from_value(value: &Value) -> crate::Result<WriterSchema> {
match value {
Value::Object(obj) if obj.get("type").and_then(|t| t.as_str()) == Some("record") => {
let mut ws = parse_record_object(obj)?;
ws.is_union_wrapped = false;
Ok(ws)
}
Value::Array(arr) => {
for item in arr {
if let Value::Object(obj) = item {
if obj.get("type").and_then(|t| t.as_str()) == Some("record") {
let mut ws = parse_record_object(obj)?;
ws.is_union_wrapped = true;
return Ok(ws);
}
}
}
Err(Error::UnexpectedError {
message: "avro schema: union does not contain a record type".into(),
source: None,
})
}
_ => Err(Error::UnexpectedError {
message: "avro schema: expected record or union containing record".into(),
source: None,
}),
}
}
fn parse_record_object(obj: &serde_json::Map<String, Value>) -> crate::Result<WriterSchema> {
let fields_arr = obj
.get("fields")
.and_then(|f| f.as_array())
.ok_or_else(|| Error::UnexpectedError {
message: "avro schema: record missing 'fields' array".into(),
source: None,
})?;
let mut fields = Vec::with_capacity(fields_arr.len());
for field_val in fields_arr.iter() {
let field_obj = field_val
.as_object()
.ok_or_else(|| Error::UnexpectedError {
message: "avro schema: field is not an object".into(),
source: None,
})?;
let name = field_obj
.get("name")
.and_then(|n| n.as_str())
.ok_or_else(|| Error::UnexpectedError {
message: "avro schema: field missing 'name'".into(),
source: None,
})?
.to_string();
let type_val = field_obj
.get("type")
.ok_or_else(|| Error::UnexpectedError {
message: format!("avro schema: field '{name}' missing 'type'"),
source: None,
})?;
let schema = parse_field_schema(type_val)?;
let (schema, nullable) = unwrap_nullable(schema);
fields.push(WriterField {
name,
schema,
nullable,
});
}
Ok(WriterSchema {
fields,
is_union_wrapped: false,
})
}
fn unwrap_nullable(schema: FieldSchema) -> (FieldSchema, bool) {
if let FieldSchema::Union(branches) = &schema {
let has_null = branches.iter().any(|b| matches!(b, FieldSchema::Null));
if has_null && branches.len() == 2 {
let FieldSchema::Union(mut branches) = schema else {
unreachable!()
};
let inner = if matches!(branches[0], FieldSchema::Null) {
branches.swap_remove(1)
} else {
branches.swap_remove(0)
};
return (inner, true);
}
}
(schema, false)
}
fn parse_field_schema(value: &Value) -> crate::Result<FieldSchema> {
match value {
Value::String(s) => match s.as_str() {
"null" => Ok(FieldSchema::Null),
"boolean" => Ok(FieldSchema::Boolean),
"int" => Ok(FieldSchema::Int),
"long" => Ok(FieldSchema::Long),
"float" => Ok(FieldSchema::Float),
"double" => Ok(FieldSchema::Double),
"bytes" => Ok(FieldSchema::Bytes),
"string" => Ok(FieldSchema::String),
_ => Err(Error::UnexpectedError {
message: format!("avro schema: unsupported named type reference: {s}"),
source: None,
}),
},
Value::Object(obj) => {
let type_str = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
match type_str {
"record" => {
let record = parse_record_object(obj)?;
Ok(FieldSchema::Record(record))
}
"array" => {
let items = obj.get("items").ok_or_else(|| Error::UnexpectedError {
message: "avro schema: array missing 'items'".into(),
source: None,
})?;
let item_schema = parse_field_schema(items)?;
Ok(FieldSchema::Array(Box::new(item_schema)))
}
"map" => {
let values = obj.get("values").ok_or_else(|| Error::UnexpectedError {
message: "avro schema: map missing 'values'".into(),
source: None,
})?;
let value_schema = parse_field_schema(values)?;
Ok(FieldSchema::Map(Box::new(value_schema)))
}
"fixed" => {
let size = obj.get("size").and_then(|s| s.as_u64()).ok_or_else(|| {
Error::UnexpectedError {
message: "avro schema: fixed missing 'size'".into(),
source: None,
}
})? as usize;
Ok(FieldSchema::Fixed(size))
}
"long" | "int" => {
if type_str == "long" {
Ok(FieldSchema::Long)
} else {
Ok(FieldSchema::Int)
}
}
"enum" => {
let symbols =
obj.get("symbols")
.and_then(|s| s.as_array())
.ok_or_else(|| Error::UnexpectedError {
message: "avro schema: enum missing 'symbols'".into(),
source: None,
})?;
Ok(FieldSchema::Enum {
symbols_count: symbols.len(),
})
}
_ => Err(Error::UnexpectedError {
message: format!("avro schema: unsupported type: {type_str}"),
source: None,
}),
}
}
Value::Array(arr) => {
let branches: Vec<FieldSchema> = arr
.iter()
.map(parse_field_schema)
.collect::<crate::Result<_>>()?;
Ok(FieldSchema::Union(branches))
}
_ => Err(Error::UnexpectedError {
message: format!("avro schema: unexpected value: {value}"),
source: None,
}),
}
}
pub fn skip_field(
cursor: &mut super::cursor::AvroCursor,
schema: &FieldSchema,
) -> crate::Result<()> {
skip_field_inner(cursor, schema, false)
}
pub fn skip_nullable_field(
cursor: &mut super::cursor::AvroCursor,
schema: &FieldSchema,
nullable: bool,
) -> crate::Result<()> {
skip_field_inner(cursor, schema, nullable)
}
fn skip_field_inner(
cursor: &mut super::cursor::AvroCursor,
schema: &FieldSchema,
nullable: bool,
) -> crate::Result<()> {
if nullable {
let idx = cursor.read_union_index()?;
if idx == 0 {
return Ok(());
}
}
match schema {
FieldSchema::Null => Ok(()),
FieldSchema::Boolean => cursor.skip_raw(1),
FieldSchema::Int | FieldSchema::Long | FieldSchema::Enum { .. } => cursor.skip_long(),
FieldSchema::Float => cursor.skip_raw(4),
FieldSchema::Double => cursor.skip_raw(8),
FieldSchema::Bytes | FieldSchema::String => cursor.skip_bytes(),
FieldSchema::Fixed(size) => cursor.skip_raw(*size),
FieldSchema::Union(branches) => {
let idx = cursor.read_union_index()? as usize;
if idx < branches.len() {
skip_field(cursor, &branches[idx])
} else {
Err(Error::UnexpectedError {
message: format!("avro skip: union index {idx} out of range"),
source: None,
})
}
}
FieldSchema::Array(item_schema) => {
loop {
let count = cursor.read_long()?;
if count == 0 {
break;
}
let count = if count < 0 {
cursor.skip_long()?;
(-count) as usize
} else {
count as usize
};
for _ in 0..count {
skip_field(cursor, item_schema)?;
}
}
Ok(())
}
FieldSchema::Map(value_schema) => {
loop {
let count = cursor.read_long()?;
if count == 0 {
break;
}
let count = if count < 0 {
cursor.skip_long()?;
(-count) as usize
} else {
count as usize
};
for _ in 0..count {
cursor.skip_bytes()?;
skip_field(cursor, value_schema)?;
}
}
Ok(())
}
FieldSchema::Record(record_schema) => {
for field in &record_schema.fields {
skip_field_inner(cursor, &field.schema, field.nullable)?;
}
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_manifest_file_meta_schema() {
let schema_json = r#"["null", {
"type": "record",
"name": "record",
"namespace": "org.apache.paimon.avro.generated",
"fields": [
{"name": "_VERSION", "type": "int"},
{"name": "_FILE_NAME", "type": "string"},
{"name": "_FILE_SIZE", "type": "long"},
{"name": "_NUM_ADDED_FILES", "type": "long"},
{"name": "_NUM_DELETED_FILES", "type": "long"},
{"name": "_PARTITION_STATS", "type": ["null", {
"type": "record",
"name": "record__PARTITION_STATS",
"fields": [
{"name": "_MIN_VALUES", "type": "bytes"},
{"name": "_MAX_VALUES", "type": "bytes"},
{"name": "_NULL_COUNTS", "type": ["null", {"type": "array", "items": ["null", "long"]}], "default": null}
]
}], "default": null},
{"name": "_SCHEMA_ID", "type": "long"}
]
}]"#;
let ws = WriterSchema::parse(schema_json).unwrap();
assert_eq!(ws.fields.len(), 7);
assert!(ws.is_union_wrapped);
assert_eq!(ws.fields[0].name, "_VERSION");
assert_eq!(ws.fields[1].name, "_FILE_NAME");
assert_eq!(ws.fields[6].name, "_SCHEMA_ID");
}
#[test]
fn test_parse_nested_record() {
let schema_json = r#"{"type": "record", "name": "test", "fields": [
{"name": "nested", "type": ["null", {"type": "record", "name": "inner", "fields": [
{"name": "x", "type": "int"}
]}]}
]}"#;
let ws = WriterSchema::parse(schema_json).unwrap();
assert!(!ws.is_union_wrapped);
assert_eq!(ws.fields.len(), 1);
assert!(ws.fields[0].nullable);
match &ws.fields[0].schema {
FieldSchema::Record(inner) => {
assert_eq!(inner.fields.len(), 1);
assert_eq!(inner.fields[0].name, "x");
}
_ => panic!("expected record"),
}
}
}