## Coverage Report

#### Created: 2025-11-22 06:00

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /Users/andrewlamb/Software/datafusion/datafusion/expr/src/logical_plan/display.rs

    Line
    Count
    Source
    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
    //! This module provides logic for displaying LogicalPlans in various styles
    19
    20
    use std::collections::HashMap;
    21
    use std::fmt;
    22
    23
    use crate::{
    24
        expr_vec_fmt, Aggregate, DescribeTable, Distinct, DistinctOn, DmlStatement, Expr,
    25
        Filter, Join, Limit, LogicalPlan, Partitioning, Projection, RecursiveQuery,
    26
        Repartition, Sort, Subquery, SubqueryAlias, TableProviderFilterPushDown, TableScan,
    27
        Unnest, Values, Window,
    28
    };
    29
    30
    use crate::dml::CopyTo;
    31
    use arrow::datatypes::Schema;
    32
    use datafusion_common::display::GraphvizBuilder;
    33
    use datafusion_common::tree_node::{TreeNodeRecursion, TreeNodeVisitor};
    34
    use datafusion_common::{internal_datafusion_err, Column, DataFusionError};
    35
    use serde_json::json;
    36
    37
    /// Formats plans with a single line per node. For example:
    38
    ///
    39
    /// Projection: id
    40
    ///    Filter: state Eq Utf8(\"CO\")\
    41
    ///       CsvScan: employee.csv projection=Some([0, 3])";
    42
    pub struct IndentVisitor<'a, 'b> {
    43
        f: &'a mut fmt::Formatter<'b>,
    44
        /// If true, includes summarized schema information
    45
        with_schema: bool,
    46
        /// The current indent
    47
        indent: usize,
    48
    }
    49
    50
    impl<'a, 'b> IndentVisitor<'a, 'b> {
    51
        /// Create a visitor that will write a formatted LogicalPlan to f. If `with_schema` is
    52
        /// true, includes schema information on each line.
    53
    0
        pub fn new(f: &'a mut fmt::Formatter<'b>, with_schema: bool) -> Self {
    54
    0
            Self {
    55
    0
                f,
    56
    0
                with_schema,
    57
    0
                indent: 0,
    58
    0
            }
    59
    0
        }
    60
    }
    61
    62
    impl<'n> TreeNodeVisitor<'n> for IndentVisitor<'_, '_> {
    63
        type Node = LogicalPlan;
    64
    65
    0
        fn f_down(
    66
    0
            &mut self,
    67
    0
            plan: &'n LogicalPlan,
    68
    0
        ) -> datafusion_common::Result<TreeNodeRecursion> {
    69
    0
            if self.indent > 0 {
    70
    0
                writeln!(self.f)?;
    71
    0
            }
    72
    0
            write!(self.f, "{:indent$}", "", indent = self.indent * 2)?;
    73
    0
            write!(self.f, "{}", plan.display())?;
    74
    0
            if self.with_schema {
    75
    0
                write!(self.f, " {}", display_schema(plan.schema().as_arrow()))?;
    76
    0
            }
    77
    78
    0
            self.indent += 1;
    79
    0
            Ok(TreeNodeRecursion::Continue)
    80
    0
        }
    81
    82
    0
        fn f_up(
    83
    0
            &mut self,
    84
    0
            _plan: &'n LogicalPlan,
    85
    0
        ) -> datafusion_common::Result<TreeNodeRecursion> {
    86
    0
            self.indent -= 1;
    87
    0
            Ok(TreeNodeRecursion::Continue)
    88
    0
        }
    89
    }
    90
    91
    /// Print the schema in a compact representation to `buf`
    92
    ///
    93
    /// For example: `foo:Utf8` if `foo` can not be null, and
    94
    /// `foo:Utf8;N` if `foo` is nullable.
    95
    ///
    96
    /// ```
    97
    /// use arrow::datatypes::{DataType, Field, Schema};
    98
    /// # use datafusion_expr::logical_plan::display_schema;
    99
    /// let schema = Schema::new(vec![
    100
    ///     Field::new("id", DataType::Int32, false),
    101
    ///     Field::new("first_name", DataType::Utf8, true),
    102
    /// ]);
    103
    ///
    104
    /// assert_eq!(
    105
    ///     "[id:Int32, first_name:Utf8;N]",
    106
    ///     format!("{}", display_schema(&schema))
    107
    /// );
    108
    /// ```
    109
    0
    pub fn display_schema(schema: &Schema) -> impl fmt::Display + '_ {
    110
        struct Wrapper<'a>(&'a Schema);
    111
    112
        impl fmt::Display for Wrapper<'_> {
    113
    0
            fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    114
    0
                write!(f, "[")?;
    115
    0
                for (idx, field) in self.0.fields().iter().enumerate() {
    116
    0
                    if idx > 0 {
    117
    0
                        write!(f, ", ")?;
    118
    0
                    }
    119
    0
                    let nullable_str = if field.is_nullable() { ";N" } else { "" };
    120
    0
                    write!(
    121
    0
                        f,
    122
    0
                        "{}:{:?}{}",
    123
    0
                        field.name(),
    124
    0
                        field.data_type(),
    125
                        nullable_str
    126
    0
                    )?;
    127
                }
    128
    0
                write!(f, "]")
    129
    0
            }
    130
        }
    131
    0
        Wrapper(schema)
    132
    0
    }
    133
    134
    /// Formats plans for graphical display using the `DOT` language. This
    135
    /// format can be visualized using software from
    136
    /// [`graphviz`](https://graphviz.org/)
    137
    pub struct GraphvizVisitor<'a, 'b> {
    138
        f: &'a mut fmt::Formatter<'b>,
    139
        graphviz_builder: GraphvizBuilder,
    140
        /// If true, includes summarized schema information
    141
        with_schema: bool,
    142
    143
        /// Holds the ids (as generated from `graphviz_builder` of all
    144
        /// parent nodes
    145
        parent_ids: Vec<usize>,
    146
    }
    147
    148
    impl<'a, 'b> GraphvizVisitor<'a, 'b> {
    149
    0
        pub fn new(f: &'a mut fmt::Formatter<'b>) -> Self {
    150
    0
            Self {
    151
    0
                f,
    152
    0
                graphviz_builder: GraphvizBuilder::default(),
    153
    0
                with_schema: false,
    154
    0
                parent_ids: Vec::new(),
    155
    0
            }
    156
    0
        }
    157
    158
        /// Sets a flag which controls if the output schema is displayed
    159
    0
        pub fn set_with_schema(&mut self, with_schema: bool) {
    160
    0
            self.with_schema = with_schema;
    161
    0
        }
    162
    163
    0
        pub fn pre_visit_plan(&mut self, label: &str) -> fmt::Result {
    164
    0
            self.graphviz_builder.start_cluster(self.f, label)
    165
    0
        }
    166
    167
    0
        pub fn post_visit_plan(&mut self) -> fmt::Result {
    168
    0
            self.graphviz_builder.end_cluster(self.f)
    169
    0
        }
    170
    171
    0
        pub fn start_graph(&mut self) -> fmt::Result {
    172
    0
            self.graphviz_builder.start_graph(self.f)
    173
    0
        }
    174
    175
    0
        pub fn end_graph(&mut self) -> fmt::Result {
    176
    0
            self.graphviz_builder.end_graph(self.f)
    177
    0
        }
    178
    }
    179
    180
    impl<'n> TreeNodeVisitor<'n> for GraphvizVisitor<'_, '_> {
    181
        type Node = LogicalPlan;
    182
    183
    0
        fn f_down(
    184
    0
            &mut self,
    185
    0
            plan: &'n LogicalPlan,
    186
    0
        ) -> datafusion_common::Result<TreeNodeRecursion> {
    187
    0
            let id = self.graphviz_builder.next_id();
    188
    189
            // Create a new graph node for `plan` such as
    190
            // id [label="foo"]
    191
    0
            let label = if self.with_schema {
    192
    0
                format!(
    193
    0
                    r"{}\nSchema: {}",
    194
    0
                    plan.display(),
    195
    0
                    display_schema(plan.schema().as_arrow())
    196
                )
    197
            } else {
    198
    0
                format!("{}", plan.display())
    199
            };
    200
    201
    0
            self.graphviz_builder
    202
    0
                .add_node(self.f, id, &label, None)
    203
    0
                .map_err(|_e| internal_datafusion_err!("Fail to format"))?;
    204
    205
            // Create an edge to our parent node, if any
    206
            //  parent_id -> id
    207
    0
            if let Some(parent_id) = self.parent_ids.last() {
    208
    0
                self.graphviz_builder
    209
    0
                    .add_edge(self.f, *parent_id, id)
    210
    0
                    .map_err(|_e| internal_datafusion_err!("Fail to format"))?;
    211
    0
            }
    212
    213
    0
            self.parent_ids.push(id);
    214
    0
            Ok(TreeNodeRecursion::Continue)
    215
    0
        }
    216
    217
    0
        fn f_up(
    218
    0
            &mut self,
    219
    0
            _plan: &LogicalPlan,
    220
    0
        ) -> datafusion_common::Result<TreeNodeRecursion> {
    221
            // always be non-empty as pre_visit always pushes
    222
            // So it should always be Ok(true)
    223
    0
            let res = self.parent_ids.pop();
    224
    0
            res.ok_or(internal_datafusion_err!("Fail to format"))
    225
    0
                .map(|_| TreeNodeRecursion::Continue)
    226
    0
        }
    227
    }
    228
    229
    /// Formats plans to display as postgresql plan json format.
    230
    ///
    231
    /// There are already many existing visualizer for this format, for example [dalibo](https://explain.dalibo.com/).
    232
    /// Unfortunately, there is no formal spec for this format, but it is widely used in the PostgreSQL community.
    233
    ///
    234
    /// Here is an example of the format:
    235
    ///
    236
    /// ```json
    237
    /// [
    238
    ///     {
    239
    ///         "Plan": {
    240
    ///             "Node Type": "Sort",
    241
    ///             "Output": [
    242
    ///                 "question_1.id",
    243
    ///                 "question_1.title",
    244
    ///                 "question_1.text",
    245
    ///                 "question_1.file",
    246
    ///                 "question_1.type",
    247
    ///                 "question_1.source",
    248
    ///                 "question_1.exam_id"
    249
    ///             ],
    250
    ///             "Sort Key": [
    251
    ///                 "question_1.id"
    252
    ///             ],
    253
    ///             "Plans": [
    254
    ///                 {
    255
    ///                     "Node Type": "Seq Scan",
    256
    ///                     "Parent Relationship": "Left",
    257
    ///                     "Relation Name": "question",
    258
    ///                     "Schema": "public",
    259
    ///                     "Alias": "question_1",
    260
    ///                     "Output": [
    261
    ///                        "question_1.id",
    262
    ///                         "question_1.title",
    263
    ///                        "question_1.text",
    264
    ///                         "question_1.file",
    265
    ///                         "question_1.type",
    266
    ///                         "question_1.source",
    267
    ///                         "question_1.exam_id"
    268
    ///                     ],
    269
    ///                     "Filter": "(question_1.exam_id = 1)"
    270
    ///                 }
    271
    ///             ]
    272
    ///         }
    273
    ///     }
    274
    /// ]
    275
    /// ```
    276
    pub struct PgJsonVisitor<'a, 'b> {
    277
        f: &'a mut fmt::Formatter<'b>,
    278
    279
        /// A mapping from plan node id to the plan node json representation.
    280
        objects: HashMap<u32, serde_json::Value>,
    281
    282
        next_id: u32,
    283
    284
        /// If true, includes summarized schema information
    285
        with_schema: bool,
    286
    287
        /// Holds the ids (as generated from `graphviz_builder` of all
    288
        /// parent nodes
    289
        parent_ids: Vec<u32>,
    290
    }
    291
    292
    impl<'a, 'b> PgJsonVisitor<'a, 'b> {
    293
    0
        pub fn new(f: &'a mut fmt::Formatter<'b>) -> Self {
    294
    0
            Self {
    295
    0
                f,
    296
    0
                objects: HashMap::new(),
    297
    0
                next_id: 0,
    298
    0
                with_schema: false,
    299
    0
                parent_ids: Vec::new(),
    300
    0
            }
    301
    0
        }
    302
    303
        /// Sets a flag which controls if the output schema is displayed
    304
    0
        pub fn with_schema(&mut self, with_schema: bool) {
    305
    0
            self.with_schema = with_schema;
    306
    0
        }
    307
    308
        /// Converts a logical plan node to a json object.
    309
    0
        fn to_json_value(node: &LogicalPlan) -> serde_json::Value {
    310
    0
            match node {
    311
                LogicalPlan::EmptyRelation(_) => {
    312
    0
                    json!({
    313
    0
                        "Node Type": "EmptyRelation",
    314
                    })
    315
                }
    316
    0
                LogicalPlan::RecursiveQuery(RecursiveQuery { is_distinct, .. }) => {
    317
    0
                    json!({
    318
    0
                        "Node Type": "RecursiveQuery",
    319
    0
                        "Is Distinct": is_distinct,
    320
                    })
    321
                }
    322
    0
                LogicalPlan::Values(Values { ref values, .. }) => {
    323
    0
                    let str_values = values
    324
    0
                        .iter()
    325
                        // limit to only 5 values to avoid horrible display
    326
    0
                        .take(5)
    327
    0
                        .map(|row| {
    328
    0
                            let item = row
    329
    0
                                .iter()
    330
    0
                                .map(|expr| expr.to_string())
    331
    0
                                .collect::<Vec<_>>()
    332
    0
                                .join(", ");
    333
    0
                            format!("({item})")
    334
    0
                        })
    335
    0
                        .collect::<Vec<_>>()
    336
    0
                        .join(", ");
    337
    338
    0
                    let eclipse = if values.len() > 5 { "..." } else { "" };
    339
    340
    0
                    let values_str = format!("{str_values}{eclipse}");
    341
    0
                    json!({
    342
    0
                        "Node Type": "Values",
    343
    0
                        "Values": values_str
    344
                    })
    345
                }
    346
                LogicalPlan::TableScan(TableScan {
    347
    0
                    ref source,
    348
    0
                    ref table_name,
    349
    0
                    ref filters,
    350
    0
                    ref fetch,
    351
                    ..
    352
                }) => {
    353
    0
                    let mut object = json!({
    354
    0
                        "Node Type": "TableScan",
    355
    0
                        "Relation Name": table_name.table(),
    356
                    });
    357
    358
    0
                    if let Some(s) = table_name.schema() {
    359
    0
                        object["Schema"] = serde_json::Value::String(s.to_string());
    360
    0
                    }
    361
    362
    0
                    if let Some(c) = table_name.catalog() {
    363
    0
                        object["Catalog"] = serde_json::Value::String(c.to_string());
    364
    0
                    }
    365
    366
    0
                    if !filters.is_empty() {
    367
    0
                        let mut full_filter = vec![];
    368
    0
                        let mut partial_filter = vec![];
    369
    0
                        let mut unsupported_filters = vec![];
    370
    0
                        let filters: Vec<&Expr> = filters.iter().collect();
    371
    372
    0
                        if let Ok(results) = source.supports_filters_pushdown(&filters) {
    373
    0
                            filters.iter().zip(results.iter()).for_each(
    374
    0
                                |(x, res)| match res {
    375
    0
                                    TableProviderFilterPushDown::Exact => full_filter.push(x),
    376
                                    TableProviderFilterPushDown::Inexact => {
    377
    0
                                        partial_filter.push(x)
    378
                                    }
    379
                                    TableProviderFilterPushDown::Unsupported => {
    380
    0
                                        unsupported_filters.push(x)
    381
                                    }
    382
    0
                                },
    383
                            );
    384
    0
                        }
    385
    386
    0
                        if !full_filter.is_empty() {
    387
    0
                            object["Full Filters"] =
    388
    0
                                serde_json::Value::String(expr_vec_fmt!(full_filter));
    389
    0
                        };
    390
    0
                        if !partial_filter.is_empty() {
    391
    0
                            object["Partial Filters"] =
    392
    0
                                serde_json::Value::String(expr_vec_fmt!(partial_filter));
    393
    0
                        }
    394
    0
                        if !unsupported_filters.is_empty() {
    395
    0
                            object["Unsupported Filters"] =
    396
    0
                                serde_json::Value::String(expr_vec_fmt!(unsupported_filters));
    397
    0
                        }
    398
    0
                    }
    399
    400
    0
                    if let Some(f) = fetch {
    401
    0
                        object["Fetch"] = serde_json::Value::Number((*f).into());
    402
    0
                    }
    403
    404
    0
                    object
    405
                }
    406
    0
                LogicalPlan::Projection(Projection { ref expr, .. }) => {
    407
    0
                    json!({
    408
    0
                        "Node Type": "Projection",
    409
    0
                        "Expressions": expr.iter().map(|e| e.to_string()).collect::<Vec<_>>()
    410
                    })
    411
                }
    412
    0
                LogicalPlan::Dml(DmlStatement { table_name, op, .. }) => {
    413
    0
                    json!({
    414
    0
                        "Node Type": "Projection",
    415
    0
                        "Operation": op.name(),
    416
    0
                        "Table Name": table_name.table()
    417
                    })
    418
                }
    419
                LogicalPlan::Copy(CopyTo {
    420
                    input: _,
    421
    0
                    output_url,
    422
    0
                    file_type,
    423
                    partition_by: _,
    424
    0
                    options,
    425
                    output_schema: _,
    426
                }) => {
    427
    0
                    let op_str = options
    428
    0
                        .iter()
    429
    0
                        .map(|(k, v)| format!("{k}={v}"))
    430
    0
                        .collect::<Vec<_>>()
    431
    0
                        .join(", ");
    432
    0
                    json!({
    433
    0
                        "Node Type": "CopyTo",
    434
    0
                        "Output URL": output_url,
    435
    0
                        "File Type": format!("{}", file_type.get_ext()),
    436
    0
                        "Options": op_str
    437
                    })
    438
                }
    439
    0
                LogicalPlan::Ddl(ddl) => {
    440
    0
                    json!({
    441
    0
                        "Node Type": "Ddl",
    442
    0
                        "Operation": format!("{}", ddl.display())
    443
                    })
    444
                }
    445
                LogicalPlan::Filter(Filter {
    446
    0
                    predicate: ref expr,
    447
                    ..
    448
                }) => {
    449
    0
                    json!({
    450
    0
                        "Node Type": "Filter",
    451
    0
                        "Condition": format!("{}", expr)
    452
                    })
    453
                }
    454
                LogicalPlan::Window(Window {
    455
    0
                    ref window_expr, ..
    456
                }) => {
    457
    0
                    json!({
    458
    0
                        "Node Type": "WindowAggr",
    459
    0
                        "Expressions": expr_vec_fmt!(window_expr)
    460
                    })
    461
                }
    462
                LogicalPlan::Aggregate(Aggregate {
    463
    0
                    ref group_expr,
    464
    0
                    ref aggr_expr,
    465
                    ..
    466
                }) => {
    467
    0
                    json!({
    468
    0
                        "Node Type": "Aggregate",
    469
    0
                        "Group By": expr_vec_fmt!(group_expr),
    470
    0
                        "Aggregates": expr_vec_fmt!(aggr_expr)
    471
                    })
    472
                }
    473
    0
                LogicalPlan::Sort(Sort { expr, fetch, .. }) => {
    474
    0
                    let mut object = json!({
    475
    0
                        "Node Type": "Sort",
    476
    0
                        "Sort Key": expr_vec_fmt!(expr),
    477
                    });
    478
    479
    0
                    if let Some(fetch) = fetch {
    480
    0
                        object["Fetch"] = serde_json::Value::Number((*fetch).into());
    481
    0
                    }
    482
    483
    0
                    object
    484
                }
    485
                LogicalPlan::Join(Join {
    486
    0
                    on: ref keys,
    487
    0
                    filter,
    488
    0
                    join_constraint,
    489
    0
                    join_type,
    490
                    ..
    491
                }) => {
    492
    0
                    let join_expr: Vec<String> =
    493
    0
                        keys.iter().map(|(l, r)| format!("{l} = {r}")).collect();
    494
    0
                    let filter_expr = filter
    495
    0
                        .as_ref()
    496
    0
                        .map(|expr| format!(" Filter: {expr}"))
    497
    0
                        .unwrap_or_else(|| "".to_string());
    498
    0
                    json!({
    499
    0
                        "Node Type": format!("{} Join", join_type),
    500
    0
                        "Join Constraint": format!("{:?}", join_constraint),
    501
    0
                        "Join Keys": join_expr.join(", "),
    502
    0
                        "Filter": format!("{}", filter_expr)
    503
                    })
    504
                }
    505
                LogicalPlan::Repartition(Repartition {
    506
    0
                    partitioning_scheme,
    507
                    ..
    508
    0
                }) => match partitioning_scheme {
    509
    0
                    Partitioning::RoundRobinBatch(n) => {
    510
    0
                        json!({
    511
    0
                            "Node Type": "Repartition",
    512
    0
                            "Partitioning Scheme": "RoundRobinBatch",
    513
    0
                            "Partition Count": n
    514
                        })
    515
                    }
    516
    0
                    Partitioning::Hash(expr, n) => {
    517
    0
                        let hash_expr: Vec<String> =
    518
    0
                            expr.iter().map(|e| format!("{e}")).collect();
    519
    520
    0
                        json!({
    521
    0
                            "Node Type": "Repartition",
    522
    0
                            "Partitioning Scheme": "Hash",
    523
    0
                            "Partition Count": n,
    524
    0
                            "Partitioning Key": hash_expr
    525
                        })
    526
                    }
    527
    0
                    Partitioning::DistributeBy(expr) => {
    528
    0
                        let dist_by_expr: Vec<String> =
    529
    0
                            expr.iter().map(|e| format!("{e}")).collect();
    530
    0
                        json!({
    531
    0
                            "Node Type": "Repartition",
    532
    0
                            "Partitioning Scheme": "DistributeBy",
    533
    0
                            "Partitioning Key": dist_by_expr
    534
                        })
    535
                    }
    536
                },
    537
                LogicalPlan::Limit(Limit {
    538
    0
                    ref skip,
    539
    0
                    ref fetch,
    540
                    ..
    541
                }) => {
    542
    0
                    let mut object = serde_json::json!(
    543
                        {
    544
    0
                            "Node Type": "Limit",
    545
                        }
    546
                    );
    547
    0
                    if let Some(s) = skip {
    548
    0
                        object["Skip"] = s.to_string().into()
    549
    0
                    };
    550
    0
                    if let Some(f) = fetch {
    551
    0
                        object["Fetch"] = f.to_string().into()
    552
    0
                    };
    553
    0
                    object
    554
                }
    555
                LogicalPlan::Subquery(Subquery { .. }) => {
    556
    0
                    json!({
    557
    0
                        "Node Type": "Subquery"
    558
                    })
    559
                }
    560
    0
                LogicalPlan::SubqueryAlias(SubqueryAlias { ref alias, .. }) => {
    561
    0
                    json!({
    562
    0
                        "Node Type": "Subquery",
    563
    0
                        "Alias": alias.table(),
    564
                    })
    565
                }
    566
    0
                LogicalPlan::Statement(statement) => {
    567
    0
                    json!({
    568
    0
                        "Node Type": "Statement",
    569
    0
                        "Statement": format!("{}", statement.display())
    570
                    })
    571
                }
    572
    0
                LogicalPlan::Distinct(distinct) => match distinct {
    573
                    Distinct::All(_) => {
    574
    0
                        json!({
    575
    0
                            "Node Type": "DistinctAll"
    576
                        })
    577
                    }
    578
                    Distinct::On(DistinctOn {
    579
    0
                        on_expr,
    580
    0
                        select_expr,
    581
    0
                        sort_expr,
    582
                        ..
    583
                    }) => {
    584
    0
                        let mut object = json!({
    585
    0
                            "Node Type": "DistinctOn",
    586
    0
                            "On": expr_vec_fmt!(on_expr),
    587
    0
                            "Select": expr_vec_fmt!(select_expr),
    588
                        });
    589
    0
                        if let Some(sort_expr) = sort_expr {
    590
    0
                            object["Sort"] =
    591
    0
                                serde_json::Value::String(expr_vec_fmt!(sort_expr));
    592
    0
                        }
    593
    594
    0
                        object
    595
                    }
    596
                },
    597
                LogicalPlan::Explain { .. } => {
    598
    0
                    json!({
    599
    0
                        "Node Type": "Explain"
    600
                    })
    601
                }
    602
                LogicalPlan::Analyze { .. } => {
    603
    0
                    json!({
    604
    0
                        "Node Type": "Analyze"
    605
                    })
    606
                }
    607
                LogicalPlan::Union(_) => {
    608
    0
                    json!({
    609
    0
                        "Node Type": "Union"
    610
                    })
    611
                }
    612
    0
                LogicalPlan::Extension(e) => {
    613
    0
                    json!({
    614
    0
                        "Node Type": e.node.name(),
    615
    0
                        "Detail": format!("{:?}", e.node)
    616
                    })
    617
                }
    618
                LogicalPlan::DescribeTable(DescribeTable { .. }) => {
    619
    0
                    json!({
    620
    0
                        "Node Type": "DescribeTable"
    621
                    })
    622
                }
    623
                LogicalPlan::Unnest(Unnest {
    624
    0
                    input: plan,
    625
    0
                    list_type_columns: list_col_indices,
    626
    0
                    struct_type_columns: struct_col_indices,
    627
                    ..
    628
                }) => {
    629
    0
                    let input_columns = plan.schema().columns();
    630
    0
                    let list_type_columns = list_col_indices
    631
    0
                        .iter()
    632
    0
                        .map(|(i, unnest_info)| {
    633
    0
                            format!(
    634
    0
                                "{}|depth={:?}",
    635
    0
                                &input_columns[*i].to_string(),
    636
                                unnest_info.depth
    637
                            )
    638
    0
                        })
    639
    0
                        .collect::<Vec<String>>();
    640
    0
                    let struct_type_columns = struct_col_indices
    641
    0
                        .iter()
    642
    0
                        .map(|i| &input_columns[*i])
    643
    0
                        .collect::<Vec<&Column>>();
    644
    0
                    json!({
    645
    0
                        "Node Type": "Unnest",
    646
    0
                        "ListColumn": expr_vec_fmt!(list_type_columns),
    647
    0
                        "StructColumn": expr_vec_fmt!(struct_type_columns),
    648
                    })
    649
                }
    650
            }
    651
    0
        }
    652
    }
    653
    654
    impl<'n> TreeNodeVisitor<'n> for PgJsonVisitor<'_, '_> {
    655
        type Node = LogicalPlan;
    656
    657
    0
        fn f_down(
    658
    0
            &mut self,
    659
    0
            node: &'n LogicalPlan,
    660
    0
        ) -> datafusion_common::Result<TreeNodeRecursion> {
    661
    0
            let id = self.next_id;
    662
    0
            self.next_id += 1;
    663
    0
            let mut object = Self::to_json_value(node);
    664
    665
    0
            object["Plans"] = serde_json::Value::Array(vec![]);
    666
    667
    0
            if self.with_schema {
    668
    0
                object["Output"] = serde_json::Value::Array(
    669
    0
                    node.schema()
    670
    0
                        .fields()
    671
    0
                        .iter()
    672
    0
                        .map(|f| f.name().to_string())
    673
    0
                        .map(serde_json::Value::String)
    674
    0
                        .collect(),
    675
                );
    676
    0
            };
    677
    678
    0
            self.objects.insert(id, object);
    679
    0
            self.parent_ids.push(id);
    680
    0
            Ok(TreeNodeRecursion::Continue)
    681
    0
        }
    682
    683
    0
        fn f_up(
    684
    0
            &mut self,
    685
    0
            _node: &Self::Node,
    686
    0
        ) -> datafusion_common::Result<TreeNodeRecursion> {
    687
    0
            let id = self.parent_ids.pop().unwrap();
    688
    689
    0
            let current_node = self
    690
    0
                .objects
    691
    0
                .remove(&id)
    692
    0
                .ok_or_else(|| internal_datafusion_err!("Missing current node!"))?;
    693
    694
    0
            if let Some(parent_id) = self.parent_ids.last() {
    695
    0
                let parent_node = self
    696
    0
                    .objects
    697
    0
                    .get_mut(parent_id)
    698
    0
                    .expect("Missing parent node!");
    699
    0
                let plans = parent_node
    700
    0
                    .get_mut("Plans")
    701
    0
                    .and_then(|p| p.as_array_mut())
    702
    0
                    .expect("Plans should be an array");
    703
    704
    0
                plans.push(current_node);
    705
            } else {
    706
                // This is the root node
    707
    0
                let plan = serde_json::json!([{"Plan": current_node}]);
    708
    0
                write!(
    709
    0
                    self.f,
    710
    0
                    "{}",
    711
    0
                    serde_json::to_string_pretty(&plan)
    712
    0
                        .map_err(|e| DataFusionError::External(Box::new(e)))?
    713
    0
                )?;
    714
            }
    715
    716
    0
            Ok(TreeNodeRecursion::Continue)
    717
    0
        }
    718
    }
    719
    720
    #[cfg(test)]
    721
    mod tests {
    722
        use arrow::datatypes::{DataType, Field};
    723
        use insta::assert_snapshot;
    724
    725
        use super::*;
    726
    727
        #[test]
    728
        fn test_display_empty_schema() {
    729
            let schema = Schema::empty();
    730
            assert_snapshot!(display_schema(&schema), @"[]");
    731
        }
    732
    733
        #[test]
    734
        fn test_display_schema() {
    735
            let schema = Schema::new(vec![
    736
                Field::new("id", DataType::Int32, false),
    737
                Field::new("first_name", DataType::Utf8, true),
    738
            ]);
    739
    740
            assert_snapshot!(display_schema(&schema), @"[id:Int32, first_name:Utf8;N]");
    741
        }
    742
    }