nodedb_sql/planner/subquery/
scalar.rs1use sqlparser::ast::{self, Expr, SetExpr};
6
7use crate::error::Result;
8use crate::functions::registry::FunctionRegistry;
9use crate::parser::normalize::normalize_ident;
10use crate::types::*;
11
12use super::extract::SubqueryJoin;
13
14fn canonical_aggregate_key(function: &str, field: &str) -> String {
15 format!("{function}({field})")
16}
17
18pub(super) struct ScalarSubqueryResult {
20 pub(super) join: SubqueryJoin,
21 pub(super) replacement_expr: Expr,
22}
23
24pub(super) fn try_plan_scalar_subquery(
32 subquery: &ast::Query,
33 catalog: &dyn SqlCatalog,
34 functions: &FunctionRegistry,
35 temporal: crate::TemporalScope,
36) -> Result<Option<ScalarSubqueryResult>> {
37 let inner_plan = crate::planner::select::plan_query(subquery, catalog, functions, temporal)?;
38
39 let result_col = match extract_scalar_column(subquery) {
41 Some(col) => col,
42 None => return Ok(None),
43 };
44
45 let replacement = Expr::Identifier(ast::Ident::new(&result_col));
46
47 Ok(Some(ScalarSubqueryResult {
48 join: SubqueryJoin {
49 on: Vec::new(),
51 inner_plan,
52 join_type: JoinType::Cross,
53 },
54 replacement_expr: replacement,
55 }))
56}
57
58fn extract_scalar_column(query: &ast::Query) -> Option<String> {
64 let select = match &*query.body {
65 SetExpr::Select(s) => s,
66 _ => return None,
67 };
68 if select.projection.len() != 1 {
69 return None;
70 }
71 match &select.projection[0] {
72 ast::SelectItem::ExprWithAlias { alias, .. } => Some(normalize_ident(alias)),
73 ast::SelectItem::UnnamedExpr(expr) => match expr {
74 Expr::Identifier(ident) => Some(normalize_ident(ident)),
75 Expr::CompoundIdentifier(parts) if parts.len() >= 3 => {
76 None
78 }
79 Expr::CompoundIdentifier(parts) if parts.len() == 2 => Some(normalize_ident(&parts[1])),
80 Expr::Function(func) => {
81 let func_name = func
82 .name
83 .0
84 .iter()
85 .map(|p| match p {
86 ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
87 _ => String::new(),
88 })
89 .collect::<Vec<_>>()
90 .join(".")
91 .to_lowercase();
92 let arg = match &func.args {
93 ast::FunctionArguments::List(arg_list) => arg_list
94 .args
95 .first()
96 .and_then(|a| match a {
97 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(
98 Expr::Identifier(ident),
99 )) => Some(normalize_ident(ident)),
100 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(
101 Expr::CompoundIdentifier(parts),
102 )) if parts.len() >= 3 => None,
103 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(
104 Expr::CompoundIdentifier(parts),
105 )) if parts.len() == 2 => Some(normalize_ident(&parts[1])),
106 ast::FunctionArg::Unnamed(
107 ast::FunctionArgExpr::Wildcard
108 | ast::FunctionArgExpr::QualifiedWildcard(_),
109 ) => Some("all".to_string()),
110 _ => None,
111 })
112 .unwrap_or_else(|| "*".to_string()),
113 _ => "*".to_string(),
114 };
115 Some(canonical_aggregate_key(&func_name, &arg))
116 }
117 _ => None,
118 },
119 _ => None,
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::extract_scalar_column;
126 use crate::parser::statement::parse_sql;
127 use sqlparser::ast::Statement;
128
129 #[test]
130 fn unaliased_scalar_aggregate_uses_canonical_aggregate_key() {
131 let statements = parse_sql("SELECT AVG(amount) FROM orders").unwrap();
132 let Statement::Query(query) = &statements[0] else {
133 panic!("expected query");
134 };
135 assert_eq!(extract_scalar_column(query), Some("avg(amount)".into()));
136 }
137}