substrait_explain/textify/
extensions.rs

1//! Extension textification support
2//!
3//! This module provides [`Textify`] implementations for extension-related
4//! types, including [`ExtensionValue`], [`ExtensionColumn`], [`ExtensionArgs`],
5//! and the various extension relation types ([`ExtensionLeafRel`],
6//! [`ExtensionSingleRel`], [`ExtensionMultiRel`]).
7
8use std::fmt;
9
10use substrait::proto::{ExtensionLeafRel, ExtensionMultiRel, ExtensionSingleRel};
11
12use crate::extensions::any::AnyRef;
13use crate::extensions::registry::ExtensionError;
14use crate::extensions::{ExtensionArgs, ExtensionColumn, ExtensionValue};
15use crate::textify::foundation::{Scope, Textify};
16use crate::textify::types::escaped;
17
18/// Decode an extension from an [`AnyRef`], and format it as text
19fn format_extension<S: Scope, W: fmt::Write>(
20    ctx: &S,
21    w: &mut W,
22    extension_type: &str,
23    detail: Option<AnyRef<'_>>,
24) -> fmt::Result {
25    let indent = ctx.indent();
26
27    match detail {
28        Some(detail) => {
29            // Decode the extension using the registry
30            let registry = ctx.extension_registry();
31            match registry.decode(detail) {
32                Ok((name, args)) => {
33                    // Success: format with extension name and args (with built-in ordering)
34                    write!(
35                        w,
36                        "{}{}:{}[{}]",
37                        indent,
38                        extension_type,
39                        name,
40                        ctx.display(&args)
41                    )?;
42                }
43                Err(error) => {
44                    // Error decoding: format with error token
45                    write!(w, "{}{}[{}]", indent, extension_type, ctx.failure(error))?;
46                }
47            }
48        }
49        None => {
50            // No detail provided: format with error token
51            let error = ExtensionError::MissingDetail;
52            write!(w, "{}{}[{}]", indent, extension_type, ctx.failure(error))?;
53        }
54    }
55
56    Ok(())
57}
58
59impl Textify for ExtensionValue {
60    fn name() -> &'static str {
61        "ExtensionValue"
62    }
63
64    fn textify<S: Scope, W: fmt::Write>(&self, _ctx: &S, w: &mut W) -> fmt::Result {
65        match self {
66            ExtensionValue::String(s) => write!(w, "'{}'", escaped(s)),
67            ExtensionValue::Integer(i) => write!(w, "{i}"),
68            ExtensionValue::Float(f) => write!(w, "{f}"),
69            ExtensionValue::Boolean(b) => write!(w, "{b}"),
70            ExtensionValue::Reference(r) => write!(w, "${r}"),
71            ExtensionValue::Expression(e) => write!(w, "{e}"),
72        }
73    }
74}
75
76impl Textify for ExtensionColumn {
77    fn name() -> &'static str {
78        "ExtensionColumn"
79    }
80
81    fn textify<S: Scope, W: fmt::Write>(&self, _ctx: &S, w: &mut W) -> fmt::Result {
82        match self {
83            ExtensionColumn::Named { name, type_spec } => write!(w, "{name}:{type_spec}"),
84            ExtensionColumn::Reference(r) => write!(w, "${r}"),
85            ExtensionColumn::Expression(e) => write!(w, "{e}"),
86        }
87    }
88}
89
90impl Textify for ExtensionArgs {
91    fn name() -> &'static str {
92        "ExtensionArgs"
93    }
94
95    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
96        let mut has_args = false;
97
98        // Add positional arguments
99        for (i, value) in self.positional.iter().enumerate() {
100            if i > 0 || has_args {
101                write!(w, ", ")?;
102            }
103            value.textify(ctx, w)?;
104            has_args = true;
105        }
106
107        // Add named arguments in display order (IndexMap preserves insertion order)
108        for (name, value) in &self.named {
109            if has_args {
110                write!(w, ", ")?;
111            }
112            write!(w, "{name}=")?;
113            value.textify(ctx, w)?;
114            has_args = true;
115        }
116
117        if !has_args {
118            write!(w, "_")?;
119        }
120
121        // Add output columns if present
122        if !self.output_columns.is_empty() {
123            write!(w, " => {}", ctx.separated(self.output_columns.iter(), ", "))?;
124        }
125
126        Ok(())
127    }
128}
129
130/// Textify children relations with proper indentation
131fn textify_children<S: Scope, W: fmt::Write>(
132    ctx: &S,
133    w: &mut W,
134    children: &[substrait::proto::Rel],
135) -> fmt::Result {
136    let child_scope = ctx.push_indent();
137    for child in children {
138        writeln!(w)?;
139        child.textify(&child_scope, w)?;
140    }
141    Ok(())
142}
143
144/// Textify a single child relation with proper indentation
145fn textify_child<S: Scope, W: fmt::Write>(
146    ctx: &S,
147    w: &mut W,
148    child: Option<&substrait::proto::Rel>,
149) -> fmt::Result {
150    if let Some(input) = child {
151        let child_scope = ctx.push_indent();
152        writeln!(w)?;
153        input.textify(&child_scope, w)?;
154    }
155    Ok(())
156}
157
158impl Textify for ExtensionLeafRel {
159    fn name() -> &'static str {
160        "ExtensionLeafRel"
161    }
162
163    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
164        // Convert prost_types::Any to AnyRef at the boundary
165        let detail_ref = self.detail.as_ref().map(AnyRef::from);
166        format_extension(ctx, w, "ExtensionLeaf", detail_ref)
167    }
168}
169
170impl Textify for ExtensionSingleRel {
171    fn name() -> &'static str {
172        "ExtensionSingleRel"
173    }
174
175    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
176        // Convert prost_types::Any to AnyRef at the boundary
177        let detail_ref = self.detail.as_ref().map(AnyRef::from);
178        format_extension(ctx, w, "ExtensionSingle", detail_ref)?;
179
180        // Add child input regardless of whether extension was resolved
181        textify_child(ctx, w, self.input.as_deref())?;
182        Ok(())
183    }
184}
185
186impl Textify for ExtensionMultiRel {
187    fn name() -> &'static str {
188        "ExtensionMultiRel"
189    }
190
191    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
192        // Convert prost_types::Any to AnyRef at the boundary
193        let detail_ref = self.detail.as_ref().map(AnyRef::from);
194        format_extension(ctx, w, "ExtensionMulti", detail_ref)?;
195
196        // Add child inputs regardless of whether extension was resolved
197        textify_children(ctx, w, &self.inputs)?;
198        Ok(())
199    }
200}