Skip to main content

rudb_exec/
written.rs

1//! Writing a bound expression back out the way an error message quotes it.
2//!
3//! One message in the engine names the expression it failed in rather than the values it failed on,
4//! which is division by zero, and the expression it names is the bound one. That means the casts the
5//! binder inserted are in the text, a column is the name its operator produces rather than the name
6//! the query wrote, and a literal is the value it was folded to. `SELECT a // 0 FROM t` says
7//! `(a // 0)` and `SELECT a::DOUBLE // 0.0 FROM t` says `(CAST(a AS DOUBLE) // 0.0)`, both measured.
8//!
9//! This is the third way an expression is written in this engine and the three are not
10//! interchangeable. The plan dump in `rudb-plan` annotates every node with its type, because a dump
11//! that cannot be read back without a catalog is not a dump. The column namer in `rudb-parse` writes
12//! the text as the user typed it, because that is what goes in the result header. This one writes
13//! what DuckDB's `ToString` writes, because the reader is somebody comparing two engines' error
14//! messages.
15
16use std::fmt::{self, Write};
17
18use rudb_plan::{Expr, ExprRef, Plan};
19
20use crate::schema::Schema;
21
22/// How this bound expression is written in an error message.
23#[must_use]
24pub fn written(plan: &Plan, expr: ExprRef, schema: &Schema) -> String {
25    let mut out = String::new();
26    // A `String` is not a writer that can fail, so the result of writing into one says nothing.
27    // Dropped rather than unwrapped, because the caller is an error message and an error message is
28    // not worth a panic.
29    let _ = form(plan, &mut out, expr, schema);
30    out
31}
32
33fn form<W: Write>(plan: &Plan, out: &mut W, expr: ExprRef, schema: &Schema) -> fmt::Result {
34    match *plan.expr(expr) {
35        Expr::Column(binding) => match schema.position_of(binding) {
36            Some(position) => out.write_str(&schema.fields()[position].name),
37            // Unreachable from a message, since an expression over a column the schema does not
38            // have fails before it computes anything. Written the way the plan dump writes it
39            // rather than panicked on, because no error message is worth a panic.
40            None => write!(out, "#{}.{}", binding.table, binding.column),
41        },
42        Expr::Constant(value) => write!(out, "{}", plan.value(value)),
43        Expr::Cast { input, try_cast } => {
44            out.write_str(if try_cast { "TRY_CAST(" } else { "CAST(" })?;
45            form(plan, out, input, schema)?;
46            write!(out, " AS {})", plan.expr_type(expr))
47        }
48        Expr::Compare { op, left, right } => {
49            out.write_char('(')?;
50            form(plan, out, left, schema)?;
51            write!(out, " {} ", op.symbol())?;
52            form(plan, out, right, schema)?;
53            out.write_char(')')
54        }
55        Expr::Conjunction { op, children } => {
56            out.write_char('(')?;
57            for (position, &child) in plan.expr_list(children).iter().enumerate() {
58                if position > 0 {
59                    write!(out, " {} ", op.keyword())?;
60                }
61                form(plan, out, child, schema)?;
62            }
63            out.write_char(')')
64        }
65        Expr::Function { name, args } | Expr::Aggregate { name, args, .. } => {
66            call(plan, out, plan.string(name), args, schema)
67        }
68        Expr::Case { arms, otherwise } => {
69            out.write_str("CASE")?;
70            for arm in plan.arm_list(arms) {
71                out.write_str(" WHEN ")?;
72                form(plan, out, arm.when, schema)?;
73                out.write_str(" THEN ")?;
74                form(plan, out, arm.then, schema)?;
75            }
76            if let Some(otherwise) = otherwise {
77                out.write_str(" ELSE ")?;
78                form(plan, out, otherwise, schema)?;
79            }
80            out.write_str(" END")
81        }
82    }
83}
84
85/// A binary operator goes between its operands and everything else goes in front of them.
86///
87/// Whether a name is an operator is the first character and nothing else: every operator in the
88/// catalog is punctuation and every named function starts with a letter, so there is no table to
89/// consult. Only the binary case is written between, which is measured rather than assumed: `a // 0`
90/// comes out as `(a // 0)` and unary minus comes out as `-(a)`, brackets and all, the same as a call
91/// to a function whose name happens to be a dash.
92fn call<W: Write>(
93    plan: &Plan,
94    out: &mut W,
95    name: &str,
96    args: rudb_plan::Slice,
97    schema: &Schema,
98) -> fmt::Result {
99    let operator = !name.starts_with(|first: char| first.is_alphabetic() || first == '_');
100    let args = plan.expr_list(args);
101    match (operator, args) {
102        (true, [left, right]) => {
103            out.write_char('(')?;
104            form(plan, out, *left, schema)?;
105            write!(out, " {name} ")?;
106            form(plan, out, *right, schema)?;
107            out.write_char(')')
108        }
109        _ => {
110            write!(out, "{name}(")?;
111            for (position, &arg) in args.iter().enumerate() {
112                if position > 0 {
113                    out.write_str(", ")?;
114                }
115                form(plan, out, arg, schema)?;
116            }
117            out.write_char(')')
118        }
119    }
120}