Skip to main content

substrait_explain/textify/
foundation.rs

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