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::Cast { expr, ty, implicit } => ScalarExpr::Cast {
207 implicit: *implicit,
208 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
209 ty: ty.clone(),
210 },
211 ScalarExpr::InSubquery {
212 expr,
213 subquery,
214 negated,
215 } => ScalarExpr::InSubquery {
216 expr: Box::new(qualify_unqualified_columns(expr, qualifier)),
217 subquery: *subquery,
218 negated: *negated,
219 },
220 ScalarExpr::ScalarSubquery(_) | ScalarExpr::Exists { .. } => expr.clone(),
221 }
222}
223
224pub fn expr_is_null_free(expr: &ScalarExpr) -> bool {
229 match expr {
230 ScalarExpr::Func { name, .. } => crate::registry::is_registered(name),
231 ScalarExpr::IsNull { .. } => true,
232 ScalarExpr::Exists { .. } => true,
233 ScalarExpr::Literal(v) => !matches!(v, uqa_core::Value::Null),
234 ScalarExpr::And(parts) | ScalarExpr::Or(parts) => parts.iter().all(expr_is_null_free),
235 ScalarExpr::Not(inner) => expr_is_null_free(inner),
236 _ => false,
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::{expr_contains_function, expr_has_unqualified_column, expr_qualifiers, ScalarExpr};
243 use crate::ast::{FrameExclusion, FrameMode};
244 use crate::{ScalarFrameBound, ScalarWindowFrame, ScalarWindowSpec};
245
246 #[test]
247 fn expression_shape_uses_complete_scalar_traversal() {
248 let expression = ScalarExpr::WindowCall {
249 name: "sum".into(),
250 args: vec![ScalarExpr::QualifiedColumn {
251 qualifier: "orders".into(),
252 column: "amount".into(),
253 }],
254 spec: ScalarWindowSpec {
255 definition: None,
256 partition_by: Vec::new(),
257 order_by: Vec::new(),
258 frame: Some(ScalarWindowFrame {
259 mode: FrameMode::Rows,
260 start: ScalarFrameBound::Preceding(Box::new(ScalarExpr::Column(
261 "frame_width".into(),
262 ))),
263 end: ScalarFrameBound::CurrentRow,
264 between: true,
265 exclusion: FrameExclusion::NoOthers,
266 }),
267 },
268 filter: None,
269 modifiers: crate::ast::WindowCallModifiers::default(),
270 };
271 assert!(expr_contains_function(&expression));
272 assert!(expr_has_unqualified_column(&expression));
273 assert_eq!(
274 expr_qualifiers(&expression),
275 std::collections::BTreeSet::from(["orders".into()])
276 );
277 }
278}