use std::borrow::Cow;
use keelson_core::clause::{HasOrderBy, OrderBy, Window};
use keelson_core::expr::{Expr, IntoExpr, IntoExprList};
use keelson_core::{Expression, Mod, SqlWriter};
#[derive(Debug, Clone, Default)]
pub struct Function {
name: Cow<'static, str>,
args: Vec<Expr>,
distinct: bool,
order_by: OrderBy,
separator: Option<Cow<'static, str>>,
over: Option<OverClause>,
}
#[derive(Debug, Clone)]
enum OverClause {
Name(Cow<'static, str>),
Definition(Window),
}
impl Function {
pub fn new(name: impl Into<Cow<'static, str>>, args: impl IntoExprList) -> Function {
Function {
name: name.into(),
args: args.into_expr_list(),
..Function::default()
}
}
#[must_use]
pub fn distinct(mut self) -> Function {
self.distinct = true;
self
}
#[must_use]
pub fn order_by(mut self, order: impl IntoExpr) -> Function {
self.order_by.append_order(order);
self
}
#[must_use]
pub fn separator(mut self, separator: impl Into<Cow<'static, str>>) -> Function {
self.separator = Some(separator.into());
self
}
#[must_use]
pub fn over(mut self, mods: impl Mod<Window>) -> Expr {
let mut w = Window::default();
mods.apply(&mut w);
self.over = Some(OverClause::Definition(w));
self.into_expr()
}
#[must_use]
pub fn over_name(mut self, name: impl Into<Cow<'static, str>>) -> Expr {
self.over = Some(OverClause::Name(name.into()));
self.into_expr()
}
#[must_use]
pub fn as_(self, alias: impl Into<Cow<'static, str>>) -> Expr {
use keelson_core::expr::Chain as _;
self.into_expr().as_(alias.into())
}
}
impl HasOrderBy for Function {
fn order_by_mut(&mut self) -> &mut OrderBy {
&mut self.order_by
}
}
impl Expression for Function {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if self.name.is_empty() {
w.record_error(keelson_core::Error::Incomplete("the name of a function"));
return;
}
w.push_str(&self.name);
w.push_str("(");
if self.distinct {
w.push_str("DISTINCT ");
}
w.write_slice(&self.args, "", ", ", "");
w.write_if(
!self.order_by.is_empty() && !self.args.is_empty(),
" ",
&self.order_by,
"",
);
if let Some(separator) = &self.separator {
w.push_str(" SEPARATOR '");
w.push_str(separator);
w.push_str("'");
}
w.push_str(")");
match &self.over {
None => {}
Some(OverClause::Name(name)) => {
w.push_str(" OVER ");
w.push_quoted(&[name]);
}
Some(OverClause::Definition(window)) => {
w.push_str(" OVER (");
w.write_expr(window);
w.push_str(")");
}
}
}
}
impl IntoExpr for Function {
fn into_expr(self) -> Expr {
Expr::custom(self)
}
}
impl IntoExprList for Function {
fn into_expr_list(self) -> Vec<Expr> {
vec![self.into_expr()]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Mysql, arg, f, frame, quote, window};
use keelson_core::build;
fn sql(e: impl Expression) -> String {
build(&Mysql, &e).expect("render").0
}
#[test]
fn a_plain_call_is_just_a_call() {
assert_eq!(sql(f("NOW", ())), "NOW()");
assert_eq!(sql(f("COUNT", "*")), "COUNT(*)");
}
#[test]
fn distinct_order_by_and_separator_all_stay_inside_the_argument_list() {
assert_eq!(
sql(f("COUNT", quote("id")).distinct()),
"COUNT(DISTINCT `id`)"
);
assert_eq!(
sql(f("GROUP_CONCAT", quote("name")).order_by(quote("id"))),
"GROUP_CONCAT(`name` ORDER BY `id`)"
);
assert_eq!(
sql(f("GROUP_CONCAT", quote("name"))
.distinct()
.order_by(quote("id"))
.separator(", ")),
"GROUP_CONCAT(DISTINCT `name` ORDER BY `id` SEPARATOR ', ')"
);
}
#[test]
fn over_takes_a_definition_a_name_or_nothing() {
assert_eq!(sql(f("ROW_NUMBER", ()).over(())), "ROW_NUMBER() OVER ()");
assert_eq!(
sql(f("AVG", quote("views")).over_name("w")),
"AVG(`views`) OVER `w`"
);
assert_eq!(
sql(f("AVG", quote("views")).over(window::based_on("w"))),
"AVG(`views`) OVER (`w`)"
);
assert_eq!(
sql(f("SUM", quote("views")).over((
window::partition_by(quote("user_id")),
window::order_by(quote("id")),
frame::rows(),
frame::from_current_row(),
frame::to_unbounded_following(),
))),
"SUM(`views`) OVER (PARTITION BY `user_id` ORDER BY `id` \
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)"
);
}
#[test]
fn an_alias_ends_the_builder_and_is_not_parenthesised() {
assert_eq!(sql(f("COUNT", arg(1i32)).as_("n")), "COUNT(?) AS `n`");
}
#[test]
fn an_unnamed_call_is_a_recorded_failure() {
let err = build(&Mysql, &Function::default()).unwrap_err();
assert!(
matches!(&err, keelson_core::Error::Incomplete(what) if what.contains("function")),
"got: {err}"
);
}
}