Skip to main content

uqa_sql/ir/
traversal.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Complete scalar IR traversal.
8
9use super::{ScalarExpr, ScalarFrameBound};
10
11impl ScalarExpr {
12    /// Visit this expression and every nested scalar expression in pre-order.
13    pub fn visit(&self, visitor: &mut impl FnMut(&Self)) {
14        self.try_visit(&mut |expression| {
15            visitor(expression);
16            Ok::<_, std::convert::Infallible>(true)
17        })
18        .unwrap_or_else(|never| match never {});
19    }
20
21    /// Visit a window call's arguments, `FILTER` condition, partition and ordering keys, and frame offsets.
22    fn try_visit_window<E>(
23        args: &[Self],
24        filter: Option<&Self>,
25        spec: &super::ScalarWindowSpec,
26        visitor: &mut impl FnMut(&Self) -> Result<bool, E>,
27    ) -> Result<(), E> {
28        for expression in args
29            .iter()
30            .chain(filter)
31            .chain(&spec.partition_by)
32            .chain(spec.order_by.iter().map(|order| &order.expr))
33        {
34            expression.try_visit(visitor)?;
35        }
36        for bound in spec
37            .frame
38            .iter()
39            .flat_map(|frame| [&frame.start, &frame.end])
40        {
41            if let ScalarFrameBound::Preceding(expression)
42            | ScalarFrameBound::Following(expression) = bound
43            {
44                expression.try_visit(visitor)?;
45            }
46        }
47        Ok(())
48    }
49
50    /// Visit in pre-order, skipping a subtree when the visitor returns false and stopping immediately on its first error.
51    pub fn try_visit<E>(
52        &self,
53        visitor: &mut impl FnMut(&Self) -> Result<bool, E>,
54    ) -> Result<(), E> {
55        if !visitor(self)? {
56            return Ok(());
57        }
58        match self {
59            Self::And(parts)
60            | Self::Or(parts)
61            | Self::Array(parts)
62            | Self::Row(parts)
63            | Self::CompositeRow { items: parts, .. } => {
64                for part in parts {
65                    part.try_visit(visitor)?;
66                }
67            }
68            Self::Not(inner)
69            | Self::UnaryMinus(inner)
70            | Self::Cast { expr: inner, .. }
71            | Self::Collated { expr: inner, .. }
72            | Self::IsNull { expr: inner, .. }
73            | Self::InSubquery { expr: inner, .. } => inner.try_visit(visitor)?,
74            Self::Binary { lhs, rhs, .. } => {
75                lhs.try_visit(visitor)?;
76                rhs.try_visit(visitor)?;
77            }
78            Self::Between { expr, low, high } => {
79                expr.try_visit(visitor)?;
80                low.try_visit(visitor)?;
81                high.try_visit(visitor)?;
82            }
83            Self::InList { expr, list, .. } => {
84                expr.try_visit(visitor)?;
85                for part in list {
86                    part.try_visit(visitor)?;
87                }
88            }
89            Self::Func {
90                args,
91                order_by,
92                filter,
93                ..
94            } => {
95                for argument in args {
96                    argument.try_visit(visitor)?;
97                }
98                for order in order_by {
99                    order.expr.try_visit(visitor)?;
100                }
101                if let Some(filter) = filter {
102                    filter.try_visit(visitor)?;
103                }
104            }
105            Self::WindowCall {
106                args, spec, filter, ..
107            } => Self::try_visit_window(args, filter.as_deref(), spec, visitor)?,
108            Self::Case {
109                base,
110                when,
111                else_branch,
112            } => {
113                if let Some(base) = base {
114                    base.try_visit(visitor)?;
115                }
116                for (condition, result) in when {
117                    condition.try_visit(visitor)?;
118                    result.try_visit(visitor)?;
119                }
120                if let Some(else_branch) = else_branch {
121                    else_branch.try_visit(visitor)?;
122                }
123            }
124            Self::Default
125            | Self::Star
126            | Self::QualifiedStar(_)
127            | Self::Column(_)
128            | Self::Position(_)
129            | Self::InternalColumn(_)
130            | Self::QualifiedColumn { .. }
131            | Self::Literal(_)
132            | Self::TypedLiteral { .. }
133            | Self::Param(_)
134            | Self::ScalarSubquery(_)
135            | Self::Exists { .. } => {}
136        }
137        Ok(())
138    }
139
140    /// Visit a window call's arguments, `FILTER` condition, partition and ordering keys, and frame offsets mutably.
141    fn visit_window_mut(
142        args: &mut [Self],
143        filter: Option<&mut Self>,
144        spec: &mut super::ScalarWindowSpec,
145        visitor: &mut impl FnMut(&mut Self),
146    ) {
147        for expression in args
148            .iter_mut()
149            .chain(filter)
150            .chain(&mut spec.partition_by)
151            .chain(spec.order_by.iter_mut().map(|order| &mut order.expr))
152        {
153            expression.visit_mut(visitor);
154        }
155        for bound in spec
156            .frame
157            .iter_mut()
158            .flat_map(|frame| [&mut frame.start, &mut frame.end])
159        {
160            if let ScalarFrameBound::Preceding(expression)
161            | ScalarFrameBound::Following(expression) = bound
162            {
163                expression.visit_mut(visitor);
164            }
165        }
166    }
167
168    /// Visit this expression and every nested scalar expression mutably in pre-order: a node is visited before its children, so the children of a node the visitor replaces are those of the replacement.
169    pub fn visit_mut(&mut self, visitor: &mut impl FnMut(&mut Self)) {
170        visitor(self);
171        match self {
172            Self::And(parts)
173            | Self::Or(parts)
174            | Self::Array(parts)
175            | Self::Row(parts)
176            | Self::CompositeRow { items: parts, .. } => {
177                for part in parts {
178                    part.visit_mut(visitor);
179                }
180            }
181            Self::Not(inner)
182            | Self::UnaryMinus(inner)
183            | Self::Cast { expr: inner, .. }
184            | Self::Collated { expr: inner, .. }
185            | Self::IsNull { expr: inner, .. }
186            | Self::InSubquery { expr: inner, .. } => inner.visit_mut(visitor),
187            Self::Binary { lhs, rhs, .. } => {
188                lhs.visit_mut(visitor);
189                rhs.visit_mut(visitor);
190            }
191            Self::Between { expr, low, high } => {
192                expr.visit_mut(visitor);
193                low.visit_mut(visitor);
194                high.visit_mut(visitor);
195            }
196            Self::InList { expr, list, .. } => {
197                expr.visit_mut(visitor);
198                for part in list {
199                    part.visit_mut(visitor);
200                }
201            }
202            Self::Func {
203                args,
204                order_by,
205                filter,
206                ..
207            } => {
208                for argument in args {
209                    argument.visit_mut(visitor);
210                }
211                for order in order_by {
212                    order.expr.visit_mut(visitor);
213                }
214                if let Some(filter) = filter {
215                    filter.visit_mut(visitor);
216                }
217            }
218            Self::WindowCall {
219                args, spec, filter, ..
220            } => Self::visit_window_mut(args, filter.as_deref_mut(), spec, visitor),
221            Self::Case {
222                base,
223                when,
224                else_branch,
225            } => {
226                if let Some(base) = base {
227                    base.visit_mut(visitor);
228                }
229                for (condition, result) in when {
230                    condition.visit_mut(visitor);
231                    result.visit_mut(visitor);
232                }
233                if let Some(else_branch) = else_branch {
234                    else_branch.visit_mut(visitor);
235                }
236            }
237            Self::Default
238            | Self::Star
239            | Self::QualifiedStar(_)
240            | Self::Column(_)
241            | Self::Position(_)
242            | Self::InternalColumn(_)
243            | Self::QualifiedColumn { .. }
244            | Self::Literal(_)
245            | Self::TypedLiteral { .. }
246            | Self::Param(_)
247            | Self::ScalarSubquery(_)
248            | Self::Exists { .. } => {}
249        }
250    }
251
252    /// Collect every column needed to evaluate this expression. Returns `false` when evaluation needs row shape or a relational child that a projected field scan cannot provide.
253    pub fn collect_columns(&self, output: &mut std::collections::BTreeSet<String>) -> bool {
254        match self.try_visit_columns(&mut |name| {
255            output.insert(name.to_owned());
256            Ok::<_, std::convert::Infallible>(())
257        }) {
258            Ok(projectable) => projectable,
259            Err(never) => match never {},
260        }
261    }
262
263    /// Borrow referenced column names in evaluation-tree order, stopping at the first visitor failure or unprojectable expression. Qualified references yield their column component, matching `collect_columns`; repeated references remain visible to the visitor. No name or result container is allocated by this traversal.
264    pub fn try_visit_columns<'a, E>(
265        &'a self,
266        visitor: &mut impl FnMut(&'a str) -> Result<(), E>,
267    ) -> Result<bool, E> {
268        match self {
269            Self::Column(name) | Self::QualifiedColumn { column: name, .. } => {
270                visitor(name)?;
271                Ok(true)
272            }
273            Self::Literal(_)
274            | Self::TypedLiteral { .. }
275            | Self::Param(_)
276            | Self::InternalColumn(_) => Ok(true),
277            Self::Func {
278                args,
279                order_by,
280                filter,
281                ..
282            } => {
283                for expression in args
284                    .iter()
285                    .chain(order_by.iter().map(|order| &order.expr))
286                    .chain(filter.as_deref())
287                {
288                    if !expression.try_visit_columns(visitor)? {
289                        return Ok(false);
290                    }
291                }
292                Ok(true)
293            }
294            Self::Array(items)
295            | Self::Row(items)
296            | Self::CompositeRow { items, .. }
297            | Self::And(items)
298            | Self::Or(items) => {
299                for item in items {
300                    if !item.try_visit_columns(visitor)? {
301                        return Ok(false);
302                    }
303                }
304                Ok(true)
305            }
306            Self::Binary { lhs, rhs, .. } => {
307                Ok(lhs.try_visit_columns(visitor)? && rhs.try_visit_columns(visitor)?)
308            }
309            Self::UnaryMinus(expr)
310            | Self::Not(expr)
311            | Self::IsNull { expr, .. }
312            | Self::Cast { expr, .. }
313            | Self::Collated { expr, .. } => expr.try_visit_columns(visitor),
314            Self::Between { expr, low, high } => Ok(expr.try_visit_columns(visitor)?
315                && low.try_visit_columns(visitor)?
316                && high.try_visit_columns(visitor)?),
317            Self::InList { expr, list, .. } => {
318                for item in std::iter::once(expr.as_ref()).chain(list) {
319                    if !item.try_visit_columns(visitor)? {
320                        return Ok(false);
321                    }
322                }
323                Ok(true)
324            }
325            Self::Case {
326                base,
327                when,
328                else_branch,
329            } => {
330                for expression in base
331                    .as_deref()
332                    .into_iter()
333                    .chain(
334                        when.iter()
335                            .flat_map(|(condition, result)| [condition, result]),
336                    )
337                    .chain(else_branch.as_deref())
338                {
339                    if !expression.try_visit_columns(visitor)? {
340                        return Ok(false);
341                    }
342                }
343                Ok(true)
344            }
345            Self::Default
346            | Self::Star
347            | Self::QualifiedStar(_)
348            | Self::Position(_)
349            | Self::WindowCall { .. }
350            | Self::ScalarSubquery(_)
351            | Self::Exists { .. }
352            | Self::InSubquery { .. } => Ok(false),
353        }
354    }
355
356    #[must_use]
357    pub fn contains_window(&self) -> bool {
358        match self {
359            Self::WindowCall { .. } => true,
360            Self::Func {
361                args,
362                order_by,
363                filter,
364                ..
365            } => {
366                args.iter().any(Self::contains_window)
367                    || order_by.iter().any(|order| order.expr.contains_window())
368                    || filter.as_deref().is_some_and(Self::contains_window)
369            }
370            Self::Array(items)
371            | Self::Row(items)
372            | Self::CompositeRow { items, .. }
373            | Self::And(items)
374            | Self::Or(items) => items.iter().any(Self::contains_window),
375            Self::Binary { lhs, rhs, .. } => lhs.contains_window() || rhs.contains_window(),
376            Self::UnaryMinus(expr)
377            | Self::Not(expr)
378            | Self::IsNull { expr, .. }
379            | Self::Cast { expr, .. }
380            | Self::Collated { expr, .. }
381            | Self::InSubquery { expr, .. } => expr.contains_window(),
382            Self::Between { expr, low, high } => {
383                expr.contains_window() || low.contains_window() || high.contains_window()
384            }
385            Self::InList { expr, list, .. } => {
386                expr.contains_window() || list.iter().any(Self::contains_window)
387            }
388            Self::Case {
389                base,
390                when,
391                else_branch,
392            } => {
393                base.as_deref().is_some_and(Self::contains_window)
394                    || when.iter().any(|(condition, result)| {
395                        condition.contains_window() || result.contains_window()
396                    })
397                    || else_branch.as_deref().is_some_and(Self::contains_window)
398            }
399            Self::Default
400            | Self::Star
401            | Self::QualifiedStar(_)
402            | Self::Column(_)
403            | Self::QualifiedColumn { .. }
404            | Self::Position(_)
405            | Self::InternalColumn(_)
406            | Self::Literal(_)
407            | Self::TypedLiteral { .. }
408            | Self::Param(_)
409            | Self::ScalarSubquery(_)
410            | Self::Exists { .. } => false,
411        }
412    }
413
414    #[must_use]
415    pub fn contains_subquery(&self) -> bool {
416        match self {
417            Self::ScalarSubquery(_) | Self::Exists { .. } | Self::InSubquery { .. } => true,
418            Self::Func {
419                args,
420                order_by,
421                filter,
422                ..
423            } => {
424                args.iter().any(Self::contains_subquery)
425                    || order_by.iter().any(|order| order.expr.contains_subquery())
426                    || filter.as_deref().is_some_and(Self::contains_subquery)
427            }
428            Self::Array(items)
429            | Self::Row(items)
430            | Self::CompositeRow { items, .. }
431            | Self::And(items)
432            | Self::Or(items) => items.iter().any(Self::contains_subquery),
433            Self::Binary { lhs, rhs, .. } => lhs.contains_subquery() || rhs.contains_subquery(),
434            Self::UnaryMinus(expr)
435            | Self::Not(expr)
436            | Self::IsNull { expr, .. }
437            | Self::Cast { expr, .. }
438            | Self::Collated { expr, .. } => expr.contains_subquery(),
439            Self::Between { expr, low, high } => {
440                expr.contains_subquery() || low.contains_subquery() || high.contains_subquery()
441            }
442            Self::InList { expr, list, .. } => {
443                expr.contains_subquery() || list.iter().any(Self::contains_subquery)
444            }
445            Self::WindowCall {
446                args, spec, filter, ..
447            } => {
448                args.iter().any(Self::contains_subquery)
449                    || filter.as_deref().is_some_and(Self::contains_subquery)
450                    || spec.partition_by.iter().any(Self::contains_subquery)
451                    || spec
452                        .order_by
453                        .iter()
454                        .any(|order| order.expr.contains_subquery())
455                    || spec.frame.as_ref().is_some_and(|frame| {
456                        frame_has(&frame.start, Self::contains_subquery)
457                            || frame_has(&frame.end, Self::contains_subquery)
458                    })
459            }
460            Self::Case {
461                base,
462                when,
463                else_branch,
464            } => {
465                base.as_deref().is_some_and(Self::contains_subquery)
466                    || when.iter().any(|(condition, result)| {
467                        condition.contains_subquery() || result.contains_subquery()
468                    })
469                    || else_branch.as_deref().is_some_and(Self::contains_subquery)
470            }
471            Self::Default
472            | Self::Star
473            | Self::QualifiedStar(_)
474            | Self::Column(_)
475            | Self::QualifiedColumn { .. }
476            | Self::Position(_)
477            | Self::InternalColumn(_)
478            | Self::Literal(_)
479            | Self::TypedLiteral { .. }
480            | Self::Param(_) => false,
481        }
482    }
483
484    #[must_use]
485    pub fn contains_parameter(&self) -> bool {
486        match self {
487            Self::Param(_) => true,
488            Self::Func {
489                args,
490                order_by,
491                filter,
492                ..
493            } => {
494                args.iter().any(Self::contains_parameter)
495                    || order_by.iter().any(|order| order.expr.contains_parameter())
496                    || filter.as_deref().is_some_and(Self::contains_parameter)
497            }
498            Self::Array(items)
499            | Self::Row(items)
500            | Self::CompositeRow { items, .. }
501            | Self::And(items)
502            | Self::Or(items) => items.iter().any(Self::contains_parameter),
503            Self::Binary { lhs, rhs, .. } => lhs.contains_parameter() || rhs.contains_parameter(),
504            Self::UnaryMinus(expr)
505            | Self::Not(expr)
506            | Self::IsNull { expr, .. }
507            | Self::Cast { expr, .. }
508            | Self::Collated { expr, .. }
509            | Self::InSubquery { expr, .. } => expr.contains_parameter(),
510            Self::Between { expr, low, high } => {
511                expr.contains_parameter() || low.contains_parameter() || high.contains_parameter()
512            }
513            Self::InList { expr, list, .. } => {
514                expr.contains_parameter() || list.iter().any(Self::contains_parameter)
515            }
516            Self::WindowCall {
517                args, spec, filter, ..
518            } => {
519                args.iter().any(Self::contains_parameter)
520                    || filter.as_deref().is_some_and(Self::contains_parameter)
521                    || spec.partition_by.iter().any(Self::contains_parameter)
522                    || spec
523                        .order_by
524                        .iter()
525                        .any(|order| order.expr.contains_parameter())
526                    || spec.frame.as_ref().is_some_and(|frame| {
527                        frame_has(&frame.start, Self::contains_parameter)
528                            || frame_has(&frame.end, Self::contains_parameter)
529                    })
530            }
531            Self::Case {
532                base,
533                when,
534                else_branch,
535            } => {
536                base.as_deref().is_some_and(Self::contains_parameter)
537                    || when.iter().any(|(condition, result)| {
538                        condition.contains_parameter() || result.contains_parameter()
539                    })
540                    || else_branch.as_deref().is_some_and(Self::contains_parameter)
541            }
542            Self::Default
543            | Self::Star
544            | Self::QualifiedStar(_)
545            | Self::Column(_)
546            | Self::QualifiedColumn { .. }
547            | Self::Position(_)
548            | Self::InternalColumn(_)
549            | Self::Literal(_)
550            | Self::TypedLiteral { .. }
551            | Self::ScalarSubquery(_)
552            | Self::Exists { .. } => false,
553        }
554    }
555
556    #[must_use]
557    pub fn contains_aggregate(&self, is_aggregate: &dyn Fn(&str) -> bool) -> bool {
558        match self {
559            Self::Func {
560                name,
561                args,
562                order_by,
563                filter,
564                ..
565            } => {
566                is_aggregate(name)
567                    || args
568                        .iter()
569                        .any(|expression| expression.contains_aggregate(is_aggregate))
570                    || order_by
571                        .iter()
572                        .any(|order| order.expr.contains_aggregate(is_aggregate))
573                    || filter
574                        .as_deref()
575                        .is_some_and(|expression| expression.contains_aggregate(is_aggregate))
576            }
577            Self::Array(items)
578            | Self::Row(items)
579            | Self::CompositeRow { items, .. }
580            | Self::And(items)
581            | Self::Or(items) => items
582                .iter()
583                .any(|expression| expression.contains_aggregate(is_aggregate)),
584            Self::Binary { lhs, rhs, .. } => {
585                lhs.contains_aggregate(is_aggregate) || rhs.contains_aggregate(is_aggregate)
586            }
587            Self::UnaryMinus(expr)
588            | Self::Not(expr)
589            | Self::IsNull { expr, .. }
590            | Self::Cast { expr, .. }
591            | Self::Collated { expr, .. }
592            | Self::InSubquery { expr, .. } => expr.contains_aggregate(is_aggregate),
593            Self::Between { expr, low, high } => {
594                expr.contains_aggregate(is_aggregate)
595                    || low.contains_aggregate(is_aggregate)
596                    || high.contains_aggregate(is_aggregate)
597            }
598            Self::InList { expr, list, .. } => {
599                expr.contains_aggregate(is_aggregate)
600                    || list
601                        .iter()
602                        .any(|item| item.contains_aggregate(is_aggregate))
603            }
604            Self::Case {
605                base,
606                when,
607                else_branch,
608            } => {
609                base.as_deref()
610                    .is_some_and(|expression| expression.contains_aggregate(is_aggregate))
611                    || when.iter().any(|(condition, result)| {
612                        condition.contains_aggregate(is_aggregate)
613                            || result.contains_aggregate(is_aggregate)
614                    })
615                    || else_branch
616                        .as_deref()
617                        .is_some_and(|expression| expression.contains_aggregate(is_aggregate))
618            }
619            Self::Default
620            | Self::Star
621            | Self::QualifiedStar(_)
622            | Self::Column(_)
623            | Self::QualifiedColumn { .. }
624            | Self::Position(_)
625            | Self::InternalColumn(_)
626            | Self::Literal(_)
627            | Self::TypedLiteral { .. }
628            | Self::Param(_)
629            | Self::ScalarSubquery(_)
630            | Self::Exists { .. }
631            | Self::WindowCall { .. } => false,
632        }
633    }
634}
635
636fn frame_has(bound: &ScalarFrameBound, predicate: fn(&ScalarExpr) -> bool) -> bool {
637    match bound {
638        ScalarFrameBound::Preceding(expression) | ScalarFrameBound::Following(expression) => {
639            predicate(expression)
640        }
641        ScalarFrameBound::UnboundedPreceding
642        | ScalarFrameBound::UnboundedFollowing
643        | ScalarFrameBound::CurrentRow => false,
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::{ScalarExpr, ScalarFrameBound};
650    use crate::ast::{FrameExclusion, FrameMode};
651    use uqa_core::Value;
652
653    #[test]
654    fn visit_includes_root_and_nested_expressions() {
655        let expression = ScalarExpr::Binary {
656            op: crate::ast::BinaryOp::Add,
657            lhs: Box::new(ScalarExpr::Column("amount".into())),
658            rhs: Box::new(ScalarExpr::Literal(Value::Int(1))),
659        };
660        let mut visited = Vec::new();
661        expression.visit(&mut |part| visited.push(part.clone()));
662        assert_eq!(visited.len(), 3);
663        assert_eq!(visited[0], expression);
664    }
665
666    #[test]
667    fn fallible_visits_skip_selected_subtrees_and_stop_before_later_siblings() {
668        let expression = ScalarExpr::Row(vec![
669            ScalarExpr::Array(vec![ScalarExpr::Column("hidden".into())]),
670            ScalarExpr::Column("reject".into()),
671            ScalarExpr::Column("unvisited".into()),
672        ]);
673        let mut visited = Vec::new();
674        let result = expression.try_visit(&mut |part| {
675            visited.push(part.clone());
676            match part {
677                ScalarExpr::Array(_) => Ok(false),
678                ScalarExpr::Column(name) if name == "reject" => Err("grouping"),
679                _ => Ok(true),
680            }
681        });
682        assert_eq!(result, Err("grouping"));
683        assert_eq!(visited.len(), 3);
684        assert!(matches!(&visited[2], ScalarExpr::Column(name) if name == "reject"));
685    }
686
687    #[test]
688    fn traversal_includes_window_frame_expressions() {
689        let expression = ScalarExpr::WindowCall {
690            name: "sum".into(),
691            args: vec![ScalarExpr::Column("amount".into())],
692            spec: super::super::ScalarWindowSpec {
693                definition: None,
694                partition_by: vec![ScalarExpr::QualifiedColumn {
695                    qualifier: "orders".into(),
696                    column: "account_id".into(),
697                }],
698                order_by: Vec::new(),
699                frame: Some(super::super::ScalarWindowFrame {
700                    mode: FrameMode::Rows,
701                    start: ScalarFrameBound::Preceding(Box::new(ScalarExpr::Param(0))),
702                    end: ScalarFrameBound::CurrentRow,
703                    between: true,
704                    exclusion: FrameExclusion::NoOthers,
705                }),
706            },
707            filter: None,
708            modifiers: crate::ast::WindowCallModifiers::default(),
709        };
710        let mut visited_parameter = false;
711        expression.visit(&mut |part| {
712            visited_parameter |= matches!(part, ScalarExpr::Param(0));
713        });
714        assert!(visited_parameter);
715        assert!(expression.contains_window());
716        assert!(expression.contains_parameter());
717    }
718
719    #[test]
720    fn mutable_visits_rewrite_nested_expressions() {
721        let literal = || ScalarExpr::Literal(Value::Str("t".into()));
722        let mut expression = ScalarExpr::Cast {
723            implicit: false,
724            expr: Box::new(ScalarExpr::Case {
725                base: None,
726                when: vec![(
727                    literal(),
728                    ScalarExpr::Cast {
729                        implicit: false,
730                        expr: Box::new(literal()),
731                        ty: "regclass".into(),
732                    },
733                )],
734                else_branch: Some(Box::new(ScalarExpr::WindowCall {
735                    name: "sum".into(),
736                    args: vec![literal()],
737                    spec: super::super::ScalarWindowSpec {
738                        definition: None,
739                        partition_by: Vec::new(),
740                        order_by: Vec::new(),
741                        frame: Some(super::super::ScalarWindowFrame {
742                            mode: FrameMode::Rows,
743                            start: ScalarFrameBound::Preceding(Box::new(literal())),
744                            end: ScalarFrameBound::CurrentRow,
745                            between: true,
746                            exclusion: FrameExclusion::NoOthers,
747                        }),
748                    },
749                    filter: Some(Box::new(literal())),
750                    modifiers: crate::ast::WindowCallModifiers::default(),
751                })),
752            }),
753            ty: "text".into(),
754        };
755        let mut rewritten = 0;
756        expression.visit_mut(&mut |part| {
757            if matches!(part, ScalarExpr::Literal(Value::Str(_))) {
758                *part = ScalarExpr::Literal(Value::Int(1));
759                rewritten += 1;
760            }
761        });
762        assert_eq!(rewritten, 5);
763        let mut remaining = 0;
764        expression.visit(&mut |part| {
765            remaining += usize::from(matches!(part, ScalarExpr::Literal(Value::Str(_))));
766        });
767        assert_eq!(remaining, 0);
768    }
769
770    #[test]
771    fn owned_walkers_preserve_column_and_aggregate_policy() {
772        let expression = ScalarExpr::Func {
773            order_syntax: crate::ast::FunctionOrderSyntax::Ordinary,
774            name: "sum".into(),
775            binding: None,
776            args: vec![ScalarExpr::QualifiedColumn {
777                qualifier: "orders".into(),
778                column: "amount".into(),
779            }],
780            distinct: false,
781            order_by: Vec::new(),
782            filter: None,
783        };
784        let mut columns = std::collections::BTreeSet::new();
785        assert!(expression.collect_columns(&mut columns));
786        assert_eq!(columns, std::collections::BTreeSet::from(["amount".into()]));
787        assert!(expression.contains_aggregate(&|name| name == "sum"));
788        assert!(!expression.contains_subquery());
789    }
790
791    #[test]
792    fn borrowed_column_visits_keep_names_and_stop_at_the_first_failure() {
793        let expression = ScalarExpr::Row(vec![
794            ScalarExpr::Column("first".into()),
795            ScalarExpr::QualifiedColumn {
796                qualifier: "table".into(),
797                column: "second".into(),
798            },
799            ScalarExpr::Column("first".into()),
800        ]);
801        let mut borrowed = Vec::new();
802        assert!(expression
803            .try_visit_columns(&mut |name| {
804                borrowed.push(name);
805                Ok::<_, &str>(())
806            })
807            .unwrap());
808        assert_eq!(borrowed, ["first", "second", "first"]);
809        let ScalarExpr::Row(items) = &expression else {
810            unreachable!()
811        };
812        let ScalarExpr::Column(first) = &items[0] else {
813            unreachable!()
814        };
815        assert_eq!(borrowed[0].as_ptr(), first.as_ptr());
816        let mut visits = 0;
817        let result = expression.try_visit_columns(&mut |_| {
818            visits += 1;
819            if visits == 2 {
820                Err("quota")
821            } else {
822                Ok(())
823            }
824        });
825        assert_eq!(result, Err("quota"));
826        assert_eq!(visits, 2);
827    }
828
829    #[test]
830    fn borrowed_column_visits_preserve_unprojectable_prefix_semantics() {
831        let expression = ScalarExpr::Array(vec![
832            ScalarExpr::Column("before".into()),
833            ScalarExpr::Position(0),
834            ScalarExpr::Column("after".into()),
835        ]);
836        let mut borrowed = Vec::new();
837        assert!(!expression
838            .try_visit_columns(&mut |name| {
839                borrowed.push(name);
840                Ok::<_, &str>(())
841            })
842            .unwrap());
843        let mut owned = std::collections::BTreeSet::new();
844        assert!(!expression.collect_columns(&mut owned));
845        assert_eq!(borrowed, ["before"]);
846        assert_eq!(owned, std::collections::BTreeSet::from(["before".into()]));
847    }
848}