Skip to main content

nodedb_sql/planner/
grouping_sets.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! ROLLUP / CUBE / GROUPING SETS expansion and canonical key extraction.
4//!
5//! The public entry point is `expand_group_by`, which inspects the raw AST
6//! GROUP BY clause and returns:
7//!
8//! - `canonical_keys`: the full deduplicated column list (indices 0..N).
9//! - `grouping_sets`: one entry per set; each entry is the subset of
10//!   canonical-key indices that are *active* (non-NULL) in that set.
11//!
12//! `GROUPING(col)` at query time resolves `col` to its canonical-key index and
13//! checks whether that bit is absent from the row's active set.
14
15use 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/// Result of expanding a GROUP BY clause that contains ROLLUP/CUBE/GROUPING SETS.
25#[derive(Debug, Clone)]
26pub struct GroupingSetsExpansion {
27    /// All distinct group-key expressions in canonical order.
28    pub canonical_keys: Vec<SqlExpr>,
29    /// One entry per logical grouping set; each entry is the indices into
30    /// `canonical_keys` that are *present* (non-NULL) for rows in that set.
31    pub grouping_sets: Vec<Vec<usize>>,
32}
33
34/// Expand the GROUP BY clause if it contains ROLLUP/CUBE/GROUPING SETS.
35///
36/// Returns `None` when the GROUP BY is a plain expression list with no
37/// extensions — callers fall back to the existing single-set path.
38pub 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    // Check whether any expression is ROLLUP / CUBE / GROUPING SETS.
49    let has_extension = exprs.iter().any(is_grouping_extension);
50    if !has_extension {
51        return Ok(None);
52    }
53
54    // Split into plain columns and the single extension expression.
55    // SQL standard: only one extension per GROUP BY; mixed is allowed but
56    // forms a cross-product with the plain columns.
57    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                // GroupingSets: each inner Vec<Expr> is one set.
88                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    // Build canonical key list: plain columns first, then extension columns
99    // (deduped by display name so identical columns share an index).
100    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    // Plain columns get canonical indices first.
115    let plain_indices: Vec<usize> = plain_ast.iter().map(|e| intern(e)).collect::<Result<_>>()?;
116
117    // Extension sets: each set is a list of ast::Expr refs → indices.
118    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    // Cross-product: plain_indices × ext_sets_indexed.
124    // For each extension set, prepend the plain indices.
125    let grouping_sets: Vec<Vec<usize>> = if ext_sets_indexed.is_empty() {
126        // Only plain columns — this shouldn't happen (caught by has_extension),
127        // but handle gracefully.
128        vec![plain_indices]
129    } else {
130        ext_sets_indexed
131            .into_iter()
132            .map(|ext_set| {
133                // plain_indices always present; ext_set columns also present.
134                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
151/// Returns true if the expression is a ROLLUP/CUBE/GROUPING SETS node.
152fn is_grouping_extension(expr: &ast::Expr) -> bool {
153    matches!(
154        expr,
155        ast::Expr::Rollup(_) | ast::Expr::Cube(_) | ast::Expr::GroupingSets(_)
156    )
157}
158
159/// Expand `ROLLUP(a, b, c)` → `[[a,b,c], [a,b], [a], []]`.
160///
161/// The input is `Vec<Vec<Expr>>` where each inner vec is one composite element.
162/// We flatten composite elements to individual expressions for simplicity — the
163/// outer product is: suffix-strip from all-present down to empty.
164fn expand_rollup(groups: &[Vec<ast::Expr>]) -> Vec<Vec<&ast::Expr>> {
165    // Flatten composite groups (e.g. `(a, b)` as one element) into atoms.
166    let atoms: Vec<&ast::Expr> = groups.iter().flat_map(|g| g.iter()).collect();
167    let n = atoms.len();
168    // Prefixes: atoms[0..n], atoms[0..n-1], ..., atoms[0..0] (empty).
169    (0..=n).rev().map(|len| atoms[..len].to_vec()).collect()
170}
171
172/// Expand `CUBE(a, b)` → all 2^N subsets.
173fn 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    // Enumerate all bitmasks from (all-present) down to 0 (empty).
179    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
189/// Resolve the canonical index for a `GROUPING(col)` argument.
190///
191/// Matches by display string against `canonical_names`.
192pub fn resolve_grouping_col(col_expr: &ast::Expr, canonical_keys: &[SqlExpr]) -> Result<usize> {
193    let display = format!("{col_expr}");
194    // Find by rebuilding the display of each canonical key via its SqlExpr.
195    // Since canonical_keys are built from the same AST exprs, we can compare
196    // their SqlExpr display strings.
197    for (i, key) in canonical_keys.iter().enumerate() {
198        if format!("{key:?}").contains(&display) {
199            return Ok(i);
200        }
201    }
202    // Fallback: try normalized ident match.
203    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        // ROLLUP(region, country) → [[0,1], [0], []]
247        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]); // (region, country)
250        assert_eq!(result.grouping_sets[1], vec![0]); // (region)
251        assert_eq!(result.grouping_sets[2], Vec::<usize>::new()); // ()
252    }
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        // CUBE(region, country) → [[0,1], [0], [1], []]
263        assert_eq!(result.canonical_keys.len(), 2);
264        assert_eq!(result.grouping_sets.len(), 4);
265        // All-present first.
266        assert!(result.grouping_sets[0].contains(&0));
267        assert!(result.grouping_sets[0].contains(&1));
268        // Empty set last.
269        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        // Canonical: a(0), b(1), c(2).
302        // Extension sets (from ROLLUP(b,c)): [[b,c], [b], []].
303        // Cross-product with plain [a]:
304        //   set 0: [a, b, c] = [0,1,2]
305        //   set 1: [a, b]    = [0,1]
306        //   set 2: [a]       = [0]
307        assert_eq!(result.canonical_keys.len(), 3);
308        assert_eq!(result.grouping_sets.len(), 3);
309        assert!(result.grouping_sets[0].contains(&0)); // a always present
310        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); // (a,b,c),(a,b),(a),()
321    }
322}