kernel/config/expression.rs
1//! The input of a `sum`/`avg`/`min`/`max` aggregate: one column, or arithmetic
2//! over the columns of the aggregated table.
3//!
4//! An [`Expression`] is a tree: columns and numeric literals at the leaves,
5//! `+ - * /` and unary minus above them. [`Expression::parse`] builds it with a
6//! hand-written tokenizer and a recursive-descent parser (no regular
7//! expression): usual precedence (`*` `/` over `+` `-`, unary minus over both),
8//! left associativity, parentheses to group.
9//!
10//! - **A plain column is the [`Expression::Column`] leaf.** It serializes,
11//! hashes and displays exactly as the bare [`ColumnName`] the aggregate held
12//! before expressions existed, so an existing schema keeps its content hash
13//! and an existing `flusso.lock` keeps its bytes.
14//! - **An expression is stored as its canonical text** ([`Display`](std::fmt::Display)): single
15//! spaces around binary operators, only the parentheses the tree needs. Two
16//! spellings of one tree (whitespace, redundant parentheses) serialize and
17//! hash the same. A double negation renders `- -a`, never `--a` or
18//! `-(-a)`, so the text never nests deeper than the tree is tall.
19//! - **Malformed input fails with a position** ([`ExpressionError`]): the
20//! 1-based character the parser stopped at.
21//! - **Depth is capped at [`MAX_DEPTH`]**, for the parser's recursion and the
22//! tree's height, so no input can overflow the stack. Every tree the parser
23//! accepts renders to text it accepts again: `parse(render(x)) == x`.
24//! - **A string [`ColumnName`] accepts is that column**, by the newtype's own
25//! rules (trim, Unicode lowercase), exactly as before expressions existed.
26//!
27//! ```
28//! use kernel::config::{BinaryOp, Expression};
29//!
30//! let total: Expression = " quantity*(unit_price - discount) ".parse().unwrap();
31//! assert_eq!(total.to_string(), "quantity * (unit_price - discount)");
32//! assert!(matches!(total, Expression::Binary { op: BinaryOp::Multiply, .. }));
33//! let names: Vec<&str> = total.columns().into_iter().map(|c| c.as_ref()).collect();
34//! assert_eq!(names, ["quantity", "unit_price", "discount"]);
35//!
36//! let plain: Expression = "amount".parse().unwrap();
37//! assert_eq!(plain.as_column().map(|c| c.as_ref()), Some("amount"));
38//!
39//! let err = "quantity *".parse::<Expression>().unwrap_err();
40//! assert_eq!(err.position, 11);
41//! ```
42
43mod parse;
44#[cfg(test)]
45mod tests;
46
47use std::fmt;
48use std::hash::{Hash, Hasher};
49use std::str::FromStr;
50
51use serde::{Deserialize, Deserializer, Serialize, Serializer};
52
53use crate::common::ColumnName;
54
55pub use parse::{ExpressionError, ExpressionErrorKind, MAX_DEPTH};
56
57/// Arithmetic over the columns of one table, evaluated per row.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum Expression {
60 /// A column of the table.
61 Column(ColumnName),
62 /// A numeric literal.
63 Number(NumberLiteral),
64 /// Unary minus.
65 Negate(Box<Expression>),
66 /// `left op right`.
67 Binary {
68 /// The operator.
69 op: BinaryOp,
70 /// The left operand.
71 left: Box<Expression>,
72 /// The right operand.
73 right: Box<Expression>,
74 },
75}
76
77/// A binary arithmetic operator.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
79pub enum BinaryOp {
80 /// `+`
81 Add,
82 /// `-`
83 Subtract,
84 /// `*`
85 Multiply,
86 /// `/`
87 Divide,
88}
89
90/// A numeric literal as written: ASCII digits, optionally a `.` and more digits.
91#[derive(Debug, Clone, PartialEq, Eq, Hash)]
92pub struct NumberLiteral(String);
93
94impl Expression {
95 /// Parse `input`. A lone column name gives [`Expression::Column`].
96 pub fn parse(input: &str) -> Result<Self, ExpressionError> {
97 parse::parse(input)
98 }
99
100 /// The column, when the expression is a plain column.
101 pub fn as_column(&self) -> Option<&ColumnName> {
102 match self {
103 Self::Column(column) => Some(column),
104 _ => None,
105 }
106 }
107
108 /// Every column the expression reads, once each, in order of appearance.
109 pub fn columns(&self) -> Vec<&ColumnName> {
110 let mut out = Vec::new();
111 self.collect_columns(&mut out);
112 out
113 }
114
115 fn collect_columns<'a>(&'a self, out: &mut Vec<&'a ColumnName>) {
116 match self {
117 Self::Column(column) => {
118 if !out.contains(&column) {
119 out.push(column);
120 }
121 }
122 Self::Number(_) => {}
123 Self::Negate(inner) => inner.collect_columns(out),
124 Self::Binary { left, right, .. } => {
125 left.collect_columns(out);
126 right.collect_columns(out);
127 }
128 }
129 }
130
131 fn precedence(&self) -> u8 {
132 match self {
133 Self::Binary { op, .. } => op.precedence(),
134 Self::Negate(_) => 3,
135 Self::Column(_) | Self::Number(_) => 4,
136 }
137 }
138}
139
140impl BinaryOp {
141 /// The operator as written: `+`, `-`, `*` or `/`.
142 pub fn symbol(self) -> &'static str {
143 match self {
144 Self::Add => "+",
145 Self::Subtract => "-",
146 Self::Multiply => "*",
147 Self::Divide => "/",
148 }
149 }
150
151 fn precedence(self) -> u8 {
152 match self {
153 Self::Add | Self::Subtract => 1,
154 Self::Multiply | Self::Divide => 2,
155 }
156 }
157}
158
159impl NumberLiteral {
160 /// The literal's text: digits, optionally a `.` and more digits.
161 pub fn as_str(&self) -> &str {
162 &self.0
163 }
164}
165
166impl From<ColumnName> for Expression {
167 fn from(column: ColumnName) -> Self {
168 Self::Column(column)
169 }
170}
171
172impl FromStr for Expression {
173 type Err = ExpressionError;
174
175 fn from_str(input: &str) -> Result<Self, Self::Err> {
176 Self::parse(input)
177 }
178}
179
180/// The canonical text: single spaces around binary operators, and parentheses
181/// only where the tree needs them (a right operand at the same precedence keeps
182/// them, since the operators associate left). A negated negation is `- -a`:
183/// the space keeps `--` out of the text, and no parentheses means the text
184/// nests no deeper than the tree is tall, so every parsed tree renders to text
185/// the parser accepts.
186impl fmt::Display for Expression {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 match self {
189 Self::Column(column) => f.write_str(column.as_ref()),
190 Self::Number(number) => f.write_str(number.as_str()),
191 Self::Negate(inner) => match **inner {
192 Self::Negate(_) => write!(f, "- {inner}"),
193 Self::Binary { .. } => write!(f, "-({inner})"),
194 Self::Column(_) | Self::Number(_) => write!(f, "-{inner}"),
195 },
196 Self::Binary { op, left, right } => {
197 write_operand(f, left, left.precedence() < op.precedence())?;
198 write!(f, " {} ", op.symbol())?;
199 write_operand(f, right, right.precedence() <= op.precedence())
200 }
201 }
202 }
203}
204
205fn write_operand(f: &mut fmt::Formatter<'_>, operand: &Expression, group: bool) -> fmt::Result {
206 if group {
207 write!(f, "({operand})")
208 } else {
209 write!(f, "{operand}")
210 }
211}
212
213/// A plain column hashes as the [`ColumnName`] it is, so an aggregate written
214/// before expressions keeps its content hash. Anything else hashes its
215/// canonical text, which is never a column name: it holds an operator, or is
216/// a number, and a column name never starts with a digit.
217impl Hash for Expression {
218 fn hash<H: Hasher>(&self, state: &mut H) {
219 match self {
220 Self::Column(column) => column.hash(state),
221 other => other.to_string().hash(state),
222 }
223 }
224}
225
226impl Serialize for Expression {
227 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
228 serializer.collect_str(self)
229 }
230}
231
232impl<'de> Deserialize<'de> for Expression {
233 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
234 let text = String::deserialize(deserializer)?;
235 Self::parse(&text)
236 .map_err(|e| serde::de::Error::custom(format!("invalid expression `{text}`: {e}")))
237 }
238}