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/// Operators [`condition_column_of`] reads as narrowing a column to values.
44/// Negations (`!=`, `<>`, `NOT IN`, `NOT LIKE`) are left out: they narrow
45/// the set *away* from a value, which no "requires a condition on this
46/// column" check should accept.
47const COLUMN_OPERATORS: [&str; 7] = ["=", ">", ">=", "<", "<=", "IN", "LIKE"];
48
49/// The column `expr` narrows to values when it is `column <op> value` —
50/// `=`, comparisons, `LIKE`, `IN` (with or without parentheses around the
51/// operand) — with one bare or quoted identifier on the left and a value on
52/// the right: a scalar, a `{}`-wrapped scalar, a deferred value, and for
53/// `IN` also a value list or a subquery.
54///
55/// Anything else reads as `None`: a raw expression, `AND`/`OR`, a negation,
56/// a table-qualified column, a column or arbitrary expression on the right
57/// (`col = other_col`, `col = 7 OR 1 = 1`).
58pub fn condition_column_of<V>(expr: &Expression<V>) -> Option<String> {
59    if expr.parameters.len() != 2 {
60        return None;
61    }
62    let rest = expr.template.trim().strip_prefix("{}")?;
63    let operand_at = rest.find("{}")?;
64    let operator = rest[..operand_at].trim();
65    let (operator, parenthesised) = match operator.strip_suffix('(') {
66        Some(op) => (op.trim(), true),
67        None => (operator, false),
68    };
69    let tail = rest[operand_at + 2..].trim();
70    if tail != if parenthesised { ")" } else { "" } {
71        return None;
72    }
73    let operator = operator.to_ascii_uppercase();
74    if !COLUMN_OPERATORS.contains(&operator.as_str()) {
75        return None;
76    }
77    let operand = &expr.parameters[1];
78    let narrows = if operator == "IN" {
79        is_value(operand) || is_value_set(operand)
80    } else {
81        is_value(operand)
82    };
83    if !narrows {
84        return None;
85    }
86    match &expr.parameters[0] {
87        ExpressiveEnum::Nested(lhs) if lhs.parameters.is_empty() => {
88            unquote_identifier(&lhs.template)
89        }
90        _ => None,
91    }
92}
93
94/// A scalar, a deferred value, or either wrapped in any number of `{}`
95/// templates — the operand shape `col.eq(v)` builds.
96fn is_value<V>(operand: &ExpressiveEnum<V>) -> bool {
97    match operand {
98        ExpressiveEnum::Scalar(_) | ExpressiveEnum::Deferred(_) => true,
99        ExpressiveEnum::Nested(e) => {
100            e.template.trim() == "{}" && e.parameters.len() == 1 && is_value(&e.parameters[0])
101        }
102    }
103}
104
105/// An `IN` operand that names a set of values: a list of values
106/// (`{}, {}, …`, or empty / `NULL` for no values) or a subquery.
107fn is_value_set<V>(operand: &ExpressiveEnum<V>) -> bool {
108    let ExpressiveEnum::Nested(e) = operand else {
109        return false;
110    };
111    let template = e.template.trim();
112    if template == "{}" && e.parameters.len() == 1 {
113        return is_value(&e.parameters[0]) || is_value_set(&e.parameters[0]);
114    }
115    if template.is_empty() || template.eq_ignore_ascii_case("NULL") {
116        return e.parameters.is_empty();
117    }
118    if is_subquery(e) {
119        return true;
120    }
121    let slots: Vec<&str> = template.split(',').map(str::trim).collect();
122    slots.iter().all(|s| *s == "{}")
123        && slots.len() == e.parameters.len()
124        && e.parameters.iter().all(is_value)
125}
126
127/// A `SELECT` (or `WITH … SELECT`) query. SQL selects render as
128/// `{}SELECT …` with the CTE slot first, so a leading `{}` is accepted
129/// only when its parameter is empty or a `WITH` clause.
130fn is_subquery<V>(e: &Expression<V>) -> bool {
131    let mut text = e.template.trim_start().trim_start_matches('(').trim_start();
132    if let Some(rest) = text.strip_prefix("{}") {
133        match e.parameters.first() {
134            Some(ExpressiveEnum::Nested(cte))
135                if cte.template.trim().is_empty() || starts_with_keyword(&cte.template, "WITH") => {
136            }
137            _ => return false,
138        }
139        text = rest;
140    }
141    starts_with_keyword(text, "SELECT") || starts_with_keyword(text, "WITH")
142}
143
144fn starts_with_keyword(text: &str, keyword: &str) -> bool {
145    let text = text.trim_start();
146    text.get(..keyword.len())
147        .is_some_and(|w| w.eq_ignore_ascii_case(keyword))
148        && !text[keyword.len()..]
149            .chars()
150            .next()
151            .is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
152}
153
154/// The identifier `text` names, with `"…"` or `` `…` `` quoting removed and
155/// doubled quotes collapsed; `None` when it is anything but one identifier.
156pub fn unquote_identifier(text: &str) -> Option<String> {
157    let text = text.trim();
158    for q in ['"', '`'] {
159        if let Some(inner) = text.strip_prefix(q).and_then(|t| t.strip_suffix(q)) {
160            let doubled: String = [q, q].iter().collect();
161            if inner.is_empty() || inner.replace(&doubled, "").contains(q) {
162                return None;
163            }
164            return Some(inner.replace(&doubled, &q.to_string()));
165        }
166    }
167    let mut chars = text.chars();
168    let first = chars.next()?;
169    ((first.is_ascii_alphabetic() || first == '_')
170        && chars.all(|c| c.is_ascii_alphanumeric() || c == '_'))
171    .then(|| text.to_string())
172}