Skip to main content

nodedb_sql/resolver/expr/convert/
entry.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Entry points and per-variant dispatch for AST expression conversion.
4
5use sqlparser::ast::Expr;
6
7use crate::error::{Result, SqlError};
8use crate::resolver::ColumnScope;
9use crate::resolver::expr::functions::convert_function_depth;
10use crate::types::*;
11
12use super::{builtins, identifier, literals, operators, predicates};
13
14/// Maximum AST nesting depth accepted by `convert_expr`.
15/// Exceeding this limit returns `Err` instead of overflowing the stack.
16const MAX_CONVERT_DEPTH: usize = 128;
17
18/// Convert a sqlparser `Expr` to our `SqlExpr`.
19pub fn convert_expr(expr: &Expr, scope: &ColumnScope<'_>) -> Result<SqlExpr> {
20    convert_expr_depth(expr, &mut 0, scope)
21}
22
23/// Internal recursive helper that carries a depth counter to enforce
24/// `MAX_CONVERT_DEPTH` and prevent stack overflow on malformed ASTs.
25pub(in crate::resolver::expr) fn convert_expr_depth(
26    expr: &Expr,
27    depth: &mut usize,
28    scope: &ColumnScope<'_>,
29) -> Result<SqlExpr> {
30    *depth += 1;
31    if *depth > MAX_CONVERT_DEPTH {
32        return Err(SqlError::Unsupported {
33            detail: format!("expression nesting depth exceeds maximum of {MAX_CONVERT_DEPTH}"),
34        });
35    }
36    let result = convert_expr_inner(expr, depth, scope);
37    *depth -= 1;
38    result
39}
40
41fn convert_expr_inner(expr: &Expr, depth: &mut usize, scope: &ColumnScope<'_>) -> Result<SqlExpr> {
42    match expr {
43        Expr::Identifier(ident) => identifier::convert_identifier(ident, scope),
44        Expr::CompoundIdentifier(parts) if parts.len() >= 2 => {
45            identifier::convert_compound_identifier(parts, scope)
46        }
47        Expr::Value(val) => literals::convert_value_expr(val),
48        Expr::BinaryOp { left, op, right } => {
49            operators::convert_binary(left, op, right, depth, scope)
50        }
51        Expr::UnaryOp { op, expr } => operators::convert_unary(op, expr, depth, scope),
52        Expr::Function(func) => convert_function_depth(func, depth, scope),
53        Expr::Nested(inner) => convert_expr_depth(inner, depth, scope),
54        Expr::IsNull(inner) => predicates::convert_is_null(inner, false, depth, scope),
55        Expr::IsNotNull(inner) => predicates::convert_is_null(inner, true, depth, scope),
56        Expr::InList {
57            expr,
58            list,
59            negated,
60        } => predicates::convert_in_list(expr, list, *negated, depth, scope),
61        Expr::Between {
62            expr,
63            low,
64            high,
65            negated,
66        } => predicates::convert_between(expr, low, high, *negated, depth, scope),
67        Expr::Like {
68            expr,
69            pattern,
70            negated,
71            ..
72        } => predicates::convert_like(expr, pattern, *negated, false, depth, scope),
73        Expr::ILike {
74            expr,
75            pattern,
76            negated,
77            ..
78        } => predicates::convert_like(expr, pattern, *negated, true, depth, scope),
79        Expr::Case {
80            operand,
81            conditions,
82            else_result,
83            ..
84        } => predicates::convert_case(
85            operand.as_deref(),
86            conditions,
87            else_result.as_deref(),
88            depth,
89            scope,
90        ),
91        Expr::TypedString(ts) => literals::convert_typed_string(ts),
92        Expr::Cast {
93            expr, data_type, ..
94        } => literals::convert_cast(expr, data_type, depth, scope),
95        Expr::Array(array) => literals::convert_array(array, depth, scope),
96        Expr::Wildcard(_) => literals::convert_wildcard(),
97        Expr::Trim { expr, .. } => builtins::convert_trim(expr, depth, scope),
98        Expr::Ceil { expr, .. } => builtins::convert_ceil(expr, depth, scope),
99        Expr::Floor { expr, .. } => builtins::convert_floor(expr, depth, scope),
100        Expr::Substring {
101            expr,
102            substring_from,
103            substring_for,
104            ..
105        } => builtins::convert_substring(
106            expr,
107            substring_from.as_deref(),
108            substring_for.as_deref(),
109            depth,
110            scope,
111        ),
112        Expr::Interval(interval) => literals::convert_interval(interval),
113        Expr::AnyOp {
114            left,
115            compare_op,
116            right,
117            ..
118        } => operators::convert_any_op(left, compare_op, right, depth, scope),
119        _ => Err(SqlError::Unsupported {
120            detail: format!("expression: {expr}"),
121        }),
122    }
123}
124
125#[cfg(test)]
126pub(super) mod tests {
127    use sqlparser::ast::{Expr, SelectItem, Statement};
128
129    use super::convert_expr;
130    use crate::error::SqlError;
131    use crate::parser::statement::parse_sql;
132    use crate::resolver::ColumnScope;
133    use crate::types::*;
134
135    /// Extract the first SELECT item expression from a simple `SELECT <expr> FROM <tbl>`.
136    pub(in crate::resolver::expr::convert) fn first_select_expr(sql: &str) -> Expr {
137        let stmts = parse_sql(sql).expect("parse failed");
138        let Statement::Query(q) = &stmts[0] else {
139            panic!("expected query");
140        };
141        let sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() else {
142            panic!("expected select body");
143        };
144        match &sel.projection[0] {
145            SelectItem::UnnamedExpr(e) => e.clone(),
146            SelectItem::ExprWithAlias { expr, .. } => expr.clone(),
147            other => panic!("unexpected projection item: {other:?}"),
148        }
149    }
150
151    /// Extract and convert the WHERE predicate from a simple
152    /// `SELECT * FROM tbl WHERE <expr>` statement.
153    pub(in crate::resolver::expr::convert) fn where_sql_expr(sql: &str) -> SqlExpr {
154        let stmts = parse_sql(sql).expect("parse failed");
155        let Statement::Query(q) = &stmts[0] else {
156            panic!("expected query");
157        };
158        let sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() else {
159            panic!("expected select body");
160        };
161        let raw = sel.selection.as_ref().expect("expected WHERE clause");
162        convert_expr(raw, &ColumnScope::Unchecked).expect("convert_expr failed")
163    }
164
165    /// Parses `SELECT <expr> FROM t` and returns the lowered `SqlExpr` for `<expr>`.
166    pub(in crate::resolver::expr::convert) fn select_expr_lowered(sql: &str) -> SqlExpr {
167        let stmts = parse_sql(sql).expect("parse failed");
168        let Statement::Query(q) = &stmts[0] else {
169            panic!("expected query");
170        };
171        let sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() else {
172            panic!("expected select body");
173        };
174        let raw = &sel.projection[0];
175        let raw_expr = match raw {
176            SelectItem::UnnamedExpr(e) => e,
177            SelectItem::ExprWithAlias { expr, .. } => expr,
178            other => panic!("unexpected projection: {other:?}"),
179        };
180        convert_expr(raw_expr, &ColumnScope::Unchecked).expect("convert_expr failed")
181    }
182
183    #[test]
184    fn ts_rank_cd_is_unsupported() {
185        use crate::parser::statement::parse_sql;
186        let sql = "SELECT ts_rank_cd(body, to_tsquery('rust')) FROM t";
187        let stmts = parse_sql(sql).expect("parse ok");
188        let Statement::Query(q) = &stmts[0] else {
189            panic!("expected query");
190        };
191        let sqlparser::ast::SetExpr::Select(sel) = q.body.as_ref() else {
192            panic!("expected select body");
193        };
194        let raw = match &sel.projection[0] {
195            SelectItem::UnnamedExpr(e) => e,
196            SelectItem::ExprWithAlias { expr, .. } => expr,
197            other => panic!("unexpected projection: {other:?}"),
198        };
199        let err = convert_expr(raw, &ColumnScope::Unchecked).unwrap_err();
200        assert!(
201            matches!(err, SqlError::Unsupported { .. }),
202            "ts_rank_cd should be Unsupported, got {err:?}"
203        );
204        let msg = format!("{err}");
205        assert!(
206            msg.contains("ts_rank_cd"),
207            "error should mention ts_rank_cd: {msg}"
208        );
209    }
210
211    #[test]
212    fn to_tsquery_lowers_to_pg_to_tsquery() {
213        let expr = select_expr_lowered("SELECT to_tsquery('rust & lang') FROM t");
214        match expr {
215            SqlExpr::Function { ref name, .. } => {
216                assert_eq!(name, "pg_to_tsquery");
217            }
218            other => panic!("expected pg_to_tsquery Function, got {other:?}"),
219        }
220    }
221
222    #[test]
223    fn plainto_tsquery_lowers_correctly() {
224        let expr = select_expr_lowered("SELECT plainto_tsquery('rust lang') FROM t");
225        match expr {
226            SqlExpr::Function { ref name, .. } => {
227                assert_eq!(name, "pg_plainto_tsquery");
228            }
229            other => panic!("expected pg_plainto_tsquery, got {other:?}"),
230        }
231    }
232
233    #[test]
234    fn ts_rank_lowers_to_pg_ts_rank() {
235        let expr = select_expr_lowered("SELECT ts_rank(body, to_tsquery('rust')) FROM t");
236        match expr {
237            SqlExpr::Function { ref name, .. } => {
238                assert_eq!(name, "pg_ts_rank");
239            }
240            other => panic!("expected pg_ts_rank, got {other:?}"),
241        }
242    }
243}