Skip to main content

keelson_mysql/
function.rs

1use std::borrow::Cow;
2
3use keelson_core::clause::{HasOrderBy, OrderBy, Window};
4use keelson_core::expr::{Expr, IntoExpr, IntoExprList};
5use keelson_core::{Expression, Mod, SqlWriter};
6
7/// A MySQL function call, with every decoration the grammar hangs off one.
8///
9/// From *14.19 Aggregate Functions* and *14.20 Window Functions*:
10///
11/// ```text
12/// name([DISTINCT] expr [, expr] ... [ORDER BY …])
13/// GROUP_CONCAT([DISTINCT] expr [, expr] ... [ORDER BY …] [SEPARATOR str_val])
14/// name(…) OVER (window_spec) | name(…) OVER window_name
15/// ```
16///
17/// **MySQL has no `FILTER (WHERE …)` and no `WITHIN GROUP`.** Both are on
18/// PostgreSQL's function builder and neither is here.
19///
20/// [`Expr::Func`](keelson_core::expr::Expr::Func) carries only what all three
21/// dialects share, so everything above lives here and reaches core through
22/// [`Expr::Custom`](keelson_core::expr::Expr::Custom).
23///
24/// ```
25/// use keelson_mysql::{f, quote, window};
26///
27/// // AVG(`views`) OVER (PARTITION BY `user_id`)
28/// let e = f("AVG", quote("views")).over(window::partition_by(quote("user_id")));
29/// ```
30#[derive(Debug, Clone, Default)]
31pub struct Function {
32    name: Cow<'static, str>,
33    args: Vec<Expr>,
34    distinct: bool,
35    order_by: OrderBy,
36    separator: Option<Cow<'static, str>>,
37    over: Option<OverClause>,
38}
39
40/// What follows `OVER`.
41///
42/// The two forms are different productions. `OVER window_name` **references** a
43/// window from the statement's `WINDOW` clause; `OVER ( … )` is a definition, and a
44/// definition that begins with an existing window's name *copies* it, which MySQL
45/// refuses when that window has a frame clause:
46///
47/// ```text
48/// ERROR 3581 (HY000): A window which depends on another cannot define partitioning.
49/// ```
50///
51/// bob only ever writes the parenthesised form, so a named framed window is
52/// unreachable there. [`Function::over_name`] is the other one.
53#[derive(Debug, Clone)]
54enum OverClause {
55    /// `OVER \`w\``.
56    Name(Cow<'static, str>),
57    /// `OVER ( … )`.
58    Definition(Window),
59}
60
61impl Function {
62    /// A call to `name` with `args`.
63    pub fn new(name: impl Into<Cow<'static, str>>, args: impl IntoExprList) -> Function {
64        Function {
65            name: name.into(),
66            args: args.into_expr_list(),
67            ..Function::default()
68        }
69    }
70
71    /// `DISTINCT`, for an aggregate that should see each distinct input once.
72    #[must_use]
73    pub fn distinct(mut self) -> Function {
74        self.distinct = true;
75        self
76    }
77
78    /// Add a sort key to the aggregate's own `ORDER BY`, rendered inside the
79    /// argument list: `GROUP_CONCAT(x ORDER BY y)`.
80    #[must_use]
81    pub fn order_by(mut self, order: impl IntoExpr) -> Function {
82        self.order_by.append_order(order);
83        self
84    }
85
86    /// `SEPARATOR 'str'`, which only `GROUP_CONCAT` takes.
87    ///
88    /// Written as a single-quoted literal with nothing escaped, exactly like
89    /// [`s`](crate::s): this is for a separator the program itself chose.
90    #[must_use]
91    pub fn separator(mut self, separator: impl Into<Cow<'static, str>>) -> Function {
92        self.separator = Some(separator.into());
93        self
94    }
95
96    /// Attach `OVER (…)`, built from window mods — `mysql::window::*` and
97    /// `mysql::frame::*`.
98    ///
99    /// Ends the builder, because `OVER` is the last thing in the grammar and
100    /// nothing may follow it. `over(())` gives the legal `OVER ()`, which means the
101    /// whole partition.
102    #[must_use]
103    pub fn over(mut self, mods: impl Mod<Window>) -> Expr {
104        let mut w = Window::default();
105        mods.apply(&mut w);
106        self.over = Some(OverClause::Definition(w));
107        self.into_expr()
108    }
109
110    /// Attach `OVER \`w\`` — a reference to a window in the statement's `WINDOW`
111    /// clause.
112    ///
113    /// Unparenthesised, which is what makes it a reference rather than a copy.
114    #[must_use]
115    pub fn over_name(mut self, name: impl Into<Cow<'static, str>>) -> Expr {
116        self.over = Some(OverClause::Name(name.into()));
117        self.into_expr()
118    }
119
120    /// `f(…) AS \`alias\`` — the select-list alias.
121    ///
122    /// Ends the builder for the same reason
123    /// [`Chain::as_`](keelson_core::expr::Chain::as_) does: an alias is not an
124    /// operand.
125    #[must_use]
126    pub fn as_(self, alias: impl Into<Cow<'static, str>>) -> Expr {
127        use keelson_core::expr::Chain as _;
128        self.into_expr().as_(alias.into())
129    }
130}
131
132impl HasOrderBy for Function {
133    fn order_by_mut(&mut self) -> &mut OrderBy {
134        &mut self.order_by
135    }
136}
137
138impl Expression for Function {
139    fn write_sql(&self, w: &mut SqlWriter<'_>) {
140        if self.name.is_empty() {
141            // A call with no name is not a fragment of anything, and there is no
142            // rendering of it that parses.
143            w.record_error(keelson_core::Error::Incomplete("the name of a function"));
144            return;
145        }
146
147        w.push_str(&self.name);
148        w.push_str("(");
149        if self.distinct {
150            w.push_str("DISTINCT ");
151        }
152        w.write_slice(&self.args, "", ", ", "");
153        // `GROUP_CONCAT(x ORDER BY y)`: the separator is only needed when there is
154        // an argument in front of it, and `f(ORDER BY x)` is not a thing.
155        w.write_if(
156            !self.order_by.is_empty() && !self.args.is_empty(),
157            " ",
158            &self.order_by,
159            "",
160        );
161        if let Some(separator) = &self.separator {
162            w.push_str(" SEPARATOR '");
163            w.push_str(separator);
164            w.push_str("'");
165        }
166        w.push_str(")");
167
168        match &self.over {
169            None => {}
170            Some(OverClause::Name(name)) => {
171                w.push_str(" OVER ");
172                w.push_quoted(&[name]);
173            }
174            Some(OverClause::Definition(window)) => {
175                w.push_str(" OVER (");
176                w.write_expr(window);
177                w.push_str(")");
178            }
179        }
180    }
181}
182
183impl IntoExpr for Function {
184    fn into_expr(self) -> Expr {
185        Expr::custom(self)
186    }
187}
188
189impl IntoExprList for Function {
190    fn into_expr_list(self) -> Vec<Expr> {
191        vec![self.into_expr()]
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::{Mysql, arg, f, frame, quote, window};
199    use keelson_core::build;
200
201    fn sql(e: impl Expression) -> String {
202        build(&Mysql, &e).expect("render").0
203    }
204
205    #[test]
206    fn a_plain_call_is_just_a_call() {
207        assert_eq!(sql(f("NOW", ())), "NOW()");
208        assert_eq!(sql(f("COUNT", "*")), "COUNT(*)");
209    }
210
211    /// *14.19.1*: `DISTINCT` and the aggregate's own `ORDER BY` are both inside the
212    /// argument parentheses, and `SEPARATOR` follows the `ORDER BY`.
213    #[test]
214    fn distinct_order_by_and_separator_all_stay_inside_the_argument_list() {
215        assert_eq!(
216            sql(f("COUNT", quote("id")).distinct()),
217            "COUNT(DISTINCT `id`)"
218        );
219        assert_eq!(
220            sql(f("GROUP_CONCAT", quote("name")).order_by(quote("id"))),
221            "GROUP_CONCAT(`name` ORDER BY `id`)"
222        );
223        assert_eq!(
224            sql(f("GROUP_CONCAT", quote("name"))
225                .distinct()
226                .order_by(quote("id"))
227                .separator(", ")),
228            "GROUP_CONCAT(DISTINCT `name` ORDER BY `id` SEPARATOR ', ')"
229        );
230    }
231
232    #[test]
233    fn over_takes_a_definition_a_name_or_nothing() {
234        assert_eq!(sql(f("ROW_NUMBER", ()).over(())), "ROW_NUMBER() OVER ()");
235        // The reference form has no parentheses; the copy form does.
236        assert_eq!(
237            sql(f("AVG", quote("views")).over_name("w")),
238            "AVG(`views`) OVER `w`"
239        );
240        assert_eq!(
241            sql(f("AVG", quote("views")).over(window::based_on("w"))),
242            "AVG(`views`) OVER (`w`)"
243        );
244        assert_eq!(
245            sql(f("SUM", quote("views")).over((
246                window::partition_by(quote("user_id")),
247                window::order_by(quote("id")),
248                frame::rows(),
249                frame::from_current_row(),
250                frame::to_unbounded_following(),
251            ))),
252            "SUM(`views`) OVER (PARTITION BY `user_id` ORDER BY `id` \
253             ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)"
254        );
255    }
256
257    #[test]
258    fn an_alias_ends_the_builder_and_is_not_parenthesised() {
259        assert_eq!(sql(f("COUNT", arg(1i32)).as_("n")), "COUNT(?) AS `n`");
260    }
261
262    #[test]
263    fn an_unnamed_call_is_a_recorded_failure() {
264        let err = build(&Mysql, &Function::default()).unwrap_err();
265        // The substring names the SQL concept (a function's name), not the
266        // message wording.
267        assert!(
268            matches!(&err, keelson_core::Error::Incomplete(what) if what.contains("function")),
269            "got: {err}"
270        );
271    }
272}