nodedb_sql/planner/
group_by.rs1use sqlparser::ast::{self, GroupByExpr};
10
11use crate::error::Result;
12use crate::parser::normalize::normalize_ident;
13use crate::resolver::ColumnScope;
14use crate::resolver::columns::TableScope;
15use crate::resolver::expr::convert_expr;
16use crate::types::{ColumnInfo, SqlExpr};
17
18pub fn convert_group_by(group_by: &GroupByExpr, scope: &TableScope) -> Result<Vec<SqlExpr>> {
20 convert_group_by_with_projection(group_by, &[], &[], scope)
21}
22
23pub fn convert_group_by_with_projection(
38 group_by: &GroupByExpr,
39 projection: &[ast::SelectItem],
40 table_columns: &[ColumnInfo],
41 scope: &TableScope,
42) -> Result<Vec<SqlExpr>> {
43 let key_scope = scope.with_output_names(
48 crate::planner::select::select_output_aliases(projection)
49 .into_iter()
50 .filter(|alias| !is_self_alias(projection, alias)),
51 );
52 let key_scope = ColumnScope::Relations(&key_scope);
53 match group_by {
54 GroupByExpr::All(_) => Ok(Vec::new()),
55 GroupByExpr::Expressions(exprs, _) => exprs
56 .iter()
57 .map(|e| {
58 reject_ordinal_out_of_range(e, projection)?;
59 match resolve_output_alias(e, projection, table_columns) {
60 Some(aliased) => convert_expr(aliased, &key_scope),
61 None => convert_expr(e, &key_scope),
62 }
63 })
64 .collect(),
65 }
66}
67
68fn is_self_alias(projection: &[ast::SelectItem], alias: &str) -> bool {
71 projection.iter().any(|item| match item {
72 ast::SelectItem::ExprWithAlias { expr, alias: name } => {
73 normalize_ident(name) == alias && expr_column_name(expr).is_some_and(|c| c == alias)
74 }
75 ast::SelectItem::UnnamedExpr(_)
76 | ast::SelectItem::ExprWithAliases { .. }
77 | ast::SelectItem::QualifiedWildcard(..)
78 | ast::SelectItem::Wildcard(_) => false,
79 })
80}
81
82fn reject_ordinal_out_of_range(expr: &ast::Expr, projection: &[ast::SelectItem]) -> Result<()> {
88 if projection.is_empty() {
89 return Ok(());
90 }
91 let ast::Expr::Value(v) = expr else {
92 return Ok(());
93 };
94 let ast::Value::Number(n, _) = &v.value else {
95 return Ok(());
96 };
97 let in_range = n
98 .parse::<usize>()
99 .is_ok_and(|position| position >= 1 && position <= projection.len());
100 if in_range {
101 return Ok(());
102 }
103 Err(crate::error::SqlError::Unsupported {
104 detail: format!("GROUP BY position {n} is not in select list"),
105 })
106}
107
108fn resolve_output_alias<'a>(
111 expr: &ast::Expr,
112 projection: &'a [ast::SelectItem],
113 table_columns: &[ColumnInfo],
114) -> Option<&'a ast::Expr> {
115 if let ast::Expr::Value(v) = expr
117 && let ast::Value::Number(n, _) = &v.value
118 && let Ok(position) = n.parse::<usize>()
119 && position >= 1
120 {
121 return match projection.get(position - 1) {
122 Some(
123 ast::SelectItem::UnnamedExpr(e) | ast::SelectItem::ExprWithAlias { expr: e, .. },
124 ) => Some(e),
125 Some(_) | None => None,
126 };
127 }
128 let ast::Expr::Identifier(ident) = expr else {
129 return None;
130 };
131 let needle = normalize_ident(ident);
132
133 if table_columns.iter().any(|c| c.name == needle) {
135 return None;
136 }
137
138 projection.iter().find_map(|item| match item {
139 ast::SelectItem::ExprWithAlias { expr, alias }
140 if normalize_ident(alias) == needle
141 && !matches!(expr, ast::Expr::Identifier(i) if normalize_ident(i) == needle) =>
144 {
145 Some(expr)
146 }
147 _ => None,
148 })
149}
150
151pub fn group_by_output_aliases(
158 projection: &[ast::SelectItem],
159 group_by: &[SqlExpr],
160) -> Vec<Option<String>> {
161 group_by
162 .iter()
163 .map(|key| {
164 key_column_name(key).and_then(|name| projection_alias_for_column(projection, name))
165 })
166 .collect()
167}
168
169pub fn key_column_name(key: &SqlExpr) -> Option<&str> {
171 match key {
172 SqlExpr::Column { name, .. } => Some(name.as_str()),
173 _ => None,
174 }
175}
176
177fn projection_alias_for_column(projection: &[ast::SelectItem], col: &str) -> Option<String> {
181 for item in projection {
182 if let ast::SelectItem::ExprWithAlias { expr, alias } = item
183 && expr_column_name(expr).is_some_and(|n| n == col)
184 {
185 return Some(normalize_ident(alias));
186 }
187 }
188 None
189}
190
191pub fn expr_column_name(expr: &ast::Expr) -> Option<String> {
194 match expr {
195 ast::Expr::Identifier(ident) => Some(normalize_ident(ident)),
196 ast::Expr::CompoundIdentifier(parts) => parts.last().map(normalize_ident),
197 _ => None,
198 }
199}