vantage_table/
conditions.rs1use vantage_expressions::{Expression, ExpressiveEnum};
2
3#[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
13pub 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
43const COLUMN_OPERATORS: [&str; 7] = ["=", ">", ">=", "<", "<=", "IN", "LIKE"];
48
49pub 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
94fn 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
105fn 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
127fn 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
154pub 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}