Skip to main content

nodedb_sql/planner/
cp_projection.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Control-Plane-computed SELECT-list items.
4//!
5//! A SELECT-list item over a relation that calls a sequence accessor cannot
6//! run on the Data Plane: the row evaluator holds no sequence state. The
7//! planner marks such an item [`Projection::CpComputed`], and the Control
8//! Plane evaluates it once per output row after the rows return. Detection
9//! runs on the AST, so an item with no accessor converts exactly as before,
10//! against the ordinary scope.
11
12use 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
24/// A copy of `scope` that resolves sequence accessors, when `scope` iterates
25/// rows and backs the statement's output SELECT. A FROM-less SELECT gets
26/// `None`: the planner evaluates the accessor itself there, and the item
27/// stays an ordinary `Computed` entry. A nested SELECT (subquery, CTE
28/// body, UNION branch, derived table, INSERT or MERGE source) gets `None`
29/// too: the Control Plane evaluates the statement's output rows only, so
30/// the ordinary scope refuses the accessor there.
31pub 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
36/// The lowercase name of the first sequence accessor `expr` calls, at any
37/// nesting depth.
38pub 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
44/// Whether `expr` calls a sequence accessor anywhere, at any nesting depth.
45pub 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
69/// Convert one SELECT item into a [`Projection::CpComputed`] when it calls a
70/// sequence accessor over a relation. `None` when the item holds no accessor
71/// or no relation is in scope; the caller converts it the ordinary way.
72///
73/// An accessor inside a subquery is not the item's own: the subquery body
74/// resolves in a nested scope, which refuses it.
75pub 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
96/// A computed item as a Control-Plane computed item.
97///
98/// A computed item that is a bare column under an alias stays `Computed`: the
99/// value is the stored column, looked up under the source name and displayed
100/// under the alias, so nothing is evaluated. Every other item passes through.
101pub 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}