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_common::Value;
19use rudb_plan::{Expr, ExprRef, Plan};
20
21use crate::schema::Schema;
22
23/// How this bound expression is written in an error message.
24#[must_use]
25pub fn written(plan: &Plan, expr: ExprRef, schema: &Schema) -> String {
26 let mut out = String::new();
27 // A `String` is not a writer that can fail, so the result of writing into one says nothing.
28 // Dropped rather than unwrapped, because the caller is an error message and an error message is
29 // not worth a panic.
30 let _ = form(plan, &mut out, expr, schema);
31 out
32}
33
34fn form<W: Write>(plan: &Plan, out: &mut W, expr: ExprRef, schema: &Schema) -> fmt::Result {
35 match *plan.expr(expr) {
36 Expr::Column(binding) => match schema.position_of(binding) {
37 Some(position) => out.write_str(&schema.fields()[position].name),
38 // Unreachable from a message, since an expression over a column the schema does not
39 // have fails before it computes anything. Written the way the plan dump writes it
40 // rather than panicked on, because no error message is worth a panic.
41 None => write!(out, "#{}.{}", binding.table, binding.column),
42 },
43 // An interval is the one constant that is quoted and cast rather than written plain, which
44 // is `'1 day'::INTERVAL`. It is also the only constant of its kind that can reach a
45 // message at all: every other non numeric type is folded away before the division that
46 // would name it, and dividing an interval by zero is the one division of a non number
47 // there is. Measured for #393.
48 Expr::Constant(value) => match plan.value(value) {
49 held @ Value::Interval { .. } => write!(out, "'{held}'::INTERVAL"),
50 held => write!(out, "{held}"),
51 },
52 Expr::Cast { input, try_cast } => {
53 out.write_str(if try_cast { "TRY_CAST(" } else { "CAST(" })?;
54 form(plan, out, input, schema)?;
55 write!(out, " AS {})", plan.expr_type(expr))
56 }
57 Expr::Compare { op, left, right } => {
58 out.write_char('(')?;
59 form(plan, out, left, schema)?;
60 write!(out, " {} ", op.symbol())?;
61 form(plan, out, right, schema)?;
62 out.write_char(')')
63 }
64 Expr::Conjunction { op, children } => {
65 out.write_char('(')?;
66 for (position, &child) in plan.expr_list(children).iter().enumerate() {
67 if position > 0 {
68 write!(out, " {} ", op.keyword())?;
69 }
70 form(plan, out, child, schema)?;
71 }
72 out.write_char(')')
73 }
74 Expr::Function { name, args } | Expr::Aggregate { name, args, .. } => {
75 call(plan, out, plan.string(name), args, schema)
76 }
77 Expr::Case { arms, otherwise } => {
78 out.write_str("CASE")?;
79 for arm in plan.arm_list(arms) {
80 out.write_str(" WHEN ")?;
81 form(plan, out, arm.when, schema)?;
82 out.write_str(" THEN ")?;
83 form(plan, out, arm.then, schema)?;
84 }
85 if let Some(otherwise) = otherwise {
86 out.write_str(" ELSE ")?;
87 form(plan, out, otherwise, schema)?;
88 }
89 out.write_str(" END")
90 }
91 }
92}
93
94/// A binary operator goes between its operands and everything else goes in front of them.
95///
96/// Whether a name is an operator is the first character and nothing else: every operator in the
97/// catalog is punctuation and every named function starts with a letter, so there is no table to
98/// consult. Only the binary case is written between, which is measured rather than assumed: `a // 0`
99/// comes out as `(a // 0)` and unary minus comes out as `-(a)`, brackets and all, the same as a call
100/// to a function whose name happens to be a dash.
101fn call<W: Write>(
102 plan: &Plan,
103 out: &mut W,
104 name: &str,
105 args: rudb_plan::Slice,
106 schema: &Schema,
107) -> fmt::Result {
108 let operator = !name.starts_with(|first: char| first.is_alphabetic() || first == '_');
109 let args = plan.expr_list(args);
110 match (operator, args) {
111 (true, [left, right]) => {
112 out.write_char('(')?;
113 form(plan, out, *left, schema)?;
114 write!(out, " {name} ")?;
115 form(plan, out, *right, schema)?;
116 out.write_char(')')
117 }
118 _ => {
119 write!(out, "{name}(")?;
120 for (position, &arg) in args.iter().enumerate() {
121 if position > 0 {
122 out.write_str(", ")?;
123 }
124 form(plan, out, arg, schema)?;
125 }
126 out.write_char(')')
127 }
128 }
129}