keelson_core/clause/
group_by.rs1use crate::expr::{Expr, IntoExpr, IntoExprList};
2use crate::writer::{Expression, SqlWriter};
3
4#[derive(Debug, Clone, Default)]
14pub struct GroupBy {
15 pub groups: Vec<Expr>,
17 pub distinct: bool,
20 pub with: Option<GroupByWith>,
22}
23
24impl GroupBy {
25 pub fn set_groups(&mut self, groups: impl IntoExprList) {
27 self.groups = groups.into_expr_list();
28 }
29
30 pub fn append_group(&mut self, group: impl IntoExpr) {
32 self.groups.push(group.into_expr());
33 }
34
35 pub fn is_empty(&self) -> bool {
37 self.groups.is_empty()
38 }
39}
40
41impl Expression for GroupBy {
42 fn write_sql(&self, w: &mut SqlWriter<'_>) {
43 if self.groups.is_empty() {
46 return;
47 }
48
49 w.push_str("GROUP BY ");
50 if self.distinct {
51 w.push_str("DISTINCT ");
52 }
53 w.write_slice(&self.groups, "", ", ", "");
54
55 if let Some(with) = &self.with {
56 w.push_str(" WITH ");
57 w.push_str(with.as_str());
58 }
59 }
60}
61
62pub trait HasGroupBy {
64 fn group_by_mut(&mut self) -> &mut GroupBy;
66}
67
68impl HasGroupBy for GroupBy {
69 fn group_by_mut(&mut self) -> &mut GroupBy {
70 self
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum GroupByWith {
77 Rollup,
79 Cube,
81}
82
83impl GroupByWith {
84 pub fn as_str(self) -> &'static str {
86 match self {
87 GroupByWith::Rollup => "ROLLUP",
88 GroupByWith::Cube => "CUBE",
89 }
90 }
91}
92
93#[derive(Debug, Clone, Default)]
99pub struct GroupingSet {
100 pub kind: GroupingSetKind,
102 pub groups: Vec<Expr>,
105}
106
107impl GroupingSet {
108 pub fn new(kind: GroupingSetKind, groups: impl IntoExprList) -> Self {
110 GroupingSet {
111 kind,
112 groups: groups.into_expr_list(),
113 }
114 }
115
116 pub fn is_empty(&self) -> bool {
118 self.groups.is_empty()
119 }
120}
121
122impl Expression for GroupingSet {
123 fn write_sql(&self, w: &mut SqlWriter<'_>) {
124 if self.groups.is_empty() {
128 return;
129 }
130 w.push_str(self.kind.keyword());
131 w.write_slice(&self.groups, " (", ", ", ")");
132 }
133}
134
135#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
137pub enum GroupingSetKind {
138 #[default]
140 GroupingSets,
141 Cube,
143 Rollup,
145}
146
147impl GroupingSetKind {
148 pub fn keyword(self) -> &'static str {
150 match self {
151 GroupingSetKind::GroupingSets => "GROUPING SETS",
152 GroupingSetKind::Cube => "CUBE",
153 GroupingSetKind::Rollup => "ROLLUP",
154 }
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use keelson_sqlcheck::testing::assert_frag_sql;
161
162 use super::*;
163 use crate::dialect::testing::Numbered;
164 use crate::expr::quote;
165 use crate::writer::build;
166
167 const FRAME: &str = r#"SELECT "status", count(*) FROM posts {}"#;
169
170 fn sql(e: &impl Expression) -> String {
171 build(&Numbered, e).expect("render").0
172 }
173
174 #[test]
175 fn no_groups_means_no_clause_even_with_modifiers() {
176 let g = GroupBy {
177 distinct: true,
178 with: Some(GroupByWith::Rollup),
179 ..GroupBy::default()
180 };
181 assert_frag_sql("SELECT count(*) FROM posts {}", &sql(&g), "");
184 assert!(g.is_empty());
185 }
186
187 #[test]
188 fn the_group_list_is_comma_separated() {
189 let mut g = GroupBy::default();
190 g.append_group(quote("status"));
191 g.append_group("1");
194 assert_frag_sql(FRAME, &sql(&g), r#"GROUP BY "status", 1"#);
195 }
196
197 #[test]
198 fn distinct_and_with_wrap_the_group_list() {
199 let mut g = GroupBy::default();
200 g.append_group(quote("status"));
201 g.append_group("1");
202 g.distinct = true;
203 g.with = Some(GroupByWith::Cube);
204 assert_eq!(
209 build(&Numbered, &g).unwrap().0,
210 r#"GROUP BY DISTINCT "status", 1 WITH CUBE"#
211 );
212 }
213
214 #[test]
215 fn a_grouping_set_is_one_grouping_element() {
216 let mut g = GroupBy::default();
219 g.append_group(Expr::custom(GroupingSet::new(
220 GroupingSetKind::Rollup,
221 (quote("status"), quote("user_id")),
222 )));
223 assert_frag_sql(
224 "SELECT count(*) FROM posts {}",
225 &sql(&g),
226 r#"GROUP BY ROLLUP ("status", "user_id")"#,
227 );
228 }
229
230 #[test]
231 fn grouping_sets_hold_row_groups_including_the_empty_one() {
232 let set = GroupingSet::new(
238 GroupingSetKind::GroupingSets,
239 (
240 Expr::group(quote("status")),
241 Expr::group((quote("status"), quote("user_id"))),
242 Expr::raw("()"),
243 ),
244 );
245 assert_frag_sql(
246 "SELECT count(*) FROM posts GROUP BY {}",
247 &sql(&set),
248 r#"GROUPING SETS (("status"), ("status", "user_id"), ())"#,
249 );
250 }
251
252 #[test]
253 fn an_empty_grouping_set_writes_nothing() {
254 assert_eq!(build(&Numbered, &GroupingSet::default()).unwrap().0, "");
257 assert!(GroupingSet::default().is_empty());
258 assert_eq!(GroupingSetKind::Cube.keyword(), "CUBE");
259 }
260}