Skip to main content

rudb_plan/
print.rs

1//! The textual form.
2//!
3//! One line per operator, two spaces of indent per level, parent before children. Every expression
4//! is written `form::TYPE`.
5//!
6//! The annotation is on every expression rather than only where a reader would need one. The
7//! alternative is a reader that re-derives types, and re-deriving the type of `upper(x)` means
8//! consulting the function catalog, and a dump that cannot be read back without a catalog is not a
9//! dump. It costs width and it buys a reader that is a pure function of the text.
10//!
11//! Nothing in here allocates a plan-sized string. It writes into whatever
12//! [`fmt::Write`](std::fmt::Write) it is handed, which for `to_string` is one growing buffer and
13//! for a test comparison can be a sink that never keeps anything.
14
15use std::fmt::{self, Write};
16
17#[cfg(test)]
18use rudb_common::LogicalType;
19use rudb_common::Value;
20
21use crate::expr::Expr;
22use crate::node::Node;
23use crate::plan::Plan;
24use crate::{ExprRef, NodeRef, Slice};
25
26/// Names that mean something in an expression, which a function of the same name has to be quoted
27/// to get past. The reader looks for these unquoted and only unquoted, so `"cast"(x)` is a call to
28/// a function called `cast` and `CAST(x)` is a cast.
29pub(crate) const RESERVED: [&str; 3] = ["CAST", "TRY_CAST", "CASE"];
30
31impl fmt::Display for Plan {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        write_node(self, f, self.root(), 0)
34    }
35}
36
37impl Plan {
38    /// One operator's own line, with no indent, no newline and none of its children.
39    ///
40    /// What `EXPLAIN` walks, since it puts something of its own after each line and so cannot use
41    /// the whole plan's text. It is the same text the whole plan's text has on that line, written
42    /// by the same code, which is the property that matters: two printers that drift are two
43    /// formats, and the plan reader only knows one.
44    #[must_use]
45    pub fn operator(&self, node: NodeRef) -> String {
46        let mut out = String::new();
47        let held = self.node(node);
48        // Both of these write into a `String`, which cannot fail, so there is nothing to report.
49        let _ = out.write_str(held.keyword());
50        let _ = write_arguments(self, &mut out, held);
51        out
52    }
53}
54
55fn write_node<W: Write>(plan: &Plan, out: &mut W, node: NodeRef, depth: usize) -> fmt::Result {
56    for _ in 0..depth {
57        out.write_str("  ")?;
58    }
59    let held = plan.node(node);
60    out.write_str(held.keyword())?;
61    write_arguments(plan, out, held)?;
62    out.write_char('\n')?;
63    for child in held.children().into_iter().flatten() {
64        write_node(plan, out, child, depth + 1)?;
65    }
66    Ok(())
67}
68
69fn write_arguments<W: Write>(plan: &Plan, out: &mut W, node: &Node) -> fmt::Result {
70    match *node {
71        Node::Dummy | Node::CrossProduct { .. } => Ok(()),
72        Node::Get { catalog, schema, table, alias, index, columns } => {
73            out.write_char(' ')?;
74            write_identifier(out, plan.string(catalog))?;
75            out.write_char('.')?;
76            write_identifier(out, plan.string(schema))?;
77            out.write_char('.')?;
78            write_identifier(out, plan.string(table))?;
79            out.write_str(" AS ")?;
80            write_identifier(out, plan.string(alias))?;
81            write!(out, " #{index} ")?;
82            write_schema(plan, out, columns)
83        }
84        Node::Values { index, columns, rows } => {
85            write!(out, " #{index} ")?;
86            write_schema(plan, out, columns)?;
87            out.write_str(" rows=[")?;
88            for (position, row) in plan.row_list(rows).iter().enumerate() {
89                if position > 0 {
90                    out.write_str(", ")?;
91                }
92                write_expr_list(plan, out, *row)?;
93            }
94            out.write_char(']')
95        }
96        Node::TableFunction { index, function, args, options, settings, columns } => {
97            out.write_char(' ')?;
98            write_identifier(out, plan.string(function))?;
99            out.write_str(" args=")?;
100            write_expr_list(plan, out, args)?;
101            // Written only when there are some, so that the plan of a call with no named parameter
102            // is the same text it was before there were any to write.
103            if options.len > 0 {
104                out.write_str(" options=[")?;
105                for (at, (&name, &value)) in
106                    plan.name_list(options).iter().zip(plan.expr_list(settings)).enumerate()
107                {
108                    if at > 0 {
109                        out.write_str(", ")?;
110                    }
111                    write_identifier(out, plan.string(name))?;
112                    out.write_char('=')?;
113                    write_expr(plan, out, value)?;
114                }
115                out.write_char(']')?;
116            }
117            write!(out, " #{index} ")?;
118            write_schema(plan, out, columns)
119        }
120        Node::Fetch { index, args, columns, row, .. } => {
121            out.write_str(" args=")?;
122            write_expr_list(plan, out, args)?;
123            out.write_str(" row=")?;
124            write_expr(plan, out, row)?;
125            write!(out, " #{index} ")?;
126            write_schema(plan, out, columns)
127        }
128        Node::Filter { predicate, .. } => {
129            out.write_char(' ')?;
130            write_expr(plan, out, predicate)
131        }
132        Node::Project { index, exprs, names, .. } => {
133            write!(out, " #{index} [")?;
134            for (position, &expr) in plan.expr_list(exprs).iter().enumerate() {
135                if position > 0 {
136                    out.write_str(", ")?;
137                }
138                write_expr(plan, out, expr)?;
139                out.write_str(" AS ")?;
140                write_identifier(out, plan.string(plan.name_list(names)[position]))?;
141            }
142            out.write_char(']')
143        }
144        Node::Aggregate { index, groups, aggregates, .. } => {
145            write!(out, " #{index} groups=")?;
146            write_expr_list(plan, out, groups)?;
147            out.write_str(" aggregates=")?;
148            write_expr_list(plan, out, aggregates)
149        }
150        Node::Sort { keys, .. } => write_sort_keys(plan, out, keys),
151        Node::Limit { count, offset, .. } => {
152            match count {
153                Some(count) => write!(out, " {count}")?,
154                None => out.write_str(" ALL")?,
155            }
156            write!(out, " offset {offset}")
157        }
158        Node::TopN { keys, count, offset, .. } => {
159            write!(out, " {count} offset {offset}")?;
160            write_sort_keys(plan, out, keys)
161        }
162        Node::Distinct { on, .. } => {
163            out.write_str(" on=")?;
164            write_expr_list(plan, out, on)
165        }
166        Node::Join { kind, conditions, .. } => {
167            write!(out, " {} on=", kind.keyword())?;
168            write_expr_list(plan, out, conditions)
169        }
170        Node::SetOp { kind, all, index, .. } => {
171            let quantifier = if all { "ALL" } else { "DISTINCT" };
172            write!(out, " {} {quantifier} #{index}", kind.keyword())
173        }
174    }
175}
176
177/// A named and typed column list, which is what a scan and a `VALUES` produce.
178fn write_schema<W: Write>(plan: &Plan, out: &mut W, columns: Slice) -> fmt::Result {
179    out.write_char('[')?;
180    for (position, field) in plan.field_list(columns).iter().enumerate() {
181        if position > 0 {
182            out.write_str(", ")?;
183        }
184        write_identifier(out, &field.name)?;
185        write!(out, "::{}", field.ty)?;
186    }
187    out.write_char(']')
188}
189
190/// The keys of a sort, in priority order, each with its direction and its null placement.
191fn write_sort_keys<W: Write>(plan: &Plan, out: &mut W, keys: Slice) -> fmt::Result {
192    out.write_str(" [")?;
193    for (position, key) in plan.sort_key_list(keys).iter().enumerate() {
194        if position > 0 {
195            out.write_str(", ")?;
196        }
197        write_expr(plan, out, key.expr)?;
198        out.write_str(if key.descending { " DESC" } else { " ASC" })?;
199        out.write_str(if key.nulls_first { " NULLS FIRST" } else { " NULLS LAST" })?;
200    }
201    out.write_char(']')
202}
203
204fn write_expr_list<W: Write>(plan: &Plan, out: &mut W, list: Slice) -> fmt::Result {
205    out.write_char('[')?;
206    for (position, &expr) in plan.expr_list(list).iter().enumerate() {
207        if position > 0 {
208            out.write_str(", ")?;
209        }
210        write_expr(plan, out, expr)?;
211    }
212    out.write_char(']')
213}
214
215fn write_expr<W: Write>(plan: &Plan, out: &mut W, expr: ExprRef) -> fmt::Result {
216    write_form(plan, out, expr)?;
217    write!(out, "::{}", plan.expr_type(expr))
218}
219
220fn write_form<W: Write>(plan: &Plan, out: &mut W, expr: ExprRef) -> fmt::Result {
221    match *plan.expr(expr) {
222        Expr::Column(binding) => write!(out, "#{}.{}", binding.table, binding.column),
223        Expr::Constant(value) => write_value(out, plan.value(value)),
224        Expr::Cast { input, try_cast } => {
225            out.write_str(if try_cast { "TRY_CAST(" } else { "CAST(" })?;
226            write_expr(plan, out, input)?;
227            out.write_char(')')
228        }
229        Expr::Compare { op, left, right } => {
230            out.write_char('(')?;
231            write_expr(plan, out, left)?;
232            write!(out, " {} ", op.symbol())?;
233            write_expr(plan, out, right)?;
234            out.write_char(')')
235        }
236        Expr::Conjunction { op, children } => {
237            out.write_char('(')?;
238            for (position, &child) in plan.expr_list(children).iter().enumerate() {
239                if position > 0 {
240                    write!(out, " {} ", op.keyword())?;
241                }
242                write_expr(plan, out, child)?;
243            }
244            out.write_char(')')
245        }
246        Expr::Function { name, args } => {
247            write_function_name(out, plan.string(name))?;
248            out.write_char('(')?;
249            write_arguments_of(plan, out, args)?;
250            out.write_char(')')
251        }
252        Expr::Aggregate { name, args, distinct, filter } => {
253            write_function_name(out, plan.string(name))?;
254            out.write_char('(')?;
255            if distinct {
256                out.write_str("DISTINCT ")?;
257            }
258            write_arguments_of(plan, out, args)?;
259            if let Some(filter) = filter {
260                // No leading space when there are no arguments, because `count_star( FILTER x)`
261                // has a space where an argument would go and reads as one that went missing.
262                if !plan.expr_list(args).is_empty() {
263                    out.write_char(' ')?;
264                }
265                out.write_str("FILTER ")?;
266                write_expr(plan, out, filter)?;
267            }
268            out.write_char(')')
269        }
270        Expr::Case { arms, otherwise } => {
271            out.write_str("CASE")?;
272            for arm in plan.arm_list(arms) {
273                out.write_str(" WHEN ")?;
274                write_expr(plan, out, arm.when)?;
275                out.write_str(" THEN ")?;
276                write_expr(plan, out, arm.then)?;
277            }
278            if let Some(otherwise) = otherwise {
279                out.write_str(" ELSE ")?;
280                write_expr(plan, out, otherwise)?;
281            }
282            out.write_str(" END")
283        }
284    }
285}
286
287fn write_arguments_of<W: Write>(plan: &Plan, out: &mut W, args: Slice) -> fmt::Result {
288    for (position, &arg) in plan.expr_list(args).iter().enumerate() {
289        if position > 0 {
290            out.write_str(", ")?;
291        }
292        write_expr(plan, out, arg)?;
293    }
294    Ok(())
295}
296
297/// Writes a constant.
298///
299/// The type annotation that follows is what says which of these a run of digits is, so nothing
300/// here has to be self describing. `19723::DATE` is a day number rather than `'2024-01-15'`,
301/// deliberately: a plan dump is diffed by a machine and compared by a test, the day number is what
302/// the executor actually holds, and a date formatter in the round trip is a second place for a
303/// calendar bug to live. [`Value`]'s own `Display` is DuckDB's user-facing rendering and is where
304/// a person reading a result set gets a date from.
305fn write_value<W: Write>(out: &mut W, value: &Value) -> fmt::Result {
306    match value {
307        Value::Null => out.write_str("NULL"),
308        Value::Boolean(held) => out.write_str(if *held { "TRUE" } else { "FALSE" }),
309        Value::TinyInt(held) => write!(out, "{held}"),
310        Value::SmallInt(held) => write!(out, "{held}"),
311        Value::Integer(held) => write!(out, "{held}"),
312        Value::BigInt(held) => write!(out, "{held}"),
313        Value::HugeInt(held) => write!(out, "{held}"),
314        Value::UTinyInt(held) => write!(out, "{held}"),
315        Value::USmallInt(held) => write!(out, "{held}"),
316        Value::UInteger(held) => write!(out, "{held}"),
317        Value::UBigInt(held) => write!(out, "{held}"),
318        Value::UHugeInt(held) => write!(out, "{held}"),
319        // The debug formatting of a float is the shortest text that reads back as the same bits,
320        // which the display formatting is not: `{}` prints 0.1f32 as 0.1 and so does 0.1f64, and
321        // those are different numbers.
322        Value::Float(held) => write!(out, "{held:?}"),
323        Value::Double(held) => write!(out, "{held:?}"),
324        Value::Decimal { unscaled, scale, .. } => out.write_str(&decimal_text(*unscaled, *scale)),
325        Value::Varchar(held) => write_string(out, held),
326        Value::Blob(held) => {
327            out.write_str("X'")?;
328            for byte in held {
329                write!(out, "{byte:02x}")?;
330            }
331            out.write_char('\'')
332        }
333        Value::Date(held) => write!(out, "{held}"),
334        Value::Time(held) | Value::Timestamp(held) => write!(out, "{held}"),
335        Value::Interval { months, days, micros } => write!(out, "{{{months}, {days}, {micros}}}"),
336        Value::List { values, .. } => {
337            out.write_char('{')?;
338            for (position, element) in values.iter().enumerate() {
339                if position > 0 {
340                    out.write_str(", ")?;
341                }
342                write_value(out, element)?;
343            }
344            out.write_char('}')
345        }
346        // The field names are in the type annotation, which is where the reader takes them from,
347        // so writing them again here would be a second copy that can disagree with the first.
348        Value::Struct(fields) => {
349            out.write_char('{')?;
350            for (position, (_, held)) in fields.iter().enumerate() {
351                if position > 0 {
352                    out.write_str(", ")?;
353                }
354                write_value(out, held)?;
355            }
356            out.write_char('}')
357        }
358        // Value is non_exhaustive, so a variant added in rudb-common lands here with no form of
359        // its own. Writing something the reader is guaranteed to reject is the loudest option
360        // available: the round trip test fails on the value that has no form rather than the dump
361        // quietly becoming a thing that cannot be read back.
362        other => write!(out, "<no textual form for {other:?}>"),
363    }
364}
365
366/// The digits of a decimal with the point where the scale says it is.
367///
368/// The unscaled integer is what the value holds and printing that instead would round trip just as
369/// exactly, but `1234::DECIMAL(6,2)` is a number nobody can read and `12.34::DECIMAL(6,2)` is the
370/// same information.
371pub(crate) fn decimal_text(unscaled: i128, scale: u8) -> String {
372    if scale == 0 {
373        return unscaled.to_string();
374    }
375    let scale = usize::from(scale);
376    let digits = unscaled.unsigned_abs().to_string();
377    // A value smaller than one unit needs leading zeros before the point, so 5 at scale 3 is 0.005
378    // and not .005 or 5.000.
379    let padded = if digits.len() <= scale {
380        format!("{}{digits}", "0".repeat(scale + 1 - digits.len()))
381    } else {
382        digits
383    };
384    let point = padded.len() - scale;
385    let sign = if unscaled < 0 { "-" } else { "" };
386    format!("{sign}{}.{}", &padded[..point], &padded[point..])
387}
388
389/// Writes a string constant, single quoted, with the quote doubled.
390///
391/// A control character goes out as `\xNN` and a backslash doubles, because a dump is compared line
392/// by line and a value holding a newline would otherwise turn one operator into two lines and the
393/// reader would see an indent that does not exist.
394fn write_string<W: Write>(out: &mut W, text: &str) -> fmt::Result {
395    out.write_char('\'')?;
396    for character in text.chars() {
397        match character {
398            '\'' => out.write_str("''")?,
399            '\\' => out.write_str("\\\\")?,
400            control if control.is_control() => write!(out, "\\x{:02x}", control as u32)?,
401            other => out.write_char(other)?,
402        }
403    }
404    out.write_char('\'')
405}
406
407/// Whether a name reads back unquoted.
408pub(crate) fn is_plain_identifier(name: &str) -> bool {
409    !name.is_empty()
410        && name.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
411        && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
412}
413
414/// Writes a name, quoting it if it would not survive being read back unquoted.
415fn write_identifier<W: Write>(out: &mut W, name: &str) -> fmt::Result {
416    if is_plain_identifier(name) {
417        return out.write_str(name);
418    }
419    out.write_char('"')?;
420    for character in name.chars() {
421        if character == '"' {
422            out.write_str("\"\"")?;
423        } else {
424            out.write_char(character)?;
425        }
426    }
427    out.write_char('"')
428}
429
430/// Writes a function name, which additionally has to get past the reserved words.
431fn write_function_name<W: Write>(out: &mut W, name: &str) -> fmt::Result {
432    if RESERVED.iter().any(|reserved| name.eq_ignore_ascii_case(reserved)) {
433        return write!(out, "\"{name}\"");
434    }
435    write_identifier(out, name)
436}
437
438/// Whether a type prints as something the reader can find the end of.
439///
440/// The reader takes a type annotation as a name, then a balanced parenthesis group, then any
441/// number of balanced bracket groups, then optionally `WITH TIME ZONE`. Every type
442/// [`LogicalType`]'s own `Display` produces fits that, and this is the assertion that says so, for
443/// the test that walks the whole type set.
444#[cfg(test)]
445pub(crate) fn prints_readably(ty: &LogicalType) -> bool {
446    let text = ty.to_string();
447    crate::parse::type_extent(&text, 0) == text.len()
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn a_decimal_gets_its_point_from_its_scale() {
456        assert_eq!(decimal_text(1234, 2), "12.34");
457        assert_eq!(decimal_text(1234, 0), "1234");
458        assert_eq!(decimal_text(5, 3), "0.005");
459        assert_eq!(decimal_text(-5, 3), "-0.005");
460        assert_eq!(decimal_text(-1234, 2), "-12.34");
461        assert_eq!(decimal_text(0, 2), "0.00");
462    }
463
464    #[test]
465    fn the_widest_decimal_still_prints() {
466        let widest = 10i128.pow(37) - 1;
467        assert_eq!(decimal_text(widest, 0).len(), 37);
468        assert_eq!(decimal_text(widest, 37).len(), 39);
469    }
470
471    #[test]
472    fn a_name_that_needs_quoting_gets_it() {
473        let quoted = |name: &str| {
474            let mut out = String::new();
475            write_identifier(&mut out, name).unwrap();
476            out
477        };
478        assert_eq!(quoted("SearchPhrase"), "SearchPhrase");
479        assert_eq!(quoted("_hidden9"), "_hidden9");
480        assert_eq!(quoted("a b"), "\"a b\"");
481        assert_eq!(quoted("9lives"), "\"9lives\"", "a name cannot start with a digit");
482        assert_eq!(quoted(""), "\"\"");
483        assert_eq!(quoted("say \"hi\""), "\"say \"\"hi\"\"\"");
484    }
485
486    #[test]
487    fn a_function_named_after_a_reserved_word_is_quoted() {
488        for name in ["cast", "CAST", "Try_Cast", "case"] {
489            let mut out = String::new();
490            write_function_name(&mut out, name).unwrap();
491            assert!(out.starts_with('"'), "{name} would be read back as syntax");
492        }
493        let mut out = String::new();
494        write_function_name(&mut out, "casting").unwrap();
495        assert_eq!(out, "casting", "only the reserved words themselves are reserved");
496    }
497
498    #[test]
499    fn a_string_never_contains_a_newline_when_it_is_written() {
500        let mut out = String::new();
501        write_string(&mut out, "one\ntwo\ttab'quote\\slash").unwrap();
502        assert!(!out.contains('\n'), "a value would split an operator across two lines");
503        assert_eq!(out, "'one\\x0atwo\\x09tab''quote\\\\slash'");
504    }
505
506    /// Floats are the one value kind where the obvious formatting is wrong, and it is wrong
507    /// quietly: `{}` on the nearest f32 to 0.1 prints 0.1, and 0.1 read back as f32 is a different
508    /// number than the one that was printed.
509    #[test]
510    fn a_float_prints_the_text_that_reads_back_as_the_same_bits() {
511        for held in [0.1f32, f32::MIN, f32::MAX, f32::EPSILON, -0.0, 1e-40] {
512            let mut out = String::new();
513            write_value(&mut out, &Value::Float(held)).unwrap();
514            let back: f32 = out.parse().expect("a float we printed parses");
515            assert_eq!(back.to_bits(), held.to_bits(), "{out} is not the same float");
516        }
517        for held in [0.1f64, f64::MIN, f64::MAX, f64::EPSILON, -0.0, 1e-308] {
518            let mut out = String::new();
519            write_value(&mut out, &Value::Double(held)).unwrap();
520            let back: f64 = out.parse().expect("a double we printed parses");
521            assert_eq!(back.to_bits(), held.to_bits(), "{out} is not the same double");
522        }
523    }
524
525    #[test]
526    fn a_plan_that_is_only_a_dummy_prints_one_line() {
527        assert_eq!(Plan::new().to_string(), "Dummy\n");
528    }
529
530    /// The reader finds the end of a type annotation by scanning rather than by parsing, and the
531    /// scan knows four shapes: a name, a balanced parenthesis group, balanced bracket groups, and
532    /// the `WITH TIME ZONE` suffix. A type that prints as something outside those four is a type
533    /// that swallows whatever comes after it in the dump, which shows up as a syntax error on the
534    /// far side of the line rather than as anything to do with the type.
535    #[test]
536    fn every_type_prints_as_something_the_reader_can_find_the_end_of() {
537        let scalars = [
538            LogicalType::Null,
539            LogicalType::Boolean,
540            LogicalType::TinyInt,
541            LogicalType::SmallInt,
542            LogicalType::Integer,
543            LogicalType::BigInt,
544            LogicalType::HugeInt,
545            LogicalType::UTinyInt,
546            LogicalType::USmallInt,
547            LogicalType::UInteger,
548            LogicalType::UBigInt,
549            LogicalType::UHugeInt,
550            LogicalType::Float,
551            LogicalType::Double,
552            LogicalType::Varchar,
553            LogicalType::Blob,
554            LogicalType::Bit,
555            LogicalType::Uuid,
556            LogicalType::Date,
557            LogicalType::Time,
558            LogicalType::TimeTz,
559            LogicalType::Timestamp,
560            LogicalType::TimestampS,
561            LogicalType::TimestampMs,
562            LogicalType::TimestampNs,
563            LogicalType::TimestampTz,
564            LogicalType::Interval,
565        ];
566        let mut all: Vec<LogicalType> = scalars.to_vec();
567        all.push(LogicalType::decimal(18, 3).expect("18 and 3 is a decimal"));
568        all.push(LogicalType::decimal(38, 0).expect("the widest decimal"));
569        for scalar in &scalars {
570            all.push(LogicalType::list(scalar.clone()));
571            all.push(LogicalType::array(scalar.clone(), 4));
572            all.push(LogicalType::map(LogicalType::Varchar, scalar.clone()));
573            all.push(LogicalType::Struct(vec![
574                rudb_common::Field::new("a", scalar.clone()),
575                rudb_common::Field::new("b b", LogicalType::Varchar),
576            ]));
577            all.push(LogicalType::Union(vec![rudb_common::Field::new("u", scalar.clone())]));
578        }
579        all.push(LogicalType::list(LogicalType::list(LogicalType::Integer)));
580        all.push(LogicalType::list(LogicalType::map(
581            LogicalType::Varchar,
582            LogicalType::TimestampTz,
583        )));
584
585        for ty in all {
586            let text = ty.to_string();
587            assert!(prints_readably(&ty), "the reader cannot find the end of {text}");
588            let back = LogicalType::parse(&text)
589                .unwrap_or_else(|error| panic!("{text} does not parse: {error}"));
590            assert_eq!(back, ty, "{text} does not read back as itself");
591        }
592    }
593}