1use crate::codec::AvroFieldBuilder;
147use crate::compression::CompressionCodec;
148use crate::errors::AvroError;
149use crate::schema::{
150 AvroSchema, Fingerprint, FingerprintAlgorithm, FingerprintStrategy, SCHEMA_METADATA_KEY,
151};
152use crate::writer::encoder::{RecordEncoder, RecordEncoderBuilder, write_long};
153use crate::writer::format::{AvroFormat, AvroOcfFormat, AvroSoeFormat};
154use arrow_array::RecordBatch;
155use arrow_schema::{Schema, SchemaRef};
156use bytes::{Bytes, BytesMut};
157use std::io::Write;
158use std::sync::Arc;
159
160mod encoder;
162pub mod format;
164
165#[derive(Debug, Clone)]
176pub struct EncodedRows {
177 data: Bytes,
178 offsets: Vec<usize>,
179}
180
181impl EncodedRows {
182 pub fn new(data: Bytes, offsets: Vec<usize>) -> Self {
187 Self { data, offsets }
188 }
189
190 #[inline]
192 pub fn len(&self) -> usize {
193 self.offsets.len().saturating_sub(1)
194 }
195
196 #[inline]
198 pub fn is_empty(&self) -> bool {
199 self.len() == 0
200 }
201
202 #[inline]
211 pub fn bytes(&self) -> &Bytes {
212 &self.data
213 }
214
215 #[inline]
220 pub fn offsets(&self) -> &[usize] {
221 &self.offsets
222 }
223
224 pub fn row(&self, n: usize) -> Result<Bytes, AvroError> {
256 if n >= self.len() {
257 return Err(AvroError::General(format!(
258 "Row index {n} out of bounds for len {}",
259 self.len()
260 )));
261 }
262 let (start, end) = unsafe {
267 (
268 *self.offsets.get_unchecked(n),
269 *self.offsets.get_unchecked(n + 1),
270 )
271 };
272 if start > end || end > self.data.len() {
273 return Err(AvroError::General(format!(
274 "Invalid row offsets for row {n}: start={start}, end={end}, data_len={}",
275 self.data.len()
276 )));
277 }
278 Ok(self.data.slice(start..end))
279 }
280
281 #[inline]
311 pub fn iter(&self) -> impl ExactSizeIterator<Item = Bytes> + '_ {
312 self.offsets.windows(2).map(|w| self.data.slice(w[0]..w[1]))
313 }
314}
315
316#[derive(Debug, Clone)]
318pub struct WriterBuilder {
319 schema: Schema,
320 codec: Option<CompressionCodec>,
321 row_capacity: Option<usize>,
322 capacity: usize,
323 fingerprint_strategy: Option<FingerprintStrategy>,
324}
325
326impl WriterBuilder {
327 pub fn new(schema: Schema) -> Self {
334 Self {
335 schema,
336 codec: None,
337 row_capacity: None,
338 capacity: 1024,
339 fingerprint_strategy: None,
340 }
341 }
342
343 pub fn with_fingerprint_strategy(mut self, strategy: FingerprintStrategy) -> Self {
346 self.fingerprint_strategy = Some(strategy);
347 self
348 }
349
350 pub fn with_compression(mut self, codec: Option<CompressionCodec>) -> Self {
352 self.codec = codec;
353 self
354 }
355
356 pub fn with_capacity(mut self, capacity: usize) -> Self {
360 self.capacity = capacity;
361 self
362 }
363
364 pub fn with_row_capacity(mut self, capacity: usize) -> Self {
369 self.row_capacity = Some(capacity);
370 self
371 }
372
373 fn prepare_encoder<F: AvroFormat>(&self) -> Result<(Arc<Schema>, RecordEncoder), AvroError> {
374 let avro_schema = match self.schema.metadata.get(SCHEMA_METADATA_KEY) {
375 Some(json) => AvroSchema::new(json.clone()),
376 None => AvroSchema::try_from(&self.schema)?,
377 };
378 let maybe_fingerprint = if F::NEEDS_PREFIX {
379 match &self.fingerprint_strategy {
380 Some(FingerprintStrategy::Id(id)) => Some(Fingerprint::Id(*id)),
381 Some(FingerprintStrategy::Id64(id)) => Some(Fingerprint::Id64(*id)),
382 Some(strategy) => {
383 Some(avro_schema.fingerprint(FingerprintAlgorithm::from(*strategy))?)
384 }
385 None => Some(
386 avro_schema
387 .fingerprint(FingerprintAlgorithm::from(FingerprintStrategy::Rabin))?,
388 ),
389 }
390 } else {
391 None
392 };
393 let mut md = self.schema.metadata().clone();
394 md.insert(
395 SCHEMA_METADATA_KEY.to_string(),
396 avro_schema.clone().json_string,
397 );
398 let schema = Arc::new(Schema::new_with_metadata(self.schema.fields().clone(), md));
399 let avro_root = AvroFieldBuilder::new(&avro_schema.schema()?).build()?;
400 let encoder = RecordEncoderBuilder::new(&avro_root, schema.as_ref())
401 .with_fingerprint(maybe_fingerprint)
402 .build()?;
403 Ok((schema, encoder))
404 }
405
406 pub fn build_encoder<F: AvroFormat>(self) -> Result<Encoder, AvroError> {
411 if F::default().sync_marker().is_some() {
412 return Err(AvroError::InvalidArgument(
413 "Encoder only supports stream formats (no OCF header/sync marker)".to_string(),
414 ));
415 }
416 let (schema, encoder) = self.prepare_encoder::<F>()?;
417 Ok(Encoder {
418 schema,
419 encoder,
420 row_capacity: self.row_capacity,
421 buffer: BytesMut::with_capacity(self.capacity),
422 offsets: vec![0],
423 })
424 }
425
426 pub fn build<W, F>(self, mut writer: W) -> Result<Writer<W, F>, AvroError>
428 where
429 W: Write,
430 F: AvroFormat,
431 {
432 let mut format = F::default();
433 if format.sync_marker().is_none() && !F::NEEDS_PREFIX {
434 return Err(AvroError::InvalidArgument(
435 "AvroBinaryFormat is only supported with Encoder, use build_encoder instead"
436 .to_string(),
437 ));
438 }
439 let (schema, encoder) = self.prepare_encoder::<F>()?;
440 format.start_stream(&mut writer, &schema, self.codec)?;
441 Ok(Writer {
442 writer,
443 schema,
444 format,
445 compression: self.codec,
446 capacity: self.capacity,
447 encoder,
448 })
449 }
450}
451
452#[derive(Debug)]
498pub struct Encoder {
499 schema: SchemaRef,
500 encoder: RecordEncoder,
501 row_capacity: Option<usize>,
502 buffer: BytesMut,
503 offsets: Vec<usize>,
504}
505
506impl Encoder {
507 pub fn encode(&mut self, batch: &RecordBatch) -> Result<(), AvroError> {
509 if batch.schema().fields() != self.schema.fields() {
510 return Err(AvroError::SchemaError(
511 "Schema of RecordBatch differs from Writer schema".to_string(),
512 ));
513 }
514 self.encoder.encode_rows(
515 batch,
516 self.row_capacity.unwrap_or(0),
517 &mut self.buffer,
518 &mut self.offsets,
519 )?;
520 Ok(())
521 }
522
523 pub fn encode_batches(&mut self, batches: &[RecordBatch]) -> Result<(), AvroError> {
525 for b in batches {
526 self.encode(b)?;
527 }
528 Ok(())
529 }
530
531 pub fn flush(&mut self) -> EncodedRows {
535 let data = self.buffer.split().freeze();
536 let mut offsets = Vec::with_capacity(self.offsets.len());
537 offsets.append(&mut self.offsets);
538 self.offsets.push(0);
539 EncodedRows::new(data, offsets)
540 }
541
542 pub fn schema(&self) -> SchemaRef {
547 self.schema.clone()
548 }
549
550 pub fn buffered_len(&self) -> usize {
552 self.offsets.len().saturating_sub(1)
553 }
554}
555
556#[derive(Debug)]
564pub struct Writer<W: Write, F: AvroFormat> {
565 writer: W,
566 schema: SchemaRef,
567 format: F,
568 compression: Option<CompressionCodec>,
569 capacity: usize,
570 encoder: RecordEncoder,
571}
572
573pub type AvroWriter<W> = Writer<W, AvroOcfFormat>;
614
615pub type AvroStreamWriter<W> = Writer<W, AvroSoeFormat>;
647
648impl<W: Write> Writer<W, AvroOcfFormat> {
649 pub fn new(writer: W, schema: Schema) -> Result<Self, AvroError> {
675 WriterBuilder::new(schema).build::<W, AvroOcfFormat>(writer)
676 }
677
678 pub fn sync_marker(&self) -> Option<&[u8; 16]> {
680 self.format.sync_marker()
681 }
682}
683
684impl<W: Write> Writer<W, AvroSoeFormat> {
685 pub fn new(writer: W, schema: Schema) -> Result<Self, AvroError> {
713 WriterBuilder::new(schema).build::<W, AvroSoeFormat>(writer)
714 }
715}
716
717impl<W: Write, F: AvroFormat> Writer<W, F> {
718 pub fn write(&mut self, batch: &RecordBatch) -> Result<(), AvroError> {
720 if batch.schema().fields() != self.schema.fields() {
721 return Err(AvroError::SchemaError(
722 "Schema of RecordBatch differs from Writer schema".to_string(),
723 ));
724 }
725 match self.format.sync_marker() {
726 Some(&sync) => self.write_ocf_block(batch, &sync),
727 None => self.write_stream(batch),
728 }
729 }
730
731 pub fn write_batches(&mut self, batches: &[&RecordBatch]) -> Result<(), AvroError> {
735 for b in batches {
736 self.write(b)?;
737 }
738 Ok(())
739 }
740
741 pub fn finish(&mut self) -> Result<(), AvroError> {
743 self.writer
744 .flush()
745 .map_err(|e| AvroError::IoError(format!("Error flushing writer: {e}"), e))
746 }
747
748 pub fn into_inner(self) -> W {
750 self.writer
751 }
752
753 fn write_ocf_block(&mut self, batch: &RecordBatch, sync: &[u8; 16]) -> Result<(), AvroError> {
754 let mut buf = Vec::<u8>::with_capacity(self.capacity);
755 self.encoder.encode(&mut buf, batch)?;
756 let encoded = match self.compression {
757 Some(codec) => codec.compress(&buf)?,
758 None => buf,
759 };
760 write_long(&mut self.writer, batch.num_rows() as i64)?;
761 write_long(&mut self.writer, encoded.len() as i64)?;
762 self.writer
763 .write_all(&encoded)
764 .map_err(|e| AvroError::IoError(format!("Error writing Avro block: {e}"), e))?;
765 self.writer
766 .write_all(sync)
767 .map_err(|e| AvroError::IoError(format!("Error writing Avro sync: {e}"), e))?;
768 Ok(())
769 }
770
771 fn write_stream(&mut self, batch: &RecordBatch) -> Result<(), AvroError> {
772 self.encoder.encode(&mut self.writer, batch)?;
773 Ok(())
774 }
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::compression::CompressionCodec;
781 use crate::reader::ReaderBuilder;
782 use crate::schema::AVRO_NAME_METADATA_KEY;
783 use crate::schema::{AvroSchema, SchemaStore};
784 use crate::test_util::arrow_test_data;
785 use arrow::datatypes::TimeUnit;
786 use arrow::util::pretty::pretty_format_batches;
787 #[cfg(not(feature = "avro_custom_types"))]
788 use arrow_array::Float32Array;
789 #[cfg(feature = "avro_custom_types")]
790 use arrow_array::RunArray;
791 use arrow_array::builder::{Int32Builder, ListBuilder};
792 use arrow_array::cast::AsArray;
793 #[cfg(feature = "avro_custom_types")]
794 use arrow_array::types::{Int16Type, Int64Type};
795 use arrow_array::types::{
796 Int32Type, Time32MillisecondType, Time64MicrosecondType, TimestampMicrosecondType,
797 TimestampMillisecondType, TimestampNanosecondType,
798 };
799 use arrow_array::{
800 Array, ArrayRef, BinaryArray, BooleanArray, Date32Array, Date64Array, Float16Array,
801 Int8Array, Int16Array, Int32Array, Int64Array, IntervalDayTimeArray,
802 IntervalMonthDayNanoArray, IntervalYearMonthArray, PrimitiveArray, RecordBatch,
803 StringArray, StructArray, Time32MillisecondArray, Time32SecondArray,
804 Time64MicrosecondArray, Time64NanosecondArray, TimestampMillisecondArray,
805 TimestampSecondArray, UInt8Array, UInt16Array, UInt32Array, UInt64Array, UnionArray,
806 };
807 use arrow_buffer::{IntervalDayTime, IntervalMonthDayNano};
808 #[cfg(not(feature = "avro_custom_types"))]
809 use arrow_schema::{DataType, Field, Schema};
810 #[cfg(feature = "avro_custom_types")]
811 use arrow_schema::{DataType, Field, Schema};
812 use arrow_schema::{IntervalUnit, UnionMode};
813 use bytes::BytesMut;
814 use half::f16;
815 use serde_json::{Value, json};
816 use std::collections::HashMap;
817 use std::collections::HashSet;
818 use std::fs::File;
819 use std::io::{BufReader, Cursor};
820 use std::path::PathBuf;
821 use std::sync::Arc;
822 use tempfile::NamedTempFile;
823
824 fn files() -> impl Iterator<Item = &'static str> {
825 [
826 #[cfg(feature = "snappy")]
828 "avro/alltypes_plain.avro",
829 #[cfg(all(feature = "snappy", not(miri)))]
831 "avro/alltypes_plain.snappy.avro",
832 #[cfg(all(feature = "zstd", not(miri)))]
833 "avro/alltypes_plain.zstandard.avro",
834 #[cfg(all(feature = "bzip2", not(miri)))]
835 "avro/alltypes_plain.bzip2.avro",
836 #[cfg(all(feature = "xz", not(miri)))]
837 "avro/alltypes_plain.xz.avro",
838 ]
839 .into_iter()
840 }
841
842 fn make_schema() -> Schema {
843 Schema::new(vec![
844 Field::new("id", DataType::Int32, false),
845 Field::new("name", DataType::Binary, false),
846 ])
847 }
848
849 fn make_batch() -> RecordBatch {
850 let ids = Int32Array::from(vec![1, 2, 3]);
851 let names = BinaryArray::from_vec(vec![b"a".as_ref(), b"b".as_ref(), b"c".as_ref()]);
852 RecordBatch::try_new(
853 Arc::new(make_schema()),
854 vec![Arc::new(ids) as ArrayRef, Arc::new(names) as ArrayRef],
855 )
856 .expect("failed to build test RecordBatch")
857 }
858
859 #[test]
860 fn test_stream_writer_writes_prefix_per_row_rt() -> Result<(), AvroError> {
861 let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
862 let batch = RecordBatch::try_new(
863 Arc::new(schema.clone()),
864 vec![Arc::new(Int32Array::from(vec![10, 20])) as ArrayRef],
865 )?;
866 let buf: Vec<u8> = Vec::new();
867 let mut writer = AvroStreamWriter::new(buf, schema.clone())?;
868 writer.write(&batch)?;
869 let encoded = writer.into_inner();
870 let mut store = SchemaStore::new(); let avro_schema = AvroSchema::try_from(&schema)?;
872 let _fp = store.register(avro_schema)?;
873 let mut decoder = ReaderBuilder::new()
874 .with_writer_schema_store(store)
875 .build_decoder()?;
876 let _consumed = decoder.decode(&encoded)?;
877 let decoded = decoder
878 .flush()?
879 .expect("expected at least one batch from decoder");
880 assert_eq!(decoded.num_columns(), 1);
881 assert_eq!(decoded.num_rows(), 2);
882 let col = decoded.column(0).as_primitive::<Int32Type>();
883 assert_eq!(col, &Int32Array::from(vec![10, 20]));
884 Ok(())
885 }
886
887 #[test]
888 fn test_nullable_struct_with_nonnullable_field_sliced_encoding() {
889 use arrow_array::{ArrayRef, Int32Array, StringArray, StructArray};
890 use arrow_buffer::NullBuffer;
891 use arrow_schema::{DataType, Field, Fields, Schema};
892 use std::sync::Arc;
893 let inner_fields = Fields::from(vec![
894 Field::new("id", DataType::Int32, false), Field::new("name", DataType::Utf8, true), ]);
897 let inner_struct_type = DataType::Struct(inner_fields.clone());
898 let schema = Schema::new(vec![
899 Field::new("before", inner_struct_type.clone(), true), Field::new("after", inner_struct_type.clone(), true), Field::new("op", DataType::Utf8, false), ]);
903 let before_ids = Int32Array::from(vec![None, None]);
904 let before_names = StringArray::from(vec![None::<&str>, None]);
905 let before_struct = StructArray::new(
906 inner_fields.clone(),
907 vec![
908 Arc::new(before_ids) as ArrayRef,
909 Arc::new(before_names) as ArrayRef,
910 ],
911 Some(NullBuffer::from(vec![false, false])),
912 );
913 let after_ids = Int32Array::from(vec![1, 2]); let after_names = StringArray::from(vec![Some("Alice"), Some("Bob")]);
915 let after_struct = StructArray::new(
916 inner_fields.clone(),
917 vec![
918 Arc::new(after_ids) as ArrayRef,
919 Arc::new(after_names) as ArrayRef,
920 ],
921 Some(NullBuffer::from(vec![true, true])),
922 );
923 let op_col = StringArray::from(vec!["r", "r"]);
924 let batch = RecordBatch::try_new(
925 Arc::new(schema.clone()),
926 vec![
927 Arc::new(before_struct) as ArrayRef,
928 Arc::new(after_struct) as ArrayRef,
929 Arc::new(op_col) as ArrayRef,
930 ],
931 )
932 .expect("failed to create test batch");
933 let mut sink = Vec::new();
934 let mut writer = WriterBuilder::new(schema)
935 .with_fingerprint_strategy(FingerprintStrategy::Id(1))
936 .build::<_, AvroSoeFormat>(&mut sink)
937 .expect("failed to create writer");
938 for row_idx in 0..batch.num_rows() {
939 let single_row = batch.slice(row_idx, 1);
940 let after_col = single_row.column(1);
941 assert_eq!(
942 after_col.null_count(),
943 0,
944 "after column should have no nulls in sliced row"
945 );
946 writer
947 .write(&single_row)
948 .unwrap_or_else(|e| panic!("Failed to encode row {row_idx}: {e}"));
949 }
950 writer.finish().expect("failed to finish writer");
951 assert!(!sink.is_empty(), "encoded output should not be empty");
952 }
953
954 #[test]
955 fn test_nullable_struct_with_decimal_and_timestamp_sliced() {
956 use arrow_array::{
957 ArrayRef, Decimal128Array, Int32Array, StringArray, StructArray,
958 TimestampMicrosecondArray,
959 };
960 use arrow_buffer::NullBuffer;
961 use arrow_schema::{DataType, Field, Fields, Schema};
962 use std::sync::Arc;
963 let row_fields = Fields::from(vec![
964 Field::new("id", DataType::Int32, false),
965 Field::new("name", DataType::Utf8, true),
966 Field::new("category", DataType::Utf8, true),
967 Field::new("price", DataType::Decimal128(10, 2), true),
968 Field::new("stock_quantity", DataType::Int32, true),
969 Field::new(
970 "created_at",
971 DataType::Timestamp(TimeUnit::Microsecond, None),
972 true,
973 ),
974 ]);
975 let row_struct_type = DataType::Struct(row_fields.clone());
976 let schema = Schema::new(vec![
977 Field::new("before", row_struct_type.clone(), true),
978 Field::new("after", row_struct_type.clone(), true),
979 Field::new("op", DataType::Utf8, false),
980 ]);
981 let before_struct = StructArray::new_null(row_fields.clone(), 2);
982 let ids = Int32Array::from(vec![1, 2]);
983 let names = StringArray::from(vec![Some("Widget"), Some("Gadget")]);
984 let categories = StringArray::from(vec![Some("Electronics"), Some("Electronics")]);
985 let prices = Decimal128Array::from(vec![Some(1999), Some(2999)])
986 .with_precision_and_scale(10, 2)
987 .unwrap();
988 let quantities = Int32Array::from(vec![Some(100), Some(50)]);
989 let timestamps = TimestampMicrosecondArray::from(vec![
990 Some(1700000000000000i64),
991 Some(1700000001000000i64),
992 ]);
993 let after_struct = StructArray::new(
994 row_fields.clone(),
995 vec![
996 Arc::new(ids) as ArrayRef,
997 Arc::new(names) as ArrayRef,
998 Arc::new(categories) as ArrayRef,
999 Arc::new(prices) as ArrayRef,
1000 Arc::new(quantities) as ArrayRef,
1001 Arc::new(timestamps) as ArrayRef,
1002 ],
1003 Some(NullBuffer::from(vec![true, true])),
1004 );
1005 let op_col = StringArray::from(vec!["r", "r"]);
1006 let batch = RecordBatch::try_new(
1007 Arc::new(schema.clone()),
1008 vec![
1009 Arc::new(before_struct) as ArrayRef,
1010 Arc::new(after_struct) as ArrayRef,
1011 Arc::new(op_col) as ArrayRef,
1012 ],
1013 )
1014 .expect("failed to create products batch");
1015 let mut sink = Vec::new();
1016 let mut writer = WriterBuilder::new(schema)
1017 .with_fingerprint_strategy(FingerprintStrategy::Id(1))
1018 .build::<_, AvroSoeFormat>(&mut sink)
1019 .expect("failed to create writer");
1020 for row_idx in 0..batch.num_rows() {
1022 let single_row = batch.slice(row_idx, 1);
1023 writer
1024 .write(&single_row)
1025 .unwrap_or_else(|e| panic!("Failed to encode product row {row_idx}: {e}"));
1026 }
1027 writer.finish().expect("failed to finish writer");
1028 assert!(!sink.is_empty());
1029 }
1030
1031 #[test]
1032 fn non_nullable_child_in_nullable_struct_should_encode_per_row() {
1033 use arrow_array::{
1034 ArrayRef, Int32Array, Int64Array, RecordBatch, StringArray, StructArray,
1035 };
1036 use arrow_schema::{DataType, Field, Fields, Schema};
1037 use std::sync::Arc;
1038 let row_fields = Fields::from(vec![
1039 Field::new("id", DataType::Int32, false),
1040 Field::new("name", DataType::Utf8, true),
1041 ]);
1042 let row_struct_dt = DataType::Struct(row_fields.clone());
1043 let before: ArrayRef = Arc::new(StructArray::new_null(row_fields.clone(), 1));
1044 let id_col: ArrayRef = Arc::new(Int32Array::from(vec![1]));
1045 let name_col: ArrayRef = Arc::new(StringArray::from(vec![None::<&str>]));
1046 let after: ArrayRef = Arc::new(StructArray::new(
1047 row_fields.clone(),
1048 vec![id_col, name_col],
1049 None,
1050 ));
1051 let schema = Arc::new(Schema::new(vec![
1052 Field::new("before", row_struct_dt.clone(), true),
1053 Field::new("after", row_struct_dt, true),
1054 Field::new("op", DataType::Utf8, false),
1055 Field::new("ts_ms", DataType::Int64, false),
1056 ]));
1057 let op = Arc::new(StringArray::from(vec!["r"])) as ArrayRef;
1058 let ts_ms = Arc::new(Int64Array::from(vec![1732900000000_i64])) as ArrayRef;
1059 let batch = RecordBatch::try_new(schema.clone(), vec![before, after, op, ts_ms]).unwrap();
1060 let mut buf = Vec::new();
1061 let mut writer = WriterBuilder::new(schema.as_ref().clone())
1062 .build::<_, AvroSoeFormat>(&mut buf)
1063 .unwrap();
1064 let single = batch.slice(0, 1);
1065 let res = writer.write(&single);
1066 assert!(
1067 res.is_ok(),
1068 "expected to encode successfully, got: {:?}",
1069 res.err()
1070 );
1071 }
1072
1073 #[test]
1074 fn test_union_nonzero_type_ids() -> Result<(), AvroError> {
1075 use arrow_array::UnionArray;
1076 use arrow_buffer::Buffer;
1077 use arrow_schema::UnionFields;
1078 let union_fields = UnionFields::try_new(
1079 vec![2, 5],
1080 vec![
1081 Field::new("v_str", DataType::Utf8, true),
1082 Field::new("v_int", DataType::Int32, true),
1083 ],
1084 )
1085 .unwrap();
1086 let strings = StringArray::from(vec!["hello", "world"]);
1087 let ints = Int32Array::from(vec![10, 20, 30]);
1088 let type_ids = Buffer::from_slice_ref([2_i8, 5, 5, 2, 5]);
1089 let offsets = Buffer::from_slice_ref([0_i32, 0, 1, 1, 2]);
1090 let union_array = UnionArray::try_new(
1091 union_fields.clone(),
1092 type_ids.into(),
1093 Some(offsets.into()),
1094 vec![Arc::new(strings) as ArrayRef, Arc::new(ints) as ArrayRef],
1095 )?;
1096 let schema = Schema::new(vec![Field::new(
1097 "union_col",
1098 DataType::Union(union_fields, UnionMode::Dense),
1099 false,
1100 )]);
1101 let batch = RecordBatch::try_new(
1102 Arc::new(schema.clone()),
1103 vec![Arc::new(union_array) as ArrayRef],
1104 )?;
1105 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
1106 assert!(
1107 writer.write(&batch).is_ok(),
1108 "Expected no error from writing"
1109 );
1110 writer.finish()?;
1111 assert!(
1112 writer.finish().is_ok(),
1113 "Expected no error from finishing writer"
1114 );
1115 Ok(())
1116 }
1117
1118 #[test]
1119 fn test_stream_writer_with_id_fingerprint_rt() -> Result<(), AvroError> {
1120 let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
1121 let batch = RecordBatch::try_new(
1122 Arc::new(schema.clone()),
1123 vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
1124 )?;
1125 let schema_id: u32 = 42;
1126 let mut writer = WriterBuilder::new(schema.clone())
1127 .with_fingerprint_strategy(FingerprintStrategy::Id(schema_id))
1128 .build::<_, AvroSoeFormat>(Vec::new())?;
1129 writer.write(&batch)?;
1130 let encoded = writer.into_inner();
1131 let mut store = SchemaStore::new_with_type(FingerprintAlgorithm::Id);
1132 let avro_schema = AvroSchema::try_from(&schema)?;
1133 let _ = store.set(Fingerprint::Id(schema_id), avro_schema)?;
1134 let mut decoder = ReaderBuilder::new()
1135 .with_writer_schema_store(store)
1136 .build_decoder()?;
1137 let _ = decoder.decode(&encoded)?;
1138 let decoded = decoder
1139 .flush()?
1140 .expect("expected at least one batch from decoder");
1141 assert_eq!(decoded.num_columns(), 1);
1142 assert_eq!(decoded.num_rows(), 3);
1143 let col = decoded.column(0).as_primitive::<Int32Type>();
1144 assert_eq!(col, &Int32Array::from(vec![1, 2, 3]));
1145 Ok(())
1146 }
1147
1148 #[test]
1149 fn test_stream_writer_with_id64_fingerprint_rt() -> Result<(), AvroError> {
1150 let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
1151 let batch = RecordBatch::try_new(
1152 Arc::new(schema.clone()),
1153 vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
1154 )?;
1155 let schema_id: u64 = 42;
1156 let mut writer = WriterBuilder::new(schema.clone())
1157 .with_fingerprint_strategy(FingerprintStrategy::Id64(schema_id))
1158 .build::<_, AvroSoeFormat>(Vec::new())?;
1159 writer.write(&batch)?;
1160 let encoded = writer.into_inner();
1161 let mut store = SchemaStore::new_with_type(FingerprintAlgorithm::Id64);
1162 let avro_schema = AvroSchema::try_from(&schema)?;
1163 let _ = store.set(Fingerprint::Id64(schema_id), avro_schema)?;
1164 let mut decoder = ReaderBuilder::new()
1165 .with_writer_schema_store(store)
1166 .build_decoder()?;
1167 let _ = decoder.decode(&encoded)?;
1168 let decoded = decoder
1169 .flush()?
1170 .expect("expected at least one batch from decoder");
1171 assert_eq!(decoded.num_columns(), 1);
1172 assert_eq!(decoded.num_rows(), 3);
1173 let col = decoded.column(0).as_primitive::<Int32Type>();
1174 assert_eq!(col, &Int32Array::from(vec![1, 2, 3]));
1175 Ok(())
1176 }
1177
1178 #[test]
1179 fn test_ocf_writer_generates_header_and_sync() -> Result<(), AvroError> {
1180 let batch = make_batch();
1181 let buffer: Vec<u8> = Vec::new();
1182 let mut writer = AvroWriter::new(buffer, make_schema())?;
1183 writer.write(&batch)?;
1184 writer.finish()?;
1185 let out = writer.into_inner();
1186 assert_eq!(&out[..4], b"Obj\x01", "OCF magic bytes missing/incorrect");
1187 let trailer = &out[out.len() - 16..];
1188 assert_eq!(trailer.len(), 16, "expected 16‑byte sync marker");
1189 Ok(())
1190 }
1191
1192 #[test]
1193 fn test_schema_mismatch_yields_error() {
1194 let batch = make_batch();
1195 let alt_schema = Schema::new(vec![Field::new("x", DataType::Int32, false)]);
1196 let buffer = Vec::<u8>::new();
1197 let mut writer = AvroWriter::new(buffer, alt_schema).unwrap();
1198 let err = writer.write(&batch).unwrap_err();
1199 assert!(matches!(err, AvroError::SchemaError(_)));
1200 }
1201
1202 #[test]
1203 fn test_write_batches_accumulates_multiple() -> Result<(), AvroError> {
1204 let batch1 = make_batch();
1205 let batch2 = make_batch();
1206 let buffer = Vec::<u8>::new();
1207 let mut writer = AvroWriter::new(buffer, make_schema())?;
1208 writer.write_batches(&[&batch1, &batch2])?;
1209 writer.finish()?;
1210 let out = writer.into_inner();
1211 assert!(out.len() > 4, "combined batches produced tiny file");
1212 Ok(())
1213 }
1214
1215 #[test]
1216 fn test_finish_without_write_adds_header() -> Result<(), AvroError> {
1217 let buffer = Vec::<u8>::new();
1218 let mut writer = AvroWriter::new(buffer, make_schema())?;
1219 writer.finish()?;
1220 let out = writer.into_inner();
1221 assert_eq!(&out[..4], b"Obj\x01", "finish() should emit OCF header");
1222 Ok(())
1223 }
1224
1225 #[test]
1226 fn test_write_long_encodes_zigzag_varint() -> Result<(), AvroError> {
1227 let mut buf = Vec::new();
1228 write_long(&mut buf, 0)?;
1229 write_long(&mut buf, -1)?;
1230 write_long(&mut buf, 1)?;
1231 write_long(&mut buf, -2)?;
1232 write_long(&mut buf, 2147483647)?;
1233 assert!(
1234 buf.starts_with(&[0x00, 0x01, 0x02, 0x03]),
1235 "zig‑zag varint encodings incorrect: {buf:?}"
1236 );
1237 Ok(())
1238 }
1239
1240 #[test]
1241 fn test_roundtrip_alltypes_roundtrip_writer() -> Result<(), AvroError> {
1242 for rel in files() {
1243 let path = arrow_test_data(rel);
1244 let rdr_file = File::open(&path).expect("open input avro");
1245 let reader = ReaderBuilder::new()
1246 .build(BufReader::new(rdr_file))
1247 .expect("build reader");
1248 let schema = reader.schema();
1249 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1250 let original =
1251 arrow::compute::concat_batches(&schema, &input_batches).expect("concat input");
1252 let tmp = NamedTempFile::new().expect("create temp file");
1253 let out_path = tmp.into_temp_path();
1254 let out_file = File::create(&out_path).expect("create temp avro");
1255 let codec = if rel.contains(".snappy.") {
1256 Some(CompressionCodec::Snappy)
1257 } else if rel.contains(".zstandard.") {
1258 Some(CompressionCodec::ZStandard)
1259 } else if rel.contains(".bzip2.") {
1260 Some(CompressionCodec::Bzip2)
1261 } else if rel.contains(".xz.") {
1262 Some(CompressionCodec::Xz)
1263 } else {
1264 None
1265 };
1266 let mut writer = WriterBuilder::new(original.schema().as_ref().clone())
1267 .with_compression(codec)
1268 .build::<_, AvroOcfFormat>(out_file)?;
1269 writer.write(&original)?;
1270 writer.finish()?;
1271 drop(writer);
1272 let rt_file = File::open(&out_path).expect("open roundtrip avro");
1273 let rt_reader = ReaderBuilder::new()
1274 .build(BufReader::new(rt_file))
1275 .expect("build roundtrip reader");
1276 let rt_schema = rt_reader.schema();
1277 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1278 let roundtrip =
1279 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
1280 assert_eq!(
1281 roundtrip, original,
1282 "Round-trip batch mismatch for file: {rel}"
1283 );
1284 }
1285 Ok(())
1286 }
1287
1288 #[test]
1289 fn test_roundtrip_nested_records_writer() -> Result<(), AvroError> {
1290 let path = arrow_test_data("avro/nested_records.avro");
1291 let rdr_file = File::open(&path).expect("open nested_records.avro");
1292 let reader = ReaderBuilder::new()
1293 .build(BufReader::new(rdr_file))
1294 .expect("build reader for nested_records.avro");
1295 let schema = reader.schema();
1296 let batches = reader.collect::<Result<Vec<_>, _>>()?;
1297 let original = arrow::compute::concat_batches(&schema, &batches).expect("concat original");
1298 let tmp = NamedTempFile::new().expect("create temp file");
1299 let out_path = tmp.into_temp_path();
1300 {
1301 let out_file = File::create(&out_path).expect("create output avro");
1302 let mut writer = AvroWriter::new(out_file, original.schema().as_ref().clone())?;
1303 writer.write(&original)?;
1304 writer.finish()?;
1305 }
1306 let rt_file = File::open(&out_path).expect("open round_trip avro");
1307 let rt_reader = ReaderBuilder::new()
1308 .build(BufReader::new(rt_file))
1309 .expect("build round_trip reader");
1310 let rt_schema = rt_reader.schema();
1311 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1312 let round_trip =
1313 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1314 assert_eq!(
1315 round_trip, original,
1316 "Round-trip batch mismatch for nested_records.avro"
1317 );
1318 Ok(())
1319 }
1320
1321 #[test]
1322 #[cfg(feature = "snappy")]
1323 fn test_roundtrip_nested_lists_writer() -> Result<(), AvroError> {
1324 let path = arrow_test_data("avro/nested_lists.snappy.avro");
1325 let rdr_file = File::open(&path).expect("open nested_lists.snappy.avro");
1326 let reader = ReaderBuilder::new()
1327 .build(BufReader::new(rdr_file))
1328 .expect("build reader for nested_lists.snappy.avro");
1329 let schema = reader.schema();
1330 let batches = reader.collect::<Result<Vec<_>, _>>()?;
1331 let original = arrow::compute::concat_batches(&schema, &batches).expect("concat original");
1332 let tmp = NamedTempFile::new().expect("create temp file");
1333 let out_path = tmp.into_temp_path();
1334 {
1335 let out_file = File::create(&out_path).expect("create output avro");
1336 let mut writer = WriterBuilder::new(original.schema().as_ref().clone())
1337 .with_compression(Some(CompressionCodec::Snappy))
1338 .build::<_, AvroOcfFormat>(out_file)?;
1339 writer.write(&original)?;
1340 writer.finish()?;
1341 }
1342 let rt_file = File::open(&out_path).expect("open round_trip avro");
1343 let rt_reader = ReaderBuilder::new()
1344 .build(BufReader::new(rt_file))
1345 .expect("build round_trip reader");
1346 let rt_schema = rt_reader.schema();
1347 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1348 let round_trip =
1349 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1350 assert_eq!(
1351 round_trip, original,
1352 "Round-trip batch mismatch for nested_lists.snappy.avro"
1353 );
1354 Ok(())
1355 }
1356
1357 #[test]
1358 fn test_round_trip_simple_fixed_ocf() -> Result<(), AvroError> {
1359 let path = arrow_test_data("avro/simple_fixed.avro");
1360 let rdr_file = File::open(&path).expect("open avro/simple_fixed.avro");
1361 let reader = ReaderBuilder::new()
1362 .build(BufReader::new(rdr_file))
1363 .expect("build avro reader");
1364 let schema = reader.schema();
1365 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1366 let original =
1367 arrow::compute::concat_batches(&schema, &input_batches).expect("concat input");
1368 let tmp = NamedTempFile::new().expect("create temp file");
1369 let out_file = File::create(tmp.path()).expect("create temp avro");
1370 let mut writer = AvroWriter::new(out_file, original.schema().as_ref().clone())?;
1371 writer.write(&original)?;
1372 writer.finish()?;
1373 drop(writer);
1374 let rt_file = File::open(tmp.path()).expect("open round_trip avro");
1375 let rt_reader = ReaderBuilder::new()
1376 .build(BufReader::new(rt_file))
1377 .expect("build round_trip reader");
1378 let rt_schema = rt_reader.schema();
1379 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1380 let round_trip =
1381 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1382 assert_eq!(round_trip, original);
1383 Ok(())
1384 }
1385
1386 #[test]
1388 #[cfg(feature = "canonical_extension_types")]
1389 fn test_round_trip_duration_and_uuid_ocf() -> Result<(), AvroError> {
1390 use arrow_schema::{DataType, IntervalUnit};
1391 let in_file =
1392 File::open("test/data/duration_uuid.avro").expect("open test/data/duration_uuid.avro");
1393 let reader = ReaderBuilder::new()
1394 .build(BufReader::new(in_file))
1395 .expect("build reader for duration_uuid.avro");
1396 let in_schema = reader.schema();
1397 let has_mdn = in_schema.fields().iter().any(|f| {
1398 matches!(
1399 f.data_type(),
1400 DataType::Interval(IntervalUnit::MonthDayNano)
1401 )
1402 });
1403 assert!(
1404 has_mdn,
1405 "expected at least one Interval(MonthDayNano) field in duration_uuid.avro"
1406 );
1407 let has_uuid_fixed = in_schema
1408 .fields()
1409 .iter()
1410 .any(|f| matches!(f.data_type(), DataType::FixedSizeBinary(16)));
1411 assert!(
1412 has_uuid_fixed,
1413 "expected at least one FixedSizeBinary(16) (uuid) field in duration_uuid.avro"
1414 );
1415 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1416 let input =
1417 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1418 let mut writer = AvroWriter::new(Vec::<u8>::new(), in_schema.as_ref().clone())?;
1420 writer.write(&input)?;
1421 writer.finish()?;
1422 let bytes = writer.into_inner();
1423 let rt_reader = ReaderBuilder::new()
1424 .build(Cursor::new(bytes))
1425 .expect("build round_trip reader");
1426 let rt_schema = rt_reader.schema();
1427 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1428 let round_trip =
1429 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1430 assert_eq!(round_trip, input);
1431 Ok(())
1432 }
1433
1434 #[test]
1436 #[cfg(not(feature = "canonical_extension_types"))]
1437 fn test_duration_and_uuid_ocf_without_extensions_round_trips_values() -> Result<(), AvroError> {
1438 use arrow::datatypes::{DataType, IntervalUnit};
1439 use std::io::BufReader;
1440
1441 let in_file =
1443 File::open("test/data/duration_uuid.avro").expect("open test/data/duration_uuid.avro");
1444 let reader = ReaderBuilder::new()
1445 .build(BufReader::new(in_file))
1446 .expect("build reader for duration_uuid.avro");
1447 let in_schema = reader.schema();
1448
1449 assert!(
1451 in_schema.fields().iter().any(|f| {
1452 matches!(
1453 f.data_type(),
1454 DataType::Interval(IntervalUnit::MonthDayNano)
1455 )
1456 }),
1457 "expected at least one Interval(MonthDayNano) field"
1458 );
1459 assert!(
1460 in_schema
1461 .fields()
1462 .iter()
1463 .any(|f| matches!(f.data_type(), DataType::FixedSizeBinary(16))),
1464 "expected a FixedSizeBinary(16) field (uuid)"
1465 );
1466
1467 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1468 let input =
1469 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1470
1471 let mut writer = AvroWriter::new(Vec::<u8>::new(), in_schema.as_ref().clone())?;
1473 writer.write(&input)?;
1474 writer.finish()?;
1475 let bytes = writer.into_inner();
1476 let rt_reader = ReaderBuilder::new()
1477 .build(Cursor::new(bytes))
1478 .expect("build round_trip reader");
1479 let rt_schema = rt_reader.schema();
1480 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1481 let round_trip =
1482 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1483
1484 assert_eq!(
1486 round_trip.column(0),
1487 input.column(0),
1488 "duration column values differ"
1489 );
1490 assert_eq!(round_trip.column(1), input.column(1), "uuid bytes differ");
1491
1492 let uuid_rt = rt_schema.field_with_name("uuid_field")?;
1495 assert_eq!(uuid_rt.data_type(), &DataType::FixedSizeBinary(16));
1496 assert_eq!(
1497 uuid_rt.metadata().get("logicalType").map(|s| s.as_str()),
1498 Some("uuid"),
1499 "expected `logicalType = \"uuid\"` on round-tripped field metadata"
1500 );
1501
1502 let dur_rt = rt_schema.field_with_name("duration_field")?;
1504 assert!(matches!(
1505 dur_rt.data_type(),
1506 DataType::Interval(IntervalUnit::MonthDayNano)
1507 ));
1508
1509 Ok(())
1510 }
1511
1512 #[test]
1516 #[cfg(feature = "snappy")]
1518 fn test_nonnullable_impala_roundtrip_writer() -> Result<(), AvroError> {
1519 let path = arrow_test_data("avro/nonnullable.impala.avro");
1521 let rdr_file = File::open(&path).expect("open avro/nonnullable.impala.avro");
1522 let reader = ReaderBuilder::new()
1523 .build(BufReader::new(rdr_file))
1524 .expect("build reader for nonnullable.impala.avro");
1525 let in_schema = reader.schema();
1527 let has_map = in_schema
1529 .fields()
1530 .iter()
1531 .any(|f| matches!(f.data_type(), DataType::Map(_, _)));
1532 assert!(
1533 has_map,
1534 "expected at least one Map field in avro/nonnullable.impala.avro"
1535 );
1536
1537 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1538 let original =
1539 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1540 let buffer = Vec::<u8>::new();
1542 let mut writer = AvroWriter::new(buffer, in_schema.as_ref().clone())?;
1543 writer.write(&original)?;
1544 writer.finish()?;
1545 let out_bytes = writer.into_inner();
1546 let rt_reader = ReaderBuilder::new()
1548 .build(Cursor::new(out_bytes))
1549 .expect("build reader for round-tripped in-memory OCF");
1550 let rt_schema = rt_reader.schema();
1551 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1552 let roundtrip =
1553 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
1554 assert_eq!(
1556 roundtrip, original,
1557 "Round-trip Avro map data mismatch for nonnullable.impala.avro"
1558 );
1559 Ok(())
1560 }
1561
1562 #[test]
1563 #[cfg(feature = "snappy")]
1565 fn test_roundtrip_decimals_via_writer() -> Result<(), AvroError> {
1566 let files: [(&str, bool); 8] = [
1568 ("avro/fixed_length_decimal.avro", true), ("avro/fixed_length_decimal_legacy.avro", true), ("avro/int32_decimal.avro", true), ("avro/int64_decimal.avro", true), ("test/data/int256_decimal.avro", false), ("test/data/fixed256_decimal.avro", false), ("test/data/fixed_length_decimal_legacy_32.avro", false), ("test/data/int128_decimal.avro", false), ];
1577 for (rel, in_test_data_dir) in files {
1578 let path: String = if in_test_data_dir {
1580 arrow_test_data(rel)
1581 } else {
1582 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1583 .join(rel)
1584 .to_string_lossy()
1585 .into_owned()
1586 };
1587 let f_in = File::open(&path).expect("open input avro");
1589 let rdr = ReaderBuilder::new().build(BufReader::new(f_in))?;
1590 let in_schema = rdr.schema();
1591 let in_batches = rdr.collect::<Result<Vec<_>, _>>()?;
1592 let original =
1593 arrow::compute::concat_batches(&in_schema, &in_batches).expect("concat input");
1594 let tmp = NamedTempFile::new().expect("create temp file");
1596 let out_path = tmp.into_temp_path();
1597 let out_file = File::create(&out_path).expect("create temp avro");
1598 let mut writer = AvroWriter::new(out_file, original.schema().as_ref().clone())?;
1599 writer.write(&original)?;
1600 writer.finish()?;
1601 let f_rt = File::open(&out_path).expect("open roundtrip avro");
1603 let rt_rdr = ReaderBuilder::new().build(BufReader::new(f_rt))?;
1604 let rt_schema = rt_rdr.schema();
1605 let rt_batches = rt_rdr.collect::<Result<Vec<_>, _>>()?;
1606 let roundtrip =
1607 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat rt");
1608 assert_eq!(roundtrip, original, "decimal round-trip mismatch for {rel}");
1609 }
1610 Ok(())
1611 }
1612
1613 #[test]
1614 fn test_named_types_complex_roundtrip() -> Result<(), AvroError> {
1615 let path =
1617 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test/data/named_types_complex.avro");
1618 let rdr_file = File::open(&path).expect("open avro/named_types_complex.avro");
1619
1620 let reader = ReaderBuilder::new()
1621 .build(BufReader::new(rdr_file))
1622 .expect("build reader for named_types_complex.avro");
1623
1624 let in_schema = reader.schema();
1626 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1627 let original =
1628 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1629
1630 {
1632 let arrow_schema = original.schema();
1633
1634 let author_field = arrow_schema.field_with_name("author")?;
1636 let author_type = author_field.data_type();
1637 let editors_field = arrow_schema.field_with_name("editors")?;
1638 let editors_item_type = match editors_field.data_type() {
1639 DataType::List(item_field) => item_field.data_type(),
1640 other => panic!("Editors field should be a List, but was {other:?}"),
1641 };
1642 assert_eq!(
1643 author_type, editors_item_type,
1644 "The DataType for the 'author' struct and the 'editors' list items must be identical"
1645 );
1646
1647 let status_field = arrow_schema.field_with_name("status")?;
1649 let status_type = status_field.data_type();
1650 assert!(
1651 matches!(status_type, DataType::Dictionary(_, _)),
1652 "Status field should be a Dictionary (Enum)"
1653 );
1654
1655 let prev_status_field = arrow_schema.field_with_name("previous_status")?;
1656 let prev_status_type = prev_status_field.data_type();
1657 assert_eq!(
1658 status_type, prev_status_type,
1659 "The DataType for 'status' and 'previous_status' enums must be identical"
1660 );
1661
1662 let content_hash_field = arrow_schema.field_with_name("content_hash")?;
1664 let content_hash_type = content_hash_field.data_type();
1665 assert!(
1666 matches!(content_hash_type, DataType::FixedSizeBinary(16)),
1667 "Content hash should be FixedSizeBinary(16)"
1668 );
1669
1670 let thumb_hash_field = arrow_schema.field_with_name("thumbnail_hash")?;
1671 let thumb_hash_type = thumb_hash_field.data_type();
1672 assert_eq!(
1673 content_hash_type, thumb_hash_type,
1674 "The DataType for 'content_hash' and 'thumbnail_hash' fixed types must be identical"
1675 );
1676 }
1677
1678 let buffer: Vec<u8> = Vec::new();
1680 let mut writer = AvroWriter::new(buffer, original.schema().as_ref().clone())?;
1681 writer.write(&original)?;
1682 writer.finish()?;
1683 let bytes = writer.into_inner();
1684
1685 let rt_reader = ReaderBuilder::new()
1687 .build(Cursor::new(bytes))
1688 .expect("build reader for round-trip");
1689 let rt_schema = rt_reader.schema();
1690 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1691 let roundtrip =
1692 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
1693
1694 assert_eq!(
1695 roundtrip, original,
1696 "Avro complex named types round-trip mismatch"
1697 );
1698
1699 Ok(())
1700 }
1701
1702 fn assert_schema_is_semantically_equivalent(expected: &Schema, actual: &Schema) {
1709 assert_metadata_is_superset(expected.metadata(), actual.metadata(), "Schema");
1711
1712 assert_eq!(
1714 expected.fields().len(),
1715 actual.fields().len(),
1716 "Schema must have the same number of fields"
1717 );
1718
1719 for (expected_field, actual_field) in expected.fields().iter().zip(actual.fields().iter()) {
1720 assert_field_is_semantically_equivalent(expected_field, actual_field);
1721 }
1722 }
1723
1724 fn assert_field_is_semantically_equivalent(expected: &Field, actual: &Field) {
1725 let context = format!("Field '{}'", expected.name());
1726
1727 assert_eq!(
1728 expected.name(),
1729 actual.name(),
1730 "{context}: names must match"
1731 );
1732 assert_eq!(
1733 expected.is_nullable(),
1734 actual.is_nullable(),
1735 "{context}: nullability must match"
1736 );
1737
1738 assert_datatype_is_semantically_equivalent(
1740 expected.data_type(),
1741 actual.data_type(),
1742 &context,
1743 );
1744
1745 assert_metadata_is_superset(expected.metadata(), actual.metadata(), &context);
1747 }
1748
1749 fn assert_datatype_is_semantically_equivalent(
1750 expected: &DataType,
1751 actual: &DataType,
1752 context: &str,
1753 ) {
1754 match (expected, actual) {
1755 (DataType::List(expected_field), DataType::List(actual_field))
1756 | (DataType::LargeList(expected_field), DataType::LargeList(actual_field))
1757 | (DataType::Map(expected_field, _), DataType::Map(actual_field, _)) => {
1758 assert_field_is_semantically_equivalent(expected_field, actual_field);
1759 }
1760 (DataType::Struct(expected_fields), DataType::Struct(actual_fields)) => {
1761 assert_eq!(
1762 expected_fields.len(),
1763 actual_fields.len(),
1764 "{context}: struct must have same number of fields"
1765 );
1766 for (ef, af) in expected_fields.iter().zip(actual_fields.iter()) {
1767 assert_field_is_semantically_equivalent(ef, af);
1768 }
1769 }
1770 (
1771 DataType::Union(expected_fields, expected_mode),
1772 DataType::Union(actual_fields, actual_mode),
1773 ) => {
1774 assert_eq!(
1775 expected_mode, actual_mode,
1776 "{context}: union mode must match"
1777 );
1778 assert_eq!(
1779 expected_fields.len(),
1780 actual_fields.len(),
1781 "{context}: union must have same number of variants"
1782 );
1783 for ((exp_id, exp_field), (act_id, act_field)) in
1784 expected_fields.iter().zip(actual_fields.iter())
1785 {
1786 assert_eq!(exp_id, act_id, "{context}: union type ids must match");
1787 assert_field_is_semantically_equivalent(exp_field, act_field);
1788 }
1789 }
1790 _ => {
1791 assert_eq!(expected, actual, "{context}: data types must be identical");
1792 }
1793 }
1794 }
1795
1796 fn assert_batch_data_is_identical(expected: &RecordBatch, actual: &RecordBatch) {
1797 assert_eq!(
1798 expected.num_columns(),
1799 actual.num_columns(),
1800 "RecordBatches must have the same number of columns"
1801 );
1802 assert_eq!(
1803 expected.num_rows(),
1804 actual.num_rows(),
1805 "RecordBatches must have the same number of rows"
1806 );
1807
1808 for i in 0..expected.num_columns() {
1809 let context = format!("Column {i}");
1810 let expected_col = expected.column(i);
1811 let actual_col = actual.column(i);
1812 assert_array_data_is_identical(expected_col, actual_col, &context);
1813 }
1814 }
1815
1816 fn assert_array_data_is_identical(expected: &dyn Array, actual: &dyn Array, context: &str) {
1818 assert_eq!(
1819 expected.nulls(),
1820 actual.nulls(),
1821 "{context}: null buffers must match"
1822 );
1823 assert_eq!(
1824 expected.len(),
1825 actual.len(),
1826 "{context}: array lengths must match"
1827 );
1828
1829 match (expected.data_type(), actual.data_type()) {
1830 (DataType::Union(expected_fields, _), DataType::Union(..)) => {
1831 let expected_union = expected.as_any().downcast_ref::<UnionArray>().unwrap();
1832 let actual_union = actual.as_any().downcast_ref::<UnionArray>().unwrap();
1833
1834 assert_eq!(
1836 &expected.to_data().buffers()[0],
1837 &actual.to_data().buffers()[0],
1838 "{context}: union type_ids buffer mismatch"
1839 );
1840
1841 if expected.to_data().buffers().len() > 1 {
1843 assert_eq!(
1844 &expected.to_data().buffers()[1],
1845 &actual.to_data().buffers()[1],
1846 "{context}: union value_offsets buffer mismatch"
1847 );
1848 }
1849
1850 for (type_id, _) in expected_fields.iter() {
1852 let child_context = format!("{context} -> child variant {type_id}");
1853 assert_array_data_is_identical(
1854 expected_union.child(type_id),
1855 actual_union.child(type_id),
1856 &child_context,
1857 );
1858 }
1859 }
1860 (DataType::Struct(_), DataType::Struct(_)) => {
1861 let expected_struct = expected.as_any().downcast_ref::<StructArray>().unwrap();
1862 let actual_struct = actual.as_any().downcast_ref::<StructArray>().unwrap();
1863 for i in 0..expected_struct.num_columns() {
1864 let child_context = format!("{context} -> struct child {i}");
1865 assert_array_data_is_identical(
1866 expected_struct.column(i),
1867 actual_struct.column(i),
1868 &child_context,
1869 );
1870 }
1871 }
1872 _ => {
1874 assert_eq!(
1875 expected.to_data().buffers(),
1876 actual.to_data().buffers(),
1877 "{context}: data buffers must match"
1878 );
1879 }
1880 }
1881 }
1882
1883 fn assert_metadata_is_superset(
1886 expected_meta: &arrow_schema::Metadata,
1887 actual_meta: &arrow_schema::Metadata,
1888 context: &str,
1889 ) {
1890 let allowed_additions: HashSet<&str> =
1891 vec!["arrowUnionMode", "arrowUnionTypeIds", "avro.name"]
1892 .into_iter()
1893 .collect();
1894 for (key, expected_value) in expected_meta {
1895 match actual_meta.get(key) {
1896 Some(actual_value) => assert_eq!(
1897 expected_value, actual_value,
1898 "{context}: preserved metadata for key '{key}' must have the same value"
1899 ),
1900 None => panic!("{context}: metadata key '{key}' was lost during roundtrip"),
1901 }
1902 }
1903 for key in actual_meta.keys() {
1904 if !expected_meta.contains_key(key) && !allowed_additions.contains(key.as_str()) {
1905 panic!("{context}: unexpected metadata key '{key}' was added during roundtrip");
1906 }
1907 }
1908 }
1909
1910 #[test]
1911 fn test_union_roundtrip() -> Result<(), AvroError> {
1912 let file_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1913 .join("test/data/union_fields.avro")
1914 .to_string_lossy()
1915 .into_owned();
1916 let rdr_file = File::open(&file_path).expect("open avro/union_fields.avro");
1917 let reader = ReaderBuilder::new()
1918 .build(BufReader::new(rdr_file))
1919 .expect("build reader for union_fields.avro");
1920 let schema = reader.schema();
1921 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1922 let original =
1923 arrow::compute::concat_batches(&schema, &input_batches).expect("concat input");
1924 let mut writer = AvroWriter::new(Vec::<u8>::new(), original.schema().as_ref().clone())?;
1925 writer.write(&original)?;
1926 writer.finish()?;
1927 let bytes = writer.into_inner();
1928 let rt_reader = ReaderBuilder::new()
1929 .build(Cursor::new(bytes))
1930 .expect("build round_trip reader");
1931 let rt_schema = rt_reader.schema();
1932 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1933 let round_trip =
1934 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
1935
1936 assert_schema_is_semantically_equivalent(&original.schema(), &round_trip.schema());
1939
1940 assert_batch_data_is_identical(&original, &round_trip);
1941 Ok(())
1942 }
1943
1944 #[test]
1945 fn test_enum_roundtrip_uses_reader_fixture() -> Result<(), AvroError> {
1946 let path = arrow_test_data("avro/simple_enum.avro");
1948 let rdr_file = File::open(&path).expect("open avro/simple_enum.avro");
1949 let reader = ReaderBuilder::new()
1950 .build(BufReader::new(rdr_file))
1951 .expect("build reader for simple_enum.avro");
1952 let in_schema = reader.schema();
1954 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
1955 let original =
1956 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
1957 let has_enum_dict = in_schema.fields().iter().any(|f| {
1959 matches!(
1960 f.data_type(),
1961 DataType::Dictionary(k, v) if **k == DataType::Int32 && **v == DataType::Utf8
1962 )
1963 });
1964 assert!(
1965 has_enum_dict,
1966 "Expected at least one enum-mapped Dictionary<Int32, Utf8> field"
1967 );
1968 let buffer: Vec<u8> = Vec::new();
1971 let mut writer = AvroWriter::new(buffer, in_schema.as_ref().clone())?;
1972 writer.write(&original)?;
1973 writer.finish()?;
1974 let bytes = writer.into_inner();
1975 let rt_reader = ReaderBuilder::new()
1977 .build(Cursor::new(bytes))
1978 .expect("reader for round-trip");
1979 let rt_schema = rt_reader.schema();
1980 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
1981 let roundtrip =
1982 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
1983 assert_eq!(roundtrip, original, "Avro enum round-trip mismatch");
1984 Ok(())
1985 }
1986
1987 #[test]
1988 fn test_builder_propagates_capacity_to_writer() -> Result<(), AvroError> {
1989 let cap = 64 * 1024;
1990 let buffer = Vec::<u8>::new();
1991 let mut writer = WriterBuilder::new(make_schema())
1992 .with_capacity(cap)
1993 .build::<_, AvroOcfFormat>(buffer)?;
1994 assert_eq!(writer.capacity, cap, "builder capacity not propagated");
1995 let batch = make_batch();
1996 writer.write(&batch)?;
1997 writer.finish()?;
1998 let out = writer.into_inner();
1999 assert_eq!(&out[..4], b"Obj\x01", "OCF magic missing/incorrect");
2000 Ok(())
2001 }
2002
2003 #[test]
2004 fn test_stream_writer_stores_capacity_direct_writes() -> Result<(), AvroError> {
2005 use arrow_array::{ArrayRef, Int32Array};
2006 use arrow_schema::{DataType, Field, Schema};
2007 let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
2008 let batch = RecordBatch::try_new(
2009 Arc::new(schema.clone()),
2010 vec![Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef],
2011 )?;
2012 let cap = 8192;
2013 let mut writer = WriterBuilder::new(schema)
2014 .with_capacity(cap)
2015 .build::<_, AvroSoeFormat>(Vec::new())?;
2016 assert_eq!(writer.capacity, cap);
2017 writer.write(&batch)?;
2018 let _bytes = writer.into_inner();
2019 Ok(())
2020 }
2021
2022 #[cfg(feature = "avro_custom_types")]
2023 #[test]
2024 fn test_roundtrip_duration_logical_types_ocf() -> Result<(), AvroError> {
2025 let file_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2026 .join("test/data/duration_logical_types.avro")
2027 .to_string_lossy()
2028 .into_owned();
2029
2030 let in_file = File::open(&file_path)
2031 .unwrap_or_else(|_| panic!("Failed to open test file: {file_path}"));
2032
2033 let reader = ReaderBuilder::new()
2034 .build(BufReader::new(in_file))
2035 .expect("build reader for duration_logical_types.avro");
2036 let in_schema = reader.schema();
2037
2038 let expected_units: HashSet<TimeUnit> = [
2039 TimeUnit::Nanosecond,
2040 TimeUnit::Microsecond,
2041 TimeUnit::Millisecond,
2042 TimeUnit::Second,
2043 ]
2044 .into_iter()
2045 .collect();
2046
2047 let found_units: HashSet<TimeUnit> = in_schema
2048 .fields()
2049 .iter()
2050 .filter_map(|f| match f.data_type() {
2051 DataType::Duration(unit) => Some(*unit),
2052 _ => None,
2053 })
2054 .collect();
2055
2056 assert_eq!(
2057 found_units, expected_units,
2058 "Expected to find all four Duration TimeUnits in the schema from the initial read"
2059 );
2060
2061 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2062 let input =
2063 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2064
2065 let tmp = NamedTempFile::new().expect("create temp file");
2066 {
2067 let out_file = File::create(tmp.path()).expect("create temp avro");
2068 let mut writer = AvroWriter::new(out_file, in_schema.as_ref().clone())?;
2069 writer.write(&input)?;
2070 writer.finish()?;
2071 }
2072
2073 let rt_file = File::open(tmp.path()).expect("open round_trip avro");
2074 let rt_reader = ReaderBuilder::new()
2075 .build(BufReader::new(rt_file))
2076 .expect("build round_trip reader");
2077 let rt_schema = rt_reader.schema();
2078 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2079 let round_trip =
2080 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2081
2082 assert_eq!(round_trip, input);
2083 Ok(())
2084 }
2085
2086 #[cfg(feature = "avro_custom_types")]
2087 #[test]
2088 fn test_run_end_encoded_roundtrip_writer() -> Result<(), AvroError> {
2089 let run_ends = Int32Array::from(vec![3, 5, 7, 8]);
2090 let run_values = Int32Array::from(vec![Some(1), Some(2), None, Some(3)]);
2091 let ree = RunArray::<Int32Type>::try_new(&run_ends, &run_values)?;
2092 let field = Field::new("x", ree.data_type().clone(), true);
2093 let schema = Schema::new(vec![field]);
2094 let batch = RecordBatch::try_new(
2095 Arc::new(schema.clone()),
2096 vec![Arc::new(ree.clone()) as ArrayRef],
2097 )?;
2098 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2099 writer.write(&batch)?;
2100 writer.finish()?;
2101 let bytes = writer.into_inner();
2102 let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2103 let out_schema = reader.schema();
2104 let batches = reader.collect::<Result<Vec<_>, _>>()?;
2105 let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2106 assert_eq!(out.num_columns(), 1);
2107 assert_eq!(out.num_rows(), 8);
2108 match out.schema().field(0).data_type() {
2109 DataType::RunEndEncoded(run_ends_field, values_field) => {
2110 assert_eq!(
2111 run_ends_field.name(),
2112 Field::REE_RUN_ENDS_FIELD_DEFAULT_NAME
2113 );
2114 assert_eq!(run_ends_field.data_type(), &DataType::Int32);
2115 assert_eq!(values_field.name(), Field::REE_VALUES_FIELD_DEFAULT_NAME);
2116 assert_eq!(values_field.data_type(), &DataType::Int32);
2117 assert!(values_field.is_nullable());
2118 let got_ree = out
2119 .column(0)
2120 .as_any()
2121 .downcast_ref::<RunArray<Int32Type>>()
2122 .expect("RunArray<Int32Type>");
2123 assert_eq!(got_ree, &ree);
2124 }
2125 other => {
2126 panic!("Unexpected DataType for round-tripped RunEndEncoded column: {other:?}")
2127 }
2128 }
2129 Ok(())
2130 }
2131
2132 #[cfg(feature = "avro_custom_types")]
2133 #[test]
2134 fn test_run_end_encoded_string_values_int16_run_ends_roundtrip_writer() -> Result<(), AvroError>
2135 {
2136 let run_ends = Int16Array::from(vec![2, 5, 7]); let run_values = StringArray::from(vec![Some("a"), None, Some("c")]);
2138 let ree = RunArray::<Int16Type>::try_new(&run_ends, &run_values)?;
2139 let field = Field::new("s", ree.data_type().clone(), true);
2140 let schema = Schema::new(vec![field]);
2141 let batch = RecordBatch::try_new(
2142 Arc::new(schema.clone()),
2143 vec![Arc::new(ree.clone()) as ArrayRef],
2144 )?;
2145 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2146 writer.write(&batch)?;
2147 writer.finish()?;
2148 let bytes = writer.into_inner();
2149 let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2150 let out_schema = reader.schema();
2151 let batches = reader.collect::<Result<Vec<_>, _>>()?;
2152 let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2153 assert_eq!(out.num_columns(), 1);
2154 assert_eq!(out.num_rows(), 7);
2155 match out.schema().field(0).data_type() {
2156 DataType::RunEndEncoded(run_ends_field, values_field) => {
2157 assert_eq!(run_ends_field.data_type(), &DataType::Int16);
2158 assert_eq!(values_field.data_type(), &DataType::Utf8);
2159 assert!(
2160 values_field.is_nullable(),
2161 "REE 'values' child should be nullable"
2162 );
2163 let got = out
2164 .column(0)
2165 .as_any()
2166 .downcast_ref::<RunArray<Int16Type>>()
2167 .expect("RunArray<Int16Type>");
2168 assert_eq!(got, &ree);
2169 }
2170 other => panic!("Unexpected DataType: {other:?}"),
2171 }
2172 Ok(())
2173 }
2174
2175 #[cfg(feature = "avro_custom_types")]
2176 #[test]
2177 fn test_run_end_encoded_int64_run_ends_numeric_values_roundtrip_writer() -> Result<(), AvroError>
2178 {
2179 let run_ends = Int64Array::from(vec![4_i64, 8_i64]);
2180 let run_values = Int32Array::from(vec![Some(999), Some(-5)]);
2181 let ree = RunArray::<Int64Type>::try_new(&run_ends, &run_values)?;
2182 let field = Field::new("y", ree.data_type().clone(), true);
2183 let schema = Schema::new(vec![field]);
2184 let batch = RecordBatch::try_new(
2185 Arc::new(schema.clone()),
2186 vec![Arc::new(ree.clone()) as ArrayRef],
2187 )?;
2188 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2189 writer.write(&batch)?;
2190 writer.finish()?;
2191 let bytes = writer.into_inner();
2192 let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2193 let out_schema = reader.schema();
2194 let batches = reader.collect::<Result<Vec<_>, _>>()?;
2195 let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2196 assert_eq!(out.num_columns(), 1);
2197 assert_eq!(out.num_rows(), 8);
2198 match out.schema().field(0).data_type() {
2199 DataType::RunEndEncoded(run_ends_field, values_field) => {
2200 assert_eq!(run_ends_field.data_type(), &DataType::Int64);
2201 assert_eq!(values_field.data_type(), &DataType::Int32);
2202 assert!(values_field.is_nullable());
2203 let got = out
2204 .column(0)
2205 .as_any()
2206 .downcast_ref::<RunArray<Int64Type>>()
2207 .expect("RunArray<Int64Type>");
2208 assert_eq!(got, &ree);
2209 }
2210 other => panic!("Unexpected DataType for REE column: {other:?}"),
2211 }
2212 Ok(())
2213 }
2214
2215 #[cfg(feature = "avro_custom_types")]
2216 #[test]
2217 fn test_run_end_encoded_sliced_roundtrip_writer() -> Result<(), AvroError> {
2218 let run_ends = Int32Array::from(vec![3, 5, 7, 8]);
2219 let run_values = Int32Array::from(vec![Some(1), Some(2), None, Some(3)]);
2220 let base = RunArray::<Int32Type>::try_new(&run_ends, &run_values)?;
2221 let offset = 1usize;
2222 let length = 6usize;
2223 let base_values = base.values().as_primitive::<Int32Type>();
2224 let mut logical_window: Vec<Option<i32>> = Vec::with_capacity(length);
2225 for i in offset..offset + length {
2226 let phys = base.get_physical_index(i);
2227 let v = if base_values.is_null(phys) {
2228 None
2229 } else {
2230 Some(base_values.value(phys))
2231 };
2232 logical_window.push(v);
2233 }
2234
2235 fn compress_run_ends_i32(vals: &[Option<i32>]) -> (Int32Array, Int32Array) {
2236 if vals.is_empty() {
2237 return (Int32Array::new_null(0), Int32Array::new_null(0));
2238 }
2239 let mut run_ends_out: Vec<i32> = Vec::new();
2240 let mut run_vals_out: Vec<Option<i32>> = Vec::new();
2241 let mut cur = vals[0];
2242 let mut len = 1i32;
2243 for v in &vals[1..] {
2244 if *v == cur {
2245 len += 1;
2246 } else {
2247 let last_end = run_ends_out.last().copied().unwrap_or(0);
2248 run_ends_out.push(last_end + len);
2249 run_vals_out.push(cur);
2250 cur = *v;
2251 len = 1;
2252 }
2253 }
2254 let last_end = run_ends_out.last().copied().unwrap_or(0);
2255 run_ends_out.push(last_end + len);
2256 run_vals_out.push(cur);
2257 (
2258 Int32Array::from(run_ends_out),
2259 Int32Array::from(run_vals_out),
2260 )
2261 }
2262 let (owned_run_ends, owned_run_values) = compress_run_ends_i32(&logical_window);
2263 let owned_slice = RunArray::<Int32Type>::try_new(&owned_run_ends, &owned_run_values)?;
2264 let field = Field::new("x", owned_slice.data_type().clone(), true);
2265 let schema = Schema::new(vec![field]);
2266 let batch = RecordBatch::try_new(
2267 Arc::new(schema.clone()),
2268 vec![Arc::new(owned_slice.clone()) as ArrayRef],
2269 )?;
2270 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2271 writer.write(&batch)?;
2272 writer.finish()?;
2273 let bytes = writer.into_inner();
2274 let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2275 let out_schema = reader.schema();
2276 let batches = reader.collect::<Result<Vec<_>, _>>()?;
2277 let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2278 assert_eq!(out.num_columns(), 1);
2279 assert_eq!(out.num_rows(), length);
2280 match out.schema().field(0).data_type() {
2281 DataType::RunEndEncoded(run_ends_field, values_field) => {
2282 assert_eq!(run_ends_field.data_type(), &DataType::Int32);
2283 assert_eq!(values_field.data_type(), &DataType::Int32);
2284 assert!(values_field.is_nullable());
2285 let got = out
2286 .column(0)
2287 .as_any()
2288 .downcast_ref::<RunArray<Int32Type>>()
2289 .expect("RunArray<Int32Type>");
2290 fn expand_ree_to_int32(a: &RunArray<Int32Type>) -> Int32Array {
2291 let vals = a.values().as_primitive::<Int32Type>();
2292 let mut out: Vec<Option<i32>> = Vec::with_capacity(a.len());
2293 for i in 0..a.len() {
2294 let phys = a.get_physical_index(i);
2295 out.push(if vals.is_null(phys) {
2296 None
2297 } else {
2298 Some(vals.value(phys))
2299 });
2300 }
2301 Int32Array::from(out)
2302 }
2303 let got_logical = expand_ree_to_int32(got);
2304 let expected_logical = Int32Array::from(logical_window);
2305 assert_eq!(
2306 got_logical, expected_logical,
2307 "Logical values differ after REE slice round-trip"
2308 );
2309 }
2310 other => panic!("Unexpected DataType for REE column: {other:?}"),
2311 }
2312 Ok(())
2313 }
2314
2315 #[cfg(not(feature = "avro_custom_types"))]
2316 #[test]
2317 fn test_run_end_encoded_roundtrip_writer_feature_off() -> Result<(), AvroError> {
2318 use arrow_schema::{DataType, Field, Schema};
2319 let run_ends = arrow_array::Int32Array::from(vec![3, 5, 7, 8]);
2320 let run_values = arrow_array::Int32Array::from(vec![Some(1), Some(2), None, Some(3)]);
2321 let ree = arrow_array::RunArray::<arrow_array::types::Int32Type>::try_new(
2322 &run_ends,
2323 &run_values,
2324 )?;
2325 let field = Field::new("x", ree.data_type().clone(), true);
2326 let schema = Schema::new(vec![field]);
2327 let batch =
2328 RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(ree) as ArrayRef])?;
2329 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2330 writer.write(&batch)?;
2331 writer.finish()?;
2332 let bytes = writer.into_inner();
2333 let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2334 let out_schema = reader.schema();
2335 let batches = reader.collect::<Result<Vec<_>, _>>()?;
2336 let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2337 assert_eq!(out.num_columns(), 1);
2338 assert_eq!(out.num_rows(), 8);
2339 assert_eq!(out.schema().field(0).data_type(), &DataType::Int32);
2340 let got = out.column(0).as_primitive::<Int32Type>();
2341 let expected = Int32Array::from(vec![
2342 Some(1),
2343 Some(1),
2344 Some(1),
2345 Some(2),
2346 Some(2),
2347 None,
2348 None,
2349 Some(3),
2350 ]);
2351 assert_eq!(got, &expected);
2352 Ok(())
2353 }
2354
2355 #[cfg(not(feature = "avro_custom_types"))]
2356 #[test]
2357 fn test_run_end_encoded_string_values_int16_run_ends_roundtrip_writer_feature_off()
2358 -> Result<(), AvroError> {
2359 use arrow_schema::{DataType, Field, Schema};
2360 let run_ends = arrow_array::Int16Array::from(vec![2, 5, 7]);
2361 let run_values = arrow_array::StringArray::from(vec![Some("a"), None, Some("c")]);
2362 let ree = arrow_array::RunArray::<arrow_array::types::Int16Type>::try_new(
2363 &run_ends,
2364 &run_values,
2365 )?;
2366 let field = Field::new("s", ree.data_type().clone(), true);
2367 let schema = Schema::new(vec![field]);
2368 let batch =
2369 RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(ree) as ArrayRef])?;
2370 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2371 writer.write(&batch)?;
2372 writer.finish()?;
2373 let bytes = writer.into_inner();
2374 let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2375 let out_schema = reader.schema();
2376 let batches = reader.collect::<Result<Vec<_>, _>>()?;
2377 let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2378 assert_eq!(out.num_columns(), 1);
2379 assert_eq!(out.num_rows(), 7);
2380 assert_eq!(out.schema().field(0).data_type(), &DataType::Utf8);
2381 let got = out
2382 .column(0)
2383 .as_any()
2384 .downcast_ref::<arrow_array::StringArray>()
2385 .expect("StringArray");
2386 let expected = arrow_array::StringArray::from(vec![
2387 Some("a"),
2388 Some("a"),
2389 None,
2390 None,
2391 None,
2392 Some("c"),
2393 Some("c"),
2394 ]);
2395 assert_eq!(got, &expected);
2396 Ok(())
2397 }
2398
2399 #[cfg(not(feature = "avro_custom_types"))]
2400 #[test]
2401 fn test_run_end_encoded_int64_run_ends_numeric_values_roundtrip_writer_feature_off()
2402 -> Result<(), AvroError> {
2403 use arrow_schema::{DataType, Field, Schema};
2404 let run_ends = arrow_array::Int64Array::from(vec![4_i64, 8_i64]);
2405 let run_values = Int32Array::from(vec![Some(999), Some(-5)]);
2406 let ree = arrow_array::RunArray::<arrow_array::types::Int64Type>::try_new(
2407 &run_ends,
2408 &run_values,
2409 )?;
2410 let field = Field::new("y", ree.data_type().clone(), true);
2411 let schema = Schema::new(vec![field]);
2412 let batch =
2413 RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(ree) as ArrayRef])?;
2414 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2415 writer.write(&batch)?;
2416 writer.finish()?;
2417 let bytes = writer.into_inner();
2418 let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2419 let out_schema = reader.schema();
2420 let batches = reader.collect::<Result<Vec<_>, _>>()?;
2421 let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2422 assert_eq!(out.num_columns(), 1);
2423 assert_eq!(out.num_rows(), 8);
2424 assert_eq!(out.schema().field(0).data_type(), &DataType::Int32);
2425 let got = out.column(0).as_primitive::<Int32Type>();
2426 let expected = Int32Array::from(vec![
2427 Some(999),
2428 Some(999),
2429 Some(999),
2430 Some(999),
2431 Some(-5),
2432 Some(-5),
2433 Some(-5),
2434 Some(-5),
2435 ]);
2436 assert_eq!(got, &expected);
2437 Ok(())
2438 }
2439
2440 #[cfg(not(feature = "avro_custom_types"))]
2441 #[test]
2442 fn test_run_end_encoded_sliced_roundtrip_writer_feature_off() -> Result<(), AvroError> {
2443 use arrow_schema::{DataType, Field, Schema};
2444 let run_ends = Int32Array::from(vec![2, 4, 6]);
2445 let run_values = Int32Array::from(vec![Some(1), Some(2), None]);
2446 let ree = arrow_array::RunArray::<arrow_array::types::Int32Type>::try_new(
2447 &run_ends,
2448 &run_values,
2449 )?;
2450 let field = Field::new("x", ree.data_type().clone(), true);
2451 let schema = Schema::new(vec![field]);
2452 let batch =
2453 RecordBatch::try_new(Arc::new(schema.clone()), vec![Arc::new(ree) as ArrayRef])?;
2454 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2455 writer.write(&batch)?;
2456 writer.finish()?;
2457 let bytes = writer.into_inner();
2458 let reader = ReaderBuilder::new().build(Cursor::new(bytes))?;
2459 let out_schema = reader.schema();
2460 let batches = reader.collect::<Result<Vec<_>, _>>()?;
2461 let out = arrow::compute::concat_batches(&out_schema, &batches).expect("concat output");
2462 assert_eq!(out.num_columns(), 1);
2463 assert_eq!(out.num_rows(), 6);
2464 assert_eq!(out.schema().field(0).data_type(), &DataType::Int32);
2465 let got = out.column(0).as_primitive::<Int32Type>();
2466 let expected = Int32Array::from(vec![Some(1), Some(1), Some(2), Some(2), None, None]);
2467 assert_eq!(got, &expected);
2468 Ok(())
2469 }
2470
2471 #[test]
2472 #[cfg(feature = "snappy")]
2474 fn test_nullable_impala_roundtrip() -> Result<(), AvroError> {
2475 let path = arrow_test_data("avro/nullable.impala.avro");
2476 let rdr_file = File::open(&path).expect("open avro/nullable.impala.avro");
2477 let reader = ReaderBuilder::new()
2478 .build(BufReader::new(rdr_file))
2479 .expect("build reader for nullable.impala.avro");
2480 let in_schema = reader.schema();
2481 assert!(
2482 in_schema.fields().iter().any(|f| f.is_nullable()),
2483 "expected at least one nullable field in avro/nullable.impala.avro"
2484 );
2485 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2486 let original =
2487 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2488 let buffer: Vec<u8> = Vec::new();
2489 let mut writer = AvroWriter::new(buffer, in_schema.as_ref().clone())?;
2490 writer.write(&original)?;
2491 writer.finish()?;
2492 let out_bytes = writer.into_inner();
2493 let rt_reader = ReaderBuilder::new()
2494 .build(Cursor::new(out_bytes))
2495 .expect("build reader for round-tripped in-memory OCF");
2496 let rt_schema = rt_reader.schema();
2497 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2498 let roundtrip =
2499 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
2500 assert_eq!(
2501 roundtrip, original,
2502 "Round-trip Avro data mismatch for nullable.impala.avro"
2503 );
2504 Ok(())
2505 }
2506
2507 #[test]
2508 #[cfg(feature = "snappy")]
2509 fn test_datapage_v2_roundtrip() -> Result<(), AvroError> {
2510 let path = arrow_test_data("avro/datapage_v2.snappy.avro");
2511 let rdr_file = File::open(&path).expect("open avro/datapage_v2.snappy.avro");
2512 let reader = ReaderBuilder::new()
2513 .build(BufReader::new(rdr_file))
2514 .expect("build reader for datapage_v2.snappy.avro");
2515 let in_schema = reader.schema();
2516 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2517 let original =
2518 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2519 let mut writer = AvroWriter::new(Vec::<u8>::new(), in_schema.as_ref().clone())?;
2520 writer.write(&original)?;
2521 writer.finish()?;
2522 let bytes = writer.into_inner();
2523 let rt_reader = ReaderBuilder::new()
2524 .build(Cursor::new(bytes))
2525 .expect("build round-trip reader");
2526 let rt_schema = rt_reader.schema();
2527 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2528 let round_trip =
2529 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2530 assert_eq!(
2531 round_trip, original,
2532 "Round-trip batch mismatch for datapage_v2.snappy.avro"
2533 );
2534 Ok(())
2535 }
2536
2537 #[test]
2538 #[cfg(feature = "snappy")]
2539 fn test_single_nan_roundtrip() -> Result<(), AvroError> {
2540 let path = arrow_test_data("avro/single_nan.avro");
2541 let in_file = File::open(&path).expect("open avro/single_nan.avro");
2542 let reader = ReaderBuilder::new()
2543 .build(BufReader::new(in_file))
2544 .expect("build reader for single_nan.avro");
2545 let in_schema = reader.schema();
2546 let in_batches = reader.collect::<Result<Vec<_>, _>>()?;
2547 let original =
2548 arrow::compute::concat_batches(&in_schema, &in_batches).expect("concat input");
2549 let mut writer = AvroWriter::new(Vec::<u8>::new(), original.schema().as_ref().clone())?;
2550 writer.write(&original)?;
2551 writer.finish()?;
2552 let bytes = writer.into_inner();
2553 let rt_reader = ReaderBuilder::new()
2554 .build(Cursor::new(bytes))
2555 .expect("build round_trip reader");
2556 let rt_schema = rt_reader.schema();
2557 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2558 let round_trip =
2559 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2560 assert_eq!(
2561 round_trip, original,
2562 "Round-trip batch mismatch for avro/single_nan.avro"
2563 );
2564 Ok(())
2565 }
2566 #[test]
2567 #[cfg(feature = "snappy")]
2569 fn test_dict_pages_offset_zero_roundtrip() -> Result<(), AvroError> {
2570 let path = arrow_test_data("avro/dict-page-offset-zero.avro");
2571 let rdr_file = File::open(&path).expect("open avro/dict-page-offset-zero.avro");
2572 let reader = ReaderBuilder::new()
2573 .build(BufReader::new(rdr_file))
2574 .expect("build reader for dict-page-offset-zero.avro");
2575 let in_schema = reader.schema();
2576 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2577 let original =
2578 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2579 let buffer: Vec<u8> = Vec::new();
2580 let mut writer = AvroWriter::new(buffer, original.schema().as_ref().clone())?;
2581 writer.write(&original)?;
2582 writer.finish()?;
2583 let bytes = writer.into_inner();
2584 let rt_reader = ReaderBuilder::new()
2585 .build(Cursor::new(bytes))
2586 .expect("build reader for round-trip");
2587 let rt_schema = rt_reader.schema();
2588 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2589 let roundtrip =
2590 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
2591 assert_eq!(
2592 roundtrip, original,
2593 "Round-trip batch mismatch for avro/dict-page-offset-zero.avro"
2594 );
2595 Ok(())
2596 }
2597
2598 #[test]
2599 #[cfg(feature = "snappy")]
2600 fn test_repeated_no_annotation_roundtrip() -> Result<(), AvroError> {
2601 let path = arrow_test_data("avro/repeated_no_annotation.avro");
2602 let in_file = File::open(&path).expect("open avro/repeated_no_annotation.avro");
2603 let reader = ReaderBuilder::new()
2604 .build(BufReader::new(in_file))
2605 .expect("build reader for repeated_no_annotation.avro");
2606 let in_schema = reader.schema();
2607 let in_batches = reader.collect::<Result<Vec<_>, _>>()?;
2608 let original =
2609 arrow::compute::concat_batches(&in_schema, &in_batches).expect("concat input");
2610 let mut writer = AvroWriter::new(Vec::<u8>::new(), original.schema().as_ref().clone())?;
2611 writer.write(&original)?;
2612 writer.finish()?;
2613 let bytes = writer.into_inner();
2614 let rt_reader = ReaderBuilder::new()
2615 .build(Cursor::new(bytes))
2616 .expect("build reader for round-trip buffer");
2617 let rt_schema = rt_reader.schema();
2618 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2619 let round_trip =
2620 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round-trip");
2621 assert_eq!(
2622 round_trip, original,
2623 "Round-trip batch mismatch for avro/repeated_no_annotation.avro"
2624 );
2625 Ok(())
2626 }
2627
2628 #[test]
2629 fn test_nested_record_type_reuse_roundtrip() -> Result<(), AvroError> {
2630 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2631 .join("test/data/nested_record_reuse.avro")
2632 .to_string_lossy()
2633 .into_owned();
2634 let in_file = File::open(&path).expect("open avro/nested_record_reuse.avro");
2635 let reader = ReaderBuilder::new()
2636 .build(BufReader::new(in_file))
2637 .expect("build reader for nested_record_reuse.avro");
2638 let in_schema = reader.schema();
2639 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2640 let input =
2641 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2642 let mut writer = AvroWriter::new(Vec::<u8>::new(), in_schema.as_ref().clone())?;
2643 writer.write(&input)?;
2644 writer.finish()?;
2645 let bytes = writer.into_inner();
2646 let rt_reader = ReaderBuilder::new()
2647 .build(Cursor::new(bytes))
2648 .expect("build round_trip reader");
2649 let rt_schema = rt_reader.schema();
2650 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2651 let round_trip =
2652 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2653 assert_eq!(
2654 round_trip, input,
2655 "Round-trip batch mismatch for nested_record_reuse.avro"
2656 );
2657 Ok(())
2658 }
2659
2660 #[test]
2661 fn test_enum_type_reuse_roundtrip() -> Result<(), AvroError> {
2662 let path =
2663 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test/data/enum_reuse.avro");
2664 let rdr_file = std::fs::File::open(&path).expect("open test/data/enum_reuse.avro");
2665 let reader = ReaderBuilder::new()
2666 .build(std::io::BufReader::new(rdr_file))
2667 .expect("build reader for enum_reuse.avro");
2668 let in_schema = reader.schema();
2669 let input_batches = reader.collect::<Result<Vec<_>, _>>()?;
2670 let original =
2671 arrow::compute::concat_batches(&in_schema, &input_batches).expect("concat input");
2672 let mut writer = AvroWriter::new(Vec::<u8>::new(), original.schema().as_ref().clone())?;
2673 writer.write(&original)?;
2674 writer.finish()?;
2675 let bytes = writer.into_inner();
2676 let rt_reader = ReaderBuilder::new()
2677 .build(std::io::Cursor::new(bytes))
2678 .expect("build round_trip reader");
2679 let rt_schema = rt_reader.schema();
2680 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2681 let round_trip =
2682 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat round_trip");
2683 assert_eq!(
2684 round_trip, original,
2685 "Avro enum type reuse round-trip mismatch"
2686 );
2687 Ok(())
2688 }
2689
2690 #[test]
2691 #[cfg_attr(miri, ignore)] fn comprehensive_e2e_test_roundtrip() -> Result<(), AvroError> {
2693 let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
2694 .join("test/data/comprehensive_e2e.avro");
2695 let rdr_file = File::open(&path).expect("open test/data/comprehensive_e2e.avro");
2696 let reader = ReaderBuilder::new()
2697 .build(BufReader::new(rdr_file))
2698 .expect("build reader for comprehensive_e2e.avro");
2699 let in_schema = reader.schema();
2700 let in_batches = reader.collect::<Result<Vec<_>, _>>()?;
2701 let original =
2702 arrow::compute::concat_batches(&in_schema, &in_batches).expect("concat input");
2703 let sink: Vec<u8> = Vec::new();
2704 let mut writer = AvroWriter::new(sink, original.schema().as_ref().clone())?;
2705 writer.write(&original)?;
2706 writer.finish()?;
2707 let bytes = writer.into_inner();
2708 let rt_reader = ReaderBuilder::new()
2709 .build(Cursor::new(bytes))
2710 .expect("build round-trip reader");
2711 let rt_schema = rt_reader.schema();
2712 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2713 let roundtrip =
2714 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
2715 assert_eq!(
2716 roundtrip, original,
2717 "Round-trip batch mismatch for comprehensive_e2e.avro"
2718 );
2719 Ok(())
2720 }
2721
2722 #[test]
2723 fn test_roundtrip_new_time_encoders_writer() -> Result<(), AvroError> {
2724 let schema = Schema::new(vec![
2725 Field::new("d32", DataType::Date32, false),
2726 Field::new("t32_ms", DataType::Time32(TimeUnit::Millisecond), false),
2727 Field::new("t64_us", DataType::Time64(TimeUnit::Microsecond), false),
2728 Field::new(
2729 "ts_ms",
2730 DataType::Timestamp(TimeUnit::Millisecond, None),
2731 false,
2732 ),
2733 Field::new(
2734 "ts_us",
2735 DataType::Timestamp(TimeUnit::Microsecond, None),
2736 false,
2737 ),
2738 Field::new(
2739 "ts_ns",
2740 DataType::Timestamp(TimeUnit::Nanosecond, None),
2741 false,
2742 ),
2743 ]);
2744 let d32 = Date32Array::from(vec![0, 1, -1]);
2745 let t32_ms: PrimitiveArray<Time32MillisecondType> =
2746 vec![0_i32, 12_345_i32, 86_399_999_i32].into();
2747 let t64_us: PrimitiveArray<Time64MicrosecondType> =
2748 vec![0_i64, 1_234_567_i64, 86_399_999_999_i64].into();
2749 let ts_ms: PrimitiveArray<TimestampMillisecondType> =
2750 vec![0_i64, -1_i64, 1_700_000_000_000_i64].into();
2751 let ts_us: PrimitiveArray<TimestampMicrosecondType> = vec![0_i64, 1_i64, -1_i64].into();
2752 let ts_ns: PrimitiveArray<TimestampNanosecondType> = vec![0_i64, 1_i64, -1_i64].into();
2753 let batch = RecordBatch::try_new(
2754 Arc::new(schema.clone()),
2755 vec![
2756 Arc::new(d32) as ArrayRef,
2757 Arc::new(t32_ms) as ArrayRef,
2758 Arc::new(t64_us) as ArrayRef,
2759 Arc::new(ts_ms) as ArrayRef,
2760 Arc::new(ts_us) as ArrayRef,
2761 Arc::new(ts_ns) as ArrayRef,
2762 ],
2763 )?;
2764 let mut writer = AvroWriter::new(Vec::<u8>::new(), schema.clone())?;
2765 writer.write(&batch)?;
2766 writer.finish()?;
2767 let bytes = writer.into_inner();
2768 let rt_reader = ReaderBuilder::new()
2769 .build(std::io::Cursor::new(bytes))
2770 .expect("build reader for round-trip of new time encoders");
2771 let rt_schema = rt_reader.schema();
2772 let rt_batches = rt_reader.collect::<Result<Vec<_>, _>>()?;
2773 let roundtrip =
2774 arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip");
2775 assert_eq!(roundtrip, batch);
2776 Ok(())
2777 }
2778
2779 fn make_encoder_schema() -> Schema {
2780 Schema::new(vec![
2781 Field::new("a", DataType::Int32, false),
2782 Field::new("b", DataType::Int32, false),
2783 ])
2784 }
2785
2786 fn make_encoder_batch(schema: &Schema) -> RecordBatch {
2787 let a = Int32Array::from(vec![1, 2, 3]);
2788 let b = Int32Array::from(vec![10, 20, 30]);
2789 RecordBatch::try_new(
2790 Arc::new(schema.clone()),
2791 vec![Arc::new(a) as ArrayRef, Arc::new(b) as ArrayRef],
2792 )
2793 .expect("failed to build test RecordBatch")
2794 }
2795
2796 fn make_real_avro_schema_and_batch() -> Result<(Schema, RecordBatch, AvroSchema), AvroError> {
2797 let avro_json = r#"
2798 {
2799 "type": "record",
2800 "name": "User",
2801 "fields": [
2802 { "name": "id", "type": "long" },
2803 { "name": "name", "type": "string" },
2804 { "name": "active", "type": "boolean" },
2805 { "name": "tags", "type": { "type": "array", "items": "int" } },
2806 { "name": "opt", "type": ["null", "string"], "default": null }
2807 ]
2808 }"#;
2809 let avro_schema = AvroSchema::new(avro_json.to_string());
2810 let mut md = HashMap::new();
2811 md.insert(
2812 SCHEMA_METADATA_KEY.to_string(),
2813 avro_schema.json_string.clone(),
2814 );
2815 let item_field = Arc::new(Field::new(
2816 Field::LIST_FIELD_DEFAULT_NAME,
2817 DataType::Int32,
2818 false,
2819 ));
2820 let schema = Schema::new_with_metadata(
2821 vec![
2822 Field::new("id", DataType::Int64, false),
2823 Field::new("name", DataType::Utf8, false),
2824 Field::new("active", DataType::Boolean, false),
2825 Field::new("tags", DataType::List(item_field.clone()), false),
2826 Field::new("opt", DataType::Utf8, true),
2827 ],
2828 md,
2829 );
2830 let id = Int64Array::from(vec![1, 2, 3]);
2831 let name = StringArray::from(vec!["alice", "bob", "carol"]);
2832 let active = BooleanArray::from(vec![true, false, true]);
2833 let mut tags_builder = ListBuilder::new(Int32Builder::new()).with_field(item_field);
2834 tags_builder.values().append_value(1);
2835 tags_builder.values().append_value(2);
2836 tags_builder.append(true);
2837 tags_builder.append(true);
2838 tags_builder.values().append_value(3);
2839 tags_builder.append(true);
2840 let tags = tags_builder.finish();
2841 let opt = StringArray::from(vec![Some("x"), None, Some("z")]);
2842 let batch = RecordBatch::try_new(
2843 Arc::new(schema.clone()),
2844 vec![
2845 Arc::new(id) as ArrayRef,
2846 Arc::new(name) as ArrayRef,
2847 Arc::new(active) as ArrayRef,
2848 Arc::new(tags) as ArrayRef,
2849 Arc::new(opt) as ArrayRef,
2850 ],
2851 )?;
2852 Ok((schema, batch, avro_schema))
2853 }
2854
2855 #[test]
2856 fn test_row_writer_matches_stream_writer_soe() -> Result<(), AvroError> {
2857 let schema = make_encoder_schema();
2858 let batch = make_encoder_batch(&schema);
2859 let mut stream = AvroStreamWriter::new(Vec::<u8>::new(), schema.clone())?;
2860 stream.write(&batch)?;
2861 stream.finish()?;
2862 let stream_bytes = stream.into_inner();
2863 let mut row_writer = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
2864 row_writer.encode(&batch)?;
2865 let rows = row_writer.flush();
2866 let row_bytes: Vec<u8> = rows.bytes().to_vec();
2867 assert_eq!(stream_bytes, row_bytes);
2868 Ok(())
2869 }
2870
2871 #[test]
2872 fn test_row_writer_flush_clears_buffer() -> Result<(), AvroError> {
2873 let schema = make_encoder_schema();
2874 let batch = make_encoder_batch(&schema);
2875 let mut row_writer = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
2876 row_writer.encode(&batch)?;
2877 assert_eq!(row_writer.buffered_len(), batch.num_rows());
2878 let out1 = row_writer.flush();
2879 assert_eq!(out1.len(), batch.num_rows());
2880 assert_eq!(row_writer.buffered_len(), 0);
2881 let out2 = row_writer.flush();
2882 assert_eq!(out2.len(), 0);
2883 Ok(())
2884 }
2885
2886 #[test]
2887 fn test_row_writer_roundtrip_decoder_soe_real_avro_data() -> Result<(), AvroError> {
2888 let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
2889 let mut store = SchemaStore::new();
2890 store.register(avro_schema.clone())?;
2891 let mut row_writer = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
2892 row_writer.encode(&batch)?;
2893 let rows = row_writer.flush();
2894 let mut decoder = ReaderBuilder::new()
2895 .with_writer_schema_store(store)
2896 .with_batch_size(1024)
2897 .build_decoder()?;
2898 for row in rows.iter() {
2899 let consumed = decoder.decode(row.as_ref())?;
2900 assert_eq!(
2901 consumed,
2902 row.len(),
2903 "decoder should consume the full row frame"
2904 );
2905 }
2906 let out = decoder.flush()?.expect("decoded batch");
2907 let expected = pretty_format_batches(std::slice::from_ref(&batch))?.to_string();
2908 let actual = pretty_format_batches(&[out])?.to_string();
2909 assert_eq!(expected, actual);
2910 Ok(())
2911 }
2912
2913 #[test]
2914 fn test_row_writer_roundtrip_decoder_soe_streaming_chunks() -> Result<(), AvroError> {
2915 let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
2916 let mut store = SchemaStore::new();
2917 store.register(avro_schema.clone())?;
2918 let mut row_writer = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
2919 row_writer.encode(&batch)?;
2920 let rows = row_writer.flush();
2921 let mut stream: Vec<u8> = Vec::new();
2923 let mut boundaries: Vec<usize> = Vec::with_capacity(rows.len() + 1);
2924 boundaries.push(0usize);
2925 for row in rows.iter() {
2926 stream.extend_from_slice(row.as_ref());
2927 boundaries.push(stream.len());
2928 }
2929 let mut decoder = ReaderBuilder::new()
2930 .with_writer_schema_store(store)
2931 .with_batch_size(1024)
2932 .build_decoder()?;
2933 let mut buffered = BytesMut::new();
2934 let chunk_rows = [1usize, 2, 3, 1, 4, 2];
2935 let mut row_idx = 0usize;
2936 let mut i = 0usize;
2937 let n_rows = rows.len();
2938 while row_idx < n_rows {
2939 let take = chunk_rows[i % chunk_rows.len()];
2940 i += 1;
2941 let end_row = (row_idx + take).min(n_rows);
2942 let byte_start = boundaries[row_idx];
2943 let byte_end = boundaries[end_row];
2944 buffered.extend_from_slice(&stream[byte_start..byte_end]);
2945 loop {
2946 let consumed = decoder.decode(&buffered)?;
2947 if consumed == 0 {
2948 break;
2949 }
2950 let _ = buffered.split_to(consumed);
2951 }
2952 assert!(
2953 buffered.is_empty(),
2954 "expected decoder to consume the entire frame-aligned chunk"
2955 );
2956 row_idx = end_row;
2957 }
2958 let out = decoder.flush()?.expect("decoded batch");
2959 let expected = pretty_format_batches(std::slice::from_ref(&batch))?.to_string();
2960 let actual = pretty_format_batches(&[out])?.to_string();
2961 assert_eq!(expected, actual);
2962 Ok(())
2963 }
2964
2965 #[test]
2966 fn test_row_writer_roundtrip_decoder_confluent_wire_format_id() -> Result<(), AvroError> {
2967 let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
2968 let schema_id: u32 = 42;
2969 let mut store = SchemaStore::new_with_type(FingerprintAlgorithm::Id);
2970 store.set(Fingerprint::Id(schema_id), avro_schema.clone())?;
2971 let mut row_writer = WriterBuilder::new(schema)
2972 .with_fingerprint_strategy(FingerprintStrategy::Id(schema_id))
2973 .build_encoder::<AvroSoeFormat>()?;
2974 row_writer.encode(&batch)?;
2975 let rows = row_writer.flush();
2976 let mut decoder = ReaderBuilder::new()
2977 .with_writer_schema_store(store)
2978 .with_batch_size(1024)
2979 .build_decoder()?;
2980 for row in rows.iter() {
2981 let consumed = decoder.decode(row.as_ref())?;
2982 assert_eq!(consumed, row.len());
2983 }
2984 let out = decoder.flush()?.expect("decoded batch");
2985 let expected = pretty_format_batches(std::slice::from_ref(&batch))?.to_string();
2986 let actual = pretty_format_batches(&[out])?.to_string();
2987 assert_eq!(expected, actual);
2988 Ok(())
2989 }
2990 #[test]
2991 fn test_encoder_encode_batches_flush_and_encoded_rows_methods_with_avro_binary_format()
2992 -> Result<(), AvroError> {
2993 use crate::writer::format::AvroBinaryFormat;
2994 use arrow_array::{ArrayRef, Int32Array, RecordBatch};
2995 use arrow_schema::{DataType, Field, Schema};
2996 use std::sync::Arc;
2997 let schema = Schema::new(vec![
2998 Field::new("a", DataType::Int32, false),
2999 Field::new("b", DataType::Int32, false),
3000 ]);
3001 let schema_ref = Arc::new(schema.clone());
3002 let batch1 = RecordBatch::try_new(
3003 schema_ref.clone(),
3004 vec![
3005 Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef,
3006 Arc::new(Int32Array::from(vec![10, 20, 30])) as ArrayRef,
3007 ],
3008 )?;
3009 let batch2 = RecordBatch::try_new(
3010 schema_ref,
3011 vec![
3012 Arc::new(Int32Array::from(vec![4, 5])) as ArrayRef,
3013 Arc::new(Int32Array::from(vec![40, 50])) as ArrayRef,
3014 ],
3015 )?;
3016 let mut encoder = WriterBuilder::new(schema).build_encoder::<AvroBinaryFormat>()?;
3017 let empty = Encoder::flush(&mut encoder);
3018 assert_eq!(EncodedRows::len(&empty), 0);
3019 assert!(EncodedRows::is_empty(&empty));
3020 assert_eq!(EncodedRows::bytes(&empty).as_ref(), &[] as &[u8]);
3021 assert_eq!(EncodedRows::offsets(&empty), &[0usize]);
3022 assert_eq!(EncodedRows::iter(&empty).count(), 0);
3023 let empty_vecs: Vec<Vec<u8>> = empty.iter().map(|b| b.to_vec()).collect();
3024 assert!(empty_vecs.is_empty());
3025 let batches = vec![batch1, batch2];
3026 Encoder::encode_batches(&mut encoder, &batches)?;
3027 assert_eq!(encoder.buffered_len(), 5);
3028 let rows = Encoder::flush(&mut encoder);
3029 assert_eq!(
3030 encoder.buffered_len(),
3031 0,
3032 "Encoder::flush should reset the internal offsets"
3033 );
3034 assert_eq!(EncodedRows::len(&rows), 5);
3035 assert!(!EncodedRows::is_empty(&rows));
3036 let expected_offsets: &[usize] = &[0, 2, 4, 6, 8, 10];
3037 assert_eq!(EncodedRows::offsets(&rows), expected_offsets);
3038 let expected_rows: Vec<Vec<u8>> = vec![
3039 vec![2, 20],
3040 vec![4, 40],
3041 vec![6, 60],
3042 vec![8, 80],
3043 vec![10, 100],
3044 ];
3045 let expected_stream: Vec<u8> = expected_rows.concat();
3046 assert_eq!(
3047 EncodedRows::bytes(&rows).as_ref(),
3048 expected_stream.as_slice()
3049 );
3050 for (i, expected) in expected_rows.iter().enumerate() {
3051 assert_eq!(EncodedRows::row(&rows, i)?.as_ref(), expected.as_slice());
3052 }
3053 let iter_rows: Vec<Vec<u8>> = EncodedRows::iter(&rows).map(|b| b.to_vec()).collect();
3054 assert_eq!(iter_rows, expected_rows);
3055 let recreated = EncodedRows::new(
3056 EncodedRows::bytes(&rows).clone(),
3057 EncodedRows::offsets(&rows).to_vec(),
3058 );
3059 assert_eq!(EncodedRows::len(&recreated), EncodedRows::len(&rows));
3060 assert_eq!(EncodedRows::bytes(&recreated), EncodedRows::bytes(&rows));
3061 assert_eq!(
3062 EncodedRows::offsets(&recreated),
3063 EncodedRows::offsets(&rows)
3064 );
3065 let rec_vecs: Vec<Vec<u8>> = recreated.iter().map(|b| b.to_vec()).collect();
3066 assert_eq!(rec_vecs, iter_rows);
3067 let empty_again = Encoder::flush(&mut encoder);
3068 assert!(EncodedRows::is_empty(&empty_again));
3069 Ok(())
3070 }
3071
3072 #[test]
3073 fn test_writer_builder_build_rejects_avro_binary_format() {
3074 use crate::writer::format::AvroBinaryFormat;
3075 use arrow_schema::{DataType, Field, Schema};
3076 let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
3077 let err = WriterBuilder::new(schema)
3078 .build::<_, AvroBinaryFormat>(Vec::<u8>::new())
3079 .unwrap_err();
3080 match err {
3081 AvroError::InvalidArgument(msg) => assert_eq!(
3082 msg,
3083 "AvroBinaryFormat is only supported with Encoder, use build_encoder instead"
3084 ),
3085 other => panic!("expected InvalidArgumentError, got {other:?}"),
3086 }
3087 }
3088 #[test]
3089 fn test_row_encoder_avro_binary_format_roundtrip_decoder_with_soe_framing()
3090 -> Result<(), AvroError> {
3091 use crate::writer::format::AvroBinaryFormat;
3092 let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
3093 let batches: Vec<RecordBatch> = vec![batch.clone(), batch.slice(1, 2)];
3094 let expected = arrow::compute::concat_batches(&batch.schema(), &batches)?;
3095 let mut binary_encoder =
3096 WriterBuilder::new(schema.clone()).build_encoder::<AvroBinaryFormat>()?;
3097 binary_encoder.encode_batches(&batches)?;
3098 let binary_rows = binary_encoder.flush();
3099 assert_eq!(
3100 binary_rows.len(),
3101 expected.num_rows(),
3102 "binary encoder row count mismatch"
3103 );
3104 let mut soe_encoder = WriterBuilder::new(schema).build_encoder::<AvroSoeFormat>()?;
3105 soe_encoder.encode_batches(&batches)?;
3106 let soe_rows = soe_encoder.flush();
3107 assert_eq!(
3108 soe_rows.len(),
3109 binary_rows.len(),
3110 "SOE vs binary row count mismatch"
3111 );
3112 let mut store = SchemaStore::new(); let fp = store.register(avro_schema)?;
3114 let fp_le_bytes = match fp {
3115 Fingerprint::Rabin(v) => v.to_le_bytes(),
3116 other => panic!("expected Rabin fingerprint from SchemaStore::new(), got {other:?}"),
3117 };
3118 const SOE_MAGIC: [u8; 2] = [0xC3, 0x01];
3119 const SOE_PREFIX_LEN: usize = 2 + 8;
3120 for i in 0..binary_rows.len() {
3121 let body = binary_rows.row(i)?;
3122 let soe = soe_rows.row(i)?;
3123 assert!(
3124 soe.len() >= SOE_PREFIX_LEN,
3125 "expected SOE row to include prefix"
3126 );
3127 assert_eq!(&soe.as_ref()[..2], &SOE_MAGIC);
3128 assert_eq!(&soe.as_ref()[2..SOE_PREFIX_LEN], &fp_le_bytes);
3129 assert_eq!(
3130 &soe.as_ref()[SOE_PREFIX_LEN..],
3131 body.as_ref(),
3132 "SOE body bytes differ from AvroBinaryFormat body bytes (row {i})"
3133 );
3134 }
3135 let mut decoder = ReaderBuilder::new()
3136 .with_writer_schema_store(store)
3137 .with_batch_size(1024)
3138 .build_decoder()?;
3139 for body in binary_rows.iter() {
3140 let mut framed = Vec::with_capacity(SOE_PREFIX_LEN + body.len());
3141 framed.extend_from_slice(&SOE_MAGIC);
3142 framed.extend_from_slice(&fp_le_bytes);
3143 framed.extend_from_slice(body.as_ref());
3144 let consumed = decoder.decode(&framed)?;
3145 assert_eq!(
3146 consumed,
3147 framed.len(),
3148 "decoder should consume the full SOE-framed message"
3149 );
3150 }
3151 let out = decoder.flush()?.expect("expected a decoded RecordBatch");
3152 let expected_str = pretty_format_batches(&[expected])?.to_string();
3153 let actual_str = pretty_format_batches(&[out])?.to_string();
3154 assert_eq!(expected_str, actual_str);
3155 Ok(())
3156 }
3157
3158 #[test]
3159 fn test_row_encoder_avro_binary_format_roundtrip_decoder_streaming_chunks()
3160 -> Result<(), AvroError> {
3161 use crate::writer::format::AvroBinaryFormat;
3162 let (schema, batch, avro_schema) = make_real_avro_schema_and_batch()?;
3163 let mut encoder = WriterBuilder::new(schema).build_encoder::<AvroBinaryFormat>()?;
3164 encoder.encode(&batch)?;
3165 let rows = encoder.flush();
3166 let mut store = SchemaStore::new();
3167 let fp = store.register(avro_schema)?;
3168 let fp_le_bytes = match fp {
3169 Fingerprint::Rabin(v) => v.to_le_bytes(),
3170 other => panic!("expected Rabin fingerprint from SchemaStore::new(), got {other:?}"),
3171 };
3172 const SOE_MAGIC: [u8; 2] = [0xC3, 0x01];
3173 const SOE_PREFIX_LEN: usize = 2 + 8;
3174 let mut stream: Vec<u8> = Vec::new();
3175 for body in rows.iter() {
3176 let msg_len: u32 = (SOE_PREFIX_LEN + body.len())
3177 .try_into()
3178 .expect("message length must fit in u32");
3179 stream.extend_from_slice(&msg_len.to_le_bytes());
3180 stream.extend_from_slice(&SOE_MAGIC);
3181 stream.extend_from_slice(&fp_le_bytes);
3182 stream.extend_from_slice(body.as_ref());
3183 }
3184 let mut decoder = ReaderBuilder::new()
3185 .with_writer_schema_store(store)
3186 .with_batch_size(1024)
3187 .build_decoder()?;
3188 let chunk_sizes = [1usize, 2, 3, 5, 8, 13, 21, 34];
3189 let mut pos = 0usize;
3190 let mut i = 0usize;
3191 let mut buffered = BytesMut::new();
3192 let mut decoded_frames = 0usize;
3193 while pos < stream.len() {
3194 let take = chunk_sizes[i % chunk_sizes.len()];
3195 i += 1;
3196 let end = (pos + take).min(stream.len());
3197 buffered.extend_from_slice(&stream[pos..end]);
3198 pos = end;
3199 loop {
3200 if buffered.len() < 4 {
3201 break;
3202 }
3203 let msg_len =
3204 u32::from_le_bytes([buffered[0], buffered[1], buffered[2], buffered[3]])
3205 as usize;
3206 if buffered.len() < 4 + msg_len {
3207 break;
3208 }
3209 let frame = buffered.split_to(4 + msg_len);
3210 let payload = &frame[4..];
3211 let consumed = decoder.decode(payload)?;
3212 assert_eq!(
3213 consumed,
3214 payload.len(),
3215 "decoder should consume the full SOE-framed message"
3216 );
3217
3218 decoded_frames += 1;
3219 }
3220 }
3221 assert!(
3222 buffered.is_empty(),
3223 "expected transport framer to consume all bytes; leftover = {}",
3224 buffered.len()
3225 );
3226 assert_eq!(
3227 decoded_frames,
3228 rows.len(),
3229 "expected to decode exactly one frame per encoded row"
3230 );
3231 let out = decoder.flush()?.expect("expected decoded RecordBatch");
3232 let expected_str = pretty_format_batches(std::slice::from_ref(&batch))?.to_string();
3233 let actual_str = pretty_format_batches(&[out])?.to_string();
3234 assert_eq!(expected_str, actual_str);
3235 Ok(())
3236 }
3237
3238 fn roundtrip_ocf(batch: &RecordBatch) -> Result<RecordBatch, AvroError> {
3240 let schema = batch.schema();
3241 let mut buffer = Vec::<u8>::new();
3242 let mut writer = AvroWriter::new(&mut buffer, schema.as_ref().clone())?;
3243 writer.write(batch)?;
3244 writer.finish()?;
3245 drop(writer);
3246 let reader = ReaderBuilder::new()
3247 .build(Cursor::new(buffer))
3248 .expect("build reader for roundtrip OCF");
3249 let avro_schema_json = reader
3251 .avro_header()
3252 .get(SCHEMA_METADATA_KEY)
3253 .map(|raw| std::str::from_utf8(raw).expect("valid UTF-8").to_string());
3254 let arrow_schema = reader.schema();
3256 let rt_schema = if let Some(json) = avro_schema_json {
3257 let mut metadata = arrow_schema.metadata().clone();
3258 metadata.insert(SCHEMA_METADATA_KEY.to_string(), json);
3259 Arc::new(Schema::new_with_metadata(
3260 arrow_schema.fields().clone(),
3261 metadata,
3262 ))
3263 } else {
3264 arrow_schema
3265 };
3266 let rt_batches: Vec<RecordBatch> = reader.collect::<Result<Vec<_>, _>>()?;
3267 Ok(arrow::compute::concat_batches(&rt_schema, &rt_batches).expect("concat roundtrip"))
3268 }
3269
3270 #[cfg(feature = "avro_custom_types")]
3272 fn assert_round_trip(array: ArrayRef) {
3273 assert_round_trip_widened(array.clone(), array);
3274 }
3275
3276 fn assert_round_trip_widened(input: ArrayRef, expected: ArrayRef) {
3278 let schema = Schema::new(vec![Field::new("val", input.data_type().clone(), true)]);
3279 let batch =
3280 RecordBatch::try_new(Arc::new(schema), vec![input]).expect("failed to create batch");
3281 let roundtrip = roundtrip_ocf(&batch).expect("roundtrip failed");
3282 assert_eq!(
3283 roundtrip.column(0).data_type(),
3284 expected.data_type(),
3285 "output data type mismatch"
3286 );
3287 assert_eq!(
3288 roundtrip.column(0).to_data(),
3289 expected.to_data(),
3290 "output data mismatch"
3291 );
3292 }
3293
3294 #[cfg(feature = "avro_custom_types")]
3295 #[test]
3296 fn test_roundtrip_int8_custom_types() {
3297 assert_round_trip(Arc::new(Int8Array::from(vec![
3298 Some(i8::MIN),
3299 Some(-1),
3300 Some(0),
3301 None,
3302 Some(1),
3303 Some(i8::MAX),
3304 ])));
3305 }
3306
3307 #[cfg(not(feature = "avro_custom_types"))]
3308 #[test]
3309 fn test_roundtrip_int8_no_custom_widens_to_int32() {
3310 assert_round_trip_widened(
3311 Arc::new(Int8Array::from(vec![
3312 Some(i8::MIN),
3313 Some(-1),
3314 Some(0),
3315 None,
3316 Some(1),
3317 Some(i8::MAX),
3318 ])),
3319 Arc::new(Int32Array::from(vec![
3320 Some(i8::MIN as i32),
3321 Some(-1),
3322 Some(0),
3323 None,
3324 Some(1),
3325 Some(i8::MAX as i32),
3326 ])),
3327 );
3328 }
3329
3330 #[cfg(feature = "avro_custom_types")]
3331 #[test]
3332 fn test_roundtrip_int16_custom_types() {
3333 assert_round_trip(Arc::new(Int16Array::from(vec![
3334 Some(i16::MIN),
3335 Some(-1),
3336 Some(0),
3337 None,
3338 Some(1),
3339 Some(i16::MAX),
3340 ])));
3341 }
3342
3343 #[cfg(not(feature = "avro_custom_types"))]
3344 #[test]
3345 fn test_roundtrip_int16_no_custom_widens_to_int32() {
3346 assert_round_trip_widened(
3347 Arc::new(Int16Array::from(vec![
3348 Some(i16::MIN),
3349 Some(-1),
3350 Some(0),
3351 None,
3352 Some(1),
3353 Some(i16::MAX),
3354 ])),
3355 Arc::new(Int32Array::from(vec![
3356 Some(i16::MIN as i32),
3357 Some(-1),
3358 Some(0),
3359 None,
3360 Some(1),
3361 Some(i16::MAX as i32),
3362 ])),
3363 );
3364 }
3365
3366 #[cfg(feature = "avro_custom_types")]
3367 #[test]
3368 fn test_roundtrip_uint8_custom_types() {
3369 assert_round_trip(Arc::new(UInt8Array::from(vec![
3370 Some(0u8),
3371 Some(1),
3372 None,
3373 Some(127),
3374 Some(u8::MAX),
3375 ])));
3376 }
3377
3378 #[cfg(not(feature = "avro_custom_types"))]
3379 #[test]
3380 fn test_roundtrip_uint8_no_custom_widens_to_int32() {
3381 assert_round_trip_widened(
3382 Arc::new(UInt8Array::from(vec![
3383 Some(0u8),
3384 Some(1),
3385 None,
3386 Some(127),
3387 Some(u8::MAX),
3388 ])),
3389 Arc::new(Int32Array::from(vec![
3390 Some(0i32),
3391 Some(1),
3392 None,
3393 Some(127),
3394 Some(u8::MAX as i32),
3395 ])),
3396 );
3397 }
3398
3399 #[cfg(feature = "avro_custom_types")]
3400 #[test]
3401 fn test_roundtrip_uint16_custom_types() {
3402 assert_round_trip(Arc::new(UInt16Array::from(vec![
3403 Some(0u16),
3404 Some(1),
3405 None,
3406 Some(32767),
3407 Some(u16::MAX),
3408 ])));
3409 }
3410
3411 #[cfg(not(feature = "avro_custom_types"))]
3412 #[test]
3413 fn test_roundtrip_uint16_no_custom_widens_to_int32() {
3414 assert_round_trip_widened(
3415 Arc::new(UInt16Array::from(vec![
3416 Some(0u16),
3417 Some(1),
3418 None,
3419 Some(32767),
3420 Some(u16::MAX),
3421 ])),
3422 Arc::new(Int32Array::from(vec![
3423 Some(0i32),
3424 Some(1),
3425 None,
3426 Some(32767),
3427 Some(u16::MAX as i32),
3428 ])),
3429 );
3430 }
3431
3432 #[cfg(feature = "avro_custom_types")]
3433 #[test]
3434 fn test_roundtrip_uint32_custom_types() {
3435 assert_round_trip(Arc::new(UInt32Array::from(vec![
3436 Some(0u32),
3437 Some(1),
3438 None,
3439 Some(i32::MAX as u32),
3440 Some(u32::MAX),
3441 ])));
3442 }
3443
3444 #[cfg(not(feature = "avro_custom_types"))]
3445 #[test]
3446 fn test_roundtrip_uint32_no_custom_widens_to_int64() {
3447 assert_round_trip_widened(
3448 Arc::new(UInt32Array::from(vec![
3449 Some(0u32),
3450 Some(1),
3451 None,
3452 Some(i32::MAX as u32),
3453 Some(u32::MAX),
3454 ])),
3455 Arc::new(Int64Array::from(vec![
3456 Some(0i64),
3457 Some(1),
3458 None,
3459 Some(i32::MAX as i64),
3460 Some(u32::MAX as i64),
3461 ])),
3462 );
3463 }
3464
3465 #[cfg(feature = "avro_custom_types")]
3466 #[test]
3467 fn test_roundtrip_uint64_custom_types() {
3468 assert_round_trip(Arc::new(UInt64Array::from(vec![
3469 Some(0u64),
3470 Some(1),
3471 None,
3472 Some(i64::MAX as u64),
3473 Some(u64::MAX),
3474 ])));
3475 }
3476
3477 #[cfg(not(feature = "avro_custom_types"))]
3478 #[test]
3479 fn test_roundtrip_uint64_no_custom_widens_to_int64() {
3480 assert_round_trip_widened(
3481 Arc::new(UInt64Array::from(vec![
3482 Some(0u64),
3483 Some(1),
3484 None,
3485 Some(i64::MAX as u64),
3486 ])),
3487 Arc::new(Int64Array::from(vec![
3488 Some(0i64),
3489 Some(1),
3490 None,
3491 Some(i64::MAX),
3492 ])),
3493 );
3494 }
3495
3496 #[cfg(not(feature = "avro_custom_types"))]
3497 #[test]
3498 fn test_roundtrip_uint64_overflow_errors_without_custom() {
3499 use arrow_array::UInt64Array;
3500 let schema = Schema::new(vec![Field::new("val", DataType::UInt64, false)]);
3501 let values: Vec<u64> = vec![u64::MAX];
3502 let array = UInt64Array::from(values);
3503 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(array) as ArrayRef])
3504 .expect("create batch");
3505 let result = roundtrip_ocf(&batch);
3506 assert!(
3507 result.is_err(),
3508 "Expected error when encoding UInt64 > i64::MAX without avro_custom_types"
3509 );
3510 }
3511
3512 #[cfg(feature = "avro_custom_types")]
3513 #[test]
3514 fn test_roundtrip_float16_custom_types() {
3515 assert_round_trip(Arc::new(Float16Array::from(vec![
3516 Some(f16::ZERO),
3517 Some(f16::ONE),
3518 None,
3519 Some(f16::NEG_ONE),
3520 Some(f16::MAX),
3521 Some(f16::MIN),
3522 ])));
3523 }
3524
3525 #[cfg(not(feature = "avro_custom_types"))]
3526 #[test]
3527 #[cfg_attr(miri, ignore)] fn test_roundtrip_float16_no_custom_widens_to_float32() {
3529 assert_round_trip_widened(
3530 Arc::new(Float16Array::from(vec![
3531 Some(f16::ZERO),
3532 Some(f16::ONE),
3533 None,
3534 Some(f16::NEG_ONE),
3535 ])),
3536 Arc::new(Float32Array::from(vec![
3537 Some(0.0f32),
3538 Some(1.0),
3539 None,
3540 Some(-1.0),
3541 ])),
3542 );
3543 }
3544
3545 #[cfg(feature = "avro_custom_types")]
3546 #[test]
3547 fn test_roundtrip_date64_custom_types() {
3548 assert_round_trip(Arc::new(Date64Array::from(vec![
3549 Some(0i64),
3550 Some(86_400_000),
3551 None,
3552 Some(1_609_459_200_000),
3553 ])));
3554 }
3555
3556 #[cfg(not(feature = "avro_custom_types"))]
3557 #[test]
3558 fn test_roundtrip_date64_no_custom_as_timestamp_millis() {
3559 assert_round_trip_widened(
3560 Arc::new(Date64Array::from(vec![
3561 Some(0i64),
3562 Some(86_400_000),
3563 None,
3564 Some(1_609_459_200_000),
3565 ])),
3566 Arc::new(TimestampMillisecondArray::from(vec![
3567 Some(0i64),
3568 Some(86_400_000),
3569 None,
3570 Some(1_609_459_200_000),
3571 ])),
3572 );
3573 }
3574
3575 #[cfg(feature = "avro_custom_types")]
3576 #[test]
3577 fn test_roundtrip_time64_nanosecond_custom_types() {
3578 assert_round_trip(Arc::new(Time64NanosecondArray::from(vec![
3579 Some(0i64),
3580 Some(1_000_000_000),
3581 None,
3582 Some(86_399_999_999_999),
3583 ])));
3584 }
3585
3586 #[cfg(not(feature = "avro_custom_types"))]
3587 #[test]
3588 fn test_roundtrip_time64_nanos_no_custom_truncates_to_micros() {
3589 assert_round_trip_widened(
3591 Arc::new(Time64NanosecondArray::from(vec![
3592 Some(0i64),
3593 Some(1_000_000_000),
3594 None,
3595 Some(86_399_999_000_000),
3596 ])),
3597 Arc::new(Time64MicrosecondArray::from(vec![
3598 Some(0i64),
3599 Some(1_000_000),
3600 None,
3601 Some(86_399_999_000),
3602 ])),
3603 );
3604 }
3605
3606 #[cfg(feature = "avro_custom_types")]
3607 #[test]
3608 fn test_roundtrip_time32_second_custom_types() {
3609 assert_round_trip(Arc::new(Time32SecondArray::from(vec![
3610 Some(0i32),
3611 Some(3600),
3612 None,
3613 Some(86399),
3614 ])));
3615 }
3616
3617 #[cfg(not(feature = "avro_custom_types"))]
3618 #[test]
3619 fn test_roundtrip_time32_second_no_custom_scales_to_millis() {
3620 assert_round_trip_widened(
3621 Arc::new(Time32SecondArray::from(vec![
3622 Some(0i32),
3623 Some(3600),
3624 None,
3625 Some(86399),
3626 ])),
3627 Arc::new(Time32MillisecondArray::from(vec![
3628 Some(0i32),
3629 Some(3_600_000),
3630 None,
3631 Some(86_399_000),
3632 ])),
3633 );
3634 }
3635
3636 #[cfg(feature = "avro_custom_types")]
3637 #[test]
3638 fn test_roundtrip_timestamp_second_custom_types() {
3639 assert_round_trip(Arc::new(
3640 TimestampSecondArray::from(vec![Some(0i64), Some(1609459200), None, Some(1735689600)])
3641 .with_timezone("+00:00"),
3642 ));
3643 }
3644
3645 #[cfg(not(feature = "avro_custom_types"))]
3646 #[test]
3647 fn test_roundtrip_timestamp_second_no_custom_scales_to_millis() {
3648 assert_round_trip_widened(
3649 Arc::new(
3650 TimestampSecondArray::from(vec![
3651 Some(0i64),
3652 Some(1609459200),
3653 None,
3654 Some(1735689600),
3655 ])
3656 .with_timezone("+00:00"),
3657 ),
3658 Arc::new(
3659 TimestampMillisecondArray::from(vec![
3660 Some(0i64),
3661 Some(1_609_459_200_000),
3662 None,
3663 Some(1_735_689_600_000),
3664 ])
3665 .with_timezone("+00:00"),
3666 ),
3667 );
3668 }
3669
3670 #[cfg(feature = "avro_custom_types")]
3671 #[test]
3672 fn test_roundtrip_interval_year_month_custom_types() {
3673 assert_round_trip(Arc::new(IntervalYearMonthArray::from(vec![
3674 Some(0i32),
3675 Some(12),
3676 None,
3677 Some(-6),
3678 Some(25),
3679 ])));
3680 }
3681
3682 #[cfg(not(feature = "avro_custom_types"))]
3683 #[test]
3684 fn test_roundtrip_interval_year_month_no_custom() {
3685 assert_round_trip_widened(
3687 Arc::new(IntervalYearMonthArray::from(vec![
3688 Some(0i32),
3689 Some(12),
3690 None,
3691 Some(25),
3692 ])),
3693 Arc::new(IntervalMonthDayNanoArray::from(vec![
3694 Some(IntervalMonthDayNano::new(0, 0, 0)),
3695 Some(IntervalMonthDayNano::new(12, 0, 0)),
3696 None,
3697 Some(IntervalMonthDayNano::new(25, 0, 0)),
3698 ])),
3699 );
3700 }
3701
3702 #[cfg(feature = "avro_custom_types")]
3703 #[test]
3704 fn test_roundtrip_interval_day_time_custom_types() {
3705 assert_round_trip(Arc::new(IntervalDayTimeArray::from(vec![
3706 Some(IntervalDayTime::new(0, 0)),
3707 Some(IntervalDayTime::new(1, 1000)),
3708 None,
3709 Some(IntervalDayTime::new(30, 3600000)),
3710 ])));
3711 }
3712
3713 #[cfg(not(feature = "avro_custom_types"))]
3714 #[test]
3715 fn test_roundtrip_interval_day_time_no_custom() {
3716 assert_round_trip_widened(
3717 Arc::new(IntervalDayTimeArray::from(vec![
3718 Some(IntervalDayTime::new(0, 0)),
3719 Some(IntervalDayTime::new(1, 1000)),
3720 None,
3721 Some(IntervalDayTime::new(30, 3600000)),
3722 ])),
3723 Arc::new(IntervalMonthDayNanoArray::from(vec![
3724 Some(IntervalMonthDayNano::new(0, 0, 0)),
3725 Some(IntervalMonthDayNano::new(0, 1, 1_000_000_000)),
3726 None,
3727 Some(IntervalMonthDayNano::new(0, 30, 3_600_000_000_000)),
3728 ])),
3729 );
3730 }
3731
3732 #[cfg(feature = "avro_custom_types")]
3733 #[test]
3734 fn test_roundtrip_interval_month_day_nano_custom_types() {
3735 assert_round_trip(Arc::new(IntervalMonthDayNanoArray::from(vec![
3736 Some(IntervalMonthDayNano::new(0, 0, 0)),
3737 Some(IntervalMonthDayNano::new(1, 2, 3)),
3738 None,
3739 Some(IntervalMonthDayNano::new(-4, -5, -6)),
3740 ])));
3741 }
3742
3743 #[cfg(not(feature = "avro_custom_types"))]
3744 #[test]
3745 fn test_roundtrip_interval_month_day_nano_no_custom() {
3746 assert_round_trip_widened(
3748 Arc::new(IntervalMonthDayNanoArray::from(vec![
3749 Some(IntervalMonthDayNano::new(0, 0, 0)),
3750 Some(IntervalMonthDayNano::new(1, 2, 3_000_000)),
3751 None,
3752 Some(IntervalMonthDayNano::new(4, 5, 6_000_000)),
3753 ])),
3754 Arc::new(IntervalMonthDayNanoArray::from(vec![
3755 Some(IntervalMonthDayNano::new(0, 0, 0)),
3756 Some(IntervalMonthDayNano::new(1, 2, 3_000_000)),
3757 None,
3758 Some(IntervalMonthDayNano::new(4, 5, 6_000_000)),
3759 ])),
3760 );
3761 }
3762
3763 fn schemas_equal_ignoring_metadata(left: &Schema, right: &Schema) -> bool {
3764 if left.fields().len() != right.fields().len() {
3765 return false;
3766 }
3767 for (l, r) in left.fields().iter().zip(right.fields().iter()) {
3768 if l.name() != r.name()
3769 || l.data_type() != r.data_type()
3770 || l.is_nullable() != r.is_nullable()
3771 {
3772 return false;
3773 }
3774 }
3775 true
3776 }
3777
3778 fn avro_field_type<'a>(avro_schema: &'a Value, name: &str) -> &'a Value {
3779 let fields = avro_schema
3780 .get("fields")
3781 .and_then(|v| v.as_array())
3782 .expect("avro schema has 'fields' array");
3783 fields
3784 .iter()
3785 .find(|f| f.get("name").and_then(|n| n.as_str()) == Some(name))
3786 .unwrap_or_else(|| panic!("avro schema missing field '{name}'"))
3787 .get("type")
3788 .expect("field has 'type'")
3789 }
3790
3791 #[test]
3792 #[cfg_attr(miri, ignore)] fn e2e_types_and_schema_alignment() -> Result<(), AvroError> {
3794 let i8_values: Vec<Option<i8>> = vec![Some(i8::MIN), Some(-1), Some(i8::MAX)];
3799 let i16_values: Vec<Option<i16>> = vec![Some(i16::MIN), Some(-1), Some(i16::MAX)];
3800 let u8_values: Vec<Option<u8>> = vec![Some(0), Some(1), Some(u8::MAX)];
3801 let u16_values: Vec<Option<u16>> = vec![Some(0), Some(1), Some(u16::MAX)];
3802 let u32_values: Vec<Option<u32>> = vec![Some(0), Some(1), Some(u32::MAX)];
3803 let u64_values: Vec<Option<u64>> = if cfg!(feature = "avro_custom_types") {
3804 vec![Some(0), Some(i64::MAX as u64), Some((i64::MAX as u64) + 1)]
3805 } else {
3806 vec![Some(0), Some((i64::MAX as u64) - 1), Some(i64::MAX as u64)]
3808 };
3809 let f16_values: Vec<Option<f16>> = vec![
3810 Some(f16::from_f32(1.5)),
3811 Some(f16::from_f32(-2.0)),
3812 Some(f16::from_f32(0.0)),
3813 ];
3814 let date64_values: Vec<Option<i64>> = vec![Some(-86_400_000), Some(0), Some(86_400_000)];
3815 let time32s_values: Vec<Option<i32>> = vec![Some(0), Some(1), Some(86_399)];
3816 let time64ns_values: Vec<Option<i64>> = vec![
3817 Some(0),
3818 Some(1_234_567_890), Some(86_399_000_000_123_i64), ];
3821 let ts_s_local_values: Vec<Option<i64>> = vec![Some(-1), Some(0), Some(1)];
3822 let ts_s_utc_values: Vec<Option<i64>> = vec![Some(1), Some(2), Some(3)];
3823 let iv_ym_values: Vec<Option<i32>> = if cfg!(feature = "avro_custom_types") {
3824 vec![Some(0), Some(-6), Some(25)]
3825 } else {
3826 vec![Some(0), Some(12), Some(25)]
3828 };
3829 let iv_dt_values: Vec<Option<IntervalDayTime>> = if cfg!(feature = "avro_custom_types") {
3830 vec![
3831 Some(IntervalDayTime::new(0, 0)),
3832 Some(IntervalDayTime::new(1, 1000)),
3833 Some(IntervalDayTime::new(-1, -1000)),
3834 ]
3835 } else {
3836 vec![
3838 Some(IntervalDayTime::new(0, 0)),
3839 Some(IntervalDayTime::new(1, 1000)),
3840 Some(IntervalDayTime::new(30, 3_600_000)),
3841 ]
3842 };
3843 let iv_mdn_values: Vec<Option<IntervalMonthDayNano>> =
3844 if cfg!(feature = "avro_custom_types") {
3845 vec![
3846 Some(IntervalMonthDayNano::new(0, 0, 0)),
3847 Some(IntervalMonthDayNano::new(1, 2, 3)), Some(IntervalMonthDayNano::new(-1, -2, -3)), ]
3850 } else {
3851 vec![
3853 Some(IntervalMonthDayNano::new(0, 0, 0)),
3854 Some(IntervalMonthDayNano::new(1, 2, 3_000_000)), Some(IntervalMonthDayNano::new(10, 20, 30_000_000_000)), ]
3857 };
3858 let schema = Schema::new(vec![
3860 Field::new("i8", DataType::Int8, false),
3861 Field::new("i16", DataType::Int16, false),
3862 Field::new("u8", DataType::UInt8, false),
3863 Field::new("u16", DataType::UInt16, false),
3864 Field::new("u32", DataType::UInt32, false),
3865 Field::new("u64", DataType::UInt64, false),
3866 Field::new("f16", DataType::Float16, false),
3867 Field::new("date64", DataType::Date64, false),
3868 Field::new("time32s", DataType::Time32(TimeUnit::Second), false),
3869 Field::new("time64ns", DataType::Time64(TimeUnit::Nanosecond), false),
3870 Field::new(
3871 "ts_s_local",
3872 DataType::Timestamp(TimeUnit::Second, None),
3873 false,
3874 ),
3875 Field::new(
3876 "ts_s_utc",
3877 DataType::Timestamp(TimeUnit::Second, Some("+00:00".into())),
3878 false,
3879 ),
3880 Field::new("iv_ym", DataType::Interval(IntervalUnit::YearMonth), false),
3881 Field::new("iv_dt", DataType::Interval(IntervalUnit::DayTime), false),
3882 Field::new(
3883 "iv_mdn",
3884 DataType::Interval(IntervalUnit::MonthDayNano),
3885 false,
3886 ),
3887 ]);
3888 let batch = RecordBatch::try_new(
3889 Arc::new(schema.clone()),
3890 vec![
3891 Arc::new(Int8Array::from(i8_values.clone())) as ArrayRef,
3892 Arc::new(Int16Array::from(i16_values.clone())) as ArrayRef,
3893 Arc::new(UInt8Array::from(u8_values.clone())) as ArrayRef,
3894 Arc::new(UInt16Array::from(u16_values.clone())) as ArrayRef,
3895 Arc::new(UInt32Array::from(u32_values.clone())) as ArrayRef,
3896 Arc::new(UInt64Array::from(u64_values.clone())) as ArrayRef,
3897 Arc::new(Float16Array::from(f16_values.clone())) as ArrayRef,
3898 Arc::new(Date64Array::from(date64_values.clone())) as ArrayRef,
3899 Arc::new(Time32SecondArray::from(time32s_values.clone())) as ArrayRef,
3900 Arc::new(Time64NanosecondArray::from(time64ns_values.clone())) as ArrayRef,
3901 Arc::new(TimestampSecondArray::from(ts_s_local_values.clone())) as ArrayRef,
3902 Arc::new(
3903 TimestampSecondArray::from(ts_s_utc_values.clone()).with_timezone("+00:00"),
3904 ) as ArrayRef,
3905 Arc::new(IntervalYearMonthArray::from(iv_ym_values.clone())) as ArrayRef,
3906 Arc::new(IntervalDayTimeArray::from(iv_dt_values.clone())) as ArrayRef,
3907 Arc::new(IntervalMonthDayNanoArray::from(iv_mdn_values.clone())) as ArrayRef,
3908 ],
3909 )?;
3910 let rt = roundtrip_ocf(&batch)?;
3911 let rt_schema = rt.schema();
3912 let avro_schema_json = rt_schema
3913 .metadata()
3914 .get(SCHEMA_METADATA_KEY)
3915 .expect("avro.schema missing in round-tripped batch metadata");
3916 let avro_schema: Value =
3917 serde_json::from_str(avro_schema_json).expect("valid avro schema json");
3918 let rt_arrow_schema = rt.schema();
3919 if cfg!(feature = "avro_custom_types") {
3920 assert!(
3921 schemas_equal_ignoring_metadata(rt_arrow_schema.as_ref(), &schema),
3922 "Schema fields mismatch.\nExpected: {schema:?}\nGot: {rt_arrow_schema:?}"
3923 );
3924 for field_name in ["u64", "f16", "iv_ym", "iv_dt", "iv_mdn"] {
3925 let field = rt_arrow_schema
3926 .field_with_name(field_name)
3927 .expect("field exists");
3928 assert!(
3929 field.metadata().get(AVRO_NAME_METADATA_KEY).is_some(),
3930 "Field '{field_name}' should have avro.name metadata"
3931 );
3932 }
3933 } else {
3934 let exp_schema = Schema::new(vec![
3938 Field::new("i8", DataType::Int32, false),
3939 Field::new("i16", DataType::Int32, false),
3940 Field::new("u8", DataType::Int32, false),
3941 Field::new("u16", DataType::Int32, false),
3942 Field::new("u32", DataType::Int64, false),
3943 Field::new("u64", DataType::Int64, false),
3944 Field::new("f16", DataType::Float32, false),
3945 Field::new(
3946 "date64",
3947 DataType::Timestamp(TimeUnit::Millisecond, None),
3948 false,
3949 ),
3950 Field::new("time32s", DataType::Time32(TimeUnit::Millisecond), false),
3951 Field::new("time64ns", DataType::Time64(TimeUnit::Microsecond), false),
3952 Field::new(
3953 "ts_s_local",
3954 DataType::Timestamp(TimeUnit::Millisecond, None),
3955 false,
3956 ),
3957 Field::new(
3958 "ts_s_utc",
3959 DataType::Timestamp(TimeUnit::Millisecond, Some("+00:00".into())),
3960 false,
3961 ),
3962 Field::new(
3963 "iv_ym",
3964 DataType::Interval(IntervalUnit::MonthDayNano),
3965 false,
3966 ),
3967 Field::new(
3968 "iv_dt",
3969 DataType::Interval(IntervalUnit::MonthDayNano),
3970 false,
3971 ),
3972 Field::new(
3973 "iv_mdn",
3974 DataType::Interval(IntervalUnit::MonthDayNano),
3975 false,
3976 ),
3977 ]);
3978 assert!(
3979 schemas_equal_ignoring_metadata(rt_arrow_schema.as_ref(), &exp_schema),
3980 "Schema fields mismatch.\nExpected: {exp_schema:?}\nGot: {rt_arrow_schema:?}"
3981 );
3982 for field_name in ["iv_ym", "iv_dt", "iv_mdn"] {
3983 let field = rt_arrow_schema
3984 .field_with_name(field_name)
3985 .expect("field exists");
3986 assert!(
3987 field.metadata().get(AVRO_NAME_METADATA_KEY).is_some(),
3988 "Field '{field_name}' should have avro.name metadata"
3989 );
3990 }
3991 }
3992 if cfg!(feature = "avro_custom_types") {
3993 assert_eq!(
3994 avro_field_type(&avro_schema, "i8"),
3995 &json!({"type":"int","logicalType":"arrow.int8"})
3996 );
3997 assert_eq!(
3998 avro_field_type(&avro_schema, "i16"),
3999 &json!({"type":"int","logicalType":"arrow.int16"})
4000 );
4001 assert_eq!(
4002 avro_field_type(&avro_schema, "u8"),
4003 &json!({"type":"int","logicalType":"arrow.uint8"})
4004 );
4005 assert_eq!(
4006 avro_field_type(&avro_schema, "u16"),
4007 &json!({"type":"int","logicalType":"arrow.uint16"})
4008 );
4009 assert_eq!(
4010 avro_field_type(&avro_schema, "u32"),
4011 &json!({"type":"long","logicalType":"arrow.uint32"})
4012 );
4013 assert_eq!(
4014 avro_field_type(&avro_schema, "u64"),
4015 &json!({"type":"fixed","name":"u64","size":8,"logicalType":"arrow.uint64"})
4016 );
4017 assert_eq!(
4018 avro_field_type(&avro_schema, "f16"),
4019 &json!({"type":"fixed","name":"f16","size":2,"logicalType":"arrow.float16"})
4020 );
4021 assert_eq!(
4022 avro_field_type(&avro_schema, "date64"),
4023 &json!({"type":"long","logicalType":"arrow.date64"})
4024 );
4025 assert_eq!(
4026 avro_field_type(&avro_schema, "time32s"),
4027 &json!({"type":"int","logicalType":"arrow.time32-second"})
4028 );
4029 assert_eq!(
4030 avro_field_type(&avro_schema, "time64ns"),
4031 &json!({"type":"long","logicalType":"arrow.time64-nanosecond"})
4032 );
4033 assert_eq!(
4034 avro_field_type(&avro_schema, "ts_s_local"),
4035 &json!({"type":"long","logicalType":"arrow.local-timestamp-second"})
4036 );
4037 assert_eq!(
4038 avro_field_type(&avro_schema, "ts_s_utc"),
4039 &json!({"type":"long","logicalType":"arrow.timestamp-second"})
4040 );
4041 assert_eq!(
4042 avro_field_type(&avro_schema, "iv_ym"),
4043 &json!({"type":"fixed","name":"iv_ym","size":4,"logicalType":"arrow.interval-year-month"})
4044 );
4045 assert_eq!(
4046 avro_field_type(&avro_schema, "iv_dt"),
4047 &json!({"type":"fixed","name":"iv_dt","size":8,"logicalType":"arrow.interval-day-time"})
4048 );
4049 assert_eq!(
4050 avro_field_type(&avro_schema, "iv_mdn"),
4051 &json!({"type":"fixed","name":"iv_mdn","size":16,"logicalType":"arrow.interval-month-day-nano"})
4052 );
4053 } else {
4054 assert_eq!(avro_field_type(&avro_schema, "i8"), &json!("int"));
4064 assert_eq!(avro_field_type(&avro_schema, "i16"), &json!("int"));
4065 assert_eq!(avro_field_type(&avro_schema, "u8"), &json!("int"));
4066 assert_eq!(avro_field_type(&avro_schema, "u16"), &json!("int"));
4067 assert_eq!(avro_field_type(&avro_schema, "u32"), &json!("long"));
4068 assert_eq!(avro_field_type(&avro_schema, "u64"), &json!("long"));
4069 assert_eq!(avro_field_type(&avro_schema, "f16"), &json!("float"));
4070 assert_eq!(
4071 avro_field_type(&avro_schema, "date64"),
4072 &json!({"type":"long","logicalType":"local-timestamp-millis"})
4073 );
4074 assert_eq!(
4075 avro_field_type(&avro_schema, "time32s"),
4076 &json!({"type":"int","logicalType":"time-millis"})
4077 );
4078 assert_eq!(
4079 avro_field_type(&avro_schema, "time64ns"),
4080 &json!({"type":"long","logicalType":"time-micros"})
4081 );
4082 assert_eq!(
4083 avro_field_type(&avro_schema, "ts_s_local"),
4084 &json!({"type":"long","logicalType":"local-timestamp-millis"})
4085 );
4086 assert_eq!(
4087 avro_field_type(&avro_schema, "ts_s_utc"),
4088 &json!({"type":"long","logicalType":"timestamp-millis"})
4089 );
4090 assert_eq!(
4091 avro_field_type(&avro_schema, "iv_ym"),
4092 &json!({"type":"fixed","name":"iv_ym","size":12,"logicalType":"duration"})
4093 );
4094 assert_eq!(
4095 avro_field_type(&avro_schema, "iv_dt"),
4096 &json!({"type":"fixed","name":"iv_dt","size":12,"logicalType":"duration"})
4097 );
4098 assert_eq!(
4099 avro_field_type(&avro_schema, "iv_mdn"),
4100 &json!({"type":"fixed","name":"iv_mdn","size":12,"logicalType":"duration"})
4101 );
4102 }
4103 if cfg!(feature = "avro_custom_types") {
4104 assert_eq!(
4105 rt.column(0).as_ref(),
4106 &Int8Array::from(i8_values) as &dyn Array
4107 );
4108 assert_eq!(
4109 rt.column(1).as_ref(),
4110 &Int16Array::from(i16_values) as &dyn Array
4111 );
4112 assert_eq!(
4113 rt.column(2).as_ref(),
4114 &UInt8Array::from(u8_values) as &dyn Array
4115 );
4116 assert_eq!(
4117 rt.column(3).as_ref(),
4118 &UInt16Array::from(u16_values) as &dyn Array
4119 );
4120 assert_eq!(
4121 rt.column(4).as_ref(),
4122 &UInt32Array::from(u32_values) as &dyn Array
4123 );
4124 assert_eq!(
4125 rt.column(5).as_ref(),
4126 &UInt64Array::from(u64_values) as &dyn Array
4127 );
4128 assert_eq!(
4129 rt.column(6).as_ref(),
4130 &Float16Array::from(f16_values) as &dyn Array
4131 );
4132 assert_eq!(
4133 rt.column(7).as_ref(),
4134 &Date64Array::from(date64_values) as &dyn Array
4135 );
4136 assert_eq!(
4137 rt.column(8).as_ref(),
4138 &Time32SecondArray::from(time32s_values) as &dyn Array
4139 );
4140 assert_eq!(
4141 rt.column(9).as_ref(),
4142 &Time64NanosecondArray::from(time64ns_values) as &dyn Array
4143 );
4144 assert_eq!(
4145 rt.column(10).as_ref(),
4146 &TimestampSecondArray::from(ts_s_local_values) as &dyn Array
4147 );
4148 assert_eq!(
4149 rt.column(11).as_ref(),
4150 &TimestampSecondArray::from(ts_s_utc_values).with_timezone("+00:00") as &dyn Array
4151 );
4152 assert_eq!(
4153 rt.column(12).as_ref(),
4154 &IntervalYearMonthArray::from(iv_ym_values) as &dyn Array
4155 );
4156 assert_eq!(
4157 rt.column(13).as_ref(),
4158 &IntervalDayTimeArray::from(iv_dt_values) as &dyn Array
4159 );
4160 assert_eq!(
4161 rt.column(14).as_ref(),
4162 &IntervalMonthDayNanoArray::from(iv_mdn_values) as &dyn Array
4163 );
4164 } else {
4165 let exp_i8: Vec<Option<i32>> = i8_values.iter().map(|v| v.map(|x| x as i32)).collect();
4166 let exp_i16: Vec<Option<i32>> =
4167 i16_values.iter().map(|v| v.map(|x| x as i32)).collect();
4168 let exp_u8: Vec<Option<i32>> = u8_values.iter().map(|v| v.map(|x| x as i32)).collect();
4169 let exp_u16: Vec<Option<i32>> =
4170 u16_values.iter().map(|v| v.map(|x| x as i32)).collect();
4171 let exp_u32: Vec<Option<i64>> =
4172 u32_values.iter().map(|v| v.map(|x| x as i64)).collect();
4173 let exp_u64: Vec<Option<i64>> =
4174 u64_values.iter().map(|v| v.map(|x| x as i64)).collect();
4175 let exp_f16: Vec<Option<f32>> =
4176 f16_values.iter().map(|v| v.map(|x| x.to_f32())).collect();
4177 let exp_time32_ms: Vec<Option<i32>> = time32s_values
4178 .iter()
4179 .map(|v| v.map(|x| x.saturating_mul(1000)))
4180 .collect();
4181 let exp_time64_us: Vec<Option<i64>> = time64ns_values
4182 .iter()
4183 .map(|v| v.map(|x| x / 1000))
4184 .collect();
4185 let exp_ts_local_ms: Vec<Option<i64>> = ts_s_local_values
4186 .iter()
4187 .map(|v| v.map(|x| x * 1000))
4188 .collect();
4189 let exp_ts_utc_ms: Vec<Option<i64>> = ts_s_utc_values
4190 .iter()
4191 .map(|v| v.map(|x| x * 1000))
4192 .collect();
4193 let exp_iv_ym: Vec<Option<IntervalMonthDayNano>> = iv_ym_values
4195 .iter()
4196 .map(|v| v.map(|months| IntervalMonthDayNano::new(months, 0, 0)))
4197 .collect();
4198 let exp_iv_dt: Vec<Option<IntervalMonthDayNano>> = iv_dt_values
4199 .iter()
4200 .map(|v| {
4201 v.map(|dt| {
4202 IntervalMonthDayNano::new(0, dt.days, (dt.milliseconds as i64) * 1_000_000)
4203 })
4204 })
4205 .collect();
4206 assert_eq!(
4207 rt.column(0).as_ref(),
4208 &Int32Array::from(exp_i8) as &dyn Array
4209 );
4210 assert_eq!(
4211 rt.column(1).as_ref(),
4212 &Int32Array::from(exp_i16) as &dyn Array
4213 );
4214 assert_eq!(
4215 rt.column(2).as_ref(),
4216 &Int32Array::from(exp_u8) as &dyn Array
4217 );
4218 assert_eq!(
4219 rt.column(3).as_ref(),
4220 &Int32Array::from(exp_u16) as &dyn Array
4221 );
4222 assert_eq!(
4223 rt.column(4).as_ref(),
4224 &arrow_array::Int64Array::from(exp_u32) as &dyn Array
4225 );
4226 assert_eq!(
4227 rt.column(5).as_ref(),
4228 &arrow_array::Int64Array::from(exp_u64) as &dyn Array
4229 );
4230 assert_eq!(
4231 rt.column(6).as_ref(),
4232 &arrow_array::Float32Array::from(exp_f16) as &dyn Array
4233 );
4234 assert_eq!(
4235 rt.column(7).as_ref(),
4236 &TimestampMillisecondArray::from(date64_values) as &dyn Array
4237 );
4238 assert_eq!(
4239 rt.column(8).as_ref(),
4240 &Time32MillisecondArray::from(exp_time32_ms) as &dyn Array
4241 );
4242 assert_eq!(
4243 rt.column(9).as_ref(),
4244 &Time64MicrosecondArray::from(exp_time64_us) as &dyn Array
4245 );
4246 assert_eq!(
4247 rt.column(10).as_ref(),
4248 &TimestampMillisecondArray::from(exp_ts_local_ms) as &dyn Array
4249 );
4250 assert_eq!(
4251 rt.column(11).as_ref(),
4252 &TimestampMillisecondArray::from(exp_ts_utc_ms).with_timezone("+00:00")
4253 as &dyn Array
4254 );
4255 assert_eq!(
4256 rt.column(12).as_ref(),
4257 &IntervalMonthDayNanoArray::from(exp_iv_ym) as &dyn Array
4258 );
4259 assert_eq!(
4260 rt.column(13).as_ref(),
4261 &IntervalMonthDayNanoArray::from(exp_iv_dt) as &dyn Array
4262 );
4263 assert_eq!(
4264 rt.column(14).as_ref(),
4265 &IntervalMonthDayNanoArray::from(iv_mdn_values) as &dyn Array
4266 );
4267 }
4268 Ok(())
4269 }
4270
4271 #[cfg(not(feature = "avro_custom_types"))]
4272 #[test]
4273 fn non_custom_uint64_overflow_errors() -> Result<(), AvroError> {
4274 let schema = Schema::new(vec![Field::new("u64", DataType::UInt64, false)]);
4275 let values: Vec<Option<u64>> = vec![Some((i64::MAX as u64) + 1)];
4276 let batch = RecordBatch::try_new(
4277 Arc::new(schema.clone()),
4278 vec![Arc::new(UInt64Array::from(values)) as ArrayRef],
4279 )?;
4280 let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4281 let err = w
4282 .write(&batch)
4283 .expect_err("expected UInt64 overflow error when avro_custom_types is disabled");
4284 match err {
4285 AvroError::InvalidArgument(msg) => {
4286 assert_eq!(
4287 msg,
4288 "UInt64 value 9223372036854775808 exceeds i64::MAX; enable avro_custom_types feature for full UInt64 support"
4289 );
4290 }
4291 other => panic!("expected InvalidArgument, got {other:?}"),
4292 }
4293 Ok(())
4294 }
4295
4296 #[cfg(not(feature = "avro_custom_types"))]
4297 #[test]
4298 fn non_custom_interval_year_month_negative_errors() -> Result<(), AvroError> {
4299 let schema = Schema::new(vec![Field::new(
4300 "iv_ym",
4301 DataType::Interval(IntervalUnit::YearMonth),
4302 false,
4303 )]);
4304 let values: Vec<Option<i32>> = vec![Some(-1)];
4305 let batch = RecordBatch::try_new(
4306 Arc::new(schema.clone()),
4307 vec![Arc::new(IntervalYearMonthArray::from(values)) as ArrayRef],
4308 )?;
4309
4310 let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4311 let err = w
4312 .write(&batch)
4313 .expect_err("expected negative Interval(YearMonth) error");
4314 match err {
4315 AvroError::InvalidArgument(msg) => {
4316 assert_eq!(
4317 msg,
4318 "Avro 'duration' cannot encode negative months; enable `avro_custom_types` to round-trip signed Arrow Interval(YearMonth)"
4319 );
4320 }
4321 other => panic!("expected InvalidArgument, got {other:?}"),
4322 }
4323 Ok(())
4324 }
4325
4326 #[cfg(not(feature = "avro_custom_types"))]
4327 #[test]
4328 fn non_custom_interval_day_time_negative_errors() -> Result<(), AvroError> {
4329 let schema = Schema::new(vec![Field::new(
4330 "iv_dt",
4331 DataType::Interval(IntervalUnit::DayTime),
4332 false,
4333 )]);
4334 let values: Vec<Option<IntervalDayTime>> = vec![Some(IntervalDayTime::new(-1, 0))];
4335 let batch = RecordBatch::try_new(
4336 Arc::new(schema.clone()),
4337 vec![Arc::new(IntervalDayTimeArray::from(values)) as ArrayRef],
4338 )?;
4339 let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4340 let err = w
4341 .write(&batch)
4342 .expect_err("expected negative Interval(DayTime) error");
4343 match err {
4344 AvroError::InvalidArgument(msg) => {
4345 assert_eq!(
4346 msg,
4347 "Avro 'duration' cannot encode negative days or milliseconds; enable `avro_custom_types` to round-trip signed Arrow Interval(DayTime)"
4348 );
4349 }
4350 other => panic!("expected InvalidArgument, got {other:?}"),
4351 }
4352 Ok(())
4353 }
4354
4355 #[cfg(not(feature = "avro_custom_types"))]
4356 #[test]
4357 fn non_custom_interval_month_day_nano_negative_errors() -> Result<(), AvroError> {
4358 let schema = Schema::new(vec![Field::new(
4359 "iv_mdn",
4360 DataType::Interval(IntervalUnit::MonthDayNano),
4361 false,
4362 )]);
4363 let values: Vec<Option<IntervalMonthDayNano>> =
4364 vec![Some(IntervalMonthDayNano::new(-1, 0, 0))];
4365 let batch = RecordBatch::try_new(
4366 Arc::new(schema.clone()),
4367 vec![Arc::new(IntervalMonthDayNanoArray::from(values)) as ArrayRef],
4368 )?;
4369 let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4370 let err = w
4371 .write(&batch)
4372 .expect_err("expected negative Interval(MonthDayNano) error");
4373 match err {
4374 AvroError::InvalidArgument(msg) => {
4375 assert_eq!(
4376 msg,
4377 "Avro 'duration' cannot encode negative months/days/nanoseconds; enable `avro_custom_types` to round-trip signed Arrow intervals"
4378 );
4379 }
4380 other => panic!("expected InvalidArgument, got {other:?}"),
4381 }
4382 Ok(())
4383 }
4384
4385 #[cfg(not(feature = "avro_custom_types"))]
4386 #[test]
4387 fn non_custom_interval_month_day_nano_sub_millis_errors() -> Result<(), AvroError> {
4388 let schema = Schema::new(vec![Field::new(
4389 "iv_mdn",
4390 DataType::Interval(IntervalUnit::MonthDayNano),
4391 false,
4392 )]);
4393 let values: Vec<Option<IntervalMonthDayNano>> =
4394 vec![Some(IntervalMonthDayNano::new(0, 0, 1))];
4395 let batch = RecordBatch::try_new(
4396 Arc::new(schema.clone()),
4397 vec![Arc::new(IntervalMonthDayNanoArray::from(values)) as ArrayRef],
4398 )?;
4399 let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4400 let err = w
4401 .write(&batch)
4402 .expect_err("expected sub-millisecond Interval(MonthDayNano) error");
4403 match err {
4404 AvroError::InvalidArgument(msg) => {
4405 assert_eq!(
4406 msg,
4407 "Avro 'duration' requires whole milliseconds; nanoseconds must be divisible by 1_000_000 (enable `avro_custom_types` to preserve nanosecond intervals)"
4408 );
4409 }
4410 other => panic!("expected InvalidArgument, got {other:?}"),
4411 }
4412 Ok(())
4413 }
4414
4415 #[cfg(not(feature = "avro_custom_types"))]
4416 #[test]
4417 fn non_custom_time32_second_scaling_overflow_errors() -> Result<(), AvroError> {
4418 let schema = Schema::new(vec![Field::new(
4419 "time32s",
4420 DataType::Time32(TimeUnit::Second),
4421 false,
4422 )]);
4423 let values: Vec<Option<i32>> = vec![Some((i32::MAX / 1000) + 1)];
4424 let batch = RecordBatch::try_new(
4425 Arc::new(schema.clone()),
4426 vec![Arc::new(Time32SecondArray::from(values)) as ArrayRef],
4427 )?;
4428 let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4429 let err = w
4430 .write(&batch)
4431 .expect_err("expected time32 seconds->millis overflow error");
4432 match err {
4433 AvroError::InvalidArgument(msg) => {
4434 assert_eq!(msg, "time32(secs) * 1000 overflowed");
4435 }
4436 other => panic!("expected InvalidArgument, got {other:?}"),
4437 }
4438 Ok(())
4439 }
4440
4441 #[cfg(not(feature = "avro_custom_types"))]
4442 #[test]
4443 fn non_custom_timestamp_second_scaling_overflow_errors() -> Result<(), AvroError> {
4444 let schema = Schema::new(vec![Field::new(
4445 "ts_s_local",
4446 DataType::Timestamp(TimeUnit::Second, None),
4447 false,
4448 )]);
4449 let values: Vec<Option<i64>> = vec![Some((i64::MAX / 1000) + 1)];
4451 let batch = RecordBatch::try_new(
4452 Arc::new(schema.clone()),
4453 vec![Arc::new(TimestampSecondArray::from(values)) as ArrayRef],
4454 )?;
4455 let mut w = AvroWriter::new(Vec::<u8>::new(), schema)?;
4456 let err = w
4457 .write(&batch)
4458 .expect_err("expected timestamp seconds->millis overflow error");
4459 match err {
4460 AvroError::InvalidArgument(msg) => {
4461 assert_eq!(msg, "timestamp(secs) * 1000 overflowed");
4462 }
4463 other => panic!("expected InvalidArgument, got {other:?}"),
4464 }
4465 Ok(())
4466 }
4467}