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    #[cfg_attr(miri, ignore)] // Unsupported inline assembly
1032    fn test_float16_display() {
1033        let values = vec![
1034            Some(f16::from_f32(f32::NAN)),
1035            Some(f16::from_f32(4.0)),
1036            Some(f16::from_f32(f32::NEG_INFINITY)),
1037        ];
1038        let array = Arc::new(values.into_iter().collect::<Float16Array>()) as ArrayRef;
1039
1040        let schema = Arc::new(Schema::new(vec![Field::new(
1041            "f16",
1042            array.data_type().clone(),
1043            true,
1044        )]));
1045
1046        let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
1047
1048        let table = pretty_format_batches(&[batch]).unwrap().to_string();
1049
1050        insta::assert_snapshot!(table, @"
1051        +------+
1052        | f16  |
1053        +------+
1054        | NaN  |
1055        | 4    |
1056        | -inf |
1057        +------+
1058        ");
1059    }
1060
1061    #[test]
1062    fn test_pretty_format_interval_day_time() {
1063        let arr = Arc::new(arrow_array::IntervalDayTimeArray::from(vec![
1064            Some(IntervalDayTime::new(-1, -600_000)),
1065            Some(IntervalDayTime::new(0, -1001)),
1066            Some(IntervalDayTime::new(0, -1)),
1067            Some(IntervalDayTime::new(0, 1)),
1068            Some(IntervalDayTime::new(0, 10)),
1069            Some(IntervalDayTime::new(0, 100)),
1070            Some(IntervalDayTime::new(0, 0)),
1071        ]));
1072
1073        let schema = Arc::new(Schema::new(vec![Field::new(
1074            "IntervalDayTime",
1075            arr.data_type().clone(),
1076            true,
1077        )]));
1078
1079        let batch = RecordBatch::try_new(schema, vec![arr]).unwrap();
1080
1081        let table = pretty_format_batches(&[batch]).unwrap().to_string();
1082
1083        insta::assert_snapshot!(table, @"
1084        +------------------+
1085        | IntervalDayTime  |
1086        +------------------+
1087        | -1 days -10 mins |
1088        | -1.001 secs      |
1089        | -0.001 secs      |
1090        | 0.001 secs       |
1091        | 0.010 secs       |
1092        | 0.100 secs       |
1093        | 0 secs           |
1094        +------------------+
1095        ");
1096    }
1097
1098    #[test]
1099    fn test_pretty_format_interval_month_day_nano_array() {
1100        let arr = Arc::new(arrow_array::IntervalMonthDayNanoArray::from(vec![
1101            Some(IntervalMonthDayNano::new(-1, -1, -600_000_000_000)),
1102            Some(IntervalMonthDayNano::new(0, 0, -1_000_000_001)),
1103            Some(IntervalMonthDayNano::new(0, 0, -1)),
1104            Some(IntervalMonthDayNano::new(0, 0, 1)),
1105            Some(IntervalMonthDayNano::new(0, 0, 10)),
1106            Some(IntervalMonthDayNano::new(0, 0, 100)),
1107            Some(IntervalMonthDayNano::new(0, 0, 1_000)),
1108            Some(IntervalMonthDayNano::new(0, 0, 10_000)),
1109            Some(IntervalMonthDayNano::new(0, 0, 100_000)),
1110            Some(IntervalMonthDayNano::new(0, 0, 1_000_000)),
1111            Some(IntervalMonthDayNano::new(0, 0, 10_000_000)),
1112            Some(IntervalMonthDayNano::new(0, 0, 100_000_000)),
1113            Some(IntervalMonthDayNano::new(0, 0, 1_000_000_000)),
1114            Some(IntervalMonthDayNano::new(0, 0, 0)),
1115        ]));
1116
1117        let schema = Arc::new(Schema::new(vec![Field::new(
1118            "IntervalMonthDayNano",
1119            arr.data_type().clone(),
1120            true,
1121        )]));
1122
1123        let batch = RecordBatch::try_new(schema, vec![arr]).unwrap();
1124
1125        let table = pretty_format_batches(&[batch]).unwrap().to_string();
1126
1127        insta::assert_snapshot!(table, @"
1128        +--------------------------+
1129        | IntervalMonthDayNano     |
1130        +--------------------------+
1131        | -1 mons -1 days -10 mins |
1132        | -1.000000001 secs        |
1133        | -0.000000001 secs        |
1134        | 0.000000001 secs         |
1135        | 0.000000010 secs         |
1136        | 0.000000100 secs         |
1137        | 0.000001000 secs         |
1138        | 0.000010000 secs         |
1139        | 0.000100000 secs         |
1140        | 0.001000000 secs         |
1141        | 0.010000000 secs         |
1142        | 0.100000000 secs         |
1143        | 1.000000000 secs         |
1144        | 0 secs                   |
1145        +--------------------------+
1146        ");
1147    }
1148
1149    #[test]
1150    fn test_format_options() {
1151        let options = FormatOptions::default()
1152            .with_null("null")
1153            .with_types_info(true);
1154        let int32_array = Int32Array::from(vec![Some(1), Some(2), None, Some(3), Some(4)]);
1155        let string_array =
1156            StringArray::from(vec![Some("foo"), Some("bar"), None, Some("baz"), None]);
1157
1158        let batch = RecordBatch::try_from_iter([
1159            ("my_int32_name", Arc::new(int32_array) as _),
1160            ("my_string_name", Arc::new(string_array) as _),
1161        ])
1162        .unwrap();
1163
1164        let column = pretty_format_columns_with_options(
1165            "my_column_name",
1166            &[batch.column(0).clone()],
1167            &options,
1168        )
1169        .unwrap()
1170        .to_string();
1171
1172        insta::assert_snapshot!(column, @"
1173        +----------------+
1174        | my_column_name |
1175        +----------------+
1176        | 1              |
1177        | 2              |
1178        | null           |
1179        | 3              |
1180        | 4              |
1181        +----------------+
1182        ");
1183
1184        let table = pretty_format_batches_with_options(&[batch], &options)
1185            .unwrap()
1186            .to_string();
1187
1188        insta::assert_snapshot!(table, @"
1189        +---------------+----------------+
1190        | my_int32_name | my_string_name |
1191        | Int32         | Utf8           |
1192        +---------------+----------------+
1193        | 1             | foo            |
1194        | 2             | bar            |
1195        | null          | null           |
1196        | 3             | baz            |
1197        | 4             | null           |
1198        +---------------+----------------+
1199        ");
1200    }
1201
1202    #[test]
1203    fn duration_pretty_and_iso_extremes() {
1204        // Build [MIN, MAX, 3661, NULL]
1205        let arr = DurationSecondArray::from(vec![Some(i64::MIN), Some(i64::MAX), Some(3661), None]);
1206        let array: ArrayRef = Arc::new(arr);
1207
1208        // Pretty formatting
1209        let opts = FormatOptions::default().with_null("null");
1210        let opts = opts.with_duration_format(DurationFormat::Pretty);
1211        let pretty =
1212            pretty_format_columns_with_options("pretty", std::slice::from_ref(&array), &opts)
1213                .unwrap()
1214                .to_string();
1215
1216        insta::assert_snapshot!(pretty, @"
1217        +------------------------------+
1218        | pretty                       |
1219        +------------------------------+
1220        | <invalid>                    |
1221        | <invalid>                    |
1222        | 0 days 1 hours 1 mins 1 secs |
1223        | null                         |
1224        +------------------------------+
1225        ");
1226
1227        // ISO8601 formatting
1228        let opts_iso = FormatOptions::default()
1229            .with_null("null")
1230            .with_duration_format(DurationFormat::ISO8601);
1231        let iso = pretty_format_columns_with_options("iso", &[array], &opts_iso)
1232            .unwrap()
1233            .to_string();
1234
1235        insta::assert_snapshot!(iso, @"
1236        +-----------+
1237        | iso       |
1238        +-----------+
1239        | <invalid> |
1240        | <invalid> |
1241        | PT3661S   |
1242        | null      |
1243        +-----------+
1244        ");
1245    }
1246
1247    //
1248    // Custom Formatting
1249    //
1250
1251    /// The factory that will create the [`ArrayFormatter`]s.
1252    #[derive(Debug)]
1253    struct TestFormatters {}
1254
1255    impl ArrayFormatterFactory for TestFormatters {
1256        fn create_array_formatter<'formatter>(
1257            &self,
1258            array: &'formatter dyn Array,
1259            options: &FormatOptions<'formatter>,
1260            field: Option<&'formatter Field>,
1261        ) -> Result<Option<ArrayFormatter<'formatter>>, ArrowError> {
1262            if field
1263                .map(|f| f.extension_type_name() == Some("my_money"))
1264                .unwrap_or(false)
1265            {
1266                // We assume that my_money always is an Int32.
1267                let array = array.as_primitive();
1268                let display_index = Box::new(MyMoneyFormatter {
1269                    array,
1270                    options: options.clone(),
1271                });
1272                return Ok(Some(ArrayFormatter::new(display_index, options.safe())));
1273            }
1274
1275            if array.data_type() == &DataType::Int32 {
1276                let array = array.as_primitive();
1277                let display_index = Box::new(MyInt32Formatter {
1278                    array,
1279                    options: options.clone(),
1280                });
1281                return Ok(Some(ArrayFormatter::new(display_index, options.safe())));
1282            }
1283
1284            Ok(None)
1285        }
1286    }
1287
1288    /// A format that will append a "€" sign to the end of the Int32 values.
1289    struct MyMoneyFormatter<'a> {
1290        array: &'a Int32Array,
1291        options: FormatOptions<'a>,
1292    }
1293
1294    impl<'a> DisplayIndex for MyMoneyFormatter<'a> {
1295        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
1296            match self.array.is_valid(idx) {
1297                true => write!(f, "{} €", self.array.value(idx))?,
1298                false => write!(f, "{}", self.options.null())?,
1299            }
1300
1301            Ok(())
1302        }
1303    }
1304
1305    /// The actual formatter
1306    struct MyInt32Formatter<'a> {
1307        array: &'a Int32Array,
1308        options: FormatOptions<'a>,
1309    }
1310
1311    impl<'a> DisplayIndex for MyInt32Formatter<'a> {
1312        fn write(&self, idx: usize, f: &mut dyn Write) -> crate::display::FormatResult {
1313            match self.array.is_valid(idx) {
1314                true => write!(f, "{} (32-Bit)", self.array.value(idx))?,
1315                false => write!(f, "{}", self.options.null())?,
1316            }
1317
1318            Ok(())
1319        }
1320    }
1321
1322    #[test]
1323    fn test_format_batches_with_custom_formatters() {
1324        // define a schema.
1325        let options = FormatOptions::new()
1326            .with_null("<NULL>")
1327            .with_formatter_factory(Some(&TestFormatters {}));
1328        let money_metadata = HashMap::from([(
1329            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1330            "my_money".to_owned(),
1331        )]);
1332        let schema = Arc::new(Schema::new(vec![
1333            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
1334        ]));
1335
1336        // define data.
1337        let batch = RecordBatch::try_new(
1338            schema,
1339            vec![Arc::new(array::Int32Array::from(vec![
1340                Some(1),
1341                None,
1342                Some(10),
1343                Some(100),
1344            ]))],
1345        )
1346        .unwrap();
1347
1348        let table = pretty_format_batches_with_options(&[batch], &options)
1349            .unwrap()
1350            .to_string();
1351
1352        insta::assert_snapshot!(table, @"
1353        +--------+
1354        | income |
1355        +--------+
1356        | 1 €    |
1357        | <NULL> |
1358        | 10 €   |
1359        | 100 €  |
1360        +--------+
1361        ");
1362    }
1363
1364    #[test]
1365    fn test_format_batches_with_custom_formatters_multi_nested_list() {
1366        // define a schema.
1367        let options = FormatOptions::new()
1368            .with_null("<NULL>")
1369            .with_formatter_factory(Some(&TestFormatters {}));
1370        let money_metadata = HashMap::from([(
1371            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1372            "my_money".to_owned(),
1373        )]);
1374        let nested_field = Arc::new(
1375            Field::new_list_field(DataType::Int32, true).with_metadata(money_metadata.clone()),
1376        );
1377
1378        // Create nested data
1379        let inner_list = ListBuilder::new(Int32Builder::new()).with_field(nested_field);
1380        let mut outer_list = FixedSizeListBuilder::new(inner_list, 2);
1381        outer_list.values().append_value([Some(1)]);
1382        outer_list.values().append_null();
1383        outer_list.append(true);
1384        outer_list.values().append_value([Some(2), Some(8)]);
1385        outer_list
1386            .values()
1387            .append_value([Some(50), Some(25), Some(25)]);
1388        outer_list.append(true);
1389        let outer_list = outer_list.finish();
1390
1391        let schema = Arc::new(Schema::new(vec![Field::new(
1392            "income",
1393            outer_list.data_type().clone(),
1394            true,
1395        )]));
1396
1397        // define data.
1398        let batch = RecordBatch::try_new(schema, vec![Arc::new(outer_list)]).unwrap();
1399
1400        let table = pretty_format_batches_with_options(&[batch], &options)
1401            .unwrap()
1402            .to_string();
1403
1404        insta::assert_snapshot!(table, @"
1405        +----------------------------------+
1406        | income                           |
1407        +----------------------------------+
1408        | [[1 €], <NULL>]                  |
1409        | [[2 €, 8 €], [50 €, 25 €, 25 €]] |
1410        +----------------------------------+
1411        ");
1412    }
1413
1414    #[test]
1415    fn test_format_batches_with_custom_formatters_nested_struct() {
1416        // define a schema.
1417        let options = FormatOptions::new()
1418            .with_null("<NULL>")
1419            .with_formatter_factory(Some(&TestFormatters {}));
1420        let money_metadata = HashMap::from([(
1421            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1422            "my_money".to_owned(),
1423        )]);
1424        let fields = Fields::from(vec![
1425            Field::new("name", DataType::Utf8, true),
1426            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
1427        ]);
1428
1429        let schema = Arc::new(Schema::new(vec![Field::new(
1430            "income",
1431            DataType::Struct(fields.clone()),
1432            true,
1433        )]));
1434
1435        // Create nested data
1436        let mut nested_data = StructBuilder::new(
1437            fields,
1438            vec![
1439                Box::new(StringBuilder::new()),
1440                Box::new(Int32Builder::new()),
1441            ],
1442        );
1443        nested_data
1444            .field_builder::<StringBuilder>(0)
1445            .unwrap()
1446            .extend([Some("Gimli"), Some("Legolas"), Some("Aragorn")]);
1447        nested_data
1448            .field_builder::<Int32Builder>(1)
1449            .unwrap()
1450            .extend([Some(10), None, Some(30)]);
1451        nested_data.append(true);
1452        nested_data.append(true);
1453        nested_data.append(true);
1454
1455        // define data.
1456        let batch = RecordBatch::try_new(schema, vec![Arc::new(nested_data.finish())]).unwrap();
1457
1458        let table = pretty_format_batches_with_options(&[batch], &options)
1459            .unwrap()
1460            .to_string();
1461
1462        insta::assert_snapshot!(table, @"
1463        +---------------------------------+
1464        | income                          |
1465        +---------------------------------+
1466        | {name: Gimli, income: 10 €}     |
1467        | {name: Legolas, income: <NULL>} |
1468        | {name: Aragorn, income: 30 €}   |
1469        +---------------------------------+
1470        ");
1471    }
1472
1473    #[test]
1474    fn test_format_batches_with_custom_formatters_nested_map() {
1475        // define a schema.
1476        let options = FormatOptions::new()
1477            .with_null("<NULL>")
1478            .with_formatter_factory(Some(&TestFormatters {}));
1479        let money_metadata = HashMap::from([(
1480            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1481            "my_money".to_owned(),
1482        )]);
1483
1484        let mut array = MapBuilder::<StringBuilder, Int32Builder>::new(
1485            None,
1486            StringBuilder::new(),
1487            Int32Builder::new(),
1488        )
1489        .with_values_field(
1490            Field::new("my_values", DataType::Int32, true).with_metadata(money_metadata.clone()),
1491        );
1492        array
1493            .keys()
1494            .extend([Some("Gimli"), Some("Legolas"), Some("Aragorn")]);
1495        array.values().extend([Some(10), None, Some(30)]);
1496        array.append(true).unwrap();
1497        let array = array.finish();
1498
1499        // define data.
1500        let schema = Arc::new(Schema::new(vec![Field::new(
1501            "income",
1502            array.data_type().clone(),
1503            true,
1504        )]));
1505        let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap();
1506
1507        let table = pretty_format_batches_with_options(&[batch], &options)
1508            .unwrap()
1509            .to_string();
1510
1511        insta::assert_snapshot!(table, @"
1512        +-----------------------------------------------+
1513        | income                                        |
1514        +-----------------------------------------------+
1515        | {Gimli: 10 €, Legolas: <NULL>, Aragorn: 30 €} |
1516        +-----------------------------------------------+
1517        ");
1518    }
1519
1520    #[test]
1521    fn test_format_batches_with_custom_formatters_nested_union() {
1522        // define a schema.
1523        let options = FormatOptions::new()
1524            .with_null("<NULL>")
1525            .with_formatter_factory(Some(&TestFormatters {}));
1526        let money_metadata = HashMap::from([(
1527            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1528            "my_money".to_owned(),
1529        )]);
1530        let fields = UnionFields::try_new(
1531            vec![0],
1532            vec![Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone())],
1533        )
1534        .unwrap();
1535
1536        // Create nested data and construct it with the correct metadata
1537        let mut array_builder = UnionBuilder::new_dense();
1538        array_builder.append::<Int32Type>("income", 1).unwrap();
1539        let (_, type_ids, offsets, children) = array_builder.build().unwrap().into_parts();
1540        let array = UnionArray::try_new(fields, type_ids, offsets, children).unwrap();
1541
1542        let schema = Arc::new(Schema::new(vec![Field::new(
1543            "income",
1544            array.data_type().clone(),
1545            true,
1546        )]));
1547
1548        // define data.
1549        let batch = RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap();
1550
1551        let table = pretty_format_batches_with_options(&[batch], &options)
1552            .unwrap()
1553            .to_string();
1554
1555        insta::assert_snapshot!(table, @"
1556        +--------------+
1557        | income       |
1558        +--------------+
1559        | {income=1 €} |
1560        +--------------+
1561        ");
1562    }
1563
1564    #[test]
1565    fn test_format_batches_with_custom_formatters_custom_schema_overrules_batch_schema() {
1566        // define a schema.
1567        let options = FormatOptions::new().with_formatter_factory(Some(&TestFormatters {}));
1568        let money_metadata = HashMap::from([(
1569            extension::EXTENSION_TYPE_NAME_KEY.to_owned(),
1570            "my_money".to_owned(),
1571        )]);
1572        let schema = Arc::new(Schema::new(vec![
1573            Field::new("income", DataType::Int32, true).with_metadata(money_metadata.clone()),
1574        ]));
1575
1576        // define data.
1577        let batch = RecordBatch::try_new(
1578            schema,
1579            vec![Arc::new(array::Int32Array::from(vec![
1580                Some(1),
1581                None,
1582                Some(10),
1583                Some(100),
1584            ]))],
1585        )
1586        .unwrap();
1587
1588        let table = create_table(
1589            // No metadata compared to test_format_batches_with_custom_formatters
1590            Some(Arc::new(Schema::new(vec![Field::new(
1591                "income",
1592                DataType::Int32,
1593                true,
1594            )]))),
1595            &[batch],
1596            &options,
1597        )
1598        .unwrap()
1599        .to_string();
1600
1601        // No € formatting as in test_format_batches_with_custom_formatters
1602        insta::assert_snapshot!(table, @"
1603        +--------------+
1604        | income       |
1605        +--------------+
1606        | 1 (32-Bit)   |
1607        |              |
1608        | 10 (32-Bit)  |
1609        | 100 (32-Bit) |
1610        +--------------+
1611        ");
1612    }
1613
1614    #[test]
1615    fn test_format_column_with_custom_formatters() {
1616        // define data.
1617        let array = Arc::new(array::Int32Array::from(vec![
1618            Some(1),
1619            None,
1620            Some(10),
1621            Some(100),
1622        ]));
1623
1624        let table = pretty_format_columns_with_options(
1625            "income",
1626            &[array],
1627            &FormatOptions::default().with_formatter_factory(Some(&TestFormatters {})),
1628        )
1629        .unwrap()
1630        .to_string();
1631
1632        insta::assert_snapshot!(table, @"
1633        +--------------+
1634        | income       |
1635        +--------------+
1636        | 1 (32-Bit)   |
1637        |              |
1638        | 10 (32-Bit)  |
1639        | 100 (32-Bit) |
1640        +--------------+
1641        ");
1642    }
1643
1644    #[test]
1645    fn test_pretty_format_batches_with_schema_with_wrong_number_of_fields() {
1646        let schema_a = Arc::new(Schema::new(vec![
1647            Field::new("a", DataType::Int32, true),
1648            Field::new("b", DataType::Utf8, true),
1649        ]));
1650        let schema_b = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, true)]));
1651
1652        // define data.
1653        let batch = RecordBatch::try_new(
1654            schema_b,
1655            vec![Arc::new(array::Int32Array::from(vec![
1656                Some(1),
1657                None,
1658                Some(10),
1659                Some(100),
1660            ]))],
1661        )
1662        .unwrap();
1663
1664        let error = pretty_format_batches_with_schema(schema_a, &[batch])
1665            .err()
1666            .unwrap();
1667        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");
1668    }
1669
1670    #[test]
1671    fn test_quoted_strings() {
1672        let schema = Arc::new(Schema::new(vec![Field::new(
1673            "strings",
1674            DataType::Utf8,
1675            true,
1676        )]));
1677
1678        let string_array = StringArray::from(vec![
1679            Some("hello"),
1680            Some("world"),
1681            Some(""),
1682            Some("tab\there"),
1683            Some("newline\ntest"),
1684            Some("quote\"test"),
1685            Some("backslash\\test"),
1686            None,
1687        ]);
1688
1689        let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(string_array)]).unwrap();
1690
1691        let options_none = FormatOptions::new().with_null("NULL");
1692        let table = pretty_format_batches_with_options(std::slice::from_ref(&batch), &options_none)
1693            .unwrap()
1694            .to_string();
1695
1696        insta::assert_snapshot!(table, @"
1697        +----------------+
1698        | strings        |
1699        +----------------+
1700        | hello          |
1701        | world          |
1702        |                |
1703        | tab	here       |
1704        | newline        |
1705        | test           |
1706        | quote\"test     |
1707        | backslash\\test |
1708        | NULL           |
1709        +----------------+
1710        ");
1711
1712        let options_quoted = FormatOptions::new()
1713            .with_null("NULL")
1714            .with_quoted_strings(true);
1715
1716        let table = pretty_format_batches_with_options(&[batch], &options_quoted)
1717            .unwrap()
1718            .to_string();
1719
1720        insta::assert_snapshot!(table, @r#"
1721        +-------------------+
1722        | strings           |
1723        +-------------------+
1724        | "hello"           |
1725        | "world"           |
1726        | ""                |
1727        | "tab\there"       |
1728        | "newline\ntest"   |
1729        | "quote\"test"     |
1730        | "backslash\\test" |
1731        | NULL              |
1732        +-------------------+
1733        "#);
1734    }
1735
1736    #[test]
1737    fn test_string_view_quoted() {
1738        let schema = Arc::new(Schema::new(vec![Field::new(
1739            "view_strings",
1740            DataType::Utf8View,
1741            true,
1742        )]));
1743
1744        let mut builder = StringViewBuilder::new();
1745        builder.append_value("hello");
1746        builder.append_null();
1747        builder.append_value("quote\"test");
1748
1749        let array: ArrayRef = Arc::new(builder.finish());
1750        let batch = RecordBatch::try_new(schema, vec![array]).unwrap();
1751
1752        let options = FormatOptions::new().with_quoted_strings(true);
1753
1754        let table = pretty_format_batches_with_options(&[batch], &options)
1755            .unwrap()
1756            .to_string();
1757
1758        insta::assert_snapshot!(table, @"
1759        +---------------+
1760        | view_strings  |
1761        +---------------+
1762        | \"hello\"       |
1763        |               |
1764        | \"quote\\\"test\" |
1765        +---------------+
1766        ");
1767    }
1768
1769    #[test]
1770    fn test_quoted_strings_in_struct() {
1771        let string_builder = StringBuilder::new();
1772        let mut name_builder = string_builder;
1773        name_builder.append_value("Alice");
1774        name_builder.append_value("");
1775        name_builder.append_value("Bob");
1776
1777        let fields = vec![Field::new("name", DataType::Utf8, false)];
1778        let mut struct_builder = StructBuilder::new(fields, vec![Box::new(name_builder)]);
1779        struct_builder.append(true);
1780        struct_builder.append(true);
1781        struct_builder.append(true);
1782
1783        let struct_array = struct_builder.finish();
1784
1785        let schema = Arc::new(Schema::new(vec![Field::new(
1786            "person",
1787            struct_array.data_type().clone(),
1788            false,
1789        )]));
1790
1791        let batch = RecordBatch::try_new(schema, vec![Arc::new(struct_array)]).unwrap();
1792
1793        let options_none = FormatOptions::new();
1794        let table = pretty_format_batches_with_options(std::slice::from_ref(&batch), &options_none)
1795            .unwrap()
1796            .to_string();
1797
1798        insta::assert_snapshot!(table, @"
1799        +---------------+
1800        | person        |
1801        +---------------+
1802        | {name: Alice} |
1803        | {name: }      |
1804        | {name: Bob}   |
1805        +---------------+
1806        ");
1807
1808        let options_quoted = FormatOptions::new().with_quoted_strings(true);
1809        let table = pretty_format_batches_with_options(&[batch], &options_quoted)
1810            .unwrap()
1811            .to_string();
1812
1813        insta::assert_snapshot!(table, @"
1814        +-----------------+
1815        | person          |
1816        +-----------------+
1817        | {name: \"Alice\"} |
1818        | {name: \"\"}      |
1819        | {name: \"Bob\"}   |
1820        +-----------------+
1821        ");
1822    }
1823}