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,
within_group: bool,
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 within_group(mut self) -> Function {
self.within_group = true;
self
}
#[must_use]
pub fn filter(mut self, condition: impl IntoExpr) -> Function {
self.filter.push(condition.into_expr());
self
}
#[must_use]
pub fn as_table(self, alias: impl Into<Cow<'static, str>>) -> TableFunction {
TableFunction::from(self).as_table(alias)
}
#[must_use]
pub fn columns<N, T>(self, columns: impl IntoIterator<Item = (N, T)>) -> TableFunction
where
N: Into<Cow<'static, str>>,
T: Into<Cow<'static, str>>,
{
TableFunction::from(self).columns(columns)
}
#[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, "", ", ", "");
if !self.within_group {
w.write_if(
!self.order_by.is_empty() && !self.args.is_empty(),
" ",
&self.order_by,
"",
);
}
w.push_str(")");
if self.within_group {
w.write_if(
!self.order_by.is_empty(),
" WITHIN GROUP (",
&self.order_by,
")",
);
}
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()]
}
}
#[derive(Debug, Clone)]
pub struct TableFunction {
function: Function,
alias: Option<Cow<'static, str>>,
columns: Vec<ColumnDef>,
}
impl TableFunction {
#[must_use]
pub fn as_table(mut self, alias: impl Into<Cow<'static, str>>) -> TableFunction {
self.alias = Some(alias.into());
self
}
#[must_use]
pub fn columns<N, T>(mut self, columns: impl IntoIterator<Item = (N, T)>) -> TableFunction
where
N: Into<Cow<'static, str>>,
T: Into<Cow<'static, str>>,
{
self.columns.extend(
columns
.into_iter()
.map(|(name, ty)| ColumnDef::new(name, ty)),
);
self
}
}
impl From<Function> for TableFunction {
fn from(function: Function) -> TableFunction {
TableFunction {
function,
alias: None,
columns: Vec::new(),
}
}
}
impl Expression for TableFunction {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
self.function.write_sql(w);
if self.alias.is_some() || !self.columns.is_empty() {
w.push_str(" AS");
if let Some(alias) = &self.alias {
w.push_str(" ");
w.push_quoted(&[alias]);
}
w.write_slice(&self.columns, " (", ", ", ")");
}
}
}
impl IntoExpr for TableFunction {
fn into_expr(self) -> Expr {
Expr::custom(self)
}
}
impl IntoExprList for TableFunction {
fn into_expr_list(self) -> Vec<Expr> {
vec![self.into_expr()]
}
}
#[derive(Debug, Clone)]
pub struct ColumnDef {
pub name: Cow<'static, str>,
pub data_type: Cow<'static, str>,
}
impl ColumnDef {
pub fn new(
name: impl Into<Cow<'static, str>>,
data_type: impl Into<Cow<'static, str>>,
) -> ColumnDef {
ColumnDef {
name: name.into(),
data_type: data_type.into(),
}
}
}
impl Expression for ColumnDef {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
w.push_quoted(&[&self.name]);
w.push_str(" ");
w.push_str(&self.data_type);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Psql, arg, f, frame, quote, window};
use keelson_core::build;
fn sql(e: impl Expression) -> String {
build(&Psql, &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_and_order_by_stay_inside_the_argument_list() {
assert_eq!(
sql(f("count", quote("id")).distinct()),
r#"count(DISTINCT "id")"#
);
assert_eq!(
sql(f("array_agg", quote("id")).order_by(quote("name"))),
r#"array_agg("id" ORDER BY "name")"#
);
}
#[test]
fn within_group_moves_the_order_by_out() {
assert_eq!(
sql(f("percentile_cont", arg(0.5f64))
.within_group()
.order_by(quote("views"))),
r#"percentile_cont($1) WITHIN GROUP (ORDER BY "views")"#
);
}
#[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 column_definitions_follow_as_with_the_alias_between() {
assert_eq!(
sql(f("json_to_recordset", arg("[]")).columns([("a", "int"), ("b", "text")])),
r#"json_to_recordset($1) AS ("a" int, "b" text)"#
);
assert_eq!(
sql(f("json_to_recordset", arg("[]"))
.as_table("t")
.columns([("a", "int")])),
r#"json_to_recordset($1) AS "t" ("a" int)"#
);
}
#[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 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 an_unnamed_call_is_a_recorded_failure() {
let err = build(&Psql, &Function::default()).unwrap_err();
assert!(
matches!(&err, keelson_core::Error::Incomplete(what) if what.contains("function")),
"got: {err}"
);
}
}