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::CompositeRow {
90 items,
91 binding,
92 bound_type,
93 } => ScalarExpr::CompositeRow {
94 items: items
95 .iter()
96 .map(|item| qualify_unqualified_columns(item, qualifier))
97 .collect(),
98 binding: binding.clone(),
99 bound_type: bound_type.clone(),
100 },
101 ScalarExpr::And(items) => ScalarExpr::And(
102 items
103 .iter()
104 .map(|item| qualify_unqualified_columns(item, qualifier))
105 .collect(),
106 ),
107 ScalarExpr::Or(items) => ScalarExpr::Or(
108 items
109 .iter()
110 .map(|item| qualify_unqualified_columns(item, qualifier))
111 .collect(),
112 ),
113 ScalarExpr::Binary { op, lhs, rhs } => ScalarExpr::Binary {
114 op: *op,
115 lhs: Box::new(qualify_unqualified_columns(lhs, qualifier)),
116 rhs: Box::new(qualify_unqualified_columns(rhs, qualifier)),
117 },
118 ScalarExpr::Not(inner) => {
119 ScalarExpr::Not(Box::new(qualify_unqualified_columns(inner, qualifier)))
120 }
121 ScalarExpr::UnaryMinus(inner) => {
122 ScalarExpr::UnaryMinus(Box::new(qualify_unqualified_columns(inner, qualifier)))
123 }
124 ScalarExpr::IsNull { expr, negated } => ScalarExpr::IsNull {
125 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
126 negated: *negated,
127 },
128 ScalarExpr::Between { expr, low, high } => ScalarExpr::Between {
129 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
130 low: Box::new(qualify_unqualified_columns(low, qualifier)),
131 high: Box::new(qualify_unqualified_columns(high, qualifier)),
132 },
133 ScalarExpr::InList {
134 expr,
135 list,
136 negated,
137 } => ScalarExpr::InList {
138 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
139 list: list
140 .iter()
141 .map(|item| qualify_unqualified_columns(item, qualifier))
142 .collect(),
143 negated: *negated,
144 },
145 ScalarExpr::Func {
146 order_syntax,
147 name,
148 binding,
149 args,
150 distinct,
151 order_by,
152 filter,
153 } => ScalarExpr::Func {
154 order_syntax: *order_syntax,
155 name: name.clone(),
156 binding: binding.clone(),
157 args: args
158 .iter()
159 .map(|arg| qualify_unqualified_columns(arg, qualifier))
160 .collect(),
161 distinct: *distinct,
162 order_by: order_by.clone(),
163 filter: filter
164 .as_ref()
165 .map(|filter| Box::new(qualify_unqualified_columns(filter, qualifier))),
166 },
167 ScalarExpr::WindowCall {
168 name,
169 args,
170 spec,
171 filter,
172 modifiers,
173 } => ScalarExpr::WindowCall {
174 modifiers: *modifiers,
175 name: name.clone(),
176 args: args
177 .iter()
178 .map(|arg| qualify_unqualified_columns(arg, qualifier))
179 .collect(),
180 spec: spec.clone(),
181 filter: filter
182 .as_ref()
183 .map(|filter| Box::new(qualify_unqualified_columns(filter, qualifier))),
184 },
185 ScalarExpr::Case {
186 base,
187 when,
188 else_branch,
189 } => ScalarExpr::Case {
190 base: base
191 .as_ref()
192 .map(|expr| Box::new(qualify_unqualified_columns(expr, qualifier))),
193 when: when
194 .iter()
195 .map(|(cond, result)| {
196 (
197 qualify_unqualified_columns(cond, qualifier),
198 qualify_unqualified_columns(result, qualifier),
199 )
200 })
201 .collect(),
202 else_branch: else_branch
203 .as_ref()
204 .map(|expr| Box::new(qualify_unqualified_columns(expr, qualifier))),
205 },
206 ScalarExpr::Collated {
207 expr,
208 ty,
209 collation,
210 } => ScalarExpr::Collated {
211 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
212 ty: ty.clone(),
213 collation: *collation,
214 },
215 ScalarExpr::Cast { expr, ty, implicit } => ScalarExpr::Cast {
216 implicit: *implicit,
217 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
218 ty: ty.clone(),
219 },
220 ScalarExpr::InSubquery {
221 expr,
222 subquery,
223 negated,
224 } => ScalarExpr::InSubquery {
225 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
226 subquery: *subquery,
227 negated: *negated,
228 },
229 ScalarExpr::ScalarSubquery(_) | ScalarExpr::Exists { .. } => expr.clone(),
230 }
231}
232
233pub fn expr_is_null_free(expr: &ScalarExpr) -> bool {
238 match expr {
239 ScalarExpr::Func { name, .. } => crate::registry::is_registered(name),
240 ScalarExpr::IsNull { .. } => true,
241 ScalarExpr::Exists { .. } => true,
242 ScalarExpr::Literal(v) => !matches!(v, uqa_core::Value::Null),
243 ScalarExpr::And(parts) | ScalarExpr::Or(parts) => parts.iter().all(expr_is_null_free),
244 ScalarExpr::Not(inner) => expr_is_null_free(inner),
245 _ => false,
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::{expr_contains_function, expr_has_unqualified_column, expr_qualifiers, ScalarExpr};
252 use crate::ast::{FrameExclusion, FrameMode};
253 use crate::{ScalarFrameBound, ScalarWindowFrame, ScalarWindowSpec};
254
255 #[test]
256 fn expression_shape_uses_complete_scalar_traversal() {
257 let expression = ScalarExpr::WindowCall {
258 name: "sum".into(),
259 args: vec![ScalarExpr::QualifiedColumn {
260 qualifier: "orders".into(),
261 column: "amount".into(),
262 }],
263 spec: ScalarWindowSpec {
264 definition: None,
265 partition_by: Vec::new(),
266 order_by: Vec::new(),
267 frame: Some(ScalarWindowFrame {
268 mode: FrameMode::Rows,
269 start: ScalarFrameBound::Preceding(Box::new(ScalarExpr::Column(
270 "frame_width".into(),
271 ))),
272 end: ScalarFrameBound::CurrentRow,
273 between: true,
274 exclusion: FrameExclusion::NoOthers,
275 }),
276 },
277 filter: None,
278 modifiers: crate::ast::WindowCallModifiers::default(),
279 };
280 assert!(expr_contains_function(&expression));
281 assert!(expr_has_unqualified_column(&expression));
282 assert_eq!(
283 expr_qualifiers(&expression),
284 std::collections::BTreeSet::from(["orders".into()])
285 );
286 }
287}