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}