1use arrow_array::{Array, ArrayRef, RecordBatch};
26use arrow_schema::{ArrowError, SchemaRef};
27use comfy_table::{Cell, Table};
28use std::fmt::Display;
29
30use crate::display::{ArrayFormatter, FormatOptions, make_array_formatter};
31
32pub fn pretty_format_batches(results: &[RecordBatch]) -> Result<impl Display + use<>, ArrowError> {
62 let options = FormatOptions::default().with_display_error(true);
63 pretty_format_batches_with_options(results, &options)
64}
65
66pub fn pretty_format_batches_with_schema(
91 schema: SchemaRef,
92 results: &[RecordBatch],
93) -> Result<impl Display + use<>, ArrowError> {
94 let options = FormatOptions::default().with_display_error(true);
95 create_table(Some(schema), results, &options)
96}
97
98pub fn pretty_format_batches_with_options(
129 results: &[RecordBatch],
130 options: &FormatOptions,
131) -> Result<impl Display + use<>, ArrowError> {
132 create_table(None, results, options)
133}
134
135pub fn pretty_format_columns(
141 col_name: &str,
142 results: &[ArrayRef],
143) -> Result<impl Display + use<>, ArrowError> {
144 let options = FormatOptions::default().with_display_error(true);
145 pretty_format_columns_with_options(col_name, results, &options)
146}
147
148pub fn pretty_format_columns_with_options(
152 col_name: &str,
153 results: &[ArrayRef],
154 options: &FormatOptions,
155) -> Result<impl Display + use<>, ArrowError> {
156 create_column(col_name, results, options)
157}
158
159pub fn print_batches(results: &[RecordBatch]) -> Result<(), ArrowError> {
161 println!("{}", pretty_format_batches(results)?);
162 Ok(())
163}
164
165pub fn print_columns(col_name: &str, results: &[ArrayRef]) -> Result<(), ArrowError> {
167 println!("{}", pretty_format_columns(col_name, results)?);
168 Ok(())
169}
170
171fn create_table(
173 schema_opt: Option<SchemaRef>,
174 results: &[RecordBatch],
175 options: &FormatOptions,
176) -> Result<Table, ArrowError> {
177 let mut table = Table::new();
178 table.load_preset("||--+-++| ++++++");
179
180 let schema_opt = schema_opt.or_else(|| {
181 if results.is_empty() {
182 None
183 } else {
184 Some(results[0].schema())
185 }
186 });
187
188 if let Some(schema) = &schema_opt {
189 let mut header = Vec::new();
190 for field in schema.fields() {
191 if options.types_info() {
192 header.push(Cell::new(format!(
193 "{}\n{}",
194 field.name(),
195 field.data_type()
196 )))
197 } else {
198 header.push(Cell::new(field.name()));
199 }
200 }
201 table.set_header(header);
202 }
203
204 if results.is_empty() {
205 return Ok(table);
206 }
207
208 for batch in results {
209 let schema = schema_opt.as_ref().unwrap_or(batch.schema_ref());
210
211 if batch.columns().len() != schema.fields().len() {
213 return Err(ArrowError::InvalidArgumentError(format!(
214 "Expected the same number of columns in a record batch ({}) as the number of fields ({}) in the schema",
215 batch.columns().len(),
216 schema.fields.len()
217 )));
218 }
219
220 let formatters = batch
221 .columns()
222 .iter()
223 .zip(schema.fields().iter())
224 .map(|(c, field)| make_array_formatter(c, options, Some(field)))
225 .collect::<Result<Vec<_>, ArrowError>>()?;
226
227 for row in 0..batch.num_rows() {
228 let mut cells = Vec::new();
229 for formatter in &formatters {
230 cells.push(Cell::new(formatter.value(row)));
231 }
232 table.add_row(cells);
233 }
234 }
235
236 Ok(table)
237}
238
239fn create_column(
240 field: &str,
241 columns: &[ArrayRef],
242 options: &FormatOptions,
243) -> Result<Table, ArrowError> {
244 let mut table = Table::new();
245 table.load_preset("||--+-++| ++++++");
246
247 if columns.is_empty() {
248 return Ok(table);
249 }
250
251 let header = vec![Cell::new(field)];
252 table.set_header(header);
253
254 for col in columns {
255 let formatter = match options.formatter_factory() {
256 None => ArrayFormatter::try_new(col.as_ref(), options)?,
257 Some(formatters) => formatters
258 .create_array_formatter(col.as_ref(), options, None)
259 .transpose()
260 .unwrap_or_else(|| ArrayFormatter::try_new(col.as_ref(), options))?,
261 };
262 for row in 0..col.len() {
263 let cells = vec![Cell::new(formatter.value(row))];
264 table.add_row(cells);
265 }
266 }
267
268 Ok(table)
269}
270
271#[cfg(test)]
272mod tests {
273 use std::collections::HashMap;
274 use std::fmt::Write;
275 use std::sync::Arc;
276
277 use arrow_array::builder::*;
278 use arrow_array::cast::AsArray;
279 use arrow_array::types::*;
280 use arrow_array::*;
281 use arrow_buffer::{IntervalDayTime, IntervalMonthDayNano, ScalarBuffer};
282 use arrow_schema::*;
283 use half::f16;
284
285 use crate::display::{
286 ArrayFormatterFactory, DisplayIndex, DurationFormat, array_value_to_string,
287 };
288
289 use super::*;
290
291 #[test]
292 fn test_pretty_format_batches() {
293 let schema = Arc::new(Schema::new(vec![
295 Field::new("a", DataType::Utf8, true),
296 Field::new("b", DataType::Int32, true),
297 ]));
298
299 let batch = RecordBatch::try_new(
301 schema,
302 vec![
303 Arc::new(array::StringArray::from(vec![
304 Some("a"),
305 Some("b"),
306 None,
307 Some("d"),
308 ])),
309 Arc::new(array::Int32Array::from(vec![
310 Some(1),
311 None,
312 Some(10),
313 Some(100),
314 ])),
315 ],
316 )
317 .unwrap();
318
319 let table = pretty_format_batches(&[batch]).unwrap().to_string();
320
321 insta::assert_snapshot!(table, @"
322 +---+-----+
323 | a | b |
324 +---+-----+
325 | a | 1 |
326 | b | |
327 | | 10 |
328 | d | 100 |
329 +---+-----+
330 ");
331 }
332
333 #[test]
334 fn test_pretty_format_columns() {
335 let columns = vec![
336 Arc::new(array::StringArray::from(vec![
337 Some("a"),
338 Some("b"),
339 None,
340 Some("d"),
341 ])) as ArrayRef,
342 Arc::new(array::StringArray::from(vec![Some("e"), None, Some("g")])),
343 ];
344
345 let table = pretty_format_columns("a", &columns).unwrap().to_string();
346
347 insta::assert_snapshot!(table, @"
348 +---+
349 | a |
350 +---+
351 | a |
352 | b |
353 | |
354 | d |
355 | e |
356 | |
357 | g |
358 +---+
359 ");
360 }
361
362 #[test]
363 fn test_pretty_format_null() {
364 let schema = Arc::new(Schema::new(vec![
365 Field::new("a", DataType::Utf8, true),
366 Field::new("b", DataType::Int32, true),
367 Field::new("c", DataType::Null, true),
368 ]));
369
370 let num_rows = 4;
371 let arrays = schema
372 .fields()
373 .iter()
374 .map(|f| new_null_array(f.data_type(), num_rows))
375 .collect();
376
377 let batch = RecordBatch::try_new(schema, arrays).unwrap();
379
380 let table = pretty_format_batches(&[batch]).unwrap().to_string();
381
382 insta::assert_snapshot!(table, @"
383 +---+---+---+
384 | a | b | c |
385 +---+---+---+
386 | | | |
387 | | | |
388 | | | |
389 | | | |
390 +---+---+---+
391 ");
392 }
393
394 #[test]
395 fn test_pretty_format_dictionary() {
396 let field = Field::new_dictionary("d1", DataType::Int32, DataType::Utf8, true);
398 let schema = Arc::new(Schema::new(vec![field]));
399
400 let mut builder = StringDictionaryBuilder::<Int32Type>::new();
401
402 builder.append_value("one");
403 builder.append_null();
404 builder.append_value("three");
405 let array = Arc::new(builder.finish());
406
407 let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
408
409 let table = pretty_format_batches(&[batch]).unwrap().to_string();
410
411 insta::assert_snapshot!(table, @"
412 +-------+
413 | d1 |
414 +-------+
415 | one |
416 | |
417 | three |
418 +-------+
419 ");
420 }
421
422 #[test]
423 fn test_pretty_format_fixed_size_list() {
424 let field_type =
426 DataType::FixedSizeList(Arc::new(Field::new_list_field(DataType::Int32, true)), 3);
427 let schema = Arc::new(Schema::new(vec![Field::new("d1", field_type, true)]));
428
429 let keys_builder = Int32Array::builder(3);
430 let mut builder = FixedSizeListBuilder::new(keys_builder, 3);
431
432 builder.values().append_slice(&[1, 2, 3]);
433 builder.append(true);
434 builder.values().append_slice(&[4, 5, 6]);
435 builder.append(false);
436 builder.values().append_slice(&[7, 8, 9]);
437 builder.append(true);
438
439 let array = Arc::new(builder.finish());
440
441 let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
442 let table = pretty_format_batches(&[batch]).unwrap().to_string();
443
444 insta::assert_snapshot!(table, @"
445 +-----------+
446 | d1 |
447 +-----------+
448 | [1, 2, 3] |
449 | |
450 | [7, 8, 9] |
451 +-----------+
452 ");
453 }
454
455 #[test]
456 fn test_pretty_format_string_view() {
457 let schema = Arc::new(Schema::new(vec![Field::new(
458 "d1",
459 DataType::Utf8View,
460 true,
461 )]));
462
463 let mut builder = StringViewBuilder::with_capacity(20);
465 builder.append_value("hello");
466 builder.append_null();
467 builder.append_value("longer than 12 bytes");
468 builder.append_value("another than 12 bytes");
469 builder.append_null();
470 builder.append_value("small");
471
472 let array: ArrayRef = Arc::new(builder.finish());
473 let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
474 let table = pretty_format_batches(&[batch]).unwrap().to_string();
475
476 insta::assert_snapshot!(table, @"
477 +-----------------------+
478 | d1 |
479 +-----------------------+
480 | hello |
481 | |
482 | longer than 12 bytes |
483 | another than 12 bytes |
484 | |
485 | small |
486 +-----------------------+
487 ");
488 }
489
490 #[test]
491 fn test_pretty_format_binary_view() {
492 let schema = Arc::new(Schema::new(vec![Field::new(
493 "d1",
494 DataType::BinaryView,
495 true,
496 )]));
497
498 let mut builder = BinaryViewBuilder::with_capacity(20);
500 builder.append_value(b"hello");
501 builder.append_null();
502 builder.append_value(b"longer than 12 bytes");
503 builder.append_value(b"another than 12 bytes");
504 builder.append_null();
505 builder.append_value(b"small");
506
507 let array: ArrayRef = Arc::new(builder.finish());
508 let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
509 let table = pretty_format_batches(&[batch]).unwrap().to_string();
510
511 insta::assert_snapshot!(table, @"
512 +--------------------------------------------+
513 | d1 |
514 +--------------------------------------------+
515 | 68656c6c6f |
516 | |
517 | 6c6f6e676572207468616e203132206279746573 |
518 | 616e6f74686572207468616e203132206279746573 |
519 | |
520 | 736d616c6c |
521 +--------------------------------------------+
522 ");
523 }
524
525 #[test]
526 fn test_pretty_format_fixed_size_binary() {
527 let field_type = DataType::FixedSizeBinary(3);
529 let schema = Arc::new(Schema::new(vec![Field::new("d1", field_type, true)]));
530
531 let mut builder = FixedSizeBinaryBuilder::with_capacity(3, 3);
532
533 builder.append_value([1, 2, 3]).unwrap();
534 builder.append_null();
535 builder.append_value([7, 8, 9]).unwrap();
536
537 let array = Arc::new(builder.finish());
538
539 let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
540 let table = pretty_format_batches(&[batch]).unwrap().to_string();
541
542 insta::assert_snapshot!(table, @"
543 +--------+
544 | d1 |
545 +--------+
546 | 010203 |
547 | |
548 | 070809 |
549 +--------+
550 ");
551 }
552
553 fn format_primitive_batch<T: ArrowPrimitiveType>(value: T::Native) -> String {
556 let mut builder = PrimitiveBuilder::<T>::with_capacity(10);
557 builder.append_value(value);
558 builder.append_null();
559 let array = builder.finish();
560 let schema = Arc::new(Schema::new(vec![Field::new(
561 "f",
562 array.data_type().clone(),
563 true,
564 )]));
565 let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap();
566 pretty_format_batches(&[batch])
567 .expect("formatting batches")
568 .to_string()
569 }
570
571 fn timestamp_batch<T: ArrowTimestampType>(timezone: &str, value: T::Native) -> RecordBatch {
572 let mut builder = PrimitiveBuilder::<T>::with_capacity(10);
573 builder.append_value(value);
574 builder.append_null();
575 let array = builder.finish();
576 let array = array.with_timezone(timezone);
577
578 let schema = Arc::new(Schema::new(vec![Field::new(
579 "f",
580 array.data_type().clone(),
581 true,
582 )]));
583 RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap()
584 }
585
586 #[test]
587 fn test_pretty_format_timestamp_second_with_fixed_offset_timezone() {
588 let batch = timestamp_batch::<TimestampSecondType>("+08:00", 11111111);
589 let table = pretty_format_batches(&[batch]).unwrap().to_string();
590
591 insta::assert_snapshot!(table, @"
592 +---------------------------+
593 | f |
594 +---------------------------+
595 | 1970-05-09T22:25:11+08:00 |
596 | |
597 +---------------------------+
598 ");
599 }
600
601 #[test]
602 fn test_pretty_format_timestamp_second() {
603 let table = format_primitive_batch::<TimestampSecondType>(11111111);
604 insta::assert_snapshot!(table, @"
605 +---------------------+
606 | f |
607 +---------------------+
608 | 1970-05-09T14:25:11 |
609 | |
610 +---------------------+
611 ");
612 }
613
614 #[test]
615 fn test_pretty_format_timestamp_millisecond() {
616 let table = format_primitive_batch::<TimestampMillisecondType>(11111111);
617 insta::assert_snapshot!(table, @"
618 +-------------------------+
619 | f |
620 +-------------------------+
621 | 1970-01-01T03:05:11.111 |
622 | |
623 +-------------------------+
624 ");
625 }
626
627 #[test]
628 fn test_pretty_format_timestamp_microsecond() {
629 let table = format_primitive_batch::<TimestampMicrosecondType>(11111111);
630 insta::assert_snapshot!(table, @"
631 +----------------------------+
632 | f |
633 +----------------------------+
634 | 1970-01-01T00:00:11.111111 |
635 | |
636 +----------------------------+
637 ");
638 }
639
640 #[test]
641 fn test_pretty_format_timestamp_nanosecond() {
642 let table = format_primitive_batch::<TimestampNanosecondType>(11111111);
643 insta::assert_snapshot!(table, @"
644 +-------------------------------+
645 | f |
646 +-------------------------------+
647 | 1970-01-01T00:00:00.011111111 |
648 | |
649 +-------------------------------+
650 ");
651 }
652
653 #[test]
654 fn test_pretty_format_date_32() {
655 let table = format_primitive_batch::<Date32Type>(1234);
656 insta::assert_snapshot!(table, @"
657 +------------+
658 | f |
659 +------------+
660 | 1973-05-19 |
661 | |
662 +------------+
663 ");
664 }
665
666 #[test]
667 fn test_pretty_format_date_64() {
668 let table = format_primitive_batch::<Date64Type>(1111111100000);
669 insta::assert_snapshot!(table, @"
670 +---------------------+
671 | f |
672 +---------------------+
673 | 2005-03-18T01:58:20 |
674 | |
675 +---------------------+
676 ");
677 }
678
679 #[test]
680 fn test_pretty_format_time_32_second() {
681 let table = format_primitive_batch::<Time32SecondType>(1111);
682 insta::assert_snapshot!(table, @"
683 +----------+
684 | f |
685 +----------+
686 | 00:18:31 |
687 | |
688 +----------+
689 ");
690 }
691
692 #[test]
693 fn test_pretty_format_time_32_millisecond() {
694 let table = format_primitive_batch::<Time32MillisecondType>(11111111);
695 insta::assert_snapshot!(table, @"
696 +--------------+
697 | f |
698 +--------------+
699 | 03:05:11.111 |
700 | |
701 +--------------+
702 ");
703 }
704
705 #[test]
706 fn test_pretty_format_time_64_microsecond() {
707 let table = format_primitive_batch::<Time64MicrosecondType>(11111111);
708 insta::assert_snapshot!(table, @"
709 +-----------------+
710 | f |
711 +-----------------+
712 | 00:00:11.111111 |
713 | |
714 +-----------------+
715 ");
716 }
717
718 #[test]
719 fn test_pretty_format_time_64_nanosecond() {
720 let table = format_primitive_batch::<Time64NanosecondType>(11111111);
721 insta::assert_snapshot!(table, @"
722 +--------------------+
723 | f |
724 +--------------------+
725 | 00:00:00.011111111 |
726 | |
727 +--------------------+
728 ");
729 }
730
731 #[test]
732 fn test_int_display() {
733 let array = Arc::new(Int32Array::from(vec![6, 3])) as ArrayRef;
734 insta::assert_snapshot!(array_value_to_string(&array, 0).unwrap(), @"6");
735 insta::assert_snapshot!(array_value_to_string(&array, 1).unwrap(), @"3");
736 }
737
738 #[test]
739 fn test_decimal_display() {
740 let precision = 10;
741 let scale = 2;
742
743 let array = [Some(101), None, Some(200), Some(3040)]
744 .into_iter()
745 .collect::<Decimal128Array>()
746 .with_precision_and_scale(precision, scale)
747 .unwrap();
748
749 let dm = Arc::new(array) as ArrayRef;
750
751 let schema = Arc::new(Schema::new(vec![Field::new(
752 "f",
753 dm.data_type().clone(),
754 true,
755 )]));
756
757 let batch = RecordBatch::try_new(schema, vec![dm]).unwrap();
758
759 let table = pretty_format_batches(&[batch]).unwrap().to_string();
760
761 insta::assert_snapshot!(table, @"
762 +-------+
763 | f |
764 +-------+
765 | 1.01 |
766 | |
767 | 2.00 |
768 | 30.40 |
769 +-------+
770 ");
771 }
772
773 #[test]
774 fn test_decimal_display_zero_scale() {
775 let precision = 5;
776 let scale = 0;
777
778 let array = [Some(101), None, Some(200), Some(3040)]
779 .into_iter()
780 .collect::<Decimal128Array>()
781 .with_precision_and_scale(precision, scale)
782 .unwrap();
783
784 let dm = Arc::new(array) as ArrayRef;
785
786 let schema = Arc::new(Schema::new(vec![Field::new(
787 "f",
788 dm.data_type().clone(),
789 true,
790 )]));
791
792 let batch = RecordBatch::try_new(schema, vec![dm]).unwrap();
793
794 let table = pretty_format_batches(&[batch]).unwrap().to_string();
795
796 insta::assert_snapshot!(table, @"
797 +------+
798 | f |
799 +------+
800 | 101 |
801 | |
802 | 200 |
803 | 3040 |
804 +------+
805 ");
806 }
807
808 #[test]
809 fn test_pretty_format_struct() {
810 let schema = Schema::new(vec![
811 Field::new_struct(
812 "c1",
813 vec![
814 Field::new("c11", DataType::Int32, true),
815 Field::new_struct(
816 "c12",
817 vec![Field::new("c121", DataType::Utf8, false)],
818 false,
819 ),
820 ],
821 false,
822 ),
823 Field::new("c2", DataType::Utf8, false),
824 ]);
825
826 let c1 = StructArray::from(vec![
827 (
828 Arc::new(Field::new("c11", DataType::Int32, true)),
829 Arc::new(Int32Array::from(vec![Some(1), None, Some(5)])) as ArrayRef,
830 ),
831 (
832 Arc::new(Field::new_struct(
833 "c12",
834 vec![Field::new("c121", DataType::Utf8, false)],
835 false,
836 )),
837 Arc::new(StructArray::from(vec![(
838 Arc::new(Field::new("c121", DataType::Utf8, false)),
839 Arc::new(StringArray::from(vec![Some("e"), Some("f"), Some("g")])) as ArrayRef,
840 )])) as ArrayRef,
841 ),
842 ]);
843 let c2 = StringArray::from(vec![Some("a"), Some("b"), Some("c")]);
844
845 let batch =
846 RecordBatch::try_new(Arc::new(schema), vec![Arc::new(c1), Arc::new(c2)]).unwrap();
847
848 let table = pretty_format_batches(&[batch]).unwrap().to_string();
849
850 insta::assert_snapshot!(table, @"
851 +--------------------------+----+
852 | c1 | c2 |
853 +--------------------------+----+
854 | {c11: 1, c12: {c121: e}} | a |
855 | {c11: , c12: {c121: f}} | b |
856 | {c11: 5, c12: {c121: g}} | c |
857 +--------------------------+----+
858 ");
859 }
860
861 #[test]
862 fn test_pretty_format_dense_union() {
863 let mut builder = UnionBuilder::new_dense();
864 builder.append::<Int32Type>("a", 1).unwrap();
865 builder.append::<Float64Type>("b", 3.2234).unwrap();
866 builder.append_null::<Float64Type>("b").unwrap();
867 builder.append_null::<Int32Type>("a").unwrap();
868 let union = builder.build().unwrap();
869
870 let schema = Schema::new(vec![Field::new_union(
871 "Teamsters",
872 vec![0, 1],
873 vec![
874 Field::new("a", DataType::Int32, false),
875 Field::new("b", DataType::Float64, false),
876 ],
877 UnionMode::Dense,
878 )]);
879
880 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(union)]).unwrap();
881 let table = pretty_format_batches(&[batch]).unwrap().to_string();
882
883 insta::assert_snapshot!(table, @"
884 +------------+
885 | Teamsters |
886 +------------+
887 | {a=1} |
888 | {b=3.2234} |
889 | {b=} |
890 | {a=} |
891 +------------+
892 ");
893 }
894
895 #[test]
896 fn test_pretty_format_sparse_union() {
897 let mut builder = UnionBuilder::new_sparse();
898 builder.append::<Int32Type>("a", 1).unwrap();
899 builder.append::<Float64Type>("b", 3.2234).unwrap();
900 builder.append_null::<Float64Type>("b").unwrap();
901 builder.append_null::<Int32Type>("a").unwrap();
902 let union = builder.build().unwrap();
903
904 let schema = Schema::new(vec![Field::new_union(
905 "Teamsters",
906 vec![0, 1],
907 vec![
908 Field::new("a", DataType::Int32, false),
909 Field::new("b", DataType::Float64, false),
910 ],
911 UnionMode::Sparse,
912 )]);
913
914 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(union)]).unwrap();
915 let table = pretty_format_batches(&[batch]).unwrap().to_string();
916
917 insta::assert_snapshot!(table, @"
918 +------------+
919 | Teamsters |
920 +------------+
921 | {a=1} |
922 | {b=3.2234} |
923 | {b=} |
924 | {a=} |
925 +------------+
926 ");
927 }
928
929 #[test]
930 fn test_pretty_format_nested_union() {
931 let mut builder = UnionBuilder::new_dense();
933 builder.append::<Int32Type>("b", 1).unwrap();
934 builder.append::<Float64Type>("c", 3.2234).unwrap();
935 builder.append_null::<Float64Type>("c").unwrap();
936 builder.append_null::<Int32Type>("b").unwrap();
937 builder.append_null::<Float64Type>("c").unwrap();
938 let inner = builder.build().unwrap();
939
940 let inner_field = Field::new_union(
941 "European Union",
942 vec![0, 1],
943 vec![
944 Field::new("b", DataType::Int32, false),
945 Field::new("c", DataType::Float64, false),
946 ],
947 UnionMode::Dense,
948 );
949
950 let a_array = Int32Array::from(vec![None, None, None, Some(1234), Some(23)]);
952 let type_ids = [1, 1, 0, 0, 1].into_iter().collect::<ScalarBuffer<i8>>();
953
954 let children = vec![Arc::new(a_array) as Arc<dyn Array>, Arc::new(inner)];
955
956 let union_fields = [
957 (0, Arc::new(Field::new("a", DataType::Int32, true))),
958 (1, Arc::new(inner_field.clone())),
959 ]
960 .into_iter()
961 .collect();
962
963 let outer = UnionArray::try_new(union_fields, type_ids, None, children).unwrap();
964
965 let schema = Schema::new(vec![Field::new_union(
966 "Teamsters",
967 vec![0, 1],
968 vec![Field::new("a", DataType::Int32, true), inner_field],
969 UnionMode::Sparse,
970 )]);
971
972 let batch = RecordBatch::try_new(Arc::new(schema), vec![Arc::new(outer)]).unwrap();
973 let table = pretty_format_batches(&[batch]).unwrap().to_string();
974
975 insta::assert_snapshot!(table, @"
976 +-----------------------------+
977 | Teamsters |
978 +-----------------------------+
979 | {European Union={b=1}} |
980 | {European Union={c=3.2234}} |
981 | {a=} |
982 | {a=1234} |
983 | {European Union={c=}} |
984 +-----------------------------+
985 ");
986 }
987
988 #[test]
989 fn test_writing_formatted_batches() {
990 let schema = Arc::new(Schema::new(vec![
992 Field::new("a", DataType::Utf8, true),
993 Field::new("b", DataType::Int32, true),
994 ]));
995
996 let batch = RecordBatch::try_new(
998 schema,
999 vec![
1000 Arc::new(array::StringArray::from(vec![
1001 Some("a"),
1002 Some("b"),
1003 None,
1004 Some("d"),
1005 ])),
1006 Arc::new(array::Int32Array::from(vec![
1007 Some(1),
1008 None,
1009 Some(10),
1010 Some(100),
1011 ])),
1012 ],
1013 )
1014 .unwrap();
1015
1016 let table = pretty_format_batches(&[batch]).unwrap().to_string();
1017
1018 insta::assert_snapshot!(table, @"
1019 +---+-----+
1020 | a | b |
1021 +---+-----+
1022 | a | 1 |
1023 | b | |
1024 | | 10 |
1025 | d | 100 |
1026 +---+-----+
1027 ");
1028 }
1029
1030 #[test]
1031 fn test_float16_display() {
1032 let values = vec![
1033 Some(f16::from_f32(f32::NAN)),
1034 Some(f16::from_f32(4.0)),
1035 Some(f16::from_f32(f32::NEG_INFINITY)),
1036 ];
1037 let array = Arc::new(values.into_iter().collect::<Float16Array>()) as ArrayRef;
1038
1039 let schema = Arc::new(Schema::new(vec![Field::new(
1040 "f16",
1041 array.data_type().clone(),
1042 true,
1043 )]));
1044
1045 let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
1046
1047 let table = pretty_format_batches(&[batch]).unwrap().to_string();
1048
1049 insta::assert_snapshot!(table, @"
1050 +------+
1051 | f16 |
1052 +------+
1053 | NaN |
1054 | 4 |
1055 | -inf |
1056 +------+
1057 ");
1058 }
1059
1060 #[test]
1061 fn test_pretty_format_interval_day_time() {
1062 let arr = Arc::new(arrow_array::IntervalDayTimeArray::from(vec![
1063 Some(IntervalDayTime::new(-1, -600_000)),
1064 Some(IntervalDayTime::new(0, -1001)),
1065 Some(IntervalDayTime::new(0, -1)),
1066 Some(IntervalDayTime::new(0, 1)),
1067 Some(IntervalDayTime::new(0, 10)),
1068 Some(IntervalDayTime::new(0, 100)),
1069 Some(IntervalDayTime::new(0, 0)),
1070 ]));
1071
1072 let schema = Arc::new(Schema::new(vec![Field::new(
1073 "IntervalDayTime",
1074 arr.data_type().clone(),
1075 true,
1076 )]));
1077
1078 let batch = RecordBatch::try_new(schema, vec![arr]).unwrap();
1079
1080 let table = pretty_format_batches(&[batch]).unwrap().to_string();
1081
1082 insta::assert_snapshot!(table, @"
1083 +------------------+
1084 | IntervalDayTime |
1085 +------------------+
1086 | -1 days -10 mins |
1087 | -1.001 secs |
1088 | -0.001 secs |
1089 | 0.001 secs |
1090 | 0.010 secs |
1091 | 0.100 secs |
1092 | 0 secs |
1093 +------------------+
1094 ");
1095 }
1096
1097 #[test]
1098 fn test_pretty_format_interval_month_day_nano_array() {
1099 let arr = Arc::new(arrow_array::IntervalMonthDayNanoArray::from(vec![
1100 Some(IntervalMonthDayNano::new(-1, -1, -600_000_000_000)),
1101 Some(IntervalMonthDayNano::new(0, 0, -1_000_000_001)),
1102 Some(IntervalMonthDayNano::new(0, 0, -1)),
1103 Some(IntervalMonthDayNano::new(0, 0, 1)),
1104 Some(IntervalMonthDayNano::new(0, 0, 10)),
1105 Some(IntervalMonthDayNano::new(0, 0, 100)),
1106 Some(IntervalMonthDayNano::new(0, 0, 1_000)),
1107 Some(IntervalMonthDayNano::new(0, 0, 10_000)),
1108 Some(IntervalMonthDayNano::new(0, 0, 100_000)),
1109 Some(IntervalMonthDayNano::new(0, 0, 1_000_000)),
1110 Some(IntervalMonthDayNano::new(0, 0, 10_000_000)),
1111 Some(IntervalMonthDayNano::new(0, 0, 100_000_000)),
1112 Some(IntervalMonthDayNano::new(0, 0, 1_000_000_000)),
1113 Some(IntervalMonthDayNano::new(0, 0, 0)),
1114 ]));
1115
1116 let schema = Arc::new(Schema::new(vec![Field::new(
1117 "IntervalMonthDayNano",
1118 arr.data_type().clone(),
1119 true,
1120 )]));
1121
1122 let batch = RecordBatch::try_new(schema, vec![arr]).unwrap();
1123
1124 let table = pretty_format_batches(&[batch]).unwrap().to_string();
1125
1126 insta::assert_snapshot!(table, @"
1127 +--------------------------+
1128 | IntervalMonthDayNano |
1129 +--------------------------+
1130 | -1 mons -1 days -10 mins |
1131 | -1.000000001 secs |
1132 | -0.000000001 secs |
1133 | 0.000000001 secs |
1134 | 0.000000010 secs |
1135 | 0.000000100 secs |
1136 | 0.000001000 secs |
1137 | 0.000010000 secs |
1138 | 0.000100000 secs |
1139 | 0.001000000 secs |
1140 | 0.010000000 secs |
1141 | 0.100000000 secs |
1142 | 1.000000000 secs |
1143 | 0 secs |
1144 +--------------------------+
1145 ");
1146 }
1147
1148 #[test]
1149 fn test_format_options() {
1150 let options = FormatOptions::default()
1151 .with_null("null")
1152 .with_types_info(true);
1153 let int32_array = Int32Array::from(vec![Some(1), Some(2), None, Some(3), Some(4)]);
1154 let string_array =
1155 StringArray::from(vec![Some("foo"), Some("bar"), None, Some("baz"), None]);
1156
1157 let batch = RecordBatch::try_from_iter([
1158 ("my_int32_name", Arc::new(int32_array) as _),
1159 ("my_string_name", Arc::new(string_array) as _),
1160 ])
1161 .unwrap();
1162
1163 let column = pretty_format_columns_with_options(
1164 "my_column_name",
1165 &[batch.column(0).clone()],
1166 &options,
1167 )
1168 .unwrap()
1169 .to_string();
1170
1171 insta::assert_snapshot!(column, @"
1172 +----------------+
1173 | my_column_name |
1174 +----------------+
1175 | 1 |
1176 | 2 |
1177 | null |
1178 | 3 |
1179 | 4 |
1180 +----------------+
1181 ");
1182
1183 let table = pretty_format_batches_with_options(&[batch], &options)
1184 .unwrap()
1185 .to_string();
1186
1187 insta::assert_snapshot!(table, @"
1188 +---------------+----------------+
1189 | my_int32_name | my_string_name |
1190 | Int32 | Utf8 |
1191 +---------------+----------------+
1192 | 1 | foo |
1193 | 2 | bar |
1194 | null | null |
1195 | 3 | baz |
1196 | 4 | null |
1197 +---------------+----------------+
1198 ");
1199 }
1200
1201 #[test]
1202 fn duration_pretty_and_iso_extremes() {
1203 let arr = DurationSecondArray::from(vec![Some(i64::MIN), Some(i64::MAX), Some(3661), None]);
1205 let array: ArrayRef = Arc::new(arr);
1206
1207 let opts = FormatOptions::default().with_null("null");
1209 let opts = opts.with_duration_format(DurationFormat::Pretty);
1210 let pretty =
1211 pretty_format_columns_with_options("pretty", std::slice::from_ref(&array), &opts)
1212 .unwrap()
1213 .to_string();
1214
1215 insta::assert_snapshot!(pretty, @"
1216 +------------------------------+
1217 | pretty |
1218 +------------------------------+
1219 | <invalid> |
1220 | <invalid> |
1221 | 0 days 1 hours 1 mins 1 secs |
1222 | null |
1223 +------------------------------+
1224 ");
1225
1226 let opts_iso = FormatOptions::default()
1228 .with_null("null")
1229 .with_duration_format(DurationFormat::ISO8601);
1230 let iso = pretty_format_columns_with_options("iso", &[array], &opts_iso)
1231 .unwrap()
1232 .to_string();
1233
1234 insta::assert_snapshot!(iso, @"
1235 +-----------+
1236 | iso |
1237 +-----------+
1238 | <invalid> |
1239 | <invalid> |
1240 | PT3661S |
1241 | null |
1242 +-----------+
1243 ");
1244 }
1245
1246 #[derive(Debug)]
1252 struct TestFormatters {}
1253
1254 impl ArrayFormatterFactory for TestFormatters {
1255 fn create_array_formatter<'formatter>(
1256 &self,
1257 array: &'formatter dyn Array,
1258 options: &FormatOptions<'formatter>,
1259 field: Option<&'formatter Field>,
1260 ) -> Result<Option<ArrayFormatter<'formatter>>, ArrowError> {
1261 if field
1262 .map(|f| f.extension_type_name() == Some("my_money"))
1263 .unwrap_or(false)
1264 {
1265 let array = array.as_primitive();
1267 let display_index = Box::new(MyMoneyFormatter {
1268 array,
1269 options: options.clone(),
1270 });
1271 return Ok(Some(ArrayFormatter::new(display_index, options.safe())));
1272 }
1273
1274 if array.data_type() == &DataType::Int32 {
1275 let array = array.as_primitive();
1276 let display_index = Box::new(MyInt32Formatter {
1277 array,
1278 options: options.clone(),
1279 });
1280 return Ok(Some(ArrayFormatter::new(display_index, options.safe())));
1281 }
1282
1283 Ok(None)
1284 }
1285 }
1286
1287 struct MyMoneyFormatter<'a> {
1289 array: &'a Int32Array,
1290 options: FormatOptions<'a>,
1291 }
1292
1293 impl<'a> DisplayIndex for MyMoneyFormatter<'a> {
1294 fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
1295 match self.array.is_valid(idx) {
1296 true => write!(f, "{} €", self.array.value(idx))?,
1297 false => write!(f, "{}", self.options.null())?,
1298 }
1299
1300 Ok(())
1301 }
1302 }
1303
1304 struct MyInt32Formatter<'a> {
1306 array: &'a Int32Array,
1307 options: FormatOptions<'a>,
1308 }
1309
1310 impl<'a> DisplayIndex for MyInt32Formatter<'a> {
1311 fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
1312 match self.array.is_valid(idx) {
1313 true => write!(f, "{} (32-Bit)", self.array.value(idx))?,
1314 false => write!(f, "{}", self.options.null())?,
1315 }
1316
1317 Ok(())
1318 }
1319 }
1320
1321 #[test]
1322 fn test_format_batches_with_custom_formatters() {
1323 let options = FormatOptions::new()
1325 .with_null("<NULL>")
1326 .with_formatter_factory(Some(&TestFormatters {}));
1327 let money_metadata = HashMap::from([(
1328 extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1329 "my_money".to_owned(),
1330 )]);
1331 let schema = Arc::new(Schema::new(vec![
1332 Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
1333 ]));
1334
1335 let batch = RecordBatch::try_new(
1337 schema,
1338 vec![Arc::new(array::Int32Array::from(vec![
1339 Some(1),
1340 None,
1341 Some(10),
1342 Some(100),
1343 ]))],
1344 )
1345 .unwrap();
1346
1347 let table = pretty_format_batches_with_options(&[batch], &options)
1348 .unwrap()
1349 .to_string();
1350
1351 insta::assert_snapshot!(table, @"
1352 +--------+
1353 | income |
1354 +--------+
1355 | 1 € |
1356 | <NULL> |
1357 | 10 € |
1358 | 100 € |
1359 +--------+
1360 ");
1361 }
1362
1363 #[test]
1364 fn test_format_batches_with_custom_formatters_multi_nested_list() {
1365 let options = FormatOptions::new()
1367 .with_null("<NULL>")
1368 .with_formatter_factory(Some(&TestFormatters {}));
1369 let money_metadata = HashMap::from([(
1370 extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1371 "my_money".to_owned(),
1372 )]);
1373 let nested_field = Arc::new(
1374 Field::new_list_field(DataType::Int32, true).with_metadata(money_metadata.clone()),
1375 );
1376
1377 let inner_list = ListBuilder::new(Int32Builder::new()).with_field(nested_field);
1379 let mut outer_list = FixedSizeListBuilder::new(inner_list, 2);
1380 outer_list.values().append_value([Some(1)]);
1381 outer_list.values().append_null();
1382 outer_list.append(true);
1383 outer_list.values().append_value([Some(2), Some(8)]);
1384 outer_list
1385 .values()
1386 .append_value([Some(50), Some(25), Some(25)]);
1387 outer_list.append(true);
1388 let outer_list = outer_list.finish();
1389
1390 let schema = Arc::new(Schema::new(vec![Field::new(
1391 "income",
1392 outer_list.data_type().clone(),
1393 true,
1394 )]));
1395
1396 let batch = RecordBatch::try_new(schema, vec![Arc::new(outer_list)]).unwrap();
1398
1399 let table = pretty_format_batches_with_options(&[batch], &options)
1400 .unwrap()
1401 .to_string();
1402
1403 insta::assert_snapshot!(table, @"
1404 +----------------------------------+
1405 | income |
1406 +----------------------------------+
1407 | [[1 €], <NULL>] |
1408 | [[2 €, 8 €], [50 €, 25 €, 25 €]] |
1409 +----------------------------------+
1410 ");
1411 }
1412
1413 #[test]
1414 fn test_format_batches_with_custom_formatters_nested_struct() {
1415 let options = FormatOptions::new()
1417 .with_null("<NULL>")
1418 .with_formatter_factory(Some(&TestFormatters {}));
1419 let money_metadata = HashMap::from([(
1420 extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1421 "my_money".to_owned(),
1422 )]);
1423 let fields = Fields::from(vec![
1424 Field::new("name", DataType::Utf8, true),
1425 Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
1426 ]);
1427
1428 let schema = Arc::new(Schema::new(vec![Field::new(
1429 "income",
1430 DataType::Struct(fields.clone()),
1431 true,
1432 )]));
1433
1434 let mut nested_data = StructBuilder::new(
1436 fields,
1437 vec![
1438 Box::new(StringBuilder::new()),
1439 Box::new(Int32Builder::new()),
1440 ],
1441 );
1442 nested_data
1443 .field_builder::<StringBuilder>(0)
1444 .unwrap()
1445 .extend([Some("Gimli"), Some("Legolas"), Some("Aragorn")]);
1446 nested_data
1447 .field_builder::<Int32Builder>(1)
1448 .unwrap()
1449 .extend([Some(10), None, Some(30)]);
1450 nested_data.append(true);
1451 nested_data.append(true);
1452 nested_data.append(true);
1453
1454 let batch = RecordBatch::try_new(schema, vec![Arc::new(nested_data.finish())]).unwrap();
1456
1457 let table = pretty_format_batches_with_options(&[batch], &options)
1458 .unwrap()
1459 .to_string();
1460
1461 insta::assert_snapshot!(table, @"
1462 +---------------------------------+
1463 | income |
1464 +---------------------------------+
1465 | {name: Gimli, income: 10 €} |
1466 | {name: Legolas, income: <NULL>} |
1467 | {name: Aragorn, income: 30 €} |
1468 +---------------------------------+
1469 ");
1470 }
1471
1472 #[test]
1473 fn test_format_batches_with_custom_formatters_nested_map() {
1474 let options = FormatOptions::new()
1476 .with_null("<NULL>")
1477 .with_formatter_factory(Some(&TestFormatters {}));
1478 let money_metadata = HashMap::from([(
1479 extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1480 "my_money".to_owned(),
1481 )]);
1482
1483 let mut array = MapBuilder::<StringBuilder, Int32Builder>::new(
1484 None,
1485 StringBuilder::new(),
1486 Int32Builder::new(),
1487 )
1488 .with_values_field(
1489 Field::new("values", DataType::Int32, true).with_metadata(money_metadata.clone()),
1490 );
1491 array
1492 .keys()
1493 .extend([Some("Gimli"), Some("Legolas"), Some("Aragorn")]);
1494 array.values().extend([Some(10), None, Some(30)]);
1495 array.append(true).unwrap();
1496 let array = array.finish();
1497
1498 let schema = Arc::new(Schema::new(vec![Field::new(
1500 "income",
1501 array.data_type().clone(),
1502 true,
1503 )]));
1504 let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap();
1505
1506 let table = pretty_format_batches_with_options(&[batch], &options)
1507 .unwrap()
1508 .to_string();
1509
1510 insta::assert_snapshot!(table, @"
1511 +-----------------------------------------------+
1512 | income |
1513 +-----------------------------------------------+
1514 | {Gimli: 10 €, Legolas: <NULL>, Aragorn: 30 €} |
1515 +-----------------------------------------------+
1516 ");
1517 }
1518
1519 #[test]
1520 fn test_format_batches_with_custom_formatters_nested_union() {
1521 let options = FormatOptions::new()
1523 .with_null("<NULL>")
1524 .with_formatter_factory(Some(&TestFormatters {}));
1525 let money_metadata = HashMap::from([(
1526 extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1527 "my_money".to_owned(),
1528 )]);
1529 let fields = UnionFields::try_new(
1530 vec![0],
1531 vec![Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone())],
1532 )
1533 .unwrap();
1534
1535 let mut array_builder = UnionBuilder::new_dense();
1537 array_builder.append::<Int32Type>("income", 1).unwrap();
1538 let (_, type_ids, offsets, children) = array_builder.build().unwrap().into_parts();
1539 let array = UnionArray::try_new(fields, type_ids, offsets, children).unwrap();
1540
1541 let schema = Arc::new(Schema::new(vec![Field::new(
1542 "income",
1543 array.data_type().clone(),
1544 true,
1545 )]));
1546
1547 let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap();
1549
1550 let table = pretty_format_batches_with_options(&[batch], &options)
1551 .unwrap()
1552 .to_string();
1553
1554 insta::assert_snapshot!(table, @"
1555 +--------------+
1556 | income |
1557 +--------------+
1558 | {income=1 €} |
1559 +--------------+
1560 ");
1561 }
1562
1563 #[test]
1564 fn test_format_batches_with_custom_formatters_custom_schema_overrules_batch_schema() {
1565 let options = FormatOptions::new().with_formatter_factory(Some(&TestFormatters {}));
1567 let money_metadata = HashMap::from([(
1568 extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1569 "my_money".to_owned(),
1570 )]);
1571 let schema = Arc::new(Schema::new(vec![
1572 Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
1573 ]));
1574
1575 let batch = RecordBatch::try_new(
1577 schema,
1578 vec![Arc::new(array::Int32Array::from(vec![
1579 Some(1),
1580 None,
1581 Some(10),
1582 Some(100),
1583 ]))],
1584 )
1585 .unwrap();
1586
1587 let table = create_table(
1588 Some(Arc::new(Schema::new(vec![Field::new(
1590 "income",
1591 DataType::Int32,
1592 true,
1593 )]))),
1594 &[batch],
1595 &options,
1596 )
1597 .unwrap()
1598 .to_string();
1599
1600 insta::assert_snapshot!(table, @"
1602 +--------------+
1603 | income |
1604 +--------------+
1605 | 1 (32-Bit) |
1606 | |
1607 | 10 (32-Bit) |
1608 | 100 (32-Bit) |
1609 +--------------+
1610 ");
1611 }
1612
1613 #[test]
1614 fn test_format_column_with_custom_formatters() {
1615 let array = Arc::new(array::Int32Array::from(vec![
1617 Some(1),
1618 None,
1619 Some(10),
1620 Some(100),
1621 ]));
1622
1623 let table = pretty_format_columns_with_options(
1624 "income",
1625 &[array],
1626 &FormatOptions::default().with_formatter_factory(Some(&TestFormatters {})),
1627 )
1628 .unwrap()
1629 .to_string();
1630
1631 insta::assert_snapshot!(table, @"
1632 +--------------+
1633 | income |
1634 +--------------+
1635 | 1 (32-Bit) |
1636 | |
1637 | 10 (32-Bit) |
1638 | 100 (32-Bit) |
1639 +--------------+
1640 ");
1641 }
1642
1643 #[test]
1644 fn test_pretty_format_batches_with_schema_with_wrong_number_of_fields() {
1645 let schema_a = Arc::new(Schema::new(vec![
1646 Field::new("a", DataType::Int32, true),
1647 Field::new("b", DataType::Utf8, true),
1648 ]));
1649 let schema_b = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
1650
1651 let batch = RecordBatch::try_new(
1653 schema_b,
1654 vec![Arc::new(array::Int32Array::from(vec![
1655 Some(1),
1656 None,
1657 Some(10),
1658 Some(100),
1659 ]))],
1660 )
1661 .unwrap();
1662
1663 let error = pretty_format_batches_with_schema(schema_a, &[batch])
1664 .err()
1665 .unwrap();
1666 insta::assert_snapshot!(error, @"Invalid argument error: Expected the same number of columns in a record batch (1) as the number of fields (2) in the schema");
1667 }
1668}