1use super::{AvroCodec, AvroOptions};
53use crate::error::FaucetError;
54use apache_avro::schema::{DecimalSchema, RecordField, UnionSchema};
55use apache_avro::types::Value as Av;
56use apache_avro::{Codec, Schema};
57use chrono::{DateTime, NaiveDate, NaiveDateTime, NaiveTime, SecondsFormat, Timelike, Utc};
58use serde_json::{Map, Value, json};
59use std::collections::{HashMap, HashSet};
60
61pub const ORIGINAL_NAME_ATTR: &str = "faucet.name";
63
64pub const INFERRED_RECORD_NAME: &str = "faucet_record";
66
67pub const VALUE_FIELD: &str = "value";
70
71fn codec(c: AvroCodec) -> Codec {
72 match c {
73 AvroCodec::Null => Codec::Null,
74 AvroCodec::Deflate => Codec::Deflate(Default::default()),
75 AvroCodec::Snappy => Codec::Snappy,
76 AvroCodec::Zstd => Codec::Zstandard(Default::default()),
77 }
78}
79
80impl AvroOptions {
81 pub fn parsed_schema(&self) -> Result<Option<Schema>, FaucetError> {
83 self.schema.as_ref().map(parse_schema).transpose()
84 }
85}
86
87pub fn parse_schema(v: &Value) -> Result<Schema, FaucetError> {
89 Schema::parse(v)
90 .map_err(|e| FaucetError::Config(format!("avro.schema is not a valid Avro schema: {e}")))
91}
92
93pub fn decode(bytes: &[u8], opts: &AvroOptions) -> Result<Vec<Value>, FaucetError> {
95 let reader_schema = opts.parsed_schema()?;
96 let mut out = Vec::new();
97 read_records(bytes, reader_schema.as_ref(), usize::MAX, &mut |chunk| {
98 out.extend(chunk);
99 Ok(())
100 })
101 .map_err(|e| FaucetError::Source(format!("avro: {e}")))?;
102 Ok(out)
103}
104
105pub fn writer_schema<R: std::io::Read>(reader: R) -> Result<Schema, FaucetError> {
107 let r = apache_avro::Reader::new(reader)
108 .map_err(|e| FaucetError::Source(format!("avro header: {e}")))?;
109 Ok(r.writer_schema().clone())
110}
111
112pub fn read_records<R: std::io::Read>(
115 reader: R,
116 reader_schema: Option<&Schema>,
117 chunk: usize,
118 f: &mut dyn FnMut(Vec<Value>) -> Result<(), FaucetError>,
119) -> Result<Schema, FaucetError> {
120 read_records_checked(reader, reader_schema, None, chunk, f)
121}
122
123pub(crate) type WriterCheck<'a> = dyn Fn(&Schema) -> Result<(), FaucetError> + 'a;
125
126pub(crate) fn read_records_checked<R: std::io::Read>(
128 reader: R,
129 reader_schema: Option<&Schema>,
130 check: Option<&WriterCheck<'_>>,
131 chunk: usize,
132 f: &mut dyn FnMut(Vec<Value>) -> Result<(), FaucetError>,
133) -> Result<Schema, FaucetError> {
134 read_with(
135 reader,
136 reader_schema,
137 check,
138 chunk,
139 Mode::Record,
140 &mut |chunk, _| f(chunk),
141 )
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub(crate) enum Mode {
148 Record,
149 Arrow,
150}
151
152pub(crate) type ChunkSink<'a> = dyn FnMut(Vec<Value>, &Schema) -> Result<(), FaucetError> + 'a;
154
155pub(crate) fn read_with<R: std::io::Read>(
156 reader: R,
157 reader_schema: Option<&Schema>,
158 check: Option<&WriterCheck<'_>>,
159 chunk: usize,
160 mode: Mode,
161 f: &mut ChunkSink<'_>,
162) -> Result<Schema, FaucetError> {
163 let builder = apache_avro::Reader::builder(reader);
164 let r = match reader_schema {
165 Some(s) => builder.reader_schema(s).build(),
166 None => builder.build(),
167 }
168 .map_err(|e| FaucetError::Source(format!("avro header: {e}")))?;
169 if let Some(check) = check {
170 check(r.writer_schema())?;
171 }
172 let schema = reader_schema
173 .cloned()
174 .unwrap_or_else(|| r.writer_schema().clone());
175 let names = named(&schema);
176 let chunk = chunk.max(1);
177 let mut buf = Vec::with_capacity(chunk.min(4096));
178 for datum in r {
179 let datum = datum.map_err(|e| FaucetError::Source(format!("avro datum: {e}")))?;
180 buf.push(root_to_json(&datum, &schema, &names, mode)?);
181 if buf.len() >= chunk {
182 f(std::mem::take(&mut buf), &schema)?;
183 }
184 }
185 if !buf.is_empty() {
186 f(buf, &schema)?;
187 }
188 Ok(schema)
189}
190
191type Names = HashMap<String, Schema>;
194
195fn named(schema: &Schema) -> Names {
196 let mut out = HashMap::new();
197 collect_named(schema, &mut out);
198 out
199}
200
201fn collect_named(s: &Schema, out: &mut Names) {
202 match s {
203 Schema::Record(r) => {
204 out.insert(r.name.fullname(None), s.clone());
205 out.entry(r.name.name().to_string())
206 .or_insert_with(|| s.clone());
207 for f in &r.fields {
208 collect_named(&f.schema, out);
209 }
210 }
211 Schema::Enum(e) => {
212 out.insert(e.name.fullname(None), s.clone());
213 out.entry(e.name.name().to_string())
214 .or_insert_with(|| s.clone());
215 }
216 Schema::Fixed(x) | Schema::Duration(x) => {
217 out.insert(x.name.fullname(None), s.clone());
218 out.entry(x.name.name().to_string())
219 .or_insert_with(|| s.clone());
220 }
221 Schema::Array(a) => collect_named(&a.items, out),
222 Schema::Map(m) => collect_named(&m.types, out),
223 Schema::Union(u) => u.variants().iter().for_each(|v| collect_named(v, out)),
224 _ => {}
225 }
226}
227
228fn resolve<'a>(s: &'a Schema, names: &'a Names) -> Result<&'a Schema, FaucetError> {
229 match s {
230 Schema::Ref { name } => names
231 .get(&name.fullname(None))
232 .or_else(|| names.get(name.name()))
233 .ok_or_else(|| {
234 FaucetError::Source(format!(
235 "avro: unresolved schema reference `{}`",
236 name.fullname(None)
237 ))
238 }),
239 other => Ok(other),
240 }
241}
242
243fn ref_name(s: &Schema) -> Option<String> {
244 match s {
245 Schema::Ref { name } => Some(name.fullname(None)),
246 Schema::Record(r) => Some(r.name.fullname(None)),
247 _ => None,
248 }
249}
250
251pub(crate) fn is_complex_union(u: &UnionSchema) -> bool {
253 u.variants()
254 .iter()
255 .filter(|v| !matches!(v, Schema::Null))
256 .count()
257 > 1
258}
259
260#[derive(Debug, Clone)]
264pub struct AvroDatumCodec {
265 schema: Schema,
266 names: Names,
267}
268
269impl AvroDatumCodec {
270 pub fn new(schema: Schema) -> Self {
272 let names = named(&schema);
273 Self { schema, names }
274 }
275
276 pub fn schema(&self) -> &Schema {
278 &self.schema
279 }
280
281 pub fn to_json(&self, datum: &Av) -> Result<Value, FaucetError> {
283 to_json(
284 datum,
285 &self.schema,
286 &self.names,
287 Mode::Record,
288 &mut Vec::new(),
289 )
290 }
291
292 pub fn from_json(&self, value: &Value, text: BytesText) -> Result<Av, String> {
294 from_json(value, &self.schema, &self.names, text, "")
295 }
296}
297
298pub fn datum_to_json(datum: &Av, schema: &Schema) -> Result<Value, FaucetError> {
303 let names = named(schema);
304 to_json(datum, schema, &names, Mode::Record, &mut Vec::new())
305}
306
307pub fn datum_from_json(value: &Value, schema: &Schema) -> Result<Av, FaucetError> {
311 datum_from_json_with(value, schema, BytesText::Base64)
312}
313
314#[derive(
316 Debug,
317 Clone,
318 Copy,
319 Default,
320 PartialEq,
321 Eq,
322 serde::Serialize,
323 serde::Deserialize,
324 schemars::JsonSchema,
325)]
326#[serde(rename_all = "snake_case")]
327#[non_exhaustive]
328pub enum BytesText {
329 #[default]
331 Base64,
332 Hex,
334 Utf8,
336}
337
338pub fn datum_from_json_with(
341 value: &Value,
342 schema: &Schema,
343 text: BytesText,
344) -> Result<Av, FaucetError> {
345 let names = named(schema);
346 from_json(value, schema, &names, text, "")
347 .map_err(|e| FaucetError::Sink(format!("json->avro: {e}")))
348}
349
350fn root_to_json(v: &Av, schema: &Schema, names: &Names, mode: Mode) -> Result<Value, FaucetError> {
351 let mut stack = Vec::new();
352 let resolved = resolve(schema, names)?;
353 let j = to_json(v, schema, names, mode, &mut stack)?;
354 Ok(match (resolved, j) {
355 (Schema::Record(_), j @ Value::Object(_)) => j,
356 (_, j) => {
357 let mut m = Map::new();
358 m.insert(VALUE_FIELD.into(), j);
359 Value::Object(m)
360 }
361 })
362}
363
364fn to_json(
365 v: &Av,
366 schema: &Schema,
367 names: &Names,
368 mode: Mode,
369 stack: &mut Vec<String>,
370) -> Result<Value, FaucetError> {
371 let s = resolve(schema, names)?;
372 if mode == Mode::Arrow
373 && let Some(n) = ref_name(schema)
374 && stack.contains(&n)
375 {
376 let inner = to_json(v, s, names, Mode::Record, &mut Vec::new())?;
377 return Ok(Value::String(crate::util::canonical_json(&inner)));
378 }
379 Ok(match (v, s) {
380 (Av::Union(i, inner), Schema::Union(u)) => {
381 let branch = u.variants().get(*i as usize).ok_or_else(|| {
382 FaucetError::Source(format!("avro: union branch {i} out of range"))
383 })?;
384 if mode == Mode::Arrow && is_complex_union(u) {
385 if matches!(**inner, Av::Null) {
386 return Ok(Value::Null);
387 }
388 let j = to_json(inner, branch, names, Mode::Record, &mut Vec::new())?;
389 return Ok(Value::String(crate::util::canonical_json(&j)));
390 }
391 to_json(inner, branch, names, mode, stack)?
392 }
393 (Av::Union(_, inner), other) => to_json(inner, other, names, mode, stack)?,
394 (Av::Null, _) => Value::Null,
395 (Av::Boolean(b), _) => Value::Bool(*b),
396 (Av::Int(i), _) => json!(i),
397 (Av::Long(i), _) => json!(i),
398 (Av::Float(f), _) if mode == Mode::Arrow && !f.is_finite() => {
399 non_finite_text(f64::from(*f))
400 }
401 (Av::Double(f), _) if mode == Mode::Arrow && !f.is_finite() => non_finite_text(*f),
402 (Av::Float(f), _) => crate::fidelity::float32(*f),
403 (Av::Double(f), _) => crate::fidelity::float64(*f),
404 (Av::Bytes(b), _) | (Av::Fixed(_, b), _) => crate::fidelity::binary(b),
405 (Av::String(t), _) => Value::String(t.clone()),
406 (Av::Enum(_, sym), _) => Value::String(sym.clone()),
407 (Av::Array(items), s) => {
408 let item_schema = match s {
409 Schema::Array(a) => &*a.items,
410 _ => s,
411 };
412 Value::Array(
413 items
414 .iter()
415 .map(|i| to_json(i, item_schema, names, mode, stack))
416 .collect::<Result<_, _>>()?,
417 )
418 }
419 (Av::Map(m), s) => {
420 let value_schema = match s {
421 Schema::Map(m) => &*m.types,
422 _ => s,
423 };
424 let mut keys: Vec<&String> = m.keys().collect();
425 keys.sort();
426 let mut out = Map::new();
427 for k in keys {
428 out.insert(k.clone(), to_json(&m[k], value_schema, names, mode, stack)?);
429 }
430 Value::Object(out)
431 }
432 (Av::Record(fields), s) => {
433 let rec = match s {
434 Schema::Record(r) => Some(r),
435 _ => None,
436 };
437 let pushed = rec.map(|r| r.name.fullname(None));
438 if let Some(n) = &pushed {
439 stack.push(n.clone());
440 }
441 let mut out = Map::new();
442 for (name, fv) in fields {
443 let fs = rec
444 .and_then(|r| r.lookup.get(name).map(|i| &r.fields[*i].schema))
445 .unwrap_or(&Schema::Null);
446 out.insert(name.clone(), to_json(fv, fs, names, mode, stack)?);
447 }
448 if pushed.is_some() {
449 stack.pop();
450 }
451 Value::Object(out)
452 }
453 (Av::Date(d), _) => Value::String(date_string(*d)?),
454 (Av::Decimal(d), s) => {
455 let scale = match s {
456 Schema::Decimal(DecimalSchema { scale, .. }) => *scale,
457 _ => 0,
458 };
459 let bytes = <Vec<u8>>::try_from(d)
460 .map_err(|e| FaucetError::Source(format!("avro decimal: {e}")))?;
461 Value::String(decimal_to_string(&bytes, scale))
462 }
463 (Av::BigDecimal(d), _) => Value::String(d.to_string()),
464 (Av::TimeMillis(ms), _) => Value::String(time_string(i64::from(*ms) * 1_000_000, 3)?),
465 (Av::TimeMicros(us), _) => Value::String(time_string(us.saturating_mul(1_000), 6)?),
466 (Av::TimestampMillis(t), _) => {
467 Value::String(utc_string(*t, 1_000_000, SecondsFormat::Millis)?)
468 }
469 (Av::TimestampMicros(t), _) => Value::String(utc_string(*t, 1_000, SecondsFormat::Micros)?),
470 (Av::TimestampNanos(t), _) => Value::String(utc_string(*t, 1, SecondsFormat::Nanos)?),
471 (Av::LocalTimestampMillis(t), _) => Value::String(local_string(*t, 1_000_000, 3)?),
472 (Av::LocalTimestampMicros(t), _) => Value::String(local_string(*t, 1_000, 6)?),
473 (Av::LocalTimestampNanos(t), _) => Value::String(local_string(*t, 1, 9)?),
474 (Av::Duration(d), _) => json!({
475 "months": u32::from(d.months()),
476 "days": u32::from(d.days()),
477 "millis": u32::from(d.millis()),
478 }),
479 (Av::Uuid(u), _) => Value::String(u.to_string()),
480 })
481}
482
483fn non_finite_text(f: f64) -> Value {
486 Value::String(
487 if f.is_nan() {
488 "NaN"
489 } else if f > 0.0 {
490 "Infinity"
491 } else {
492 "-Infinity"
493 }
494 .into(),
495 )
496}
497
498fn unhex(s: &str) -> Result<Vec<u8>, String> {
499 if !s.len().is_multiple_of(2) {
500 return Err(format!("{s:?} is not hex (odd length)"));
501 }
502 (0..s.len())
503 .step_by(2)
504 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| format!("{s:?} is not hex")))
505 .collect()
506}
507
508const EPOCH: NaiveDate = NaiveDate::from_ymd_opt(1970, 1, 1).expect("valid epoch");
509
510fn date_string(days: i32) -> Result<String, FaucetError> {
511 EPOCH
512 .checked_add_signed(chrono::Duration::days(i64::from(days)))
513 .map(|d| d.format("%Y-%m-%d").to_string())
514 .ok_or_else(|| FaucetError::Source(format!("avro date {days} is out of range")))
515}
516
517fn time_string(nanos: i64, digits: usize) -> Result<String, FaucetError> {
518 let secs = u32::try_from(nanos.div_euclid(1_000_000_000)).ok();
519 let frac = nanos.rem_euclid(1_000_000_000) as u32;
520 let t = secs
521 .and_then(|s| NaiveTime::from_num_seconds_from_midnight_opt(s, frac))
522 .ok_or_else(|| FaucetError::Source(format!("avro time {nanos}ns is out of range")))?;
523 Ok(format!(
524 "{}.{:0w$}",
525 t.format("%H:%M:%S"),
526 t.nanosecond() / 10u32.pow(9 - digits as u32),
527 w = digits
528 ))
529}
530
531fn instant(t: i64, nanos_per_unit: i64) -> Option<DateTime<Utc>> {
532 let n = i128::from(t) * i128::from(nanos_per_unit);
533 let secs = i64::try_from(n.div_euclid(1_000_000_000)).ok()?;
534 DateTime::from_timestamp(secs, n.rem_euclid(1_000_000_000) as u32)
535}
536
537fn utc_string(t: i64, nanos_per_unit: i64, fmt: SecondsFormat) -> Result<String, FaucetError> {
538 instant(t, nanos_per_unit)
539 .map(|d| d.to_rfc3339_opts(fmt, true))
540 .ok_or_else(|| FaucetError::Source(format!("avro timestamp {t} is out of range")))
541}
542
543fn local_string(t: i64, nanos_per_unit: i64, digits: usize) -> Result<String, FaucetError> {
544 let d = instant(t, nanos_per_unit)
545 .ok_or_else(|| FaucetError::Source(format!("avro local timestamp {t} is out of range")))?
546 .naive_utc();
547 Ok(format!(
548 "{}.{:0w$}",
549 d.format("%Y-%m-%dT%H:%M:%S"),
550 d.nanosecond() / 10u32.pow(9 - digits as u32),
551 w = digits
552 ))
553}
554
555pub(crate) fn decimal_to_string(bytes: &[u8], scale: usize) -> String {
557 let negative = bytes.first().is_some_and(|b| b & 0x80 != 0);
558 let mut mag: Vec<u8> = if negative {
559 negate(bytes)
560 } else {
561 bytes.to_vec()
562 };
563 let mut digits = Vec::new();
564 while mag.iter().any(|b| *b != 0) {
565 let mut rem: u32 = 0;
566 for b in mag.iter_mut() {
567 let cur = (rem << 8) | u32::from(*b);
568 *b = (cur / 10) as u8;
569 rem = cur % 10;
570 }
571 digits.push(b'0' + rem as u8);
572 }
573 while digits.len() <= scale {
574 digits.push(b'0');
575 }
576 digits.reverse();
577 let (int, frac) = digits.split_at(digits.len() - scale);
578 let mut s = String::new();
579 if negative {
580 s.push('-');
581 }
582 s.push_str(std::str::from_utf8(int).expect("ascii digits"));
583 if scale > 0 {
584 s.push('.');
585 s.push_str(std::str::from_utf8(frac).expect("ascii digits"));
586 }
587 s
588}
589
590fn negate(bytes: &[u8]) -> Vec<u8> {
591 let mut out: Vec<u8> = bytes.iter().map(|b| !b).collect();
592 for b in out.iter_mut().rev() {
593 let (v, carry) = b.overflowing_add(1);
594 *b = v;
595 if !carry {
596 break;
597 }
598 }
599 out
600}
601
602pub(crate) fn decimal_from_str(
608 text: &str,
609 scale: usize,
610 precision: usize,
611) -> Result<Vec<u8>, String> {
612 let t = text.trim();
613 let (negative, body) = match t.strip_prefix('-') {
614 Some(rest) => (true, rest),
615 None => (false, t.strip_prefix('+').unwrap_or(t)),
616 };
617 let (mantissa, exp) = match body.find(['e', 'E']) {
618 Some(i) => (
619 &body[..i],
620 body[i + 1..]
621 .parse::<i64>()
622 .map_err(|_| format!("{text:?} is not a decimal"))?,
623 ),
624 None => (body, 0),
625 };
626 let (int, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
627 if int.is_empty() && frac.is_empty()
628 || !int.bytes().chain(frac.bytes()).all(|b| b.is_ascii_digit())
629 {
630 return Err(format!("{text:?} is not a decimal"));
631 }
632 let mut digits: Vec<u8> = int.bytes().chain(frac.bytes()).map(|b| b - b'0').collect();
633 let out_of_range = || format!("{text:?} is out of range for precision {precision}");
634 let wanted = (int.len() as i64)
635 .checked_add(exp)
636 .and_then(|p| p.checked_add(scale as i64))
637 .ok_or_else(out_of_range)?;
638 match digits.iter().position(|d| *d != 0) {
641 None => digits.clear(),
642 Some(first)
643 if wanted > first as i64 && (wanted - first as i64) as u128 > precision as u128 =>
644 {
645 return Err(format!(
646 "{text:?} needs {} digits, more than the precision {precision}",
647 wanted - first as i64
648 ));
649 }
650 Some(_) => {}
651 }
652 if digits.is_empty() {
653 } else if wanted < 0 {
655 return Err(format!("{text:?} has more than {scale} fractional digits"));
656 } else {
657 let wanted = wanted as usize;
658 if digits.len() > wanted {
659 if digits[wanted..].iter().any(|d| *d != 0) {
660 return Err(format!("{text:?} has more than {scale} fractional digits"));
661 }
662 digits.truncate(wanted);
663 } else {
664 digits.resize(wanted, 0);
665 }
666 }
667 let first = digits.iter().position(|d| *d != 0).unwrap_or(digits.len());
668 let digits = &digits[first..];
669 if digits.len() > precision {
670 return Err(format!(
671 "{text:?} needs {} digits, more than the precision {precision}",
672 digits.len()
673 ));
674 }
675 let mut mag: Vec<u8> = vec![0];
676 for d in digits {
677 let mut carry = u32::from(*d);
678 for b in mag.iter_mut().rev() {
679 let cur = u32::from(*b) * 10 + carry;
680 *b = (cur & 0xff) as u8;
681 carry = cur >> 8;
682 }
683 while carry > 0 {
684 mag.insert(0, (carry & 0xff) as u8);
685 carry >>= 8;
686 }
687 }
688 if mag[0] & 0x80 != 0 {
689 mag.insert(0, 0);
690 }
691 while mag.len() > 1 && mag[0] == 0 && mag[1] & 0x80 == 0 {
692 mag.remove(0);
693 }
694 Ok(if negative && mag.iter().any(|b| *b != 0) {
695 negate(&mag)
696 } else {
697 mag
698 })
699}
700
701fn sign_extend(bytes: Vec<u8>, size: usize) -> Result<Vec<u8>, String> {
702 if bytes.len() > size {
703 return Err(format!(
704 "decimal needs {} bytes, the fixed holds {size}",
705 bytes.len()
706 ));
707 }
708 let fill = if bytes.first().is_some_and(|b| b & 0x80 != 0) {
709 0xff
710 } else {
711 0
712 };
713 let mut out = vec![fill; size - bytes.len()];
714 out.extend(bytes);
715 Ok(out)
716}
717
718pub fn encode(records: &[Value], opts: &AvroOptions) -> Result<Vec<u8>, FaucetError> {
722 let schema = match opts.parsed_schema()? {
723 Some(s) => s,
724 None => infer_schema(records)?,
725 };
726 let names = named(&schema);
727 let mut w = apache_avro::Writer::with_codec(&schema, Vec::new(), codec(opts.codec))
728 .map_err(|e| FaucetError::Sink(format!("avro writer: {e}")))?;
729 for (i, r) in records.iter().enumerate() {
730 let datum = root_from_json(r, &schema, &names)
731 .map_err(|e| FaucetError::Sink(format!("avro: record {i}: {e}")))?;
732 w.append_value_ref(&datum)
733 .map_err(|e| FaucetError::Sink(format!("avro: record {i}: {e}")))?;
734 }
735 w.into_inner()
736 .map_err(|e| FaucetError::Sink(format!("avro writer: {e}")))
737}
738
739fn root_from_json(v: &Value, schema: &Schema, names: &Names) -> Result<Av, String> {
740 let resolved = resolve(schema, names).map_err(|e| e.to_string())?;
741 match resolved {
742 Schema::Record(_) => from_json(v, schema, names, BytesText::Base64, ""),
743 _ => {
744 let inner = v.get(VALUE_FIELD).unwrap_or(v);
745 from_json(inner, schema, names, BytesText::Base64, "")
746 }
747 }
748}
749
750fn at(path: &str) -> String {
751 if path.is_empty() {
752 "the record".into()
753 } else {
754 format!("`{path}`")
755 }
756}
757
758fn from_json(
759 v: &Value,
760 schema: &Schema,
761 names: &Names,
762 text: BytesText,
763 path: &str,
764) -> Result<Av, String> {
765 let s = resolve(schema, names).map_err(|e| e.to_string())?;
766 let want = |what: &str| format!("{}: expected {what}, got {}", at(path), short(v));
767 Ok(match s {
768 Schema::Null => match v {
769 Value::Null => Av::Null,
770 _ => return Err(want("null")),
771 },
772 Schema::Boolean => Av::Boolean(v.as_bool().ok_or_else(|| want("a boolean"))?),
773 Schema::Int => {
774 let i = v.as_i64().ok_or_else(|| want("an integer"))?;
775 Av::Int(
776 i32::try_from(i)
777 .map_err(|_| format!("{}: {i} does not fit an Avro int", at(path)))?,
778 )
779 }
780 Schema::Long => Av::Long(v.as_i64().ok_or_else(|| want("an integer"))?),
781 Schema::Float => Av::Float(float_from(v).ok_or_else(|| want("a number"))? as f32),
782 Schema::Double => Av::Double(float_from(v).ok_or_else(|| want("a number"))?),
783 Schema::Bytes => Av::Bytes(bytes_from(v, text).map_err(|e| format!("{}: {e}", at(path)))?),
784 Schema::String => Av::String(match v {
785 Value::String(t) => t.clone(),
786 Value::Null => return Err(want("a string")),
787 other => super::cell_text(other),
788 }),
789 Schema::Array(a) => {
790 let items = v.as_array().ok_or_else(|| want("an array"))?;
791 Av::Array(
792 items
793 .iter()
794 .enumerate()
795 .map(|(i, x)| from_json(x, &a.items, names, text, &format!("{path}[{i}]")))
796 .collect::<Result<_, _>>()?,
797 )
798 }
799 Schema::Map(m) => {
800 let obj = v.as_object().ok_or_else(|| want("an object"))?;
801 Av::Map(
802 obj.iter()
803 .map(|(k, x)| {
804 Ok((
805 k.clone(),
806 from_json(x, &m.types, names, text, &join(path, k))?,
807 ))
808 })
809 .collect::<Result<_, String>>()?,
810 )
811 }
812 Schema::Record(r) => {
813 let obj = v.as_object().ok_or_else(|| want("an object"))?;
814 let mut fields = Vec::with_capacity(r.fields.len());
815 for f in &r.fields {
816 let key = source_name(f);
817 let fv = obj.get(key).unwrap_or(&Value::Null);
818 let datum = match (fv, accepts_null(&f.schema, names), &f.default) {
819 (Value::Null, false, Some(default)) => {
820 from_json(default, &f.schema, names, text, &join(path, key))?
821 }
822 (Value::Null, false, None) => {
823 return Err(format!(
824 "{}: required field is missing or null",
825 at(&join(path, key))
826 ));
827 }
828 _ => from_json(fv, &f.schema, names, text, &join(path, key))?,
829 };
830 fields.push((f.name.clone(), datum));
831 }
832 Av::Record(fields)
833 }
834 Schema::Union(u) => {
835 let variants = u.variants();
836 if v.is_null()
837 && let Some(i) = variants.iter().position(|b| matches!(b, Schema::Null))
838 {
839 return Ok(Av::Union(i as u32, Box::new(Av::Null)));
840 }
841 let mut last = None;
842 for (i, b) in variants.iter().enumerate() {
843 if matches!(b, Schema::Null) {
844 continue;
845 }
846 match from_json(v, b, names, text, path) {
847 Ok(d) => return Ok(Av::Union(i as u32, Box::new(d))),
848 Err(e) => last = Some(e),
849 }
850 }
851 return Err(last.unwrap_or_else(|| want("a value matching the union")));
852 }
853 Schema::Enum(e) => {
854 let sym = v.as_str().ok_or_else(|| want("an enum symbol"))?;
855 let i =
856 e.symbols.iter().position(|x| x == sym).ok_or_else(|| {
857 format!("{}: {sym:?} is not one of {:?}", at(path), e.symbols)
858 })?;
859 Av::Enum(i as u32, sym.to_string())
860 }
861 Schema::Fixed(x) => {
862 let b = bytes_from(v, text).map_err(|e| format!("{}: {e}", at(path)))?;
863 if b.len() != x.size {
864 return Err(format!(
865 "{}: fixed({}) got {} bytes",
866 at(path),
867 x.size,
868 b.len()
869 ));
870 }
871 Av::Fixed(x.size, b)
872 }
873 Schema::Decimal(d) => {
874 let text = match v {
875 Value::String(t) => t.clone(),
876 Value::Number(n) => n.to_string(),
877 _ => return Err(want("a decimal string or number")),
878 };
879 let bytes = decimal_from_str(&text, d.scale, d.precision)
880 .map_err(|e| format!("{}: {e}", at(path)))?;
881 let bytes = match &d.inner {
882 apache_avro::schema::InnerDecimalSchema::Fixed(f) => {
883 sign_extend(bytes, f.size).map_err(|e| format!("{}: {e}", at(path)))?
884 }
885 _ => bytes,
886 };
887 Av::Decimal(apache_avro::Decimal::from(bytes))
888 }
889 Schema::BigDecimal => {
890 let text = match v {
891 Value::String(t) => t.clone(),
892 Value::Number(n) => n.to_string(),
893 _ => return Err(want("a decimal string or number")),
894 };
895 Av::BigDecimal(
896 text.parse::<apache_avro::BigDecimal>()
897 .map_err(|_| format!("{}: {text:?} is not a decimal", at(path)))?,
898 )
899 }
900 Schema::Uuid(_) => {
901 let t = v.as_str().ok_or_else(|| want("a UUID string"))?;
902 Av::Uuid(
903 apache_avro::Uuid::parse_str(t)
904 .map_err(|_| format!("{}: {t:?} is not a UUID", at(path)))?,
905 )
906 }
907 Schema::Date => Av::Date(match v {
908 Value::String(t) => {
909 let d = NaiveDate::parse_from_str(t, "%Y-%m-%d")
910 .map_err(|_| format!("{}: {t:?} is not YYYY-MM-DD", at(path)))?;
911 i32::try_from((d - EPOCH).num_days())
912 .map_err(|_| format!("{}: {t:?} is out of range", at(path)))?
913 }
914 _ => i32::try_from(v.as_i64().ok_or_else(|| want("a date"))?)
915 .map_err(|_| format!("{}: date out of range", at(path)))?,
916 }),
917 Schema::TimeMillis => Av::TimeMillis(
918 i32::try_from(time_from(v, 1_000_000).map_err(|e| format!("{}: {e}", at(path)))?)
919 .map_err(|_| format!("{}: time out of range", at(path)))?,
920 ),
921 Schema::TimeMicros => {
922 Av::TimeMicros(time_from(v, 1_000).map_err(|e| format!("{}: {e}", at(path)))?)
923 }
924 Schema::TimestampMillis => Av::TimestampMillis(
925 ts_from(v, 1_000_000, true).map_err(|e| format!("{}: {e}", at(path)))?,
926 ),
927 Schema::TimestampMicros => {
928 Av::TimestampMicros(ts_from(v, 1_000, true).map_err(|e| format!("{}: {e}", at(path)))?)
929 }
930 Schema::TimestampNanos => {
931 Av::TimestampNanos(ts_from(v, 1, true).map_err(|e| format!("{}: {e}", at(path)))?)
932 }
933 Schema::LocalTimestampMillis => Av::LocalTimestampMillis(
934 ts_from(v, 1_000_000, false).map_err(|e| format!("{}: {e}", at(path)))?,
935 ),
936 Schema::LocalTimestampMicros => Av::LocalTimestampMicros(
937 ts_from(v, 1_000, false).map_err(|e| format!("{}: {e}", at(path)))?,
938 ),
939 Schema::LocalTimestampNanos => {
940 Av::LocalTimestampNanos(ts_from(v, 1, false).map_err(|e| format!("{}: {e}", at(path)))?)
941 }
942 Schema::Duration(_) => {
943 let part = |k: &str| -> Result<u32, String> {
944 v.get(k)
945 .and_then(Value::as_u64)
946 .and_then(|n| u32::try_from(n).ok())
947 .ok_or_else(|| format!("{}: duration needs an unsigned `{k}`", at(path)))
948 };
949 Av::Duration(apache_avro::Duration::new(
950 apache_avro::Months::new(part("months")?),
951 apache_avro::Days::new(part("days")?),
952 apache_avro::Millis::new(part("millis")?),
953 ))
954 }
955 Schema::Ref { .. } => unreachable!("resolved above"),
956 })
957}
958
959fn source_name(f: &RecordField) -> &str {
960 f.custom_attributes
961 .get(ORIGINAL_NAME_ATTR)
962 .and_then(Value::as_str)
963 .unwrap_or(&f.name)
964}
965
966fn accepts_null(s: &Schema, names: &Names) -> bool {
967 match resolve(s, names) {
968 Ok(Schema::Null) => true,
969 Ok(Schema::Union(u)) => u.is_nullable(),
970 _ => false,
971 }
972}
973
974fn join(path: &str, k: &str) -> String {
975 if path.is_empty() {
976 k.to_string()
977 } else {
978 format!("{path}.{k}")
979 }
980}
981
982fn short(v: &Value) -> String {
983 let s = v.to_string();
984 if s.len() > 60 {
985 format!("{}…", &s[..s.floor_char_boundary(60)])
986 } else {
987 s
988 }
989}
990
991fn float_from(v: &Value) -> Option<f64> {
992 match v {
993 Value::Number(n) => n.as_f64(),
994 Value::String(s) => match s.as_str() {
995 "NaN" => Some(f64::NAN),
996 "Infinity" => Some(f64::INFINITY),
997 "-Infinity" => Some(f64::NEG_INFINITY),
998 _ => None,
999 },
1000 _ => None,
1001 }
1002}
1003
1004fn bytes_from(v: &Value, text: BytesText) -> Result<Vec<u8>, String> {
1005 match v {
1006 Value::String(s) if text == BytesText::Utf8 => Ok(s.as_bytes().to_vec()),
1007 Value::String(s) if text == BytesText::Hex => unhex(s),
1008 Value::String(s) => {
1009 use base64::Engine as _;
1010 base64::engine::general_purpose::STANDARD
1011 .decode(s)
1012 .map_err(|_| format!("{s:?} is not base64"))
1013 }
1014 Value::Array(items) => items
1015 .iter()
1016 .map(|i| {
1017 i.as_u64()
1018 .and_then(|n| u8::try_from(n).ok())
1019 .ok_or_else(|| "byte array holds a non-byte".to_string())
1020 })
1021 .collect(),
1022 other => Err(format!("expected base64 bytes, got {}", short(other))),
1023 }
1024}
1025
1026fn time_from(v: &Value, nanos_per_unit: i64) -> Result<i64, String> {
1027 match v {
1028 Value::String(t) => {
1029 let parsed = NaiveTime::parse_from_str(t, "%H:%M:%S%.f")
1030 .map_err(|_| format!("{t:?} is not HH:MM:SS[.fff]"))?;
1031 let nanos = i64::from(parsed.num_seconds_from_midnight()) * 1_000_000_000
1032 + i64::from(parsed.nanosecond());
1033 Ok(nanos / nanos_per_unit)
1034 }
1035 other => other
1036 .as_i64()
1037 .ok_or_else(|| format!("expected a time, got {}", short(other))),
1038 }
1039}
1040
1041fn ts_from(v: &Value, nanos_per_unit: i64, utc: bool) -> Result<i64, String> {
1042 let t = match v {
1043 Value::String(t) => t,
1044 other => {
1045 return other
1046 .as_i64()
1047 .ok_or_else(|| format!("expected a timestamp, got {}", short(other)));
1048 }
1049 };
1050 let dt: NaiveDateTime = match DateTime::parse_from_rfc3339(t) {
1051 Ok(d) if utc => d.with_timezone(&Utc).naive_utc(),
1052 Ok(d) => d.naive_local(),
1053 Err(_) => [
1054 "%Y-%m-%dT%H:%M:%S%.f",
1055 "%Y-%m-%d %H:%M:%S%.f",
1056 "%Y-%m-%dT%H:%M:%S",
1057 "%Y-%m-%d %H:%M:%S",
1058 ]
1059 .iter()
1060 .find_map(|f| NaiveDateTime::parse_from_str(t, f).ok())
1061 .ok_or_else(|| format!("{t:?} is not an RFC 3339 / ISO 8601 timestamp"))?,
1062 };
1063 let nanos = dt
1064 .and_utc()
1065 .timestamp_nanos_opt()
1066 .map(i128::from)
1067 .unwrap_or_else(|| {
1068 i128::from(dt.and_utc().timestamp()) * 1_000_000_000
1069 + i128::from(dt.and_utc().timestamp_subsec_nanos())
1070 });
1071 i64::try_from(nanos / i128::from(nanos_per_unit)).map_err(|_| format!("{t:?} is out of range"))
1072}
1073
1074pub fn infer_schema(records: &[Value]) -> Result<Schema, FaucetError> {
1078 let json_schema = crate::schema::infer_schema(records);
1079 let order = super::header_union(records);
1080 let avro =
1081 record_json(INFERRED_RECORD_NAME, &json_schema, Some(&order)).map_err(FaucetError::Sink)?;
1082 Schema::parse(&avro)
1083 .map_err(|e| FaucetError::Sink(format!("avro: inferred schema is invalid: {e}")))
1084}
1085
1086fn record_json(name: &str, js: &Value, order: Option<&[String]>) -> Result<Value, String> {
1087 let props = js
1088 .get("properties")
1089 .and_then(Value::as_object)
1090 .cloned()
1091 .unwrap_or_default();
1092 let keys: Vec<String> = match order {
1093 Some(o) => o
1094 .iter()
1095 .filter(|k| props.contains_key(*k))
1096 .cloned()
1097 .collect(),
1098 None => {
1099 let mut k: Vec<String> = props.keys().cloned().collect();
1100 k.sort();
1101 k
1102 }
1103 };
1104 let mut seen: HashMap<String, String> = HashMap::new();
1105 let mut fields = Vec::with_capacity(keys.len());
1106 for key in keys {
1107 let clean = avro_name(&key);
1108 if let Some(prev) = seen.insert(clean.clone(), key.clone()) {
1109 return Err(format!(
1110 "avro: fields {prev:?} and {key:?} both sanitize to the Avro name {clean:?}"
1111 ));
1112 }
1113 let (ty, nullable) = type_json(&props[&key], &format!("{name}_{clean}"))?;
1114 let mut f = Map::new();
1115 f.insert("name".into(), Value::String(clean.clone()));
1116 if nullable {
1117 f.insert("type".into(), json!(["null", ty]));
1118 f.insert("default".into(), Value::Null);
1119 } else {
1120 f.insert("type".into(), ty);
1121 }
1122 if clean != key {
1123 f.insert(ORIGINAL_NAME_ATTR.into(), Value::String(key));
1124 }
1125 fields.push(Value::Object(f));
1126 }
1127 Ok(json!({"type": "record", "name": name, "fields": fields}))
1128}
1129
1130fn type_json(js: &Value, name: &str) -> Result<(Value, bool), String> {
1131 let types: HashSet<&str> = match js.get("type") {
1132 Some(Value::String(t)) => std::iter::once(t.as_str()).collect(),
1133 Some(Value::Array(a)) => a.iter().filter_map(Value::as_str).collect(),
1134 _ => HashSet::new(),
1135 };
1136 let nullable = types.contains("null");
1137 let non_null: Vec<&str> = types.iter().copied().filter(|t| *t != "null").collect();
1138 let ty = match non_null.as_slice() {
1139 [] => return Ok((json!("null"), false)),
1140 ["boolean"] => json!("boolean"),
1141 ["integer"] => json!("long"),
1142 ["number"] => json!("double"),
1143 ["string"] => json!("string"),
1144 ["array"] => {
1145 let items = match js.get("items") {
1146 Some(i) => {
1147 let (t, n) = type_json(i, &format!("{name}_item"))?;
1148 if n { json!(["null", t]) } else { t }
1149 }
1150 None => json!(["null", "string"]),
1151 };
1152 json!({"type": "array", "items": items})
1153 }
1154 ["object"] => record_json(name, js, None)?,
1155 _ => json!("string"),
1156 };
1157 Ok((ty, nullable))
1158}
1159
1160pub fn avro_name(s: &str) -> String {
1162 let mut out: String = s
1163 .chars()
1164 .map(|c| {
1165 if c.is_ascii_alphanumeric() || c == '_' {
1166 c
1167 } else {
1168 '_'
1169 }
1170 })
1171 .collect();
1172 if out.is_empty() || out.starts_with(|c: char| c.is_ascii_digit()) {
1173 out.insert(0, '_');
1174 }
1175 out
1176}
1177
1178#[cfg(feature = "arrow")]
1182pub fn arrow_schema(schema: &Schema) -> Result<arrow::datatypes::SchemaRef, FaucetError> {
1183 use arrow::datatypes::{Field, Schema as ArrowSchema};
1184 let names = named(schema);
1185 let mut stack = Vec::new();
1186 let root = resolve(schema, &names)?;
1187 let fields: Vec<Field> = match root {
1188 Schema::Record(r) => {
1189 stack.push(r.name.fullname(None));
1190 r.fields
1191 .iter()
1192 .map(|f| {
1193 let (dt, nullable) = arrow_type(&f.schema, &names, &mut stack)?;
1194 Ok(Field::new(&f.name, dt, nullable))
1195 })
1196 .collect::<Result<_, FaucetError>>()?
1197 }
1198 other => {
1199 let (dt, nullable) = arrow_type(other, &names, &mut stack)?;
1200 vec![Field::new(VALUE_FIELD, dt, nullable)]
1201 }
1202 };
1203 Ok(std::sync::Arc::new(ArrowSchema::new(fields)))
1204}
1205
1206#[cfg(feature = "arrow")]
1207fn arrow_type(
1208 schema: &Schema,
1209 names: &Names,
1210 stack: &mut Vec<String>,
1211) -> Result<(arrow::datatypes::DataType, bool), FaucetError> {
1212 use arrow::datatypes::{DataType as D, Field, Fields, TimeUnit};
1213 use std::sync::Arc;
1214 if let Some(n) = ref_name(schema)
1215 && stack.contains(&n)
1216 {
1217 return Ok((D::Utf8, true));
1218 }
1219 let s = resolve(schema, names)?;
1220 let utc: Option<Arc<str>> = Some("UTC".into());
1221 Ok(match s {
1222 Schema::Null => (D::Null, true),
1223 Schema::Boolean => (D::Boolean, false),
1224 Schema::Int => (D::Int32, false),
1225 Schema::Long => (D::Int64, false),
1226 Schema::Float => (D::Float32, false),
1227 Schema::Double => (D::Float64, false),
1228 Schema::Bytes => (D::Binary, false),
1229 Schema::String | Schema::Enum(_) | Schema::Uuid(_) | Schema::BigDecimal => (D::Utf8, false),
1230 Schema::Fixed(f) => (
1231 D::FixedSizeBinary(i32::try_from(f.size).unwrap_or(i32::MAX)),
1232 false,
1233 ),
1234 Schema::Decimal(d) => match (u8::try_from(d.precision), i8::try_from(d.scale)) {
1235 (Ok(p), Ok(sc)) if p <= 38 => (D::Decimal128(p, sc), false),
1236 (Ok(p), Ok(sc)) if p <= 76 => (D::Decimal256(p, sc), false),
1237 _ => (D::Utf8, false),
1238 },
1239 Schema::Date => (D::Date32, false),
1240 Schema::TimeMillis => (D::Time32(TimeUnit::Millisecond), false),
1241 Schema::TimeMicros => (D::Time64(TimeUnit::Microsecond), false),
1242 Schema::TimestampMillis => (D::Timestamp(TimeUnit::Millisecond, utc), false),
1243 Schema::TimestampMicros => (D::Timestamp(TimeUnit::Microsecond, utc), false),
1244 Schema::TimestampNanos => (D::Timestamp(TimeUnit::Nanosecond, utc), false),
1245 Schema::LocalTimestampMillis => (D::Timestamp(TimeUnit::Millisecond, None), false),
1246 Schema::LocalTimestampMicros => (D::Timestamp(TimeUnit::Microsecond, None), false),
1247 Schema::LocalTimestampNanos => (D::Timestamp(TimeUnit::Nanosecond, None), false),
1248 Schema::Duration(_) => (
1249 D::Struct(Fields::from(vec![
1250 Field::new("months", D::Int64, false),
1251 Field::new("days", D::Int64, false),
1252 Field::new("millis", D::Int64, false),
1253 ])),
1254 false,
1255 ),
1256 Schema::Array(a) => {
1257 let (dt, n) = arrow_type(&a.items, names, stack)?;
1258 (D::List(Arc::new(Field::new("item", dt, n))), false)
1259 }
1260 Schema::Map(m) => {
1261 let (dt, n) = arrow_type(&m.types, names, stack)?;
1262 let entries = Field::new(
1263 "entries",
1264 D::Struct(Fields::from(vec![
1265 Field::new("key", D::Utf8, false),
1266 Field::new("value", dt, n),
1267 ])),
1268 false,
1269 );
1270 (D::Map(Arc::new(entries), false), false)
1271 }
1272 Schema::Record(r) => {
1273 stack.push(r.name.fullname(None));
1274 let fields = r
1275 .fields
1276 .iter()
1277 .map(|f| {
1278 let (dt, n) = arrow_type(&f.schema, names, stack)?;
1279 Ok(Field::new(&f.name, dt, n))
1280 })
1281 .collect::<Result<Vec<_>, FaucetError>>();
1282 stack.pop();
1283 (D::Struct(Fields::from(fields?)), false)
1284 }
1285 Schema::Union(u) => {
1286 let non_null: Vec<&Schema> = u
1287 .variants()
1288 .iter()
1289 .filter(|v| !matches!(v, Schema::Null))
1290 .collect();
1291 match non_null.as_slice() {
1292 [] => (D::Null, true),
1293 [one] => {
1294 let (dt, _) = arrow_type(one, names, stack)?;
1295 (dt, u.is_nullable())
1296 }
1297 _ => (D::Utf8, u.is_nullable()),
1298 }
1299 }
1300 Schema::Ref { .. } => unreachable!("resolved above"),
1301 })
1302}
1303
1304#[cfg(feature = "arrow")]
1306pub fn read_batches<R: std::io::Read>(
1307 reader: R,
1308 reader_schema: Option<&Schema>,
1309 batch_size: usize,
1310 f: &mut dyn FnMut(arrow::array::RecordBatch) -> Result<(), FaucetError>,
1311) -> Result<(Schema, arrow::datatypes::SchemaRef), FaucetError> {
1312 read_batches_checked(reader, reader_schema, None, batch_size, f)
1313}
1314
1315#[cfg(feature = "arrow")]
1317pub(crate) fn read_batches_checked<R: std::io::Read>(
1318 reader: R,
1319 reader_schema: Option<&Schema>,
1320 check: Option<&WriterCheck<'_>>,
1321 batch_size: usize,
1322 f: &mut dyn FnMut(arrow::array::RecordBatch) -> Result<(), FaucetError>,
1323) -> Result<(Schema, arrow::datatypes::SchemaRef), FaucetError> {
1324 let chunk = if batch_size == 0 {
1325 usize::MAX
1326 } else {
1327 batch_size
1328 };
1329 let mut arrow: Option<arrow::datatypes::SchemaRef> = None;
1330 let schema = read_with(
1331 reader,
1332 reader_schema,
1333 check,
1334 chunk,
1335 Mode::Arrow,
1336 &mut |rows, schema| {
1337 let target = match &arrow {
1338 Some(a) => a.clone(),
1339 None => {
1340 let a = arrow_schema(schema)?;
1341 arrow = Some(a.clone());
1342 a
1343 }
1344 };
1345 f(crate::columnar::values_to_record_batch(&rows, target)?)
1346 },
1347 )?;
1348 let arrow = match arrow {
1349 Some(a) => a,
1350 None => arrow_schema(&schema)?,
1351 };
1352 Ok((schema, arrow))
1353}
1354
1355#[cfg(test)]
1356mod tests {
1357 use super::*;
1358
1359 #[test]
1362 fn complex_union_text_is_canonical_json() {
1363 let schema = Schema::parse_str(
1364 r#"["null", "string", {"type":"record","name":"r","fields":[
1365 {"name":"z","type":"long"},{"name":"a","type":"long"}]}]"#,
1366 )
1367 .unwrap();
1368 let datum = Av::Union(
1369 2,
1370 Box::new(Av::Record(vec![
1371 ("z".into(), Av::Long(1)),
1372 ("a".into(), Av::Long(2)),
1373 ])),
1374 );
1375 let names = named(&schema);
1376 let v = to_json(&datum, &schema, &names, Mode::Arrow, &mut Vec::new()).unwrap();
1377 assert_eq!(v, json!(r#"{"a":2,"z":1}"#));
1378 }
1379
1380 #[test]
1381 fn a_prepared_codec_matches_the_free_functions() {
1382 let schema = Schema::parse_str(
1383 r#"{"type":"record","name":"R","fields":[
1384 {"name":"a","type":"long"},
1385 {"name":"n","type":["null",{"type":"record","name":"In","fields":[{"name":"x","type":"string"}]}]},
1386 {"name":"again","type":["null","In"]}]}"#,
1387 )
1388 .unwrap();
1389 let codec = AvroDatumCodec::new(schema.clone());
1390 assert_eq!(codec.schema(), &schema);
1391 let json = serde_json::json!({"a": 1, "n": {"x": "y"}, "again": {"x": "z"}});
1392 let datum = codec.from_json(&json, BytesText::Hex).unwrap();
1393 assert_eq!(
1394 datum,
1395 datum_from_json_with(&json, &schema, BytesText::Hex).unwrap()
1396 );
1397 assert_eq!(
1398 codec.to_json(&datum).unwrap(),
1399 datum_to_json(&datum, &schema).unwrap()
1400 );
1401 assert_eq!(codec.to_json(&datum).unwrap(), json);
1402 }
1403
1404 #[test]
1405 fn datum_conversion_keeps_logical_types_exact_and_round_trips() {
1406 let schema = Schema::parse_str(
1407 r#"{"type":"record","name":"r","fields":[
1408 {"name":"amount","type":{"type":"bytes","logicalType":"decimal","precision":10,"scale":2}},
1409 {"name":"raw","type":"bytes"},
1410 {"name":"day","type":{"type":"int","logicalType":"date"}}
1411 ]}"#,
1412 )
1413 .unwrap();
1414 let v = json!({"amount": "12.34", "raw": "0aff", "day": "2026-10-07"});
1415 let datum = datum_from_json(&v, &schema).unwrap();
1416 assert_eq!(datum_to_json(&datum, &schema).unwrap(), v);
1417 assert_eq!(
1418 datum_to_json(&Av::Long(7), &Schema::Long).unwrap(),
1419 json!(7),
1420 "a non-record datum is not wrapped"
1421 );
1422 let err = datum_from_json(&json!({"amount": 1}), &schema).unwrap_err();
1423 assert!(
1424 matches!(&err, FaucetError::Sink(m) if m.starts_with("json->avro: ")),
1425 "{err}"
1426 );
1427 }
1428
1429 fn logical_schema() -> Value {
1430 json!({
1431 "type": "record",
1432 "name": "payment",
1433 "namespace": "test",
1434 "fields": [
1435 {"name": "id", "type": "long"},
1436 {"name": "amount", "type": {"type": "bytes", "logicalType": "decimal", "precision": 10, "scale": 2}},
1437 {"name": "fee", "type": {"type": "fixed", "name": "fee_t", "size": 8, "logicalType": "decimal", "precision": 12, "scale": 3}},
1438 {"name": "day", "type": {"type": "int", "logicalType": "date"}},
1439 {"name": "at", "type": {"type": "long", "logicalType": "timestamp-micros"}},
1440 {"name": "at_ms", "type": {"type": "long", "logicalType": "timestamp-millis"}},
1441 {"name": "at_ns", "type": {"type": "long", "logicalType": "timestamp-nanos"}},
1442 {"name": "local", "type": {"type": "long", "logicalType": "local-timestamp-micros"}},
1443 {"name": "local_ms", "type": {"type": "long", "logicalType": "local-timestamp-millis"}},
1444 {"name": "local_ns", "type": {"type": "long", "logicalType": "local-timestamp-nanos"}},
1445 {"name": "t_ms", "type": {"type": "int", "logicalType": "time-millis"}},
1446 {"name": "t_us", "type": {"type": "long", "logicalType": "time-micros"}},
1447 {"name": "uid", "type": {"type": "string", "logicalType": "uuid"}},
1448 {"name": "span", "type": {"type": "fixed", "name": "dur", "size": 12, "logicalType": "duration"}},
1449 {"name": "note", "type": ["null", "string"], "default": null},
1450 {"name": "either", "type": ["null", "long", "string"], "default": null},
1451 {"name": "kind", "type": {"type": "enum", "name": "kind_t", "symbols": ["A", "B"]}},
1452 {"name": "raw", "type": "bytes"},
1453 {"name": "tags", "type": {"type": "array", "items": "string"}},
1454 {"name": "attrs", "type": {"type": "map", "values": "int"}},
1455 {"name": "ratio", "type": "float"},
1456 {"name": "score", "type": "double"},
1457 {"name": "ok", "type": "boolean"},
1458 {"name": "small", "type": "int"},
1459 {"name": "big", "type": {"type": "bytes", "logicalType": "big-decimal"}}
1460 ]
1461 })
1462 }
1463
1464 fn logical_record() -> Value {
1465 json!({
1466 "id": 7,
1467 "amount": "-1234.50",
1468 "fee": "0.125",
1469 "day": "2024-02-29",
1470 "at": "2024-02-29T12:34:56.123456Z",
1471 "at_ms": "2024-02-29T12:34:56.123Z",
1472 "at_ns": "2024-02-29T12:34:56.123456789Z",
1473 "local": "2024-02-29T12:34:56.123456",
1474 "local_ms": "2024-02-29T12:34:56.123",
1475 "local_ns": "2024-02-29T12:34:56.123456789",
1476 "t_ms": "01:02:03.004",
1477 "t_us": "01:02:03.000004",
1478 "uid": "6f1c2b1e-4b8a-4b8e-9a38-3c3f3f0f9e11",
1479 "span": {"months": 1, "days": 2, "millis": 3},
1480 "note": null,
1481 "either": "text",
1482 "kind": "B",
1483 "raw": "AP8Q",
1484 "tags": ["x", "y"],
1485 "attrs": {"a": 1, "b": 2},
1486 "ratio": 0.5,
1487 "score": "NaN",
1488 "ok": true,
1489 "small": 3,
1490 "big": "123456789012345678901234567890.5"
1491 })
1492 }
1493
1494 #[test]
1495 fn logical_types_round_trip_through_an_ocf() {
1496 for codec in [
1497 AvroCodec::Null,
1498 AvroCodec::Deflate,
1499 AvroCodec::Snappy,
1500 AvroCodec::Zstd,
1501 ] {
1502 let opts = AvroOptions {
1503 schema: Some(logical_schema()),
1504 codec,
1505 };
1506 let bytes = encode(&[logical_record()], &opts).expect("encode");
1507 let back = decode(&bytes, &AvroOptions::default()).expect("decode");
1508 let mut expected = logical_record();
1509 expected["score"] = Value::Null;
1510 assert_eq!(back, vec![expected], "{codec:?}");
1511 }
1512 }
1513
1514 #[test]
1515 fn epoch_integers_are_accepted_for_temporal_types() {
1516 let schema = json!({"type": "record", "name": "r", "fields": [
1517 {"name": "d", "type": {"type": "int", "logicalType": "date"}},
1518 {"name": "t", "type": {"type": "long", "logicalType": "timestamp-millis"}},
1519 {"name": "tm", "type": {"type": "int", "logicalType": "time-millis"}}
1520 ]});
1521 let opts = AvroOptions {
1522 schema: Some(schema),
1523 ..Default::default()
1524 };
1525 let bytes = encode(&[json!({"d": 1, "t": 1000, "tm": 1500})], &opts).expect("encode");
1526 let back = decode(&bytes, &AvroOptions::default()).expect("decode");
1527 assert_eq!(
1528 back[0],
1529 json!({"d": "1970-01-02", "t": "1970-01-01T00:00:01.000Z", "tm": "00:00:01.500"})
1530 );
1531 }
1532
1533 #[test]
1534 fn inferred_schema_widens_nullable_nested_and_mixed_fields() {
1535 let recs = vec![
1536 json!({"id": 1, "name": "a", "first-name": "x", "nested": {"k": 1.5}, "mixed": 1, "list": [1, 2], "empty": []}),
1537 json!({"id": 2, "name": null, "first-name": "y", "nested": {"k": 2}, "mixed": "two", "list": [], "empty": []}),
1538 ];
1539 let bytes = encode(&recs, &AvroOptions::default()).expect("encode");
1540 let schema = writer_schema(&bytes[..]).expect("schema");
1541 let text = serde_json::to_string(&schema).expect("json");
1542 assert!(text.contains("\"first_name\""), "{text}");
1543 assert!(text.contains(ORIGINAL_NAME_ATTR), "{text}");
1544 let back = decode(&bytes, &AvroOptions::default()).expect("decode");
1545 assert_eq!(back[0]["id"], json!(1));
1546 assert_eq!(back[1]["name"], Value::Null);
1547 assert_eq!(back[0]["first_name"], json!("x"));
1548 assert_eq!(back[1]["nested"], json!({"k": 2.0}));
1549 assert_eq!(back[0]["mixed"], json!("1"));
1550 assert_eq!(back[1]["mixed"], json!("two"));
1551 assert_eq!(back[0]["list"], json!([1, 2]));
1552 assert_eq!(back[0]["empty"], json!([]));
1553 }
1554
1555 #[test]
1556 fn inference_refuses_colliding_sanitized_names() {
1557 let err =
1558 encode(&[json!({"a-b": 1, "a_b": 2})], &AvroOptions::default()).expect_err("collision");
1559 assert!(err.to_string().contains("sanitize"), "{err}");
1560 }
1561
1562 #[test]
1563 fn avro_names_are_sanitized() {
1564 assert_eq!(avro_name("ok_1"), "ok_1");
1565 assert_eq!(avro_name("first name"), "first_name");
1566 assert_eq!(avro_name("1st"), "_1st");
1567 assert_eq!(avro_name(""), "_");
1568 }
1569
1570 #[test]
1571 fn reader_schema_projects_and_defaults_added_fields() {
1572 let bytes = encode(&[json!({"a": 1, "b": "x"})], &AvroOptions::default()).expect("encode");
1573 let reader = json!({"type": "record", "name": INFERRED_RECORD_NAME, "fields": [
1574 {"name": "b", "type": "string"},
1575 {"name": "c", "type": "long", "default": 9}
1576 ]});
1577 let back = decode(
1578 &bytes,
1579 &AvroOptions {
1580 schema: Some(reader),
1581 ..Default::default()
1582 },
1583 )
1584 .expect("decode");
1585 assert_eq!(back, vec![json!({"b": "x", "c": 9})]);
1586 }
1587
1588 #[test]
1589 fn a_non_record_root_is_wrapped_in_value() {
1590 let opts = AvroOptions {
1591 schema: Some(json!("long")),
1592 ..Default::default()
1593 };
1594 let bytes = encode(&[json!({"value": 5}), json!(6)], &opts).expect("encode");
1595 let back = decode(&bytes, &AvroOptions::default()).expect("decode");
1596 assert_eq!(back, vec![json!({"value": 5}), json!({"value": 6})]);
1597 }
1598
1599 #[test]
1600 fn recursive_records_decode_as_nested_objects() {
1601 let schema = json!({"type": "record", "name": "node", "fields": [
1602 {"name": "v", "type": "long"},
1603 {"name": "next", "type": ["null", "node"], "default": null}
1604 ]});
1605 let rec = json!({"v": 1, "next": {"v": 2, "next": null}});
1606 let opts = AvroOptions {
1607 schema: Some(schema),
1608 ..Default::default()
1609 };
1610 let bytes = encode(std::slice::from_ref(&rec), &opts).expect("encode");
1611 assert_eq!(
1612 decode(&bytes, &AvroOptions::default()).expect("decode"),
1613 vec![rec]
1614 );
1615 }
1616
1617 #[test]
1618 fn encoding_errors_name_the_field() {
1619 let schema = json!({"type": "record", "name": "r", "fields": [
1620 {"name": "n", "type": "int"},
1621 {"name": "req", "type": "string"}
1622 ]});
1623 let opts = AvroOptions {
1624 schema: Some(schema),
1625 ..Default::default()
1626 };
1627 let err = encode(&[json!({"n": "x", "req": "a"})], &opts).expect_err("type");
1628 assert!(err.to_string().contains("`n`"), "{err}");
1629 let err = encode(&[json!({"n": 1})], &opts).expect_err("missing");
1630 assert!(err.to_string().contains("required"), "{err}");
1631 let err = encode(&[json!({"n": 1i64 << 40, "req": "a"})], &opts).expect_err("range");
1632 assert!(err.to_string().contains("does not fit"), "{err}");
1633 }
1634
1635 #[test]
1636 fn invalid_schemas_and_bodies_are_typed_errors() {
1637 let opts = AvroOptions {
1638 schema: Some(json!({"type": "nope"})),
1639 ..Default::default()
1640 };
1641 assert!(matches!(opts.parsed_schema(), Err(FaucetError::Config(_))));
1642 let err = decode(b"not avro", &AvroOptions::default()).expect_err("garbage");
1643 assert!(err.to_string().contains("avro"), "{err}");
1644 }
1645
1646 #[test]
1647 fn decimals_render_and_parse_exactly() {
1648 let cases = [
1649 ("0", 0),
1650 ("-1", 0),
1651 ("12.30", 2),
1652 ("-0.05", 2),
1653 ("170141183460469231731687303715884105727", 0),
1654 ];
1655 for (text, scale) in cases {
1656 let bytes = decimal_from_str(text, scale, 60).expect(text);
1657 assert_eq!(decimal_to_string(&bytes, scale), text, "{text}");
1658 }
1659 assert_eq!(
1660 decimal_to_string(&decimal_from_str("1.5e1", 1, 5).expect("exp"), 1),
1661 "15.0"
1662 );
1663 assert_eq!(
1664 decimal_to_string(&decimal_from_str("1.230", 2, 5).expect("trailing zero"), 2),
1665 "1.23"
1666 );
1667 assert!(decimal_from_str("1.234", 2, 5).is_err());
1668 for huge in ["1e18", "1e9223372036854775807", "-5e400000000000"] {
1670 let err = decimal_from_str(huge, 2, 10).unwrap_err();
1671 assert!(err.contains("precision"), "{huge}: {err}");
1672 }
1673 assert_eq!(
1674 decimal_to_string(&decimal_from_str("0e400000000000", 2, 5).expect("zero"), 2),
1675 "0.00"
1676 );
1677 assert!(decimal_from_str("1e-9223372036854775807", 2, 5).is_err());
1678 assert!(decimal_from_str("123456", 0, 5).is_err());
1679 assert!(decimal_from_str("abc", 0, 5).is_err());
1680 assert!(decimal_from_str("1e-9", 2, 5).is_err());
1681 assert_eq!(decimal_from_str("0e-9", 2, 5).expect("zero"), vec![0]);
1682 assert_eq!(
1683 sign_extend(vec![0xff], 3).expect("neg"),
1684 vec![0xff, 0xff, 0xff]
1685 );
1686 assert!(sign_extend(vec![1, 2, 3], 2).is_err());
1687 }
1688
1689 #[test]
1690 fn value_coercions_and_their_refusals() {
1691 let names = HashMap::new();
1692 assert!(
1693 matches!(from_json(&json!(1), &Schema::String, &names, BytesText::Hex, "s"), Ok(Av::String(s)) if s == "1")
1694 );
1695 assert!(from_json(&Value::Null, &Schema::String, &names, BytesText::Hex, "s").is_err());
1696 assert!(from_json(&json!(1), &Schema::Null, &names, BytesText::Hex, "s").is_err());
1697 assert!(
1698 matches!(from_json(&json!([1, 2]), &Schema::Bytes, &names, BytesText::Hex, "b"), Ok(Av::Bytes(b)) if b == vec![1, 2])
1699 );
1700 assert!(from_json(&json!("abc"), &Schema::Bytes, &names, BytesText::Hex, "b").is_err());
1701 assert!(from_json(&json!("zz"), &Schema::Bytes, &names, BytesText::Hex, "b").is_err());
1702 assert!(from_json(&json!([300]), &Schema::Bytes, &names, BytesText::Hex, "b").is_err());
1703 assert!(from_json(&json!(true), &Schema::Bytes, &names, BytesText::Hex, "b").is_err());
1704 assert!(
1705 from_json(
1706 &json!("Infinity"),
1707 &Schema::Double,
1708 &names,
1709 BytesText::Hex,
1710 "d"
1711 )
1712 .is_ok()
1713 );
1714 assert!(
1715 from_json(
1716 &json!("-Infinity"),
1717 &Schema::Double,
1718 &names,
1719 BytesText::Hex,
1720 "d"
1721 )
1722 .is_ok()
1723 );
1724 assert!(from_json(&json!("x"), &Schema::Double, &names, BytesText::Hex, "d").is_err());
1725 assert!(from_json(&json!("x"), &Schema::Date, &names, BytesText::Hex, "d").is_err());
1726 assert!(
1727 from_json(
1728 &json!("x"),
1729 &Schema::TimestampMicros,
1730 &names,
1731 BytesText::Hex,
1732 "d"
1733 )
1734 .is_err()
1735 );
1736 assert!(
1737 from_json(
1738 &json!("2024-01-01 00:00:00"),
1739 &Schema::TimestampMicros,
1740 &names,
1741 BytesText::Hex,
1742 "d"
1743 )
1744 .is_ok()
1745 );
1746 assert!(
1747 from_json(
1748 &json!("2024-01-01T00:00:00+02:00"),
1749 &Schema::LocalTimestampMicros,
1750 &names,
1751 BytesText::Hex,
1752 "d"
1753 )
1754 .is_ok()
1755 );
1756 assert!(
1757 from_json(
1758 &json!(true),
1759 &Schema::TimeMicros,
1760 &names,
1761 BytesText::Hex,
1762 "d"
1763 )
1764 .is_err()
1765 );
1766 assert!(
1767 from_json(
1768 &json!("bad"),
1769 &Schema::TimeMicros,
1770 &names,
1771 BytesText::Hex,
1772 "d"
1773 )
1774 .is_err()
1775 );
1776 assert!(from_json(&json!(5), &Schema::TimeMicros, &names, BytesText::Hex, "d").is_ok());
1777 assert!(
1778 from_json(
1779 &json!(true),
1780 &Schema::TimestampMicros,
1781 &names,
1782 BytesText::Hex,
1783 "d"
1784 )
1785 .is_err()
1786 );
1787 assert!(
1788 from_json(
1789 &json!({"months": 1}),
1790 &parse_schema(
1791 &json!({"type": "fixed", "name": "d", "size": 12, "logicalType": "duration"})
1792 )
1793 .unwrap(),
1794 &names,
1795 BytesText::Hex,
1796 "d"
1797 )
1798 .is_err()
1799 );
1800 let e = parse_schema(&json!({"type": "enum", "name": "e", "symbols": ["A"]})).unwrap();
1801 assert!(from_json(&json!("Z"), &e, &named(&e), BytesText::Hex, "e").is_err());
1802 let f = parse_schema(&json!({"type": "fixed", "name": "f", "size": 2})).unwrap();
1803 assert!(from_json(&json!("00"), &f, &named(&f), BytesText::Hex, "f").is_err());
1804 let u = parse_schema(&json!({"type": "string", "logicalType": "uuid"})).unwrap();
1805 assert!(from_json(&json!("nope"), &u, &names, BytesText::Hex, "u").is_err());
1806 let bd = parse_schema(&json!({"type": "bytes", "logicalType": "big-decimal"})).unwrap();
1807 assert!(from_json(&json!(1.5), &bd, &names, BytesText::Hex, "b").is_ok());
1808 assert!(from_json(&json!("x"), &bd, &names, BytesText::Hex, "b").is_err());
1809 assert!(from_json(&json!(true), &bd, &names, BytesText::Hex, "b").is_err());
1810 let dec = parse_schema(
1811 &json!({"type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 1}),
1812 )
1813 .unwrap();
1814 assert!(from_json(&json!(1.5), &dec, &names, BytesText::Hex, "d").is_ok());
1815 assert!(from_json(&json!(true), &dec, &names, BytesText::Hex, "d").is_err());
1816 let un = parse_schema(&json!(["long", "boolean"])).unwrap();
1817 assert!(from_json(&json!("x"), &un, &names, BytesText::Hex, "u").is_err());
1818 assert!(from_json(&Value::Null, &un, &names, BytesText::Hex, "u").is_err());
1819 assert!(short(&json!("x".repeat(100))).ends_with('…'));
1820 }
1821
1822 #[test]
1823 fn utf8_bytes_text_reads_strings_as_their_bytes() {
1824 let schema = parse_schema(&json!({"type": "record", "name": "r", "fields": [
1825 {"name": "raw", "type": "bytes"},
1826 {"name": "tag", "type": {"type": "fixed", "name": "t", "size": 4}}
1827 ]}))
1828 .unwrap();
1829 let v = json!({"raw": "cafe", "tag": "abcd"});
1830 assert_eq!(
1831 datum_from_json_with(&v, &schema, BytesText::Utf8).unwrap(),
1832 Av::Record(vec![
1833 ("raw".into(), Av::Bytes(b"cafe".to_vec())),
1834 ("tag".into(), Av::Fixed(4, b"abcd".to_vec())),
1835 ])
1836 );
1837 assert!(datum_from_json(&json!({"raw": "hello", "tag": "abcd"}), &schema).is_err());
1838 assert!(
1839 datum_from_json_with(
1840 &json!({"raw": [1, 2], "tag": "abcd"}),
1841 &schema,
1842 BytesText::Utf8
1843 )
1844 .is_ok()
1845 );
1846 assert_eq!(BytesText::default(), BytesText::Base64);
1847 assert_eq!(
1848 datum_from_json_with(
1849 &json!({"raw": "cafe", "tag": "61626364"}),
1850 &schema,
1851 BytesText::Hex
1852 )
1853 .unwrap(),
1854 Av::Record(vec![
1855 ("raw".into(), Av::Bytes(vec![0xca, 0xfe])),
1856 ("tag".into(), Av::Fixed(4, b"abcd".to_vec())),
1857 ])
1858 );
1859 }
1860
1861 #[test]
1862 fn out_of_range_temporal_values_are_errors() {
1863 assert!(date_string(i32::MAX).is_err());
1864 assert!(time_string(-1, 3).is_err());
1865 assert!(utc_string(i64::MAX, 1_000_000, SecondsFormat::Millis).is_err());
1866 assert!(local_string(i64::MAX, 1_000_000, 3).is_err());
1867 assert_eq!(crate::fidelity::float64(f64::NEG_INFINITY), Value::Null);
1868 }
1869
1870 #[cfg(feature = "arrow")]
1871 #[test]
1872 fn arrow_batches_carry_logical_types() {
1873 use arrow::datatypes::{DataType as D, TimeUnit};
1874 let opts = AvroOptions {
1875 schema: Some(logical_schema()),
1876 ..Default::default()
1877 };
1878 let rec = logical_record();
1879 let bytes = encode(&[rec.clone(), rec], &opts).expect("encode");
1880 let mut batches = Vec::new();
1881 let (_, schema) = read_batches(&bytes[..], None, 1, &mut |b| {
1882 batches.push(b);
1883 Ok(())
1884 })
1885 .expect("batches");
1886 assert_eq!(batches.len(), 2);
1887 let ty = |n: &str| schema.field_with_name(n).expect(n).data_type().clone();
1888 assert_eq!(ty("amount"), D::Decimal128(10, 2));
1889 assert_eq!(ty("day"), D::Date32);
1890 assert_eq!(
1891 ty("at"),
1892 D::Timestamp(TimeUnit::Microsecond, Some("UTC".into()))
1893 );
1894 assert_eq!(ty("local"), D::Timestamp(TimeUnit::Microsecond, None));
1895 assert_eq!(ty("t_ms"), D::Time32(TimeUnit::Millisecond));
1896 assert_eq!(ty("either"), D::Utf8);
1897 assert!(schema.field_with_name("note").unwrap().is_nullable());
1898 let back = crate::columnar::record_batch_to_values(&batches[0]).expect("values");
1899 let amount = batches[0]
1900 .column_by_name("amount")
1901 .and_then(|c| c.as_any().downcast_ref::<arrow::array::Decimal128Array>())
1902 .expect("decimal column");
1903 assert_eq!(amount.value(0), -123_450);
1904 assert_eq!(back[0]["day"], json!("2024-02-29"));
1905 assert_eq!(back[0]["either"], json!("\"text\""));
1906 }
1907
1908 #[cfg(feature = "arrow")]
1909 #[test]
1910 fn arrow_schema_handles_roots_recursion_and_wide_decimals() {
1911 use arrow::datatypes::DataType as D;
1912 let s = parse_schema(&json!("long")).unwrap();
1913 assert_eq!(arrow_schema(&s).unwrap().field(0).name(), VALUE_FIELD);
1914 let rec = parse_schema(&json!({"type": "record", "name": "node", "fields": [
1915 {"name": "next", "type": ["null", "node"], "default": null},
1916 {"name": "wide", "type": {"type": "bytes", "logicalType": "decimal", "precision": 50, "scale": 2}},
1917 {"name": "huge", "type": {"type": "bytes", "logicalType": "decimal", "precision": 90, "scale": 2}},
1918 {"name": "only_null", "type": ["null"]},
1919 {"name": "n", "type": "null"}
1920 ]}))
1921 .unwrap();
1922 let a = arrow_schema(&rec).unwrap();
1923 assert_eq!(a.field(0).data_type(), &D::Utf8);
1924 assert_eq!(a.field(1).data_type(), &D::Decimal256(50, 2));
1925 assert_eq!(a.field(2).data_type(), &D::Utf8);
1926 assert_eq!(a.field(3).data_type(), &D::Null);
1927 let opts = AvroOptions {
1928 schema: Some(json!({"type": "record", "name": "node", "fields": [
1929 {"name": "v", "type": "long"},
1930 {"name": "next", "type": ["null", "node"], "default": null}
1931 ]})),
1932 ..Default::default()
1933 };
1934 let bytes = encode(&[json!({"v": 1, "next": {"v": 2, "next": null}})], &opts).unwrap();
1935 let mut rows = Vec::new();
1936 read_batches(&bytes[..], None, 0, &mut |b| {
1937 rows.extend(crate::columnar::record_batch_to_values(&b)?);
1938 Ok(())
1939 })
1940 .unwrap();
1941 let next: Value = serde_json::from_str(rows[0]["next"].as_str().unwrap()).unwrap();
1942 assert_eq!(next, json!({"v": 2, "next": null}));
1943 }
1944}