nodedb_sql/planner/
cp_projection.rs1use core::ops::ControlFlow;
13
14use sqlparser::ast::{self, Expr, Visit, Visitor};
15
16use crate::error::Result;
17use crate::functions::sequence_accessor::is_sequence_accessor;
18use crate::resolver::ColumnScope;
19use crate::resolver::columns::TableScope;
20use crate::resolver::expr::convert_expr;
21use crate::types::plan::contains_sequence_accessor;
22use crate::types::{Projection, SqlExpr};
23
24pub fn cp_projection_scope(scope: &TableScope) -> Option<TableScope> {
32 (scope.is_row_scope() && scope.is_statement_output())
33 .then(|| scope.clone().allowing_cp_functions())
34}
35
36pub fn ast_sequence_accessor(expr: &Expr) -> Option<String> {
39 let mut detector = AccessorDetector { found: None };
40 let _ = expr.visit(&mut detector);
41 detector.found
42}
43
44pub fn ast_calls_sequence_accessor(expr: &Expr) -> bool {
46 ast_sequence_accessor(expr).is_some()
47}
48
49struct AccessorDetector {
50 found: Option<String>,
51}
52
53impl Visitor for AccessorDetector {
54 type Break = ();
55
56 fn pre_visit_expr(&mut self, expr: &Expr) -> ControlFlow<()> {
57 if let Expr::Function(f) = expr
58 && f.name.0.len() == 1
59 && let Some(ast::ObjectNamePart::Identifier(ident)) = f.name.0.first()
60 && is_sequence_accessor(&ident.value)
61 {
62 self.found = Some(ident.value.to_ascii_lowercase());
63 return ControlFlow::Break(());
64 }
65 ControlFlow::Continue(())
66 }
67}
68
69pub fn convert_cp_item(
76 expr: &Expr,
77 alias: String,
78 cp_scope: Option<&TableScope>,
79) -> Result<Option<Projection>> {
80 let Some(cp_scope) = cp_scope else {
81 return Ok(None);
82 };
83 if !ast_calls_sequence_accessor(expr) {
84 return Ok(None);
85 }
86 let converted = convert_expr(expr, &ColumnScope::Relations(cp_scope))?;
87 if !contains_sequence_accessor(&converted) {
88 return Ok(None);
89 }
90 Ok(Some(Projection::CpComputed {
91 expr: converted,
92 alias,
93 }))
94}
95
96pub fn to_cp_computed(projection: Projection) -> Projection {
102 match projection {
103 Projection::Computed { expr, alias } => match expr {
104 SqlExpr::Column { .. } => Projection::Computed { expr, alias },
105 SqlExpr::Function { .. }
106 | SqlExpr::Literal(_)
107 | SqlExpr::BinaryOp { .. }
108 | SqlExpr::UnaryOp { .. }
109 | SqlExpr::Case { .. }
110 | SqlExpr::Cast { .. }
111 | SqlExpr::Subquery(_)
112 | SqlExpr::Wildcard
113 | SqlExpr::IsNull { .. }
114 | SqlExpr::InList { .. }
115 | SqlExpr::Between { .. }
116 | SqlExpr::Like { .. }
117 | SqlExpr::ArrayLiteral(_) => Projection::CpComputed { expr, alias },
118 },
119 Projection::Column(_)
120 | Projection::Star
121 | Projection::QualifiedStar(_)
122 | Projection::CpComputed { .. } => projection,
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::resolver::columns::test_support::open_scope;
130 use sqlparser::dialect::GenericDialect;
131 use sqlparser::parser::Parser;
132
133 fn select_expr(sql: &str) -> Expr {
134 let stmts = Parser::parse_sql(&GenericDialect {}, sql).unwrap();
135 match stmts.into_iter().next().unwrap() {
136 ast::Statement::Query(q) => match *q.body {
137 ast::SetExpr::Select(s) => match s.projection.into_iter().next().unwrap() {
138 ast::SelectItem::UnnamedExpr(e)
139 | ast::SelectItem::ExprWithAlias { expr: e, .. } => e,
140 other => panic!("unexpected projection item {other:?}"),
141 },
142 _ => panic!("expected SELECT"),
143 },
144 _ => panic!("expected query"),
145 }
146 }
147
148 #[test]
149 fn ast_detection_sees_nested_accessors_only() {
150 assert!(ast_calls_sequence_accessor(&select_expr(
151 "SELECT CASE WHEN a > 0 THEN nextval('s') * 2 ELSE 0 END"
152 )));
153 assert!(ast_calls_sequence_accessor(&select_expr(
154 "SELECT coalesce(currval('s'), 0)"
155 )));
156 assert!(!ast_calls_sequence_accessor(&select_expr(
157 "SELECT upper(a) || 'nextval'"
158 )));
159 }
160
161 #[test]
162 fn a_from_less_scope_never_marks_an_item() {
163 let scope = TableScope::new().as_statement_output();
164 assert!(cp_projection_scope(&scope).is_none());
165 let item = convert_cp_item(&select_expr("SELECT nextval('s')"), "n".into(), None).unwrap();
166 assert!(item.is_none());
167 }
168
169 #[test]
170 fn a_nested_select_scope_never_marks_an_item() {
171 assert!(cp_projection_scope(&open_scope("t")).is_none());
172 let nested = open_scope("t")
173 .as_statement_output()
174 .nested_in(TableScope::new());
175 assert!(cp_projection_scope(&nested).is_none());
176 }
177
178 #[test]
179 fn an_accessor_item_over_a_relation_is_cp_computed() {
180 let cp_scope = cp_projection_scope(&open_scope("t").as_statement_output())
181 .expect("statement-output row scope");
182 let item = convert_cp_item(
183 &select_expr("SELECT nextval('s') + id"),
184 "n".into(),
185 Some(&cp_scope),
186 )
187 .unwrap()
188 .expect("accessor item is Control-Plane computed");
189 match item {
190 Projection::CpComputed { expr, alias } => {
191 assert_eq!(alias, "n");
192 assert!(matches!(expr, SqlExpr::BinaryOp { .. }));
193 }
194 other => panic!("expected CpComputed, got {other:?}"),
195 }
196 }
197
198 #[test]
199 fn a_plain_item_over_a_relation_is_left_to_the_ordinary_path() {
200 let cp_scope = cp_projection_scope(&open_scope("t").as_statement_output())
201 .expect("statement-output row scope");
202 let item =
203 convert_cp_item(&select_expr("SELECT id + 1"), "n".into(), Some(&cp_scope)).unwrap();
204 assert!(item.is_none());
205 }
206}