keelson_mysql/
function.rs1use 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#[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#[derive(Debug, Clone)]
54enum OverClause {
55 Name(Cow<'static, str>),
57 Definition(Window),
59}
60
61impl Function {
62 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 #[must_use]
73 pub fn distinct(mut self) -> Function {
74 self.distinct = true;
75 self
76 }
77
78 #[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 #[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 #[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 #[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 #[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 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 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 #[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 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 assert!(
268 matches!(&err, keelson_core::Error::Incomplete(what) if what.contains("function")),
269 "got: {err}"
270 );
271 }
272}