Skip to main content

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 ([`substrait::proto::ExtensionLeafRel`],
6//! [`substrait::proto::ExtensionSingleRel`], [`substrait::proto::ExtensionMultiRel`]).
7
8use std::fmt;
9
10use crate::FormatError;
11use crate::extensions::{Expr, ExtensionArgs, ExtensionColumn, ExtensionValue, TupleValue};
12use crate::textify::foundation::{Scope, Textify};
13use crate::textify::types::{Name, escaped};
14
15impl Textify for TupleValue {
16    fn name() -> &'static str {
17        "TupleValue"
18    }
19
20    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
21        write!(w, "(")?;
22        if self.len() == 1 {
23            self.iter().next().unwrap().textify(ctx, w)?;
24            write!(w, ",")?;
25        } else {
26            write!(w, "{}", ctx.separated(self, ", "))?;
27        }
28        write!(w, ")")
29    }
30}
31
32impl Textify for ExtensionValue {
33    fn name() -> &'static str {
34        "ExtensionValue"
35    }
36
37    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
38        match self {
39            ExtensionValue::String(s) => write!(w, "'{}'", escaped(s)),
40            ExtensionValue::Integer(i) => write!(w, "{i}"),
41            ExtensionValue::Float(f) => write!(w, "{f}"),
42            ExtensionValue::Boolean(b) => write!(w, "{b}"),
43            ExtensionValue::Null => write!(w, "null"),
44            ExtensionValue::Error(error) => {
45                write!(w, "{}", ctx.failure(FormatError::Extension(error.clone())))
46            }
47            ExtensionValue::Expr(expr) => expr.textify(ctx, w),
48            ExtensionValue::Enum(e) => write!(w, "&{e}"),
49            ExtensionValue::Tuple(tv) => tv.textify(ctx, w),
50        }
51    }
52}
53
54impl Textify for Expr {
55    fn name() -> &'static str {
56        "Expr"
57    }
58
59    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
60        write!(w, "{}", ctx.display(self.as_proto()))
61    }
62}
63
64impl Textify for ExtensionColumn {
65    fn name() -> &'static str {
66        "ExtensionColumn"
67    }
68
69    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
70        match self {
71            ExtensionColumn::Named { name, r#type: ty } => {
72                write!(w, "{}:{}", Name(name), ctx.display(ty))
73            }
74            ExtensionColumn::Expr(expr) => expr.textify(ctx, w),
75        }
76    }
77}
78
79impl Textify for ExtensionArgs {
80    fn name() -> &'static str {
81        "ExtensionArgs"
82    }
83
84    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
85        let mut has_args = false;
86
87        // Add positional arguments
88        for (i, value) in self.positional.iter().enumerate() {
89            if i > 0 || has_args {
90                write!(w, ", ")?;
91            }
92            value.textify(ctx, w)?;
93            has_args = true;
94        }
95
96        // Add named arguments in display order (IndexMap preserves insertion order)
97        for (name, value) in &self.named {
98            if has_args {
99                write!(w, ", ")?;
100            }
101            write!(w, "{name}=")?;
102            value.textify(ctx, w)?;
103            has_args = true;
104        }
105
106        if !has_args {
107            write!(w, "_")?;
108        }
109
110        // Add output columns if present
111        if !self.output_columns.is_empty() {
112            write!(w, " => {}", ctx.separated(self.output_columns.iter(), ", "))?;
113        }
114
115        Ok(())
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::extensions::ExtensionError;
123    use crate::fixtures::TestContext;
124
125    #[test]
126    fn error_value_renders_failure_and_continues() {
127        let mut args = ExtensionArgs::default();
128        args.insert("bad", ExtensionError::Custom("malformed field".to_string()));
129        args.insert("good", "continues");
130
131        let (rendered, errors) = TestContext::new().textify(&args);
132
133        assert_eq!(rendered, "bad=!{extension}, good='continues'");
134        assert!(matches!(
135            errors.0.as_slice(),
136            [FormatError::Extension(ExtensionError::Custom(message))]
137                if message == "malformed field"
138        ));
139    }
140}