Skip to main content

arrow_cast/
pretty.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Utilities for pretty printing [`RecordBatch`]es and [`Array`]s.
19//!
20//! Note this module is not available unless `feature = "prettyprint"` is enabled.
21//!
22//! [`RecordBatch`]: arrow_array::RecordBatch
23//! [`Array`]: arrow_array::Array
24
25use 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
32/// Create a visual representation of [`RecordBatch`]es
33///
34/// Uses default values for display. See [`pretty_format_batches_with_options`]
35/// for more control.
36///
37/// # Example
38/// ```
39/// # use std::sync::Arc;
40/// # use arrow_array::{ArrayRef, Int32Array, RecordBatch, StringArray};
41/// # use arrow_cast::pretty::pretty_format_batches;
42/// # let batch = RecordBatch::try_from_iter(vec![
43/// #       ("a", Arc::new(Int32Array::from(vec![1, 2, 3, 4, 5])) as ArrayRef),
44/// #       ("b", Arc::new(StringArray::from(vec![Some("a"), Some("b"), None, Some("d"), Some("e")]))),
45/// # ]).unwrap();
46/// // Note, returned object implements `Display`
47/// let pretty_table = pretty_format_batches(&[batch]).unwrap();
48/// let table_str = format!("Batches:\n{pretty_table}");
49/// assert_eq!(table_str,
50/// r#"Batches:
51/// +---+---+
52/// | a | b |
53/// +---+---+
54/// | 1 | a |
55/// | 2 | b |
56/// | 3 |   |
57/// | 4 | d |
58/// | 5 | e |
59/// +---+---+"#);
60/// ```
61pub 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
66/// Create a visual representation of [`RecordBatch`]es with a provided schema.
67///
68/// Useful to display empty batches.
69///
70/// # Example
71/// ```
72/// # use std::sync::Arc;
73/// # use arrow_array::{ArrayRef, Int32Array, RecordBatch, StringArray};
74/// # use arrow_cast::pretty::pretty_format_batches_with_schema;
75/// # use arrow_schema::{DataType, Field, Schema};
76/// let schema = Arc::new(Schema::new(vec![
77///     Field::new("a", DataType::Int32, false),
78///     Field::new("b", DataType::Utf8, true),
79/// ]));
80/// // Note, returned object implements `Display`
81/// let pretty_table = pretty_format_batches_with_schema(schema, &[]).unwrap();
82/// let table_str = format!("Batches:\n{pretty_table}");
83/// assert_eq!(table_str,
84/// r#"Batches:
85/// +---+---+
86/// | a | b |
87/// +---+---+
88/// +---+---+"#);
89/// ```
90pub 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
98/// Create a visual representation of [`RecordBatch`]es with formatting options.
99///
100/// # Arguments
101/// * `results` - A slice of record batches to display
102/// * `options` - [`FormatOptions`] that control the resulting display
103///
104/// # Example
105/// ```
106/// # use std::sync::Arc;
107/// # use arrow_array::{ArrayRef, Int32Array, RecordBatch, StringArray};
108/// # use arrow_cast::display::FormatOptions;
109/// # use arrow_cast::pretty::{pretty_format_batches, pretty_format_batches_with_options};
110/// # let batch = RecordBatch::try_from_iter(vec![
111/// #       ("a", Arc::new(Int32Array::from(vec![1, 2])) as ArrayRef),
112/// #       ("b", Arc::new(StringArray::from(vec![Some("a"), None]))),
113/// # ]).unwrap();
114/// let options = FormatOptions::new()
115///   .with_null("<NULL>");
116/// // Note, returned object implements `Display`
117/// let pretty_table = pretty_format_batches_with_options(&[batch], &options).unwrap();
118/// let table_str = format!("Batches:\n{pretty_table}");
119/// assert_eq!(table_str,
120/// r#"Batches:
121/// +---+--------+
122/// | a | b      |
123/// +---+--------+
124/// | 1 | a      |
125/// | 2 | <NULL> |
126/// +---+--------+"#);
127/// ```
128pub 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
135/// Create a visual representation of [`ArrayRef`]
136///
137/// Uses default values for display. See [`pretty_format_columns_with_options`]
138///
139/// See [`pretty_format_batches`] for an example
140pub 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
148/// Create a visual representation of [`ArrayRef`] with formatting options.
149///
150/// See [`pretty_format_batches_with_options`] for an example
151pub 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
159/// Prints a visual representation of record batches to stdout
160pub fn print_batches(results: &[RecordBatch]) -> Result<(), ArrowError> {
161    println!("{}", pretty_format_batches(results)?);
162    Ok(())
163}
164
165/// Prints a visual representation of a list of column to stdout
166pub fn print_columns(col_name: &str, results: &[ArrayRef]) -> Result<(), ArrowError> {
167    println!("{}", pretty_format_columns(col_name, results)?);
168    Ok(())
169}
170
171/// Convert a series of record batches into a table
172fn 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        // Could be a custom schema that was provided.
212        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        // define a schema.
294        let schema = Arc::new(Schema::new(vec![
295            Field::new("a", DataType::Utf8, true),
296            Field::new("b", DataType::Int32, true),
297        ]));
298
299        // define data.
300        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        // define data (null)
378        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        // define a schema.
397        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        // define a schema.
425        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        // Use a small capacity so we end up with multiple views
464        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        // Use a small capacity so we end up with multiple views
499        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        // define a schema.
528        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    /// Generate an array of [`ArrowPrimitiveType`] with a numeric `value`,
554    /// then format it with `pretty_format_batches`.
555    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        //Inner UnionArray
932        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        // Can't use UnionBuilder with non-primitive types, so manually build outer UnionArray
951        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        // define a schema.
991        let schema = Arc::new(Schema::new(vec![
992            Field::new("a", DataType::Utf8, true),
993            Field::new("b", DataType::Int32, true),
994        ]));
995
996        // define data.
997        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        // Build [MIN, MAX, 3661, NULL]
1204        let arr = DurationSecondArray::from(vec![Some(i64::MIN), Some(i64::MAX), Some(3661), None]);
1205        let array: ArrayRef = Arc::new(arr);
1206
1207        // Pretty formatting
1208        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        // ISO8601 formatting
1227        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    //
1247    // Custom Formatting
1248    //
1249
1250    /// The factory that will create the [`ArrayFormatter`]s.
1251    #[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                // We assume that my_money always is an Int32.
1266                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    /// A format that will append a "€" sign to the end of the Int32 values.
1288    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    /// The actual formatter
1305    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        // define a schema.
1324        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        // define data.
1336        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        // define a schema.
1366        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        // Create nested data
1378        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        // define data.
1397        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        // define a schema.
1416        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        // Create nested data
1435        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        // define data.
1455        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        // define a schema.
1475        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        // define data.
1499        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        // define a schema.
1522        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        // Create nested data and construct it with the correct metadata
1536        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        // define data.
1548        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        // define a schema.
1566        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        // define data.
1576        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            // No metadata compared to test_format_batches_with_custom_formatters
1589            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        // No € formatting as in test_format_batches_with_custom_formatters
1601        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        // define data.
1616        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        // define data.
1652        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}