use crate::expr::{Expr, IntoExpr, IntoExprList};
use crate::writer::{Expression, SqlWriter};
#[derive(Debug, Clone, Default)]
pub struct GroupBy {
pub groups: Vec<Expr>,
pub distinct: bool,
pub with: Option<GroupByWith>,
}
impl GroupBy {
pub fn set_groups(&mut self, groups: impl IntoExprList) {
self.groups = groups.into_expr_list();
}
pub fn append_group(&mut self, group: impl IntoExpr) {
self.groups.push(group.into_expr());
}
pub fn is_empty(&self) -> bool {
self.groups.is_empty()
}
}
impl Expression for GroupBy {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if self.groups.is_empty() {
return;
}
w.push_str("GROUP BY ");
if self.distinct {
w.push_str("DISTINCT ");
}
w.write_slice(&self.groups, "", ", ", "");
if let Some(with) = &self.with {
w.push_str(" WITH ");
w.push_str(with.as_str());
}
}
}
pub trait HasGroupBy {
fn group_by_mut(&mut self) -> &mut GroupBy;
}
impl HasGroupBy for GroupBy {
fn group_by_mut(&mut self) -> &mut GroupBy {
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GroupByWith {
Rollup,
Cube,
}
impl GroupByWith {
pub fn as_str(self) -> &'static str {
match self {
GroupByWith::Rollup => "ROLLUP",
GroupByWith::Cube => "CUBE",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct GroupingSet {
pub kind: GroupingSetKind,
pub groups: Vec<Expr>,
}
impl GroupingSet {
pub fn new(kind: GroupingSetKind, groups: impl IntoExprList) -> Self {
GroupingSet {
kind,
groups: groups.into_expr_list(),
}
}
pub fn is_empty(&self) -> bool {
self.groups.is_empty()
}
}
impl Expression for GroupingSet {
fn write_sql(&self, w: &mut SqlWriter<'_>) {
if self.groups.is_empty() {
return;
}
w.push_str(self.kind.keyword());
w.write_slice(&self.groups, " (", ", ", ")");
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GroupingSetKind {
#[default]
GroupingSets,
Cube,
Rollup,
}
impl GroupingSetKind {
pub fn keyword(self) -> &'static str {
match self {
GroupingSetKind::GroupingSets => "GROUPING SETS",
GroupingSetKind::Cube => "CUBE",
GroupingSetKind::Rollup => "ROLLUP",
}
}
}
#[cfg(test)]
mod tests {
use keelson_sqlcheck::testing::assert_frag_sql;
use super::*;
use crate::dialect::testing::Numbered;
use crate::expr::quote;
use crate::writer::build;
const FRAME: &str = r#"SELECT "status", count(*) FROM posts {}"#;
fn sql(e: &impl Expression) -> String {
build(&Numbered, e).expect("render").0
}
#[test]
fn no_groups_means_no_clause_even_with_modifiers() {
let g = GroupBy {
distinct: true,
with: Some(GroupByWith::Rollup),
..GroupBy::default()
};
assert_frag_sql("SELECT count(*) FROM posts {}", &sql(&g), "");
assert!(g.is_empty());
}
#[test]
fn the_group_list_is_comma_separated() {
let mut g = GroupBy::default();
g.append_group(quote("status"));
g.append_group("1");
assert_frag_sql(FRAME, &sql(&g), r#"GROUP BY "status", 1"#);
}
#[test]
fn distinct_and_with_wrap_the_group_list() {
let mut g = GroupBy::default();
g.append_group(quote("status"));
g.append_group("1");
g.distinct = true;
g.with = Some(GroupByWith::Cube);
assert_eq!(
build(&Numbered, &g).unwrap().0,
r#"GROUP BY DISTINCT "status", 1 WITH CUBE"#
);
}
#[test]
fn a_grouping_set_is_one_grouping_element() {
let mut g = GroupBy::default();
g.append_group(Expr::custom(GroupingSet::new(
GroupingSetKind::Rollup,
(quote("status"), quote("user_id")),
)));
assert_frag_sql(
"SELECT count(*) FROM posts {}",
&sql(&g),
r#"GROUP BY ROLLUP ("status", "user_id")"#,
);
}
#[test]
fn grouping_sets_hold_row_groups_including_the_empty_one() {
let set = GroupingSet::new(
GroupingSetKind::GroupingSets,
(
Expr::group(quote("status")),
Expr::group((quote("status"), quote("user_id"))),
Expr::raw("()"),
),
);
assert_frag_sql(
"SELECT count(*) FROM posts GROUP BY {}",
&sql(&set),
r#"GROUPING SETS (("status"), ("status", "user_id"), ())"#,
);
}
#[test]
fn an_empty_grouping_set_writes_nothing() {
assert_eq!(build(&Numbered, &GroupingSet::default()).unwrap().0, "");
assert!(GroupingSet::default().is_empty());
assert_eq!(GroupingSetKind::Cube.keyword(), "CUBE");
}
}