Skip to main content

substrait_explain/textify/
foundation.rs

1//! Foundation types for textifying a plan.
2
3use std::borrow::Cow;
4use std::fmt;
5use std::rc::Rc;
6use std::sync::mpsc;
7
8use thiserror::Error;
9
10use crate::extensions::registry::ExtensionError;
11use crate::extensions::simple::MissingReference;
12use crate::extensions::{ExtensionRegistry, InsertError, SimpleExtensions};
13
14pub(crate) const NONSPECIFIC: Option<&'static str> = None;
15
16#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
17pub enum Visibility {
18    /// Never show the information
19    Never,
20    /// Show the information if it is required for the output to be complete.
21    Required,
22    /// Show it always.
23    Always,
24}
25
26/// OutputOptions holds the options for textifying a Substrait type.
27#[derive(Debug, Clone)]
28pub struct OutputOptions {
29    /// Show the extension URNs in the output.
30    pub show_extension_urns: bool,
31    /// Show the extensions in the output. By default, simple extensions are
32    /// expanded into the input.
33    pub show_simple_extensions: bool,
34    /// Show the anchors of simple extensions in the output, and not just their
35    /// names.
36    ///
37    /// If `Required`, the anchor is shown for all simple extensions.
38    pub show_simple_extension_anchors: Visibility,
39    /// Instead of showing the emitted columns inline, show the emits directly.
40    pub show_emit: bool,
41
42    /// Show the types for columns in a read
43    pub read_types: bool,
44    /// Show the types for literals. If `Required`, the type is shown for anything other than
45    /// `i64`, `fp64`, `boolean`, or `string`.
46    pub literal_types: Visibility,
47    /// Show the nullability of types
48    pub nullability: bool,
49    /// The indent to use for nested types
50    pub indent: String,
51    /// Show the binary values for literal types as hex strings. Normally, they
52    /// are shown as '{{binary}}'
53    pub show_literal_binaries: bool,
54    /// A `Read:Virtual` with at least this many rows is emitted across multiple
55    /// lines (one `- row` per line) instead of inline. Set to a very large
56    /// value to keep virtual tables inline regardless of row count. Defaults to 3.
57    pub virtual_table_multiline_threshold: usize,
58}
59
60impl Default for OutputOptions {
61    fn default() -> Self {
62        Self {
63            show_extension_urns: false,
64            show_simple_extensions: false,
65            show_simple_extension_anchors: Visibility::Required,
66            literal_types: Visibility::Required,
67            show_emit: false,
68            read_types: false,
69            nullability: false,
70            indent: "  ".to_string(),
71            show_literal_binaries: false,
72            virtual_table_multiline_threshold: 3,
73        }
74    }
75}
76
77impl OutputOptions {
78    /// A verbose output options that shows all the necessary information for
79    /// reconstructing a plan.
80    pub fn verbose() -> Self {
81        Self {
82            show_extension_urns: true,
83            show_simple_extensions: true,
84            show_simple_extension_anchors: Visibility::Always,
85            literal_types: Visibility::Always,
86            // Emits are not required for a complete plan - just not a precise one.
87            show_emit: false,
88            read_types: true,
89            nullability: true,
90            indent: "  ".to_string(),
91            show_literal_binaries: true,
92            virtual_table_multiline_threshold: 3,
93        }
94    }
95}
96pub(crate) trait ErrorAccumulator: Clone {
97    fn push(&self, e: FormatError);
98}
99
100#[derive(Debug, Clone)]
101pub(crate) struct ErrorQueue {
102    sender: mpsc::Sender<FormatError>,
103    receiver: Rc<mpsc::Receiver<FormatError>>,
104}
105
106impl Default for ErrorQueue {
107    fn default() -> Self {
108        let (sender, receiver) = mpsc::channel();
109        Self {
110            sender,
111            receiver: Rc::new(receiver),
112        }
113    }
114}
115
116impl From<ErrorQueue> for Vec<FormatError> {
117    fn from(v: ErrorQueue) -> Vec<FormatError> {
118        v.receiver.try_iter().collect()
119    }
120}
121
122impl ErrorAccumulator for ErrorQueue {
123    fn push(&self, e: FormatError) {
124        self.sender.send(e).unwrap();
125    }
126}
127
128impl fmt::Display for ErrorQueue {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        for (i, e) in self.receiver.try_iter().enumerate() {
131            if i == 0 {
132                writeln!(f, "Warnings during conversion:")?;
133            }
134            let error_number = i + 1;
135            writeln!(f, "  - {error_number}: {e}")?;
136        }
137        Ok(())
138    }
139}
140
141impl ErrorQueue {
142    #[cfg(test)]
143    pub(crate) fn errs(self) -> Result<(), ErrorList> {
144        let errors: Vec<FormatError> = self.receiver.try_iter().collect();
145        if errors.is_empty() {
146            Ok(())
147        } else {
148            Err(ErrorList(errors))
149        }
150    }
151}
152
153// A list of errors, used to return errors from textify operations.
154#[cfg(test)]
155pub(crate) struct ErrorList(pub(crate) Vec<FormatError>);
156
157#[cfg(test)]
158impl ErrorList {
159    pub(crate) fn first(&self) -> &FormatError {
160        self.0
161            .first()
162            .expect("Expected at least one error in ErrorList")
163    }
164
165    pub(crate) fn is_empty(&self) -> bool {
166        self.0.is_empty()
167    }
168}
169
170#[cfg(test)]
171impl fmt::Display for ErrorList {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        for (i, e) in self.0.iter().enumerate() {
174            if i > 0 {
175                writeln!(f)?;
176            }
177            write!(f, "{e}")?;
178        }
179        Ok(())
180    }
181}
182
183#[cfg(test)]
184impl fmt::Debug for ErrorList {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        for (i, e) in self.0.iter().enumerate() {
187            if i == 0 {
188                writeln!(f, "Errors:")?;
189            }
190            writeln!(f, "! {e:?}")?;
191        }
192        Ok(())
193    }
194}
195
196#[cfg(test)]
197impl std::error::Error for ErrorList {}
198
199impl<'e> IntoIterator for &'e ErrorQueue {
200    type Item = FormatError;
201    type IntoIter = std::sync::mpsc::TryIter<'e, FormatError>;
202
203    fn into_iter(self) -> Self::IntoIter {
204        self.receiver.try_iter()
205    }
206}
207
208pub(crate) trait IndentTracker {
209    // TODO: Use this and remove the allow(dead_code)
210    #[allow(dead_code)]
211    fn indent<W: fmt::Write>(&self, w: &mut W) -> fmt::Result;
212    fn push(self) -> Self;
213}
214
215#[derive(Debug, Copy, Clone)]
216pub(crate) struct IndentStack<'a> {
217    count: u32,
218    indent: &'a str,
219}
220
221impl<'a> fmt::Display for IndentStack<'a> {
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        for _ in 0..self.count {
224            f.write_str(self.indent)?;
225        }
226        Ok(())
227    }
228}
229
230#[derive(Debug, Copy, Clone)]
231pub(crate) struct ScopedContext<'a, Err: ErrorAccumulator> {
232    errors: &'a Err,
233    options: &'a OutputOptions,
234    extensions: &'a SimpleExtensions,
235    indent: IndentStack<'a>,
236    extension_registry: &'a ExtensionRegistry,
237}
238
239impl<'a> IndentStack<'a> {
240    pub(crate) fn new(indent: &'a str) -> Self {
241        Self { count: 0, indent }
242    }
243}
244
245impl<'a> IndentTracker for IndentStack<'a> {
246    fn indent<W: fmt::Write>(&self, w: &mut W) -> fmt::Result {
247        for _ in 0..self.count {
248            w.write_str(self.indent)?;
249        }
250        Ok(())
251    }
252
253    fn push(mut self) -> Self {
254        self.count += 1;
255        self
256    }
257}
258
259impl<'a, Err: ErrorAccumulator> ScopedContext<'a, Err> {
260    pub(crate) fn new(
261        options: &'a OutputOptions,
262        errors: &'a Err,
263        extensions: &'a SimpleExtensions,
264        extension_registry: &'a ExtensionRegistry,
265    ) -> Self {
266        Self {
267            options,
268            errors,
269            extensions,
270            indent: IndentStack::new(options.indent.as_str()),
271            extension_registry,
272        }
273    }
274}
275
276/// Errors that can occur when formatting a plan.
277#[derive(Error, Debug, Clone)]
278pub enum FormatError {
279    /// Error in adding extensions to the plan - e.g. duplicates, invalid URN
280    /// references, etc.
281    #[error("Error adding simple extension: {0}")]
282    Insert(#[from] InsertError),
283    /// Error in looking up an extension - e.g. missing URN, anchor, name, etc.
284    #[error("Error finding simple extension: {0}")]
285    Lookup(#[from] MissingReference),
286    /// Error in extension registry operations - e.g. extension not found, parse errors, etc.
287    #[error("Extension error: {0}")]
288    Extension(#[from] ExtensionError),
289    /// Error in formatting the plan - e.g. invalid value, unimplemented, etc.
290    #[error("Error formatting output: {0}")]
291    Format(#[from] PlanError),
292}
293
294impl FormatError {
295    pub fn message(&self) -> &'static str {
296        match self {
297            FormatError::Lookup(MissingReference::MissingUrn(_)) => "uri",
298            FormatError::Lookup(MissingReference::MissingAnchor(k, _)) => k.name(),
299            FormatError::Lookup(MissingReference::MissingName(k, _)) => k.name(),
300            FormatError::Lookup(MissingReference::Mismatched(k, _, _)) => k.name(),
301            FormatError::Lookup(MissingReference::DuplicateName(k, _)) => k.name(),
302            FormatError::Extension(_) => "extension",
303            FormatError::Format(m) => m.message,
304            FormatError::Insert(InsertError::MissingMappingType) => "extension",
305            FormatError::Insert(InsertError::DuplicateUrnAnchor { .. }) => "uri",
306            FormatError::Insert(InsertError::DuplicateAnchor { .. }) => "extension",
307            FormatError::Insert(InsertError::MissingUrn { .. }) => "uri",
308            FormatError::Insert(InsertError::DuplicateAndMissingUrn { .. }) => "uri",
309            FormatError::Insert(InsertError::InvalidName { .. }) => "extension",
310        }
311    }
312}
313
314#[derive(Debug, Clone)]
315pub struct PlanError {
316    // The message this originated in
317    pub message: &'static str,
318    // The specific lookup that failed, if specifiable
319    pub lookup: Option<Cow<'static, str>>,
320    // The description of the error
321    pub description: Cow<'static, str>,
322    // The error type
323    pub error_type: FormatErrorType,
324}
325
326impl PlanError {
327    pub fn invalid(
328        message: &'static str,
329        specific: Option<impl Into<Cow<'static, str>>>,
330        description: impl Into<Cow<'static, str>>,
331    ) -> Self {
332        Self {
333            message,
334            lookup: specific.map(|s| s.into()),
335            description: description.into(),
336            error_type: FormatErrorType::InvalidValue,
337        }
338    }
339
340    pub fn unimplemented(
341        message: &'static str,
342        specific: Option<impl Into<Cow<'static, str>>>,
343        description: impl Into<Cow<'static, str>>,
344    ) -> Self {
345        Self {
346            message,
347            lookup: specific.map(|s| s.into()),
348            description: description.into(),
349            error_type: FormatErrorType::Unimplemented,
350        }
351    }
352
353    pub fn internal(
354        message: &'static str,
355        specific: Option<impl Into<Cow<'static, str>>>,
356        description: impl Into<Cow<'static, str>>,
357    ) -> Self {
358        Self {
359            message,
360            lookup: specific.map(|s| s.into()),
361            description: description.into(),
362            error_type: FormatErrorType::Internal,
363        }
364    }
365}
366
367#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord)]
368pub enum FormatErrorType {
369    InvalidValue,
370    Unimplemented,
371    Internal,
372}
373
374impl fmt::Display for FormatErrorType {
375    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376        match self {
377            FormatErrorType::InvalidValue => write!(f, "InvalidValue"),
378            FormatErrorType::Unimplemented => write!(f, "Unimplemented"),
379            FormatErrorType::Internal => write!(f, "Internal"),
380        }
381    }
382}
383
384impl std::error::Error for PlanError {}
385
386impl fmt::Display for PlanError {
387    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388        write!(
389            f,
390            "{} Error writing {}: {}",
391            self.error_type, self.message, self.description
392        )
393    }
394}
395
396#[derive(Debug, Copy, Clone)]
397/// A token used to represent an error in the textified output.
398pub(crate) struct ErrorToken(
399    /// The kind of item this is in place of.
400    pub &'static str,
401);
402
403impl fmt::Display for ErrorToken {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        write!(f, "!{{{}}}", self.0)
406    }
407}
408
409#[derive(Debug, Copy, Clone)]
410pub(crate) struct MaybeToken<V: fmt::Display>(pub(crate) Result<V, ErrorToken>);
411
412impl<V: fmt::Display> fmt::Display for MaybeToken<V> {
413    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
414        match &self.0 {
415            Ok(t) => t.fmt(f),
416            Err(e) => e.fmt(f),
417        }
418    }
419}
420
421/// A trait for types that can be output in text format.
422///
423/// This trait is used to convert a Substrait type into a string representation
424/// that can be written to a text writer.
425pub(crate) trait Textify {
426    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result;
427
428    // TODO: Prost can give this to us if substrait was generated with `enable_type_names`
429    // <https://docs.rs/prost-build/latest/prost_build/struct.Config.html#method.enable_type_names>
430    fn name() -> &'static str;
431}
432
433/// Holds the context for outputting a plan.
434///
435/// This includes:
436/// - Options (`&`[`OutputOptions`]) for formatting
437/// - Errors (`&`[`ErrorAccumulator`]) for collecting errors
438/// - Extensions (`&`[`SimpleExtensions`]) for looking up function and type
439///   names
440/// - Indent (`&`[`IndentTracker`]) for tracking the current indent level
441///
442/// The `Scope` trait is used to provide the context for textifying a plan.
443pub(crate) trait Scope: Sized {
444    /// The type of errors that can occur when textifying a plan.
445    type Errors: ErrorAccumulator;
446    type Indent: IndentTracker;
447
448    /// Get the current indent level.
449    fn indent(&self) -> impl fmt::Display;
450    /// Push a new indent level.
451    fn push_indent(&self) -> Self;
452
453    /// Get the options for formatting the plan.
454    fn options(&self) -> &OutputOptions;
455    fn extensions(&self) -> &SimpleExtensions;
456
457    /// Get the extension registry
458    fn extension_registry(&self) -> &ExtensionRegistry;
459    fn errors(&self) -> &Self::Errors;
460
461    fn push_error(&self, e: FormatError) {
462        self.errors().push(e);
463    }
464
465    /// Handle a failure to textify a value. Textify errors are written as
466    /// "!{name}" to the output (where "name" is the type name), and
467    /// the error is pushed to the error accumulator.
468    fn failure<E: Into<FormatError>>(&self, e: E) -> ErrorToken {
469        let e = e.into();
470        let token = ErrorToken(e.message());
471        self.push_error(e);
472        token
473    }
474
475    fn expect<'a, T: Textify>(&'a self, t: Option<&'a T>) -> MaybeToken<impl fmt::Display> {
476        match t {
477            Some(t) => MaybeToken(Ok(self.display(t))),
478            None => {
479                let err = PlanError::invalid(
480                    T::name(),
481                    // TODO: Make this an optional input
482                    NONSPECIFIC,
483                    "Required field expected, None found",
484                );
485                let err_token = self.failure(err);
486                MaybeToken(Err(err_token))
487            }
488        }
489    }
490
491    fn display<'a, T: Textify>(&'a self, value: &'a T) -> Displayable<'a, Self, T> {
492        Displayable { scope: self, value }
493    }
494
495    /// Wrap an iterator over textifiable items into a displayable that will
496    /// separate them with the given separator.
497    ///
498    /// # Example
499    ///
500    /// ```ignore
501    /// let items = vec![1, 2, 3];
502    /// let separated = self.separated(&items, ", ");
503    /// ```
504    fn separated<'a, T: Textify, I: IntoIterator<Item = &'a T> + Clone>(
505        &'a self,
506        items: I,
507        separator: &'static str,
508    ) -> Separated<'a, Self, T, I> {
509        Separated {
510            scope: self,
511            items,
512            separator,
513        }
514    }
515
516    // TODO: this was last used on 2026-06-15, before a refactor removed it; if its not used again, perhaps we should drop it?
517    #[allow(dead_code)]
518    fn optional<'a, T: Textify>(
519        &'a self,
520        value: &'a T,
521        option: bool,
522    ) -> OptionalDisplayable<'a, Self, T> {
523        let value = if option { Some(value) } else { None };
524        OptionalDisplayable { scope: self, value }
525    }
526}
527
528#[derive(Clone)]
529pub(crate) struct Separated<'a, S: Scope, T: Textify + 'a, I: IntoIterator<Item = &'a T> + Clone> {
530    scope: &'a S,
531    items: I,
532    separator: &'static str,
533}
534
535impl<'a, S: Scope, T: Textify, I: IntoIterator<Item = &'a T> + Clone> fmt::Display
536    for Separated<'a, S, T, I>
537{
538    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
539        for (i, item) in self.items.clone().into_iter().enumerate() {
540            if i > 0 {
541                f.write_str(self.separator)?;
542            }
543            item.textify(self.scope, f)?;
544        }
545        Ok(())
546    }
547}
548
549impl<'a, S: Scope, T: Textify, I: IntoIterator<Item = &'a T> + Clone + fmt::Debug> fmt::Debug
550    for Separated<'a, S, T, I>
551{
552    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553        write!(
554            f,
555            "Separated{{items: {:?}, separator: {:?}}}",
556            self.items, self.separator
557        )
558    }
559}
560
561#[derive(Copy, Clone)]
562pub(crate) struct Displayable<'a, S: Scope, T: Textify> {
563    scope: &'a S,
564    value: &'a T,
565}
566
567impl<'a, S: Scope, T: Textify + fmt::Debug> fmt::Debug for Displayable<'a, S, T> {
568    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
569        write!(f, "Displayable({:?})", self.value)
570    }
571}
572
573impl<'a, S: Scope, T: Textify> fmt::Display for Displayable<'a, S, T> {
574    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
575        self.value.textify(self.scope, f)
576    }
577}
578
579#[derive(Copy, Clone)]
580#[allow(dead_code)]
581pub(crate) struct OptionalDisplayable<'a, S: Scope, T: Textify> {
582    scope: &'a S,
583    value: Option<&'a T>,
584}
585
586impl<'a, S: Scope, T: Textify> fmt::Display for OptionalDisplayable<'a, S, T> {
587    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588        match &self.value {
589            Some(t) => t.textify(self.scope, f),
590            None => Ok(()),
591        }
592    }
593}
594
595impl<'a, S: Scope, T: Textify + fmt::Debug> fmt::Debug for OptionalDisplayable<'a, S, T> {
596    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
597        write!(f, "OptionalDisplayable({:?})", self.value)
598    }
599}
600
601impl<'a, Err: ErrorAccumulator> Scope for ScopedContext<'a, Err> {
602    type Errors = Err;
603    type Indent = IndentStack<'a>;
604
605    fn indent(&self) -> impl fmt::Display {
606        self.indent
607    }
608
609    fn push_indent(&self) -> Self {
610        Self {
611            indent: self.indent.push(),
612            ..*self
613        }
614    }
615
616    fn options(&self) -> &OutputOptions {
617        self.options
618    }
619
620    fn errors(&self) -> &Self::Errors {
621        self.errors
622    }
623
624    fn extensions(&self) -> &SimpleExtensions {
625        self.extensions
626    }
627
628    fn extension_registry(&self) -> &ExtensionRegistry {
629        self.extension_registry
630    }
631}