substrait_explain/textify/
plan.rs1use std::fmt;
2
3use substrait::proto;
4
5use super::Textify;
6use crate::extensions::{ExtensionRegistry, SimpleExtensions};
7use crate::parser::{PLAN_HEADER, VERSION_HEADER};
8use crate::textify::foundation::ErrorAccumulator;
9use crate::textify::{OutputOptions, ScopedContext};
10
11#[derive(Debug, Clone)]
12pub(crate) struct PlanWriter<'a, E: ErrorAccumulator + Default> {
13 options: &'a OutputOptions,
14 version: Option<&'a proto::Version>,
15 extensions: SimpleExtensions,
16 relations: &'a [proto::PlanRel],
17 errors: E,
18 extension_registry: &'a ExtensionRegistry,
19}
20
21impl<'a, E: ErrorAccumulator + Default + Clone> PlanWriter<'a, E> {
22 pub(crate) fn new(
23 options: &'a OutputOptions,
24 plan: &'a proto::Plan,
25 extension_registry: &'a ExtensionRegistry,
26 ) -> (Self, E) {
27 let (extensions, errs) =
28 SimpleExtensions::from_extensions(&plan.extension_urns, &plan.extensions);
29
30 let errors = E::default();
31 for err in errs {
32 errors.push(err.into());
33 }
34
35 let relations = plan.relations.as_slice();
36
37 (
38 Self {
39 options,
40 version: plan.version.as_ref(),
41 extensions,
42 relations,
43 errors: errors.clone(),
44 extension_registry,
45 },
46 errors,
47 )
48 }
49
50 pub(crate) fn scope(&'a self) -> ScopedContext<'a, E> {
51 ScopedContext::new(
52 self.options,
53 &self.errors,
54 &self.extensions,
55 self.extension_registry,
56 )
57 }
58
59 pub(crate) fn write_version(&self, w: &mut impl fmt::Write) -> fmt::Result {
62 let Some(version) = self.version else {
63 return Ok(());
64 };
65 if version == &proto::Version::default() {
66 return Ok(());
67 }
68
69 writeln!(
70 w,
71 "{VERSION_HEADER} {}.{}.{}",
72 version.major_number, version.minor_number, version.patch_number
73 )?;
74 if !version.producer.is_empty() {
75 writeln!(
76 w,
77 "{}producer: {}",
78 self.options.indent,
79 version.producer.trim()
80 )?;
81 }
82 if !version.git_hash.is_empty() {
83 writeln!(
84 w,
85 "{}git_hash: {}",
86 self.options.indent,
87 version.git_hash.trim()
88 )?;
89 }
90 Ok(())
91 }
92
93 pub(crate) fn write_extensions(&self, w: &mut impl fmt::Write) -> fmt::Result {
94 self.extensions.write(w, &self.options.indent)
95 }
96
97 pub(crate) fn write_relations(&self, w: &mut impl fmt::Write) -> fmt::Result {
98 writeln!(w, "{PLAN_HEADER}")?;
100 let scope = self.scope();
101 for (i, relation) in self.relations.iter().enumerate() {
102 if i > 0 {
103 writeln!(w)?;
104 writeln!(w)?;
105 }
106 relation.textify(&scope, w)?;
107 }
108 Ok(())
109 }
110}
111
112impl<'a, E: ErrorAccumulator + Default> fmt::Display for PlanWriter<'a, E> {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 self.write_version(f)?;
115 self.write_extensions(f)?;
116 if !self.extensions.is_empty() {
117 writeln!(f)?;
118 }
119 self.write_relations(f)?;
120 writeln!(f)?;
121 Ok(())
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use std::fmt::Write;
128
129 use pext::simple_extension_declaration::{ExtensionFunction, MappingType};
130 use substrait::proto::expression::{RexType, ScalarFunction};
131 use substrait::proto::function_argument::ArgType;
132 use substrait::proto::read_rel::{NamedTable, ReadType};
133 use substrait::proto::r#type::{I64, Kind, Nullability, Struct};
134 use substrait::proto::{
135 Expression, FunctionArgument, NamedStruct, ReadRel, Type, extensions as pext,
136 };
137
138 use super::*;
139 use crate::parser::expressions::FieldIndex;
140 use crate::textify::ErrorQueue;
141
142 #[test]
147 fn test_plan_writer() {
148 let mut plan = proto::Plan::default();
149
150 plan.extension_urns.push(pext::SimpleExtensionUrn {
152 extension_urn_anchor: 1,
153 urn: "https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml".to_string(),
154 });
155
156 plan.extensions.push(pext::SimpleExtensionDeclaration {
158 mapping_type: Some(MappingType::ExtensionFunction(ExtensionFunction {
159 extension_urn_reference: 1,
160 function_anchor: 10,
161 name: "add".to_string(),
162 })),
163 });
164
165 let read_rel = ReadRel {
167 read_type: Some(ReadType::NamedTable(NamedTable {
168 names: vec!["table1".to_string()],
169 ..Default::default()
170 })),
171 base_schema: Some(NamedStruct {
172 names: vec!["col1".to_string(), "col2".to_string()],
173 r#struct: Some(Struct {
174 types: vec![
175 Type {
176 kind: Some(Kind::I32(proto::r#type::I32 {
177 nullability: Nullability::Nullable as i32,
178 type_variation_reference: 0,
179 })),
180 },
181 Type {
182 kind: Some(Kind::I32(proto::r#type::I32 {
183 nullability: Nullability::Nullable as i32,
184 type_variation_reference: 0,
185 })),
186 },
187 ],
188 ..Default::default()
189 }),
190 }),
191 ..Default::default()
192 };
193
194 let add_function = ScalarFunction {
196 function_reference: 10,
197 arguments: vec![
198 FunctionArgument {
199 arg_type: Some(ArgType::Value(Expression {
200 rex_type: Some(RexType::Selection(Box::new(
201 FieldIndex(0).to_field_reference(),
202 ))),
203 })),
204 },
205 FunctionArgument {
206 arg_type: Some(ArgType::Value(Expression {
207 rex_type: Some(RexType::Selection(Box::new(
208 FieldIndex(1).to_field_reference(),
209 ))),
210 })),
211 },
212 ],
213 options: vec![],
214 output_type: Some(Type {
215 kind: Some(Kind::I64(I64 {
216 nullability: Nullability::Required as i32,
217 type_variation_reference: 0,
218 })),
219 }),
220 #[allow(deprecated)]
221 args: vec![],
222 };
223
224 let project_rel = proto::ProjectRel {
225 expressions: vec![Expression {
226 rex_type: Some(RexType::ScalarFunction(add_function)),
227 }],
228 input: Some(Box::new(proto::Rel {
229 rel_type: Some(proto::rel::RelType::Read(Box::new(read_rel))),
230 })),
231 common: None,
232 advanced_extension: None,
233 };
234
235 plan.relations.push(proto::PlanRel {
237 rel_type: Some(proto::plan_rel::RelType::Rel(proto::Rel {
238 rel_type: Some(proto::rel::RelType::Project(Box::new(project_rel))),
239 })),
240 });
241
242 let options = OutputOptions::default();
243 let extension_registry = ExtensionRegistry::new();
244 let (writer, errors) = PlanWriter::<ErrorQueue>::new(&options, &plan, &extension_registry);
245 let mut output = String::new();
246 write!(output, "{writer}").unwrap();
247
248 let errors: Vec<_> = errors.into();
250 assert!(errors.is_empty(), "Expected no errors, got: {errors:?}");
251
252 let expected = r#"
253=== Extensions
254URNs:
255 @ 1: https://github.com/substrait-io/substrait/blob/main/extensions/functions_arithmetic.yaml
256Functions:
257 # 10 @ 1: add
258
259=== Plan
260Project[$0, $1, add($0, $1):i64]
261 Read[table1 => col1:i32?, col2:i32?]
262"#
263 .trim_start();
264
265 assert_eq!(
266 output, expected,
267 "Output:\n---\n{output}\n---\nExpected:\n---\n{expected}\n---"
268 );
269 }
270}