1use super::collect_from_qualifiers;
10use crate::plan::SourcePlan;
11use crate::ScalarExpr;
12use std::collections::BTreeSet;
13
14pub fn expr_contains_function(expression: &ScalarExpr) -> bool {
15 let mut contains_function = false;
16 expression.visit(&mut |part| {
17 contains_function |= matches!(
18 part,
19 ScalarExpr::Func { .. } | ScalarExpr::WindowCall { .. }
20 );
21 });
22 contains_function
23}
24
25pub fn flatten_and_filter_parts(expr: &ScalarExpr) -> Vec<&ScalarExpr> {
26 match expr {
27 ScalarExpr::And(items) => items.iter().flat_map(flatten_and_filter_parts).collect(),
28 other => vec![other],
29 }
30}
31
32pub fn from_qualifier_set(from: &SourcePlan) -> BTreeSet<String> {
33 let mut qualifiers = Vec::new();
34 collect_from_qualifiers(from, &mut qualifiers);
35 qualifiers.into_iter().collect()
36}
37
38pub fn expr_qualifiers(expr: &ScalarExpr) -> BTreeSet<String> {
39 let mut qualifiers = BTreeSet::new();
40 collect_expr_qualifiers(expr, &mut qualifiers);
41 qualifiers
42}
43
44pub fn collect_expr_qualifiers(expr: &ScalarExpr, qualifiers: &mut BTreeSet<String>) {
45 expr.visit(&mut |part| match part {
46 ScalarExpr::QualifiedColumn { qualifier, .. } | ScalarExpr::QualifiedStar(qualifier) => {
47 qualifiers.insert(qualifier.clone());
48 }
49 _ => {}
50 });
51}
52
53pub fn expr_has_unqualified_column(expr: &ScalarExpr) -> bool {
54 let mut found = false;
55 expr.visit(&mut |part| {
56 found |= matches!(part, ScalarExpr::Column(_));
57 });
58 found
59}
60
61#[expect(
62 clippy::too_many_lines,
63 reason = "preserves SELECT schema and row identity"
64)]
65pub fn qualify_unqualified_columns(expr: &ScalarExpr, qualifier: &str) -> ScalarExpr {
66 match expr {
67 ScalarExpr::Column(column) => ScalarExpr::qualified_column(qualifier, column),
68 ScalarExpr::Default
69 | ScalarExpr::Position(_)
70 | ScalarExpr::InternalColumn(_)
71 | ScalarExpr::QualifiedColumn { .. }
72 | ScalarExpr::QualifiedStar(_)
73 | ScalarExpr::Literal(_)
74 | ScalarExpr::TypedLiteral { .. }
75 | ScalarExpr::Param(_)
76 | ScalarExpr::Star => expr.clone(),
77 ScalarExpr::Array(items) => ScalarExpr::Array(
78 items
79 .iter()
80 .map(|item| qualify_unqualified_columns(item, qualifier))
81 .collect(),
82 ),
83 ScalarExpr::Row(items) => ScalarExpr::Row(
84 items
85 .iter()
86 .map(|item| qualify_unqualified_columns(item, qualifier))
87 .collect(),
88 ),
89 ScalarExpr::And(items) => ScalarExpr::And(
90 items
91 .iter()
92 .map(|item| qualify_unqualified_columns(item, qualifier))
93 .collect(),
94 ),
95 ScalarExpr::Or(items) => ScalarExpr::Or(
96 items
97 .iter()
98 .map(|item| qualify_unqualified_columns(item, qualifier))
99 .collect(),
100 ),
101 ScalarExpr::Binary { op, lhs, rhs } => ScalarExpr::Binary {
102 op: *op,
103 lhs: Box::new(qualify_unqualified_columns(lhs, qualifier)),
104 rhs: Box::new(qualify_unqualified_columns(rhs, qualifier)),
105 },
106 ScalarExpr::Not(inner) => {
107 ScalarExpr::Not(Box::new(qualify_unqualified_columns(inner, qualifier)))
108 }
109 ScalarExpr::UnaryMinus(inner) => {
110 ScalarExpr::UnaryMinus(Box::new(qualify_unqualified_columns(inner, qualifier)))
111 }
112 ScalarExpr::IsNull { expr, negated } => ScalarExpr::IsNull {
113 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
114 negated: *negated,
115 },
116 ScalarExpr::Between { expr, low, high } => ScalarExpr::Between {
117 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
118 low: Box::new(qualify_unqualified_columns(low, qualifier)),
119 high: Box::new(qualify_unqualified_columns(high, qualifier)),
120 },
121 ScalarExpr::InList {
122 expr,
123 list,
124 negated,
125 } => ScalarExpr::InList {
126 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
127 list: list
128 .iter()
129 .map(|item| qualify_unqualified_columns(item, qualifier))
130 .collect(),
131 negated: *negated,
132 },
133 ScalarExpr::Func {
134 name,
135 binding,
136 args,
137 distinct,
138 order_by,
139 filter,
140 } => ScalarExpr::Func {
141 name: name.clone(),
142 binding: binding.clone(),
143 args: args
144 .iter()
145 .map(|arg| qualify_unqualified_columns(arg, qualifier))
146 .collect(),
147 distinct: *distinct,
148 order_by: order_by.clone(),
149 filter: filter
150 .as_ref()
151 .map(|filter| Box::new(qualify_unqualified_columns(filter, qualifier))),
152 },
153 ScalarExpr::WindowCall { name, args, spec } => ScalarExpr::WindowCall {
154 name: name.clone(),
155 args: args
156 .iter()
157 .map(|arg| qualify_unqualified_columns(arg, qualifier))
158 .collect(),
159 spec: spec.clone(),
160 },
161 ScalarExpr::Case {
162 base,
163 when,
164 else_branch,
165 } => ScalarExpr::Case {
166 base: base
167 .as_ref()
168 .map(|expr| Box::new(qualify_unqualified_columns(expr, qualifier))),
169 when: when
170 .iter()
171 .map(|(cond, result)| {
172 (
173 qualify_unqualified_columns(cond, qualifier),
174 qualify_unqualified_columns(result, qualifier),
175 )
176 })
177 .collect(),
178 else_branch: else_branch
179 .as_ref()
180 .map(|expr| Box::new(qualify_unqualified_columns(expr, qualifier))),
181 },
182 ScalarExpr::Cast { expr, ty } => ScalarExpr::Cast {
183 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
184 ty: ty.clone(),
185 },
186 ScalarExpr::InSubquery {
187 expr,
188 subquery,
189 negated,
190 } => ScalarExpr::InSubquery {
191 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
192 subquery: *subquery,
193 negated: *negated,
194 },
195 ScalarExpr::ScalarSubquery(_) | ScalarExpr::Exists { .. } => expr.clone(),
196 }
197}
198
199pub fn expr_is_null_free(expr: &ScalarExpr) -> bool {
204 match expr {
205 ScalarExpr::Func { name, .. } => crate::registry::is_registered(name),
206 ScalarExpr::IsNull { .. } => true,
207 ScalarExpr::Exists { .. } => true,
208 ScalarExpr::Literal(v) => !matches!(v, uqa_core::Value::Null),
209 ScalarExpr::And(parts) | ScalarExpr::Or(parts) => parts.iter().all(expr_is_null_free),
210 ScalarExpr::Not(inner) => expr_is_null_free(inner),
211 _ => false,
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use super::{expr_contains_function, expr_has_unqualified_column, expr_qualifiers, ScalarExpr};
218 use crate::ast::FrameMode;
219 use crate::{ScalarFrameBound, ScalarWindowFrame, ScalarWindowSpec};
220
221 #[test]
222 fn expression_shape_uses_complete_scalar_traversal() {
223 let expression = ScalarExpr::WindowCall {
224 name: "sum".into(),
225 args: vec![ScalarExpr::QualifiedColumn {
226 qualifier: "orders".into(),
227 column: "amount".into(),
228 }],
229 spec: ScalarWindowSpec {
230 partition_by: Vec::new(),
231 order_by: Vec::new(),
232 frame: Some(ScalarWindowFrame {
233 mode: FrameMode::Rows,
234 start: ScalarFrameBound::Preceding(Box::new(ScalarExpr::Column(
235 "frame_width".into(),
236 ))),
237 end: ScalarFrameBound::CurrentRow,
238 }),
239 },
240 };
241 assert!(expr_contains_function(&expression));
242 assert!(expr_has_unqualified_column(&expression));
243 assert_eq!(
244 expr_qualifiers(&expression),
245 std::collections::BTreeSet::from(["orders".into()])
246 );
247 }
248}