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