Skip to main content

vantage_table/
conditions.rs

1use vantage_expressions::{Expression, ExpressiveEnum};
2
3/// Handle for temporary conditions that can be removed
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct ConditionHandle(pub(crate) i64);
6
7impl ConditionHandle {
8    pub(crate) fn new(id: i64) -> Self {
9        Self(id)
10    }
11}
12
13/// When `expr` is a literal `column = value` — the shape `column.eq(value)`
14/// builds on every expression backend — return the column and the value.
15///
16/// The left side must be one bare or quoted identifier (`parent`,
17/// `"parent"`, `` `parent` ``). A table-qualified one (`"t"."parent"`) is a
18/// correlation, not a set definition, and reads as `None`, as does any
19/// operator other than `=`.
20pub fn literal_equality<V: Clone>(expr: &Expression<V>) -> Option<(String, V)> {
21    if expr.template.trim() != "{} = {}" || expr.parameters.len() != 2 {
22        return None;
23    }
24    let column = match &expr.parameters[0] {
25        ExpressiveEnum::Nested(lhs) if lhs.parameters.is_empty() => {
26            unquote_identifier(&lhs.template)?
27        }
28        _ => return None,
29    };
30    let value = match &expr.parameters[1] {
31        ExpressiveEnum::Scalar(v) => v.clone(),
32        ExpressiveEnum::Nested(rhs) if rhs.template.trim() == "{}" && rhs.parameters.len() == 1 => {
33            match &rhs.parameters[0] {
34                ExpressiveEnum::Scalar(v) => v.clone(),
35                _ => return None,
36            }
37        }
38        _ => return None,
39    };
40    Some((column, value))
41}
42
43/// The identifier `text` names, with `"…"` or `` `…` `` quoting removed and
44/// doubled quotes collapsed; `None` when it is anything but one identifier.
45fn unquote_identifier(text: &str) -> Option<String> {
46    let text = text.trim();
47    for q in ['"', '`'] {
48        if let Some(inner) = text.strip_prefix(q).and_then(|t| t.strip_suffix(q)) {
49            let doubled: String = [q, q].iter().collect();
50            if inner.is_empty() || inner.replace(&doubled, "").contains(q) {
51                return None;
52            }
53            return Some(inner.replace(&doubled, &q.to_string()));
54        }
55    }
56    let mut chars = text.chars();
57    let first = chars.next()?;
58    ((first.is_ascii_alphabetic() || first == '_')
59        && chars.all(|c| c.is_ascii_alphanumeric() || c == '_'))
60    .then(|| text.to_string())
61}
62
63#[cfg(test)]
64mod tests {
65    use super::literal_equality;
66    use serde_json::{Value, json};
67    use vantage_expressions::{Expression, ExpressiveEnum};
68
69    fn eq(lhs: &str, v: Value) -> Expression<Value> {
70        Expression::new(
71            "{} = {}",
72            vec![
73                ExpressiveEnum::Nested(Expression::new(lhs, vec![])),
74                ExpressiveEnum::Nested(Expression::new("{}", vec![ExpressiveEnum::Scalar(v)])),
75            ],
76        )
77    }
78
79    #[test]
80    fn literal_equality_reads_column_eq_value() {
81        assert_eq!(
82            literal_equality(&eq("parent", json!("p1"))),
83            Some(("parent".into(), json!("p1")))
84        );
85        assert_eq!(
86            literal_equality(&eq("\"parent\"", json!(1))),
87            Some(("parent".into(), json!(1)))
88        );
89        assert_eq!(
90            literal_equality(&eq("`parent`", json!(1))),
91            Some(("parent".into(), json!(1)))
92        );
93        assert_eq!(
94            literal_equality(&eq("\"t\".\"parent\"", json!(1))),
95            None,
96            "qualified: correlated, not an invariant"
97        );
98        assert_eq!(literal_equality(&eq("\"\"", json!(1))), None, "empty name");
99        assert_eq!(literal_equality(&eq("``", json!(1))), None, "empty name");
100        let gt = Expression::new("{} > {}", eq("price", json!(1)).parameters);
101        assert_eq!(literal_equality(&gt), None);
102    }
103}