nodedb_sql/planner/
grouping_sets.rs1use sqlparser::ast::{self, GroupByExpr};
16
17use crate::error::{Result, SqlError};
18use crate::parser::normalize::normalize_ident;
19use crate::resolver::ColumnScope;
20use crate::resolver::columns::TableScope;
21use crate::resolver::expr::convert_expr;
22use crate::types::SqlExpr;
23
24#[derive(Debug, Clone)]
26pub struct GroupingSetsExpansion {
27 pub canonical_keys: Vec<SqlExpr>,
29 pub grouping_sets: Vec<Vec<usize>>,
32}
33
34pub fn expand_group_by(
39 group_by: &GroupByExpr,
40 scope: &TableScope,
41) -> Result<Option<GroupingSetsExpansion>> {
42 let scope = ColumnScope::Relations(scope);
43 let exprs = match group_by {
44 GroupByExpr::All(_) => return Ok(None),
45 GroupByExpr::Expressions(exprs, _) => exprs,
46 };
47
48 let has_extension = exprs.iter().any(is_grouping_extension);
50 if !has_extension {
51 return Ok(None);
52 }
53
54 let mut plain_ast: Vec<&ast::Expr> = Vec::new();
58 let mut extension_sets: Option<Vec<Vec<&ast::Expr>>> = None;
59
60 for expr in exprs {
61 match expr {
62 ast::Expr::Rollup(groups) => {
63 if extension_sets.is_some() {
64 return Err(SqlError::Unsupported {
65 detail: "only one ROLLUP/CUBE/GROUPING SETS per GROUP BY is supported"
66 .into(),
67 });
68 }
69 extension_sets = Some(expand_rollup(groups));
70 }
71 ast::Expr::Cube(groups) => {
72 if extension_sets.is_some() {
73 return Err(SqlError::Unsupported {
74 detail: "only one ROLLUP/CUBE/GROUPING SETS per GROUP BY is supported"
75 .into(),
76 });
77 }
78 extension_sets = Some(expand_cube(groups));
79 }
80 ast::Expr::GroupingSets(sets) => {
81 if extension_sets.is_some() {
82 return Err(SqlError::Unsupported {
83 detail: "only one ROLLUP/CUBE/GROUPING SETS per GROUP BY is supported"
84 .into(),
85 });
86 }
87 extension_sets = Some(sets.iter().map(|s| s.iter().collect()).collect());
89 }
90 other => {
91 plain_ast.push(other);
92 }
93 }
94 }
95
96 let ext_sets = extension_sets.unwrap_or_default();
97
98 let mut canonical_names: Vec<String> = Vec::new();
101 let mut canonical_exprs: Vec<SqlExpr> = Vec::new();
102
103 let mut intern = |e: &ast::Expr| -> Result<usize> {
104 let display = format!("{e}");
105 if let Some(pos) = canonical_names.iter().position(|n| n == &display) {
106 return Ok(pos);
107 }
108 let idx = canonical_names.len();
109 canonical_names.push(display);
110 canonical_exprs.push(convert_expr(e, &scope)?);
111 Ok(idx)
112 };
113
114 let plain_indices: Vec<usize> = plain_ast.iter().map(|e| intern(e)).collect::<Result<_>>()?;
116
117 let ext_sets_indexed: Vec<Vec<usize>> = ext_sets
119 .into_iter()
120 .map(|set| set.into_iter().map(&mut intern).collect::<Result<_>>())
121 .collect::<Result<_>>()?;
122
123 let grouping_sets: Vec<Vec<usize>> = if ext_sets_indexed.is_empty() {
126 vec![plain_indices]
129 } else {
130 ext_sets_indexed
131 .into_iter()
132 .map(|ext_set| {
133 let mut combined = plain_indices.clone();
135 for idx in &ext_set {
136 if !combined.contains(idx) {
137 combined.push(*idx);
138 }
139 }
140 combined
141 })
142 .collect()
143 };
144
145 Ok(Some(GroupingSetsExpansion {
146 canonical_keys: canonical_exprs,
147 grouping_sets,
148 }))
149}
150
151fn is_grouping_extension(expr: &ast::Expr) -> bool {
153 matches!(
154 expr,
155 ast::Expr::Rollup(_) | ast::Expr::Cube(_) | ast::Expr::GroupingSets(_)
156 )
157}
158
159fn expand_rollup(groups: &[Vec<ast::Expr>]) -> Vec<Vec<&ast::Expr>> {
165 let atoms: Vec<&ast::Expr> = groups.iter().flat_map(|g| g.iter()).collect();
167 let n = atoms.len();
168 (0..=n).rev().map(|len| atoms[..len].to_vec()).collect()
170}
171
172fn expand_cube(groups: &[Vec<ast::Expr>]) -> Vec<Vec<&ast::Expr>> {
174 let atoms: Vec<&ast::Expr> = groups.iter().flat_map(|g| g.iter()).collect();
175 let n = atoms.len();
176 let count = 1usize << n;
177 let mut sets: Vec<Vec<&ast::Expr>> = Vec::with_capacity(count);
178 for mask in (0..count).rev() {
180 let set: Vec<&ast::Expr> = (0..n)
181 .filter(|i| (mask >> i) & 1 == 1)
182 .map(|i| atoms[i])
183 .collect();
184 sets.push(set);
185 }
186 sets
187}
188
189pub fn resolve_grouping_col(col_expr: &ast::Expr, canonical_keys: &[SqlExpr]) -> Result<usize> {
193 let display = format!("{col_expr}");
194 for (i, key) in canonical_keys.iter().enumerate() {
198 if format!("{key:?}").contains(&display) {
199 return Ok(i);
200 }
201 }
202 if let ast::Expr::Identifier(ident) = col_expr {
204 let name = normalize_ident(ident);
205 for (i, key) in canonical_keys.iter().enumerate() {
206 if let SqlExpr::Column { name: col_name, .. } = key
207 && col_name.eq_ignore_ascii_case(&name)
208 {
209 return Ok(i);
210 }
211 }
212 }
213 Err(SqlError::Unsupported {
214 detail: format!(
215 "GROUPING({col_expr}) references a column not found in the canonical key list"
216 ),
217 })
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223 use crate::resolver::columns::test_support::open_scope;
224
225 fn parse_group_by(sql: &str) -> GroupByExpr {
226 use sqlparser::dialect::GenericDialect;
227 use sqlparser::parser::Parser;
228 let stmts = Parser::parse_sql(&GenericDialect {}, sql).unwrap();
229 match stmts.into_iter().next().unwrap() {
230 ast::Statement::Query(q) => match *q.body {
231 ast::SetExpr::Select(s) => s.group_by,
232 _ => panic!("expected SELECT"),
233 },
234 _ => panic!("expected query"),
235 }
236 }
237
238 #[test]
239 fn rollup_two_cols() {
240 let gb = parse_group_by(
241 "SELECT region, country, SUM(sales) FROM orders GROUP BY ROLLUP (region, country)",
242 );
243 let result = expand_group_by(&gb, &open_scope("orders"))
244 .unwrap()
245 .unwrap();
246 assert_eq!(result.canonical_keys.len(), 2);
248 assert_eq!(result.grouping_sets.len(), 3);
249 assert_eq!(result.grouping_sets[0], vec![0, 1]); assert_eq!(result.grouping_sets[1], vec![0]); assert_eq!(result.grouping_sets[2], Vec::<usize>::new()); }
253
254 #[test]
255 fn cube_two_cols() {
256 let gb = parse_group_by(
257 "SELECT region, country, SUM(sales) FROM orders GROUP BY CUBE (region, country)",
258 );
259 let result = expand_group_by(&gb, &open_scope("orders"))
260 .unwrap()
261 .unwrap();
262 assert_eq!(result.canonical_keys.len(), 2);
264 assert_eq!(result.grouping_sets.len(), 4);
265 assert!(result.grouping_sets[0].contains(&0));
267 assert!(result.grouping_sets[0].contains(&1));
268 assert_eq!(*result.grouping_sets.last().unwrap(), Vec::<usize>::new());
270 }
271
272 #[test]
273 fn grouping_sets_explicit() {
274 let gb = parse_group_by(
275 "SELECT region, country, SUM(sales) FROM orders \
276 GROUP BY GROUPING SETS ((region, country), (region), ())",
277 );
278 let result = expand_group_by(&gb, &open_scope("orders"))
279 .unwrap()
280 .unwrap();
281 assert_eq!(result.canonical_keys.len(), 2);
282 assert_eq!(result.grouping_sets.len(), 3);
283 assert_eq!(result.grouping_sets[0], vec![0, 1]);
284 assert_eq!(result.grouping_sets[1], vec![0]);
285 assert_eq!(result.grouping_sets[2], Vec::<usize>::new());
286 }
287
288 #[test]
289 fn plain_group_by_returns_none() {
290 let gb = parse_group_by("SELECT region, COUNT(*) FROM orders GROUP BY region");
291 let result = expand_group_by(&gb, &open_scope("orders")).unwrap();
292 assert!(result.is_none());
293 }
294
295 #[test]
296 fn mixed_plain_and_rollup() {
297 let gb = parse_group_by("SELECT a, b, c, SUM(x) FROM t GROUP BY a, ROLLUP (b, c)");
298 let result = expand_group_by(&gb, &open_scope("orders"))
299 .unwrap()
300 .unwrap();
301 assert_eq!(result.canonical_keys.len(), 3);
308 assert_eq!(result.grouping_sets.len(), 3);
309 assert!(result.grouping_sets[0].contains(&0)); assert!(result.grouping_sets[1].contains(&0));
311 assert!(result.grouping_sets[2].contains(&0));
312 }
313
314 #[test]
315 fn rollup_three_cols() {
316 let gb = parse_group_by("SELECT a, b, c, SUM(x) FROM t GROUP BY ROLLUP (a, b, c)");
317 let result = expand_group_by(&gb, &open_scope("orders"))
318 .unwrap()
319 .unwrap();
320 assert_eq!(result.grouping_sets.len(), 4); }
322}