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,
filter: Vec<Expr>,
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 filter(mut self, condition: impl IntoExpr) -> Function {
self.filter.push(condition.into_expr());
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,
"",
);
w.push_str(")");
w.write_slice(&self.filter, " FILTER (WHERE ", " AND ", ")");
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::{Sqlite, arg, f, frame, quote, window};
use keelson_core::build;
fn sql(e: impl Expression) -> String {
build(&Sqlite, &e).expect("render").0
}
#[test]
fn a_plain_call_is_just_a_call() {
assert_eq!(sql(f("date", ())), "date()");
assert_eq!(sql(f("count", "*")), "count(*)");
}
#[test]
fn distinct_and_order_by_stay_inside_the_argument_list() {
assert_eq!(
sql(f("count", quote("id")).distinct()),
r#"count(DISTINCT "id")"#
);
assert_eq!(
sql(f("group_concat", quote("name")).order_by(quote("id"))),
r#"group_concat("name" ORDER BY "id")"#
);
}
#[test]
fn filter_conditions_are_and_joined_inside_one_where() {
assert_eq!(
sql(f("count", "*").filter(quote("a")).filter(quote("b"))),
r#"count(*) FILTER (WHERE "a" AND "b")"#
);
}
#[test]
fn filter_is_written_before_over() {
assert_eq!(
sql(f("count", "*")
.filter(quote("a"))
.over(window::partition_by(quote("b")))),
r#"count(*) FILTER (WHERE "a") OVER (PARTITION BY "b")"#
);
}
#[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")),
r#"avg("views") OVER "w""#
);
assert_eq!(
sql(f("avg", quote("views")).over(window::based_on("w"))),
r#"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(),
))),
r#"sum("views") OVER (PARTITION BY "user_id" ORDER BY "id" ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)"#
);
}
#[test]
fn arguments_are_numbered_across_the_whole_call() {
let (sql, args) = build(
&Sqlite,
&f("max", (arg(1i32), arg(2i32))).filter(quote("a")).over(()),
)
.unwrap();
assert_eq!(sql, r#"max(?1, ?2) FILTER (WHERE "a") OVER ()"#);
assert_eq!(args.len(), 2);
}
#[test]
fn an_unnamed_call_is_a_recorded_failure() {
let err = build(&Sqlite, &Function::default()).unwrap_err();
assert!(
matches!(&err, keelson_core::Error::Incomplete(what) if what.contains("function")),
"got: {err}"
);
}
}