Skip to main content

keelson_core/expr/
func.rs

1use std::borrow::Cow;
2
3use crate::writer::{Expression, SqlWriter};
4
5use super::convert::{IntoExpr, IntoExprList};
6use super::node::Expr;
7
8/// A function call under construction: `f("row_number", ()).over(window)`.
9///
10/// [`Expr::Func`] carries the optional `OVER` window, but `OVER` is only
11/// meaningful on a function call, so the method that sets it lives here rather
12/// than on [`Expr`] — where it would have to either silently do nothing or panic
13/// for the fifteen other variants.
14///
15/// A `FuncExpr` is an [`Expression`] and an [`IntoExpr`], so it can be used
16/// wherever an expression is expected without being finished first. Call
17/// [`IntoExpr::into_expr`] to get the [`Expr`] and continue with the operator
18/// chain.
19///
20/// The richer function forms — `DISTINCT`, `FILTER (WHERE ..)`,
21/// `WITHIN GROUP (..)`, column definition lists — differ per dialect, and belong
22/// to that dialect's own function builder. It reaches core as
23/// [`Expr::Custom`](Expr::Custom).
24#[derive(Debug, Clone)]
25pub struct FuncExpr {
26    name: Cow<'static, str>,
27    args: Vec<Expr>,
28}
29
30impl FuncExpr {
31    /// A function call: `name(args..)`.
32    pub fn new(name: impl Into<Cow<'static, str>>, args: impl IntoExprList) -> FuncExpr {
33        FuncExpr {
34            name: name.into(),
35            args: args.into_expr_list(),
36        }
37    }
38
39    /// Attach a window: `name(args..) OVER (window)`.
40    ///
41    /// The window may be a definition or the name of one declared in a `WINDOW`
42    /// clause; both render inside the same parentheses. An empty expression gives
43    /// the valid and occasionally useful `OVER ()`.
44    ///
45    /// Ends the builder, because there is nothing further to configure.
46    #[must_use]
47    pub fn over(self, window: impl IntoExpr) -> Expr {
48        Expr::Func {
49            name: self.name,
50            args: self.args,
51            over: Some(Box::new(window.into_expr())),
52        }
53    }
54}
55
56impl IntoExpr for FuncExpr {
57    fn into_expr(self) -> Expr {
58        Expr::Func {
59            name: self.name,
60            args: self.args,
61            over: None,
62        }
63    }
64}
65
66impl IntoExprList for FuncExpr {
67    fn into_expr_list(self) -> Vec<Expr> {
68        vec![self.into_expr()]
69    }
70}
71
72impl Expression for FuncExpr {
73    fn write_sql(&self, w: &mut SqlWriter<'_>) {
74        // Cheap enough to go through the enum: cloning the parts costs one Vec
75        // and one Cow, and it keeps the rendering rules in exactly one place.
76        w.write_expr(&self.clone().into_expr());
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use keelson_sqlcheck::testing::assert_frag_sql;
83
84    use super::super::{f, quote};
85    use super::*;
86    use crate::dialect::testing::Numbered;
87    use crate::expr::Chain;
88    use crate::writer::build;
89
90    /// A call is a value, so the select list is where it goes. `LEAD` and friends
91    /// are window functions and need an `OVER`, which is why some frames supply
92    /// one; and a placeholder needs a column beside it to get a type.
93    const VALUE: &str = "SELECT {} FROM posts";
94    const OVER_NOTHING: &str = "SELECT {} OVER () FROM posts";
95
96    fn sql(e: Expr) -> String {
97        build(&Numbered, &e).expect("render").0
98    }
99
100    #[test]
101    fn a_call_with_no_arguments_still_has_its_parentheses() {
102        assert_frag_sql(VALUE, &sql(f("NOW", ()).into_expr()), "NOW()");
103    }
104
105    #[test]
106    fn arguments_are_comma_separated_and_may_be_anything() {
107        assert_frag_sql(
108            OVER_NOTHING,
109            &sql(f("LEAD", ("published_at", 1, f("NOW", ()))).into_expr()),
110            "LEAD(published_at, 1, NOW())",
111        );
112    }
113
114    /// The `psql` fixtures pin all of these: a window by name, an empty one, and a
115    /// definition written out.
116    #[test]
117    fn a_window_may_be_a_definition_a_name_or_empty() {
118        assert_frag_sql(
119            "SELECT {} FROM posts WINDOW w AS ()",
120            &sql(f("avg", "views").over("w")),
121            "avg(views) OVER (w)",
122        );
123        assert_frag_sql(
124            VALUE,
125            &sql(f("row_number", ()).over("")),
126            "row_number() OVER ()",
127        );
128        assert_frag_sql(
129            VALUE,
130            &sql(f("LEAD", ("published_at", 1)).over("PARTITION BY user_id")),
131            "LEAD(published_at, 1) OVER (PARTITION BY user_id)",
132        );
133    }
134
135    #[test]
136    fn a_windowed_call_continues_into_the_operator_chain() {
137        let e = f("LEAD", ("published_at", 1))
138            .over("PARTITION BY user_id")
139            .minus(quote("published_at"))
140            .as_("difference");
141        assert_frag_sql(
142            VALUE,
143            &sql(e),
144            concat!(
145                r#"(LEAD(published_at, 1) OVER (PARTITION BY user_id)"#,
146                r#" - "published_at") AS "difference""#
147            ),
148        );
149    }
150
151    #[test]
152    fn a_call_can_be_used_as_an_expression_directly() {
153        // Without finishing the builder: the writer takes `&impl Expression`.
154        let (s, args) = build(&Numbered, &f("count", "*")).unwrap();
155        assert_frag_sql(VALUE, &s, "count(*)");
156        assert!(args.is_empty());
157    }
158
159    #[test]
160    fn arguments_inside_a_call_are_numbered_in_order() {
161        let e = f("coalesce", (Expr::arg(1i32), Expr::arg(2i32))).into_expr();
162        let (s, args) = build(&Numbered, &e).unwrap();
163        // The cast is the frame's: `coalesce($1, $2)` has no argument of known
164        // type, so PostgreSQL cannot resolve the call at all without being told
165        // what it returns ("No operator matches the given name and argument
166        // types"). Comparing it to an integer column is not enough.
167        assert_frag_sql(
168            r#"SELECT "id" FROM posts WHERE "views" = CAST({} AS integer)"#,
169            &s,
170            "coalesce($1, $2)",
171        );
172        assert_eq!(args.len(), 2);
173    }
174}