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