Skip to main content

substrait_explain/textify/
values.rs

1//! Shared value-rendering primitives ([`Value`], [`NamedArg`], [`Arguments`])
2//! used by both relation and expression textification.
3
4use std::borrow::Cow;
5use std::convert::TryFrom;
6use std::fmt;
7
8use prost::UnknownEnumValue;
9use substrait::proto::aggregate_function::AggregationInvocation;
10use substrait::proto::sort_field::{SortDirection, SortKind};
11use substrait::proto::{
12    AggregateFunction, AggregationPhase, Expression, SortField, Type, join_rel, set_rel,
13};
14
15use super::types::Name;
16use super::{PlanError, Scope, Textify};
17use crate::extensions::{ExtensionColumn, ExtensionValue};
18
19/// A trait for enum types that can be rendered as `&VariantName` in the text
20/// format.
21pub trait ValueEnum {
22    fn as_enum_str(&self) -> Result<Cow<'static, str>, PlanError>;
23}
24
25#[derive(Debug, Clone)]
26pub struct NamedArg<'a> {
27    pub name: Cow<'a, str>,
28    pub value: Value<'a>,
29}
30
31#[derive(Debug, Clone)]
32pub enum Value<'a> {
33    TableName(Vec<Name<'a>>),
34    Field(Option<Name<'a>>, Option<&'a Type>),
35    Tuple(Vec<Value<'a>>),
36    Reference(i32),
37    Expression(&'a Expression),
38    AggregateFunction(&'a AggregateFunction),
39    /// Represents a missing, invalid, or unspecified value.
40    Missing(PlanError),
41    /// Represents a valid enum value as a string for textification.
42    Enum(Cow<'a, str>),
43    EmptyGroup,
44    Integer(i64),
45    /// A decoded extension argument value.
46    ExtensionArgument(ExtensionValue),
47    /// A decoded extension output column.
48    ExtColumn(ExtensionColumn),
49}
50
51impl<'a> Value<'a> {
52    pub fn expect(maybe_value: Option<Self>, f: impl FnOnce() -> PlanError) -> Self {
53        match maybe_value {
54            Some(s) => s,
55            None => Value::Missing(f()),
56        }
57    }
58}
59
60impl<'a> From<Result<Vec<Name<'a>>, PlanError>> for Value<'a> {
61    fn from(token: Result<Vec<Name<'a>>, PlanError>) -> Self {
62        match token {
63            Ok(value) => Value::TableName(value),
64            Err(err) => Value::Missing(err),
65        }
66    }
67}
68
69impl<'a> Textify for Value<'a> {
70    fn name() -> &'static str {
71        "Value"
72    }
73
74    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
75        match self {
76            Value::TableName(names) => write!(w, "{}", ctx.separated(names, ".")),
77            Value::Field(name, typ) => {
78                write!(w, "{}:{}", ctx.expect(name.as_ref()), ctx.expect(*typ))
79            }
80            Value::Tuple(values) => write!(w, "({})", ctx.separated(values, ", ")),
81            // Field-reference syntax (`$N`); inlined rather than importing `expressions::Reference`.
82            Value::Reference(i) => write!(w, "${i}"),
83            Value::Expression(e) => write!(w, "{}", ctx.display(*e)),
84            Value::AggregateFunction(agg_fn) => agg_fn.textify(ctx, w),
85            Value::Missing(err) => write!(w, "{}", ctx.failure(err.clone())),
86            Value::Enum(res) => write!(w, "&{res}"),
87            Value::Integer(i) => write!(w, "{i}"),
88            Value::EmptyGroup => write!(w, "_"),
89            Value::ExtensionArgument(ev) => ev.textify(ctx, w),
90            Value::ExtColumn(ec) => ec.textify(ctx, w),
91        }
92    }
93}
94
95/// A comma-separated argument list: positional arguments first, then named
96/// arguments. Renders as `arg, arg, name=arg`, or `_` when empty.
97#[derive(Debug, Clone, Default)]
98pub struct Arguments<'a> {
99    /// Positional arguments (e.g., a filter condition, group-bys, etc.)
100    pub positional: Vec<Value<'a>>,
101    /// Named arguments (e.g., limit=10, offset=5)
102    pub named: Vec<NamedArg<'a>>,
103}
104
105impl<'a> Arguments<'a> {
106    pub fn new(positional: Vec<Value<'a>>, named: Vec<NamedArg<'a>>) -> Self {
107        Arguments { positional, named }
108    }
109}
110
111impl<'a> Textify for Arguments<'a> {
112    fn name() -> &'static str {
113        "Arguments"
114    }
115    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
116        if self.positional.is_empty() && self.named.is_empty() {
117            return write!(w, "_");
118        }
119
120        write!(w, "{}", ctx.separated(self.positional.iter(), ", "))?;
121        if !self.positional.is_empty() && !self.named.is_empty() {
122            write!(w, ", ")?;
123        }
124        write!(w, "{}", ctx.separated(self.named.iter(), ", "))
125    }
126}
127
128impl<'a> From<&'a SortField> for Value<'a> {
129    fn from(sf: &'a SortField) -> Self {
130        let field = match &sf.expr {
131            Some(expr) => Value::Expression(expr),
132            None => Value::Missing(PlanError::unimplemented(
133                "SortField",
134                Some("expr"),
135                "Missing expr",
136            )),
137        };
138        let direction = match &sf.sort_kind {
139            Some(kind) => Value::from(kind),
140            None => Value::Missing(PlanError::invalid(
141                "SortKind",
142                Some(Cow::Borrowed("sort_kind")),
143                "Missing sort_kind",
144            )),
145        };
146        Value::Tuple(vec![field, direction])
147    }
148}
149
150/// Converts an [`ValueEnum::as_enum_str`] result into a [`Value`]. Shared by
151/// the blanket `From<&T>` impl below and by callers that only have an owned
152/// enum value (and so can't borrow it for the lifetime `Value<'a>` requires).
153pub(crate) fn enum_str_value<'a>(result: Result<Cow<'static, str>, PlanError>) -> Value<'a> {
154    match result {
155        Ok(s) => Value::Enum(s),
156        Err(e) => Value::Missing(e),
157    }
158}
159
160/// Decode a raw protobuf enum field (`i32`) into its shared [`Value`]
161/// rendering: convert to the enum type and then to its `&Variant` string, or
162/// produce a field-specific diagnostic when the raw value matches no variant.
163///
164/// Keeps the decode-or-diagnose shape in one place for callers that render an
165/// enum field straight from its `i32` (currently `SetRel`'s `op`). `message` is
166/// the proto message tag used for the failure token, `field` the offending
167/// field name; both are named in the diagnostic so it identifies which field
168/// carried the unknown value.
169pub(crate) fn decode_enum_field<'a, T>(
170    raw: i32,
171    message: &'static str,
172    field: &'static str,
173) -> Value<'a>
174where
175    T: TryFrom<i32> + ValueEnum,
176{
177    match T::try_from(raw) {
178        Ok(v) => enum_str_value(v.as_enum_str()),
179        Err(_) => Value::Missing(PlanError::invalid(
180            message,
181            Some(field),
182            format!("Unknown {message}.{field}: {raw}"),
183        )),
184    }
185}
186
187impl<'a, T: ValueEnum + ?Sized> From<&'a T> for Value<'a> {
188    fn from(enum_val: &'a T) -> Self {
189        enum_str_value(enum_val.as_enum_str())
190    }
191}
192
193impl ValueEnum for SortKind {
194    fn as_enum_str(&self) -> Result<Cow<'static, str>, PlanError> {
195        let d = match self {
196            &SortKind::Direction(d) => SortDirection::try_from(d),
197            SortKind::ComparisonFunctionReference(f) => {
198                return Err(PlanError::invalid(
199                    "SortKind",
200                    Some(Cow::Owned(format!("function reference{f}"))),
201                    "SortKind::ComparisonFunctionReference unimplemented",
202                ));
203            }
204        };
205        let s = match d {
206            Err(UnknownEnumValue(d)) => {
207                return Err(PlanError::invalid(
208                    "SortKind",
209                    Some(Cow::Owned(format!("unknown variant: {d:?}"))),
210                    "Unknown SortDirection",
211                ));
212            }
213            Ok(SortDirection::AscNullsFirst) => "AscNullsFirst",
214            Ok(SortDirection::AscNullsLast) => "AscNullsLast",
215            Ok(SortDirection::DescNullsFirst) => "DescNullsFirst",
216            Ok(SortDirection::DescNullsLast) => "DescNullsLast",
217            Ok(SortDirection::Clustered) => "Clustered",
218            Ok(SortDirection::Unspecified) => {
219                return Err(PlanError::invalid(
220                    "SortKind",
221                    Option::<Cow<str>>::None,
222                    "Unspecified SortDirection",
223                ));
224            }
225        };
226        Ok(Cow::Borrowed(s))
227    }
228}
229
230impl ValueEnum for join_rel::JoinType {
231    fn as_enum_str(&self) -> Result<Cow<'static, str>, PlanError> {
232        let s = match self {
233            join_rel::JoinType::Unspecified => {
234                return Err(PlanError::invalid(
235                    "JoinType",
236                    Option::<Cow<str>>::None,
237                    "Unspecified JoinType",
238                ));
239            }
240            join_rel::JoinType::Inner => "Inner",
241            join_rel::JoinType::Outer => "Outer",
242            join_rel::JoinType::Left => "Left",
243            join_rel::JoinType::Right => "Right",
244            join_rel::JoinType::LeftSemi => "LeftSemi",
245            join_rel::JoinType::RightSemi => "RightSemi",
246            join_rel::JoinType::LeftAnti => "LeftAnti",
247            join_rel::JoinType::RightAnti => "RightAnti",
248            join_rel::JoinType::LeftSingle => "LeftSingle",
249            join_rel::JoinType::RightSingle => "RightSingle",
250            join_rel::JoinType::LeftMark => "LeftMark",
251            join_rel::JoinType::RightMark => "RightMark",
252        };
253        Ok(Cow::Borrowed(s))
254    }
255}
256
257impl ValueEnum for set_rel::SetOp {
258    fn as_enum_str(&self) -> Result<Cow<'static, str>, PlanError> {
259        let s = match self {
260            set_rel::SetOp::Unspecified => {
261                return Err(PlanError::invalid(
262                    "SetOp",
263                    Option::<Cow<str>>::None,
264                    "Unspecified SetOp",
265                ));
266            }
267            set_rel::SetOp::MinusPrimary => "MinusPrimary",
268            set_rel::SetOp::MinusPrimaryAll => "MinusPrimaryAll",
269            set_rel::SetOp::MinusMultiset => "MinusMultiset",
270            set_rel::SetOp::IntersectionPrimary => "IntersectionPrimary",
271            set_rel::SetOp::IntersectionMultiset => "IntersectionMultiset",
272            set_rel::SetOp::IntersectionMultisetAll => "IntersectionMultisetAll",
273            set_rel::SetOp::UnionDistinct => "UnionDistinct",
274            set_rel::SetOp::UnionAll => "UnionAll",
275        };
276        Ok(Cow::Borrowed(s))
277    }
278}
279
280impl ValueEnum for AggregationPhase {
281    fn as_enum_str(&self) -> Result<Cow<'static, str>, PlanError> {
282        let s = match self {
283            AggregationPhase::Unspecified => "Unspecified",
284            AggregationPhase::InitialToIntermediate => "InitialToIntermediate",
285            AggregationPhase::IntermediateToIntermediate => "IntermediateToIntermediate",
286            AggregationPhase::InitialToResult => "InitialToResult",
287            AggregationPhase::IntermediateToResult => "IntermediateToResult",
288        };
289        Ok(Cow::Borrowed(s))
290    }
291}
292
293impl ValueEnum for AggregationInvocation {
294    fn as_enum_str(&self) -> Result<Cow<'static, str>, PlanError> {
295        let s = match self {
296            AggregationInvocation::Unspecified => "Unspecified",
297            AggregationInvocation::All => "All",
298            AggregationInvocation::Distinct => "Distinct",
299        };
300        Ok(Cow::Borrowed(s))
301    }
302}
303
304impl<'a> Textify for NamedArg<'a> {
305    fn name() -> &'static str {
306        "NamedArg"
307    }
308    fn textify<S: Scope, W: fmt::Write>(&self, ctx: &S, w: &mut W) -> fmt::Result {
309        write!(w, "{}=", self.name)?;
310        self.value.textify(ctx, w)
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use crate::fixtures::TestContext;
318
319    #[test]
320    fn test_arguments_textify_positional_only() {
321        let ctx = TestContext::new();
322        let args = Arguments::new(vec![Value::Integer(42), Value::Integer(7)], vec![]);
323        let (result, errors) = ctx.textify(&args);
324        assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
325        assert_eq!(result, "42, 7");
326    }
327
328    #[test]
329    fn test_arguments_textify_named_only() {
330        let ctx = TestContext::new();
331        let args = Arguments::new(
332            vec![],
333            vec![
334                NamedArg {
335                    name: Cow::Borrowed("limit"),
336                    value: Value::Integer(10),
337                },
338                NamedArg {
339                    name: Cow::Borrowed("offset"),
340                    value: Value::Integer(5),
341                },
342            ],
343        );
344        let (result, errors) = ctx.textify(&args);
345        assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
346        assert_eq!(result, "limit=10, offset=5");
347    }
348
349    #[test]
350    fn test_arguments_textify_both() {
351        let ctx = TestContext::new();
352        let args = Arguments::new(
353            vec![Value::Integer(1)],
354            vec![NamedArg {
355                name: "foo".into(),
356                value: Value::Integer(2),
357            }],
358        );
359        let (result, errors) = ctx.textify(&args);
360        assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
361        assert_eq!(result, "1, foo=2");
362    }
363
364    #[test]
365    fn test_arguments_textify_empty() {
366        let ctx = TestContext::new();
367        let args = Arguments::new(vec![], vec![]);
368        let (result, errors) = ctx.textify(&args);
369        assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
370        assert_eq!(result, "_");
371    }
372
373    #[test]
374    fn test_named_arg_textify_error_token() {
375        let ctx = TestContext::new();
376        let named_arg = NamedArg {
377            name: "foo".into(),
378            value: Value::Missing(PlanError::invalid(
379                "my_enum",
380                Some(Cow::Borrowed("my_enum")),
381                Cow::Borrowed("my_enum"),
382            )),
383        };
384        let (result, errors) = ctx.textify(&named_arg);
385        // Should show !{my_enum} in the output
386        assert!(result.contains("foo=!{my_enum}"), "Output: {result}");
387        // Should also accumulate an error
388        assert!(!errors.is_empty(), "Expected error for error token");
389    }
390
391    #[test]
392    fn test_decode_enum_field_known_variant() {
393        let value =
394            decode_enum_field::<set_rel::SetOp>(set_rel::SetOp::UnionAll as i32, "SetRel", "op");
395        match value {
396            Value::Enum(s) => assert_eq!(s, "UnionAll"),
397            other => panic!("Expected Value::Enum, got {other:?}"),
398        }
399    }
400
401    #[test]
402    fn test_decode_enum_field_unknown_variant_names_field() {
403        let value = decode_enum_field::<set_rel::SetOp>(99, "SetRel", "op");
404        match value {
405            Value::Missing(err) => {
406                assert_eq!(err.message, "SetRel");
407                assert_eq!(err.lookup.as_deref(), Some("op"));
408                // The description names the offending field, not just the
409                // message, so the diagnostic is actionable on its own.
410                assert_eq!(err.description, "Unknown SetRel.op: 99");
411            }
412            other => panic!("Expected Value::Missing, got {other:?}"),
413        }
414    }
415}