Skip to main content

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}