1use itertools::Itertools;
34use proptest_derive::Arbitrary;
35use serde::{Deserialize, Serialize};
36use std::borrow::Cow;
37use std::collections::{BTreeMap, BTreeSet};
38use std::fmt;
39use std::fmt::{Display, Formatter};
40
41use mz_ore::stack::RecursionLimitError;
42use mz_ore::str::{Indent, bracketed, separated};
43
44use crate::explain::dot::{DisplayDot, dot_string};
45use crate::explain::json::{DisplayJson, json_string};
46use crate::explain::text::{DisplayText, text_string};
47use crate::optimize::OptimizerFeatureOverrides;
48use crate::{GlobalId, ReprColumnType, ReprScalarType, SqlColumnType, SqlScalarType};
49
50pub mod dot;
51pub mod json;
52pub mod text;
53#[cfg(feature = "tracing")]
54pub mod tracing;
55
56#[cfg(feature = "tracing")]
57pub use crate::explain::tracing::trace_plan;
58
59#[derive(Debug, Clone, Copy, Eq, PartialEq)]
61pub enum ExplainFormat {
62 Text,
63 Json,
64 Dot,
65}
66
67impl fmt::Display for ExplainFormat {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 ExplainFormat::Text => f.write_str("TEXT"),
71 ExplainFormat::Json => f.write_str("JSON"),
72 ExplainFormat::Dot => f.write_str("DOT"),
73 }
74 }
75}
76
77#[allow(missing_debug_implementations)]
81pub enum UnsupportedFormat {}
82
83#[derive(Debug)]
86pub enum ExplainError {
87 UnsupportedFormat(ExplainFormat),
88 FormatError(fmt::Error),
89 AnyhowError(anyhow::Error),
90 RecursionLimitError(RecursionLimitError),
91 SerdeJsonError(serde_json::Error),
92 LinearChainsPlusRecursive,
93 UnknownError(String),
94}
95
96impl fmt::Display for ExplainError {
97 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
98 write!(f, "error while rendering explain output: ")?;
99 match self {
100 ExplainError::UnsupportedFormat(format) => {
101 write!(f, "{} format is not supported", format)
102 }
103 ExplainError::FormatError(error) => {
104 write!(f, "{}", error)
105 }
106 ExplainError::AnyhowError(error) => {
107 write!(f, "{}", error)
108 }
109 ExplainError::RecursionLimitError(error) => {
110 write!(f, "{}", error)
111 }
112 ExplainError::SerdeJsonError(error) => {
113 write!(f, "{}", error)
114 }
115 ExplainError::LinearChainsPlusRecursive => {
116 write!(
117 f,
118 "The linear_chains option is not supported with WITH MUTUALLY RECURSIVE."
119 )
120 }
121 ExplainError::UnknownError(error) => {
122 write!(f, "{}", error)
123 }
124 }
125 }
126}
127
128impl From<fmt::Error> for ExplainError {
129 fn from(error: fmt::Error) -> Self {
130 ExplainError::FormatError(error)
131 }
132}
133
134impl From<anyhow::Error> for ExplainError {
135 fn from(error: anyhow::Error) -> Self {
136 ExplainError::AnyhowError(error)
137 }
138}
139
140impl From<RecursionLimitError> for ExplainError {
141 fn from(error: RecursionLimitError) -> Self {
142 ExplainError::RecursionLimitError(error)
143 }
144}
145
146impl From<serde_json::Error> for ExplainError {
147 fn from(error: serde_json::Error) -> Self {
148 ExplainError::SerdeJsonError(error)
149 }
150}
151
152#[derive(Clone, Debug)]
154pub struct ExplainConfig {
155 pub subtree_size: bool,
159 pub arity: bool,
161 pub types: bool,
163 pub keys: bool,
165 pub non_negative: bool,
167 pub cardinality: bool,
169 pub column_names: bool,
171 pub equivalences: bool,
173 pub join_impls: bool,
179 pub humanized_exprs: bool,
181 pub linear_chains: bool,
183 pub no_fast_path: bool,
186 pub no_notices: bool,
188 pub node_ids: bool,
190 pub raw_plans: bool,
192 pub raw_syntax: bool,
194 pub verbose_syntax: bool,
196 pub redacted: bool,
198 pub timing: bool,
200 pub filter_pushdown: bool,
202
203 pub features: OptimizerFeatureOverrides,
205}
206
207impl Default for ExplainConfig {
208 fn default() -> Self {
209 Self {
210 redacted: !mz_ore::assert::soft_assertions_enabled(),
212 arity: false,
213 cardinality: false,
214 column_names: false,
215 filter_pushdown: false,
216 humanized_exprs: false,
217 join_impls: true,
218 keys: false,
219 linear_chains: false,
220 no_fast_path: true,
221 no_notices: false,
222 node_ids: false,
223 non_negative: false,
224 raw_plans: true,
225 raw_syntax: false,
226 verbose_syntax: false,
227 subtree_size: false,
228 timing: false,
229 types: false,
230 equivalences: false,
231 features: Default::default(),
232 }
233 }
234}
235
236impl ExplainConfig {
237 pub fn requires_analyses(&self) -> bool {
238 self.subtree_size
239 || self.non_negative
240 || self.arity
241 || self.types
242 || self.keys
243 || self.cardinality
244 || self.column_names
245 || self.equivalences
246 }
247}
248
249#[derive(Clone, Debug)]
251pub enum Explainee {
252 MaterializedView(GlobalId),
254 Index(GlobalId),
256 Dataflow(GlobalId),
260 Select,
263}
264
265pub trait Explain<'a>: 'a {
271 type Context;
274
275 type Text: DisplayText;
278
279 type Json: DisplayJson;
282
283 type Dot: DisplayDot;
286
287 fn explain(
302 &'a mut self,
303 format: &'a ExplainFormat,
304 context: &'a Self::Context,
305 ) -> Result<String, ExplainError> {
306 match format {
307 ExplainFormat::Text => self.explain_text(context).map(|e| text_string(&e)),
308 ExplainFormat::Json => self.explain_json(context).map(|e| json_string(&e)),
309 ExplainFormat::Dot => self.explain_dot(context).map(|e| dot_string(&e)),
310 }
311 }
312
313 #[allow(unused_variables)]
325 fn explain_text(&'a mut self, context: &'a Self::Context) -> Result<Self::Text, ExplainError> {
326 Err(ExplainError::UnsupportedFormat(ExplainFormat::Text))
327 }
328
329 #[allow(unused_variables)]
341 fn explain_json(&'a mut self, context: &'a Self::Context) -> Result<Self::Json, ExplainError> {
342 Err(ExplainError::UnsupportedFormat(ExplainFormat::Json))
343 }
344
345 #[allow(unused_variables)]
357 fn explain_dot(&'a mut self, context: &'a Self::Context) -> Result<Self::Dot, ExplainError> {
358 Err(ExplainError::UnsupportedFormat(ExplainFormat::Dot))
359 }
360}
361
362#[derive(Debug)]
366pub struct RenderingContext<'a> {
367 pub indent: Indent,
368 pub humanizer: &'a dyn ExprHumanizer,
369}
370
371impl<'a> RenderingContext<'a> {
372 pub fn new(indent: Indent, humanizer: &'a dyn ExprHumanizer) -> RenderingContext<'a> {
373 RenderingContext { indent, humanizer }
374 }
375}
376
377impl<'a> AsMut<Indent> for RenderingContext<'a> {
378 fn as_mut(&mut self) -> &mut Indent {
379 &mut self.indent
380 }
381}
382
383impl<'a> AsRef<&'a dyn ExprHumanizer> for RenderingContext<'a> {
384 fn as_ref(&self) -> &&'a dyn ExprHumanizer {
385 &self.humanizer
386 }
387}
388
389#[allow(missing_debug_implementations)]
390pub struct PlanRenderingContext<'a, T> {
391 pub indent: Indent,
392 pub humanizer: &'a dyn ExprHumanizer,
393 pub annotations: BTreeMap<&'a T, Analyses>,
394 pub config: &'a ExplainConfig,
395 pub ambiguous_ids: BTreeSet<GlobalId>,
397}
398
399impl<'a, T> PlanRenderingContext<'a, T> {
400 pub fn new(
401 indent: Indent,
402 humanizer: &'a dyn ExprHumanizer,
403 annotations: BTreeMap<&'a T, Analyses>,
404 config: &'a ExplainConfig,
405 ambiguous_ids: BTreeSet<GlobalId>,
406 ) -> PlanRenderingContext<'a, T> {
407 PlanRenderingContext {
408 indent,
409 humanizer,
410 annotations,
411 config,
412 ambiguous_ids,
413 }
414 }
415
416 pub fn humanize_id_maybe_unqualified(&self, id: GlobalId) -> Option<String> {
418 if self.ambiguous_ids.contains(&id) {
419 self.humanizer.humanize_id(id)
420 } else {
421 self.humanizer.humanize_id_unqualified(id)
422 }
423 }
424}
425
426impl<'a, T> AsMut<Indent> for PlanRenderingContext<'a, T> {
427 fn as_mut(&mut self) -> &mut Indent {
428 &mut self.indent
429 }
430}
431
432impl<'a, T> AsRef<&'a dyn ExprHumanizer> for PlanRenderingContext<'a, T> {
433 fn as_ref(&self) -> &&'a dyn ExprHumanizer {
434 &self.humanizer
435 }
436}
437
438pub trait ExprHumanizer: fmt::Debug + Sync {
443 fn humanize_id(&self, id: GlobalId) -> Option<String>;
446
447 fn humanize_id_unqualified(&self, id: GlobalId) -> Option<String>;
449
450 fn humanize_id_parts(&self, id: GlobalId) -> Option<Vec<String>>;
453
454 fn humanize_sql_scalar_type(&self, ty: &SqlScalarType, postgres_compat: bool) -> String;
459
460 fn humanize_scalar_type(&self, typ: &ReprScalarType) -> String {
465 typ.to_string()
466 }
467
468 fn humanize_sql_column_type(&self, typ: &SqlColumnType, postgres_compat: bool) -> String {
473 format!(
474 "{}{}",
475 self.humanize_sql_scalar_type(&typ.scalar_type, postgres_compat),
476 if typ.nullable { "?" } else { "" }
477 )
478 }
479
480 fn humanize_column_type(&self, typ: &ReprColumnType) -> String {
485 typ.to_string()
486 }
487
488 fn column_names_for_id(&self, id: GlobalId) -> Option<Vec<String>>;
490
491 fn humanize_column(&self, id: GlobalId, column: usize) -> Option<String>;
493
494 fn id_exists(&self, id: GlobalId) -> bool;
496}
497
498#[derive(Debug)]
501pub struct ExprHumanizerExt<'a> {
502 items: BTreeMap<GlobalId, TransientItem>,
505 inner: &'a dyn ExprHumanizer,
508}
509
510impl<'a> ExprHumanizerExt<'a> {
511 pub fn new(items: BTreeMap<GlobalId, TransientItem>, inner: &'a dyn ExprHumanizer) -> Self {
512 Self { items, inner }
513 }
514}
515
516impl<'a> ExprHumanizer for ExprHumanizerExt<'a> {
517 fn humanize_id(&self, id: GlobalId) -> Option<String> {
518 match self.items.get(&id) {
519 Some(item) => item
520 .humanized_id_parts
521 .as_ref()
522 .map(|parts| parts.join(".")),
523 None => self.inner.humanize_id(id),
524 }
525 }
526
527 fn humanize_id_unqualified(&self, id: GlobalId) -> Option<String> {
528 match self.items.get(&id) {
529 Some(item) => item
530 .humanized_id_parts
531 .as_ref()
532 .and_then(|parts| parts.last().cloned()),
533 None => self.inner.humanize_id_unqualified(id),
534 }
535 }
536
537 fn humanize_id_parts(&self, id: GlobalId) -> Option<Vec<String>> {
538 match self.items.get(&id) {
539 Some(item) => item.humanized_id_parts.clone(),
540 None => self.inner.humanize_id_parts(id),
541 }
542 }
543
544 fn humanize_sql_scalar_type(&self, ty: &SqlScalarType, postgres_compat: bool) -> String {
545 self.inner.humanize_sql_scalar_type(ty, postgres_compat)
546 }
547
548 fn column_names_for_id(&self, id: GlobalId) -> Option<Vec<String>> {
549 match self.items.get(&id) {
550 Some(item) => item.column_names.clone(),
551 None => self.inner.column_names_for_id(id),
552 }
553 }
554
555 fn humanize_column(&self, id: GlobalId, column: usize) -> Option<String> {
556 match self.items.get(&id) {
557 Some(item) => match &item.column_names {
558 Some(column_names) => Some(column_names[column].clone()),
559 None => None,
560 },
561 None => self.inner.humanize_column(id, column),
562 }
563 }
564
565 fn id_exists(&self, id: GlobalId) -> bool {
566 self.items.contains_key(&id) || self.inner.id_exists(id)
567 }
568}
569
570#[derive(Debug)]
574pub struct TransientItem {
575 humanized_id_parts: Option<Vec<String>>,
576 column_names: Option<Vec<String>>,
577}
578
579impl TransientItem {
580 pub fn new(humanized_id_parts: Option<Vec<String>>, column_names: Option<Vec<String>>) -> Self {
581 Self {
582 humanized_id_parts,
583 column_names,
584 }
585 }
586}
587
588#[derive(Debug)]
594pub struct DummyHumanizer;
595
596impl ExprHumanizer for DummyHumanizer {
597 fn humanize_id(&self, _: GlobalId) -> Option<String> {
598 None
601 }
602
603 fn humanize_id_unqualified(&self, _id: GlobalId) -> Option<String> {
604 None
605 }
606
607 fn humanize_id_parts(&self, _id: GlobalId) -> Option<Vec<String>> {
608 None
609 }
610
611 fn humanize_sql_scalar_type(&self, ty: &SqlScalarType, _postgres_compat: bool) -> String {
612 format!("{:?}", ty)
614 }
615
616 fn column_names_for_id(&self, _id: GlobalId) -> Option<Vec<String>> {
617 None
618 }
619
620 fn humanize_column(&self, _id: GlobalId, _column: usize) -> Option<String> {
621 None
622 }
623
624 fn id_exists(&self, _id: GlobalId) -> bool {
625 false
626 }
627}
628
629#[derive(Debug)]
631pub struct Indices<'a>(pub &'a [usize]);
632
633#[derive(Debug)]
638pub struct CompactScalarSeq<'a, T: ScalarOps>(pub &'a [T]); #[derive(Debug)]
645pub struct CompactScalars<T, I>(pub I)
646where
647 T: ScalarOps,
648 I: Iterator<Item = T> + Clone;
649
650pub trait ScalarOps {
651 fn match_col_ref(&self) -> Option<usize>;
652
653 fn references(&self, col_ref: usize) -> bool;
654}
655
656#[allow(missing_debug_implementations)]
659pub struct AnnotatedPlan<'a, T> {
660 pub plan: &'a T,
661 pub annotations: BTreeMap<&'a T, Analyses>,
662}
663
664#[derive(Clone, Default, Debug)]
666pub struct Analyses {
667 pub non_negative: Option<bool>,
668 pub subtree_size: Option<usize>,
669 pub arity: Option<usize>,
670 pub types: Option<Option<Vec<ReprColumnType>>>,
671 pub keys: Option<Vec<Vec<usize>>>,
672 pub cardinality: Option<String>,
673 pub column_names: Option<Vec<String>>,
674 pub equivalences: Option<String>,
675}
676
677#[derive(Debug, Clone)]
678pub struct HumanizedAnalyses<'a> {
679 analyses: &'a Analyses,
680 humanizer: &'a dyn ExprHumanizer,
681 config: &'a ExplainConfig,
682}
683
684impl<'a> HumanizedAnalyses<'a> {
685 pub fn new<T>(analyses: &'a Analyses, ctx: &PlanRenderingContext<'a, T>) -> Self {
686 Self {
687 analyses,
688 humanizer: ctx.humanizer,
689 config: ctx.config,
690 }
691 }
692}
693
694impl<'a> Display for HumanizedAnalyses<'a> {
695 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
700 let mut builder = f.debug_struct("//");
701
702 if self.config.subtree_size {
703 let subtree_size = self.analyses.subtree_size.expect("subtree_size");
704 builder.field("subtree_size", &subtree_size);
705 }
706
707 if self.config.non_negative {
708 let non_negative = self.analyses.non_negative.expect("non_negative");
709 builder.field("non_negative", &non_negative);
710 }
711
712 if self.config.arity {
713 let arity = self.analyses.arity.expect("arity");
714 builder.field("arity", &arity);
715 }
716
717 if self.config.types {
718 let types = match self.analyses.types.as_ref().expect("types") {
719 Some(types) => {
720 let types = types
721 .into_iter()
722 .map(|c| self.humanizer.humanize_column_type(c))
723 .collect::<Vec<_>>();
724
725 bracketed("(", ")", separated(", ", types)).to_string()
726 }
727 None => "(<error>)".to_string(),
728 };
729 builder.field("types", &types);
730 }
731
732 if self.config.keys {
733 let keys = self
734 .analyses
735 .keys
736 .as_ref()
737 .expect("keys")
738 .into_iter()
739 .map(|key| bracketed("[", "]", separated(", ", key)).to_string());
740 let keys = bracketed("(", ")", separated(", ", keys)).to_string();
741 builder.field("keys", &keys);
742 }
743
744 if self.config.cardinality {
745 let cardinality = self.analyses.cardinality.as_ref().expect("cardinality");
746 builder.field("cardinality", cardinality);
747 }
748
749 if self.config.column_names {
750 let column_names = self.analyses.column_names.as_ref().expect("column_names");
751 let column_names = column_names.into_iter().enumerate().map(|(i, c)| {
752 if c.is_empty() {
753 Cow::Owned(format!("#{i}"))
754 } else {
755 Cow::Borrowed(c)
756 }
757 });
758 let column_names = bracketed("(", ")", separated(", ", column_names)).to_string();
759 builder.field("column_names", &column_names);
760 }
761
762 if self.config.equivalences {
763 let equivs = self.analyses.equivalences.as_ref().expect("equivalences");
764 builder.field("equivs", equivs);
765 }
766
767 builder.finish()
768 }
769}
770
771#[derive(Clone, Debug, Default)]
780pub struct UsedIndexes(BTreeSet<(GlobalId, Vec<IndexUsageType>)>);
781
782impl UsedIndexes {
783 pub fn new(values: BTreeSet<(GlobalId, Vec<IndexUsageType>)>) -> UsedIndexes {
784 UsedIndexes(values)
785 }
786
787 pub fn is_empty(&self) -> bool {
788 self.0.is_empty()
789 }
790
791 pub fn ambiguous_ids(&self, humanizer: &dyn ExprHumanizer) -> BTreeSet<GlobalId> {
793 let humanized = self
794 .0
795 .iter()
796 .flat_map(|(id, _)| humanizer.humanize_id_unqualified(*id).map(|hum| (hum, *id)));
797
798 let mut by_humanization = BTreeMap::<String, BTreeSet<GlobalId>>::new();
799 for (hum, id) in humanized {
800 by_humanization.entry(hum).or_default().insert(id);
801 }
802
803 by_humanization
804 .values()
805 .filter(|ids| ids.len() > 1)
806 .flatten()
807 .cloned()
808 .collect()
809 }
810}
811
812#[derive(
813 Debug,
814 Clone,
815 Arbitrary,
816 Serialize,
817 Deserialize,
818 Eq,
819 PartialEq,
820 Ord,
821 PartialOrd,
822 Hash
823)]
824pub enum IndexUsageType {
825 FullScan,
827 DifferentialJoin,
829 DeltaJoin(DeltaJoinIndexUsageType),
831 Lookup(GlobalId),
836 PlanRootNoArrangement,
842 SinkExport,
845 IndexExport,
848 FastPathLimit,
853 DanglingArrangeBy,
859 Unknown,
862}
863
864#[derive(
868 Debug,
869 Clone,
870 Arbitrary,
871 Serialize,
872 Deserialize,
873 Eq,
874 PartialEq,
875 Ord,
876 PartialOrd,
877 Hash
878)]
879pub enum DeltaJoinIndexUsageType {
880 Unknown,
881 Lookup,
882 FirstInputFullScan,
883}
884
885impl std::fmt::Display for IndexUsageType {
886 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
887 write!(
888 f,
889 "{}",
890 match self {
891 IndexUsageType::FullScan => "*** full scan ***",
892 IndexUsageType::Lookup(_idx_id) => "lookup",
893 IndexUsageType::DifferentialJoin => "differential join",
894 IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::FirstInputFullScan) =>
895 "delta join 1st input (full scan)",
896 IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Lookup) => "delta join lookup",
903 IndexUsageType::DeltaJoin(DeltaJoinIndexUsageType::Unknown) =>
904 "*** INTERNAL ERROR (unknown delta join usage) ***",
905 IndexUsageType::PlanRootNoArrangement => "plan root (no new arrangement)",
906 IndexUsageType::SinkExport => "sink export",
907 IndexUsageType::IndexExport => "index export",
908 IndexUsageType::FastPathLimit => "fast path limit",
909 IndexUsageType::DanglingArrangeBy => "*** INTERNAL ERROR (dangling ArrangeBy) ***",
910 IndexUsageType::Unknown => "*** INTERNAL ERROR (unknown usage) ***",
911 }
912 )
913 }
914}
915
916impl IndexUsageType {
917 pub fn display_vec<'a, I>(usage_types: I) -> impl Display + Sized + 'a
918 where
919 I: IntoIterator<Item = &'a IndexUsageType>,
920 {
921 separated(", ", usage_types.into_iter().sorted().dedup())
922 }
923}
924
925#[cfg(test)]
926mod tests {
927 use mz_ore::assert_ok;
928
929 use super::*;
930
931 struct Environment {
932 name: String,
933 }
934
935 impl Default for Environment {
936 fn default() -> Self {
937 Environment {
938 name: "test env".to_string(),
939 }
940 }
941 }
942
943 struct Frontiers<T> {
944 since: T,
945 upper: T,
946 }
947
948 impl<T> Frontiers<T> {
949 fn new(since: T, upper: T) -> Self {
950 Self { since, upper }
951 }
952 }
953
954 struct ExplainContext<'a> {
955 env: &'a mut Environment,
956 config: &'a ExplainConfig,
957 frontiers: Frontiers<u64>,
958 }
959
960 struct TestExpr {
962 lhs: i32,
963 rhs: i32,
964 }
965
966 struct TestExplanation<'a> {
967 expr: &'a TestExpr,
968 context: &'a ExplainContext<'a>,
969 }
970
971 impl<'a> DisplayText for TestExplanation<'a> {
972 fn fmt_text(&self, f: &mut fmt::Formatter<'_>, _ctx: &mut ()) -> fmt::Result {
973 let lhs = &self.expr.lhs;
974 let rhs = &self.expr.rhs;
975 writeln!(f, "expr = {lhs} + {rhs}")?;
976
977 if self.context.config.timing {
978 let since = &self.context.frontiers.since;
979 let upper = &self.context.frontiers.upper;
980 writeln!(f, "at t ∊ [{since}, {upper})")?;
981 }
982
983 let name = &self.context.env.name;
984 writeln!(f, "env = {name}")?;
985
986 Ok(())
987 }
988 }
989
990 impl<'a> Explain<'a> for TestExpr {
991 type Context = ExplainContext<'a>;
992 type Text = TestExplanation<'a>;
993 type Json = UnsupportedFormat;
994 type Dot = UnsupportedFormat;
995
996 fn explain_text(
997 &'a mut self,
998 context: &'a Self::Context,
999 ) -> Result<Self::Text, ExplainError> {
1000 Ok(TestExplanation {
1001 expr: self,
1002 context,
1003 })
1004 }
1005 }
1006
1007 fn do_explain(
1008 env: &mut Environment,
1009 frontiers: Frontiers<u64>,
1010 ) -> Result<String, ExplainError> {
1011 let mut expr = TestExpr { lhs: 1, rhs: 2 };
1012
1013 let format = ExplainFormat::Text;
1014 let config = &ExplainConfig {
1015 redacted: false,
1016 arity: false,
1017 cardinality: false,
1018 column_names: false,
1019 filter_pushdown: false,
1020 humanized_exprs: false,
1021 join_impls: false,
1022 keys: false,
1023 linear_chains: false,
1024 no_fast_path: false,
1025 no_notices: false,
1026 node_ids: false,
1027 non_negative: false,
1028 raw_plans: false,
1029 raw_syntax: false,
1030 verbose_syntax: true,
1031 subtree_size: false,
1032 equivalences: false,
1033 timing: true,
1034 types: false,
1035 features: Default::default(),
1036 };
1037 let context = ExplainContext {
1038 env,
1039 config,
1040 frontiers,
1041 };
1042
1043 expr.explain(&format, &context)
1044 }
1045
1046 #[mz_ore::test]
1047 fn test_mutable_context() {
1048 let mut env = Environment::default();
1049 let frontiers = Frontiers::<u64>::new(3, 7);
1050
1051 let act = do_explain(&mut env, frontiers);
1052 let exp = "expr = 1 + 2\nat t ∊ [3, 7)\nenv = test env\n".to_string();
1053
1054 assert_ok!(act);
1055 assert_eq!(act.unwrap(), exp);
1056 }
1057}