mz_repr/explain/
text.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Structs and traits for `EXPLAIN AS TEXT`.
11
12use std::fmt;
13
14use mz_ore::str::Indent;
15
16use crate::explain::{
17    CompactScalarSeq, CompactScalars, ExprHumanizer, IndexUsageType, Indices, ScalarOps,
18    UnsupportedFormat, UsedIndexes,
19};
20
21/// A trait implemented by explanation types that can be rendered as
22/// [`super::ExplainFormat::Text`].
23pub trait DisplayText<C = ()>
24where
25    Self: Sized,
26{
27    fn fmt_text(&self, f: &mut fmt::Formatter<'_>, ctx: &mut C) -> fmt::Result;
28}
29
30impl<T> DisplayText for &T
31where
32    T: DisplayText,
33{
34    fn fmt_text(&self, f: &mut fmt::Formatter<'_>, ctx: &mut ()) -> fmt::Result {
35        (*self).fmt_text(f, ctx)
36    }
37}
38
39impl<A, C> DisplayText<C> for Box<A>
40where
41    A: DisplayText<C>,
42{
43    fn fmt_text(&self, f: &mut fmt::Formatter<'_>, ctx: &mut C) -> fmt::Result {
44        self.as_ref().fmt_text(f, ctx)
45    }
46}
47
48impl<A, C> DisplayText<C> for Option<A>
49where
50    A: DisplayText<C>,
51{
52    fn fmt_text(&self, f: &mut fmt::Formatter<'_>, ctx: &mut C) -> fmt::Result {
53        if let Some(val) = self {
54            val.fmt_text(f, ctx)
55        } else {
56            fmt::Result::Ok(())
57        }
58    }
59}
60
61impl DisplayText for UnsupportedFormat {
62    fn fmt_text(&self, _f: &mut fmt::Formatter<'_>, _ctx: &mut ()) -> fmt::Result {
63        unreachable!()
64    }
65}
66
67impl<'a, C> DisplayText<C> for UsedIndexes
68where
69    C: AsMut<Indent> + AsRef<&'a dyn ExprHumanizer>,
70{
71    fn fmt_text(&self, f: &mut fmt::Formatter<'_>, ctx: &mut C) -> fmt::Result {
72        writeln!(f, "{}Used Indexes:", ctx.as_mut())?;
73        *ctx.as_mut() += 1;
74        for (id, usage_types) in &self.0 {
75            let usage_types = IndexUsageType::display_vec(usage_types);
76            if let Some(name) = ctx.as_ref().humanize_id(*id) {
77                writeln!(f, "{}- {} ({})", ctx.as_mut(), name, usage_types)?;
78            } else {
79                writeln!(f, "{}- [DELETED INDEX] ({})", ctx.as_mut(), usage_types)?;
80            }
81        }
82        *ctx.as_mut() -= 1;
83        Ok(())
84    }
85}
86
87impl<'a> fmt::Display for Indices<'a> {
88    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
89        let mut is_first = true;
90        let mut slice = self.0;
91        while !slice.is_empty() {
92            if !is_first {
93                write!(f, ", ")?;
94            }
95            is_first = false;
96            let lead = &slice[0];
97            if slice.len() > 2 && slice[1] == lead + 1 && slice[2] == lead + 2 {
98                let mut last = 3;
99                while slice.get(last) == Some(&(lead + last)) {
100                    last += 1;
101                }
102                write!(f, "#{}..=#{}", lead, lead + last - 1)?;
103                slice = &slice[last..];
104            } else {
105                write!(f, "#{}", slice[0])?;
106                slice = &slice[1..];
107            }
108        }
109        Ok(())
110    }
111}
112
113impl<'a, T> std::fmt::Display for CompactScalarSeq<'a, T>
114where
115    T: ScalarOps + fmt::Display,
116{
117    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
118        let mut is_first = true;
119        let mut slice = self.0;
120        while !slice.is_empty() {
121            if !is_first {
122                write!(f, ", ")?;
123            }
124            is_first = false;
125            if let Some(lead) = slice[0].match_col_ref() {
126                if slice.len() > 2 && slice[1].references(lead + 1) && slice[2].references(lead + 2)
127                {
128                    let mut last = 3;
129                    while slice
130                        .get(last)
131                        .map(|expr| expr.references(lead + last))
132                        .unwrap_or(false)
133                    {
134                        last += 1;
135                    }
136                    slice[0].fmt(f)?;
137                    write!(f, "..=")?;
138                    slice[last - 1].fmt(f)?;
139                    slice = &slice[last..];
140                } else {
141                    slice[0].fmt(f)?;
142                    slice = &slice[1..];
143                }
144            } else {
145                slice[0].fmt(f)?;
146                slice = &slice[1..];
147            }
148        }
149        Ok(())
150    }
151}
152
153impl<T, I> fmt::Display for CompactScalars<T, I>
154where
155    T: ScalarOps + fmt::Display,
156    I: Iterator<Item = T> + Clone,
157{
158    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
159        enum State<T> {
160            Start,
161            FoundOne(T, usize),    // (x, x_col)
162            FoundTwo(T, T, usize), // (x, y, y_col)
163            FoundRun(T, T, usize), // (x, y, y_col)
164        }
165
166        let mut state = State::Start;
167
168        let mut is_first = true;
169        let mut prefix = || {
170            if std::mem::replace(&mut is_first, false) {
171                ""
172            } else {
173                ", "
174            }
175        };
176
177        for n in self.0.clone() {
178            state = match state {
179                State::Start => match n.match_col_ref() {
180                    Some(n_col) => {
181                        State::FoundOne(n, n_col) // Next state
182                    }
183                    None => {
184                        write!(f, "{}{n}", prefix())?;
185                        State::Start // New match
186                    }
187                },
188                State::FoundOne(x, x_col) => match n.match_col_ref() {
189                    Some(n_col) => {
190                        if x_col + 1 == n_col {
191                            State::FoundTwo(x, n, n_col) // Next state
192                        } else {
193                            write!(f, "{}{x}", prefix())?;
194                            State::FoundOne(n, n_col) // Reset match
195                        }
196                    }
197                    None => {
198                        write!(f, "{}{x}, {n}", prefix())?;
199                        State::Start // New match
200                    }
201                },
202                State::FoundTwo(x, y, y_col) => match n.match_col_ref() {
203                    Some(n_col) => {
204                        if y_col + 1 == n_col {
205                            State::FoundRun(x, n, n_col) // Next state
206                        } else {
207                            write!(f, "{}{x}, {y}", prefix())?;
208                            State::FoundOne(n, n_col) // Reset match
209                        }
210                    }
211                    None => {
212                        write!(f, "{}{x}, {y}, {n}", prefix())?;
213                        State::Start // New match
214                    }
215                },
216                State::FoundRun(x, y, y_col) => match n.match_col_ref() {
217                    Some(n_col) => {
218                        if y_col + 1 == n_col {
219                            State::FoundRun(x, n, n_col) // Extend run
220                        } else {
221                            write!(f, "{}{x}..={y}", prefix())?;
222                            State::FoundOne(n, n_col) // Reset match
223                        }
224                    }
225                    None => {
226                        write!(f, "{}{x}..={y}, {n}", prefix())?;
227                        State::Start // Reset state
228                    }
229                },
230            };
231        }
232
233        match state {
234            State::Start => {
235                // Do nothing
236            }
237            State::FoundOne(x, _) => {
238                write!(f, "{}{x}", prefix())?;
239            }
240            State::FoundTwo(x, y, _) => {
241                write!(f, "{}{x}, {y}", prefix())?;
242            }
243            State::FoundRun(x, y, _) => {
244                write!(f, "{}{x}..={y}", prefix())?;
245            }
246        }
247
248        Ok(())
249    }
250}
251
252/// Render a type `t: T` as [`super::ExplainFormat::Text`].
253///
254/// # Panics
255///
256/// Panics if the [`DisplayText::fmt_text`] call returns a [`fmt::Error`].
257pub fn text_string<T: DisplayText>(t: &T) -> String {
258    struct TextString<'a, T>(&'a T);
259
260    impl<'a, F: DisplayText> fmt::Display for TextString<'a, F> {
261        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262            self.0.fmt_text(f, &mut ())
263        }
264    }
265
266    TextString::<'_>(t).to_string()
267}
268
269/// Apply `f: F` to create a rendering context of type `C` and render the given
270/// tree `t: T` within that context.
271/// # Panics
272///
273/// Panics if the [`DisplayText::fmt_text`] call returns a [`fmt::Error`].
274pub fn text_string_at<'a, T: DisplayText<C>, C, F: Fn() -> C>(t: &'a T, f: F) -> String {
275    struct TextStringAt<'a, T, C, F: Fn() -> C> {
276        t: &'a T,
277        f: F,
278    }
279
280    impl<T: DisplayText<C>, C, F: Fn() -> C> DisplayText<()> for TextStringAt<'_, T, C, F> {
281        fn fmt_text(&self, f: &mut fmt::Formatter<'_>, _ctx: &mut ()) -> fmt::Result {
282            let mut ctx = (self.f)();
283            self.t.fmt_text(f, &mut ctx)
284        }
285    }
286
287    text_string(&TextStringAt { t, f })
288}