Skip to main content

nodedb_sql/resolver/expr/
convert.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Convert sqlparser AST expressions to our SqlExpr IR.
4
5use sqlparser::ast::{self, Expr, Value};
6
7use crate::error::{Result, SqlError};
8use crate::parser::normalize::{SCHEMA_QUALIFIED_MSG, normalize_ident};
9use crate::types::*;
10
11use super::binary_ops::{convert_binary_op, convert_unary_op};
12use super::functions::convert_function_depth;
13use super::value::{convert_value, parse_interval_to_micros};
14
15/// Maximum AST nesting depth accepted by `convert_expr`.
16/// Exceeding this limit returns `Err` instead of overflowing the stack.
17const MAX_CONVERT_DEPTH: usize = 128;
18
19/// SQL-standard niladic functions: written without parentheses. Parsers
20/// emit them as bare identifiers; we promote them to function calls so
21/// they fold to a value at plan time instead of resolving to a column.
22fn is_zero_arg_keyword_function(name: &str) -> bool {
23    matches!(
24        name,
25        "current_timestamp"
26            | "current_date"
27            | "current_time"
28            | "localtime"
29            | "localtimestamp"
30            | "current_user"
31            | "current_role"
32            | "current_schema"
33            | "session_user"
34            | "user"
35            | "version"
36    )
37}
38
39/// Convert a sqlparser `Expr` to our `SqlExpr`.
40pub fn convert_expr(expr: &Expr) -> Result<SqlExpr> {
41    convert_expr_depth(expr, &mut 0)
42}
43
44/// Internal recursive helper that carries a depth counter to enforce
45/// `MAX_CONVERT_DEPTH` and prevent stack overflow on malformed ASTs.
46pub(super) fn convert_expr_depth(expr: &Expr, depth: &mut usize) -> Result<SqlExpr> {
47    *depth += 1;
48    if *depth > MAX_CONVERT_DEPTH {
49        return Err(SqlError::Unsupported {
50            detail: format!("expression nesting depth exceeds maximum of {MAX_CONVERT_DEPTH}"),
51        });
52    }
53    let result = convert_expr_inner(expr, depth);
54    *depth -= 1;
55    result
56}
57
58fn convert_expr_inner(expr: &Expr, depth: &mut usize) -> Result<SqlExpr> {
59    match expr {
60        Expr::Identifier(ident) => {
61            let name = normalize_ident(ident);
62            // SQL-standard zero-arg keyword functions parse as bare
63            // identifiers (no parentheses): `SELECT current_timestamp`,
64            // `SELECT current_user`, etc. Promote them to function calls
65            // so const folding evaluates them like the parenthesised form.
66            if is_zero_arg_keyword_function(&name) {
67                return Ok(SqlExpr::Function {
68                    name,
69                    args: vec![],
70                    distinct: false,
71                });
72            }
73            Ok(SqlExpr::Column { table: None, name })
74        }
75        Expr::CompoundIdentifier(parts) if parts.len() >= 3 => {
76            let qualified: String = parts
77                .iter()
78                .map(normalize_ident)
79                .collect::<Vec<_>>()
80                .join(".");
81            Err(SqlError::Unsupported {
82                detail: format!(
83                    "schema-qualified column reference '{qualified}': {SCHEMA_QUALIFIED_MSG}"
84                ),
85            })
86        }
87        Expr::CompoundIdentifier(parts) if parts.len() == 2 => Ok(SqlExpr::Column {
88            table: Some(normalize_ident(&parts[0])),
89            name: normalize_ident(&parts[1]),
90        }),
91        Expr::Value(val) => Ok(SqlExpr::Literal(convert_value(&val.value)?)),
92        Expr::BinaryOp { left, op, right } => {
93            // JSON and FTS operators are lowered to function calls before the
94            // generic binary-op path so they are never passed to
95            // convert_binary_op.
96            use ast::BinaryOperator;
97            let json_fn: Option<&str> = match op {
98                BinaryOperator::Arrow => Some("pg_json_get"),
99                BinaryOperator::LongArrow => Some("pg_json_get_text"),
100                BinaryOperator::HashArrow => Some("pg_json_path_get"),
101                BinaryOperator::HashLongArrow => Some("pg_json_path_get_text"),
102                BinaryOperator::AtArrow => Some("pg_json_contains"),
103                BinaryOperator::ArrowAt => Some("pg_json_contained_by"),
104                BinaryOperator::Question => Some("pg_json_has_key"),
105                BinaryOperator::QuestionAnd => Some("pg_json_has_all_keys"),
106                BinaryOperator::QuestionPipe => Some("pg_json_has_any_key"),
107                _ => None,
108            };
109            if let Some(name) = json_fn {
110                return Ok(SqlExpr::Function {
111                    name: name.into(),
112                    args: vec![
113                        convert_expr_depth(left, depth)?,
114                        convert_expr_depth(right, depth)?,
115                    ],
116                    distinct: false,
117                });
118            }
119            // `col @@ query` → pg_fts_match(col, query)
120            if matches!(op, BinaryOperator::AtAt) {
121                let col_expr = convert_expr_depth(left, depth)?;
122                let query_expr = convert_expr_depth(right, depth)?;
123                return Ok(crate::functions::fts_ops::pg_fts_funcs::lower_pg_fts_match(
124                    col_expr, query_expr,
125                ));
126            }
127            Ok(SqlExpr::BinaryOp {
128                left: Box::new(convert_expr_depth(left, depth)?),
129                op: convert_binary_op(op)?,
130                right: Box::new(convert_expr_depth(right, depth)?),
131            })
132        }
133        Expr::UnaryOp { op, expr } => Ok(SqlExpr::UnaryOp {
134            op: convert_unary_op(op)?,
135            expr: Box::new(convert_expr_depth(expr, depth)?),
136        }),
137        Expr::Function(func) => convert_function_depth(func, depth),
138        Expr::Nested(inner) => convert_expr_depth(inner, depth),
139        Expr::IsNull(inner) => Ok(SqlExpr::IsNull {
140            expr: Box::new(convert_expr_depth(inner, depth)?),
141            negated: false,
142        }),
143        Expr::IsNotNull(inner) => Ok(SqlExpr::IsNull {
144            expr: Box::new(convert_expr_depth(inner, depth)?),
145            negated: true,
146        }),
147        Expr::InList {
148            expr,
149            list,
150            negated,
151        } => Ok(SqlExpr::InList {
152            expr: Box::new(convert_expr_depth(expr, depth)?),
153            list: list
154                .iter()
155                .map(|e| convert_expr_depth(e, depth))
156                .collect::<Result<_>>()?,
157            negated: *negated,
158        }),
159        Expr::Between {
160            expr,
161            low,
162            high,
163            negated,
164        } => Ok(SqlExpr::Between {
165            expr: Box::new(convert_expr_depth(expr, depth)?),
166            low: Box::new(convert_expr_depth(low, depth)?),
167            high: Box::new(convert_expr_depth(high, depth)?),
168            negated: *negated,
169        }),
170        Expr::Like {
171            expr,
172            pattern,
173            negated,
174            ..
175        } => Ok(SqlExpr::Like {
176            expr: Box::new(convert_expr_depth(expr, depth)?),
177            pattern: Box::new(convert_expr_depth(pattern, depth)?),
178            negated: *negated,
179            case_insensitive: false,
180        }),
181        Expr::ILike {
182            expr,
183            pattern,
184            negated,
185            ..
186        } => Ok(SqlExpr::Like {
187            expr: Box::new(convert_expr_depth(expr, depth)?),
188            pattern: Box::new(convert_expr_depth(pattern, depth)?),
189            negated: *negated,
190            case_insensitive: true,
191        }),
192        Expr::Case {
193            operand,
194            conditions,
195            else_result,
196            ..
197        } => {
198            let when_then = conditions
199                .iter()
200                .map(|cw| {
201                    Ok((
202                        convert_expr_depth(&cw.condition, depth)?,
203                        convert_expr_depth(&cw.result, depth)?,
204                    ))
205                })
206                .collect::<Result<Vec<_>>>()?;
207            Ok(SqlExpr::Case {
208                operand: operand
209                    .as_ref()
210                    .map(|e| convert_expr_depth(e, depth).map(Box::new))
211                    .transpose()?,
212                when_then,
213                else_expr: else_result
214                    .as_ref()
215                    .map(|e| convert_expr_depth(e, depth).map(Box::new))
216                    .transpose()?,
217            })
218        }
219        Expr::TypedString(ts) => {
220            // TIMESTAMP '...' and TIMESTAMPTZ '...' typed string literals.
221            let type_str = format!("{}", ts.data_type).to_ascii_uppercase();
222            let raw = match &ts.value.value {
223                Value::SingleQuotedString(s) => s.clone(),
224                other => {
225                    return Err(SqlError::Unsupported {
226                        detail: format!("typed string value: {other}"),
227                    });
228                }
229            };
230            match type_str.as_str() {
231                "TIMESTAMP" => {
232                    let dt =
233                        nodedb_types::NdbDateTime::parse(&raw).ok_or_else(|| SqlError::Parse {
234                            detail: format!("cannot parse TIMESTAMP literal: '{raw}'"),
235                        })?;
236                    return Ok(SqlExpr::Literal(SqlValue::Timestamp(dt)));
237                }
238                "TIMESTAMPTZ" | "TIMESTAMP WITH TIME ZONE" => {
239                    let dt =
240                        nodedb_types::NdbDateTime::parse(&raw).ok_or_else(|| SqlError::Parse {
241                            detail: format!("cannot parse TIMESTAMPTZ literal: '{raw}'"),
242                        })?;
243                    return Ok(SqlExpr::Literal(SqlValue::Timestamptz(dt)));
244                }
245                _ => {}
246            }
247            // Fall through: return as a generic literal string.
248            Ok(SqlExpr::Literal(SqlValue::String(raw)))
249        }
250        Expr::Cast {
251            expr, data_type, ..
252        } => {
253            // `::tsvector` and `::tsquery` casts are PG surface notation; the
254            // inner expression is the actual text value.  Elide the cast and
255            // return the inner expression directly — no runtime type change is
256            // needed since we operate on plain strings internally.
257            let type_str = format!("{data_type}").to_ascii_lowercase();
258            if type_str == "tsvector" || type_str == "tsquery" {
259                return convert_expr_depth(expr, depth);
260            }
261            // `'...'::TIMESTAMP` and `'...'::TIMESTAMPTZ` — promote string literals
262            // to typed SqlValue when the inner expression is a string literal.
263            let upper = type_str.to_uppercase();
264            if (upper == "TIMESTAMP"
265                || upper == "TIMESTAMPTZ"
266                || upper == "TIMESTAMP WITH TIME ZONE")
267                && let Expr::Value(v) = expr.as_ref()
268                && let Value::SingleQuotedString(s) = &v.value
269            {
270                let dt = nodedb_types::NdbDateTime::parse(s).ok_or_else(|| SqlError::Parse {
271                    detail: format!("cannot parse timestamp cast: '{s}'"),
272                })?;
273                return Ok(SqlExpr::Literal(if upper == "TIMESTAMP" {
274                    SqlValue::Timestamp(dt)
275                } else {
276                    SqlValue::Timestamptz(dt)
277                }));
278            }
279            Ok(SqlExpr::Cast {
280                expr: Box::new(convert_expr_depth(expr, depth)?),
281                to_type: format!("{data_type}"),
282            })
283        }
284        Expr::Array(ast::Array { elem, .. }) => {
285            let elems = elem
286                .iter()
287                .map(|e| convert_expr_depth(e, depth))
288                .collect::<Result<_>>()?;
289            Ok(SqlExpr::ArrayLiteral(elems))
290        }
291        Expr::Wildcard(_) => Ok(SqlExpr::Wildcard),
292        // TRIM([BOTH|LEADING|TRAILING] [what FROM] expr)
293        Expr::Trim { expr, .. } => Ok(SqlExpr::Function {
294            name: "trim".into(),
295            args: vec![convert_expr_depth(expr, depth)?],
296            distinct: false,
297        }),
298        // CEIL(expr) / FLOOR(expr)
299        Expr::Ceil { expr, .. } => Ok(SqlExpr::Function {
300            name: "ceil".into(),
301            args: vec![convert_expr_depth(expr, depth)?],
302            distinct: false,
303        }),
304        Expr::Floor { expr, .. } => Ok(SqlExpr::Function {
305            name: "floor".into(),
306            args: vec![convert_expr_depth(expr, depth)?],
307            distinct: false,
308        }),
309        // SUBSTRING(expr FROM start FOR len)
310        Expr::Substring {
311            expr,
312            substring_from,
313            substring_for,
314            ..
315        } => {
316            let mut args = vec![convert_expr_depth(expr, depth)?];
317            if let Some(from) = substring_from {
318                args.push(convert_expr_depth(from, depth)?);
319            }
320            if let Some(len) = substring_for {
321                args.push(convert_expr_depth(len, depth)?);
322            }
323            Ok(SqlExpr::Function {
324                name: "substring".into(),
325                args,
326                distinct: false,
327            })
328        }
329        Expr::Interval(interval) => {
330            // INTERVAL '1 hour' → microseconds as i64 literal.
331            // The interval value is typically a string literal.
332            let interval_str = match interval.value.as_ref() {
333                Expr::Value(v) => match &v.value {
334                    Value::SingleQuotedString(s) => s.clone(),
335                    Value::Number(n, _) => {
336                        // INTERVAL 5 HOUR → combine number with leading_field.
337                        if let Some(ref field) = interval.leading_field {
338                            format!("{n} {field}")
339                        } else {
340                            n.clone()
341                        }
342                    }
343                    _ => {
344                        return Err(SqlError::Unsupported {
345                            detail: format!("INTERVAL value: {}", interval.value),
346                        });
347                    }
348                },
349                _ => {
350                    return Err(SqlError::Unsupported {
351                        detail: format!("INTERVAL expression: {}", interval.value),
352                    });
353                }
354            };
355
356            // If leading_field is specified, append it: INTERVAL '5' HOUR → "5 HOUR"
357            let full_str = if interval_str.chars().all(|c| c.is_ascii_digit())
358                && let Some(ref field) = interval.leading_field
359            {
360                format!("{interval_str} {field}")
361            } else {
362                interval_str
363            };
364
365            let micros = parse_interval_to_micros(&full_str).ok_or_else(|| SqlError::Parse {
366                detail: format!("cannot parse INTERVAL '{full_str}'"),
367            })?;
368
369            Ok(SqlExpr::Literal(SqlValue::Int(micros)))
370        }
371        // `left = ANY(right)` — desugar into InList over array elements.
372        // When `right` resolves to an ArrayLiteral (or a function call that
373        // the bridge/evaluator will fold to an array), emit InList so the
374        // downstream scan filter path handles it natively.
375        Expr::AnyOp {
376            left,
377            compare_op,
378            right,
379            ..
380        } => {
381            // Only support `=` comparison for now; reject other operators
382            // with a clear, non-AST-leaking message.
383            use ast::BinaryOperator;
384            if !matches!(compare_op, BinaryOperator::Eq) {
385                return Err(SqlError::Unsupported {
386                    detail: "ANY operator with non-equality comparison is not supported".into(),
387                });
388            }
389            let left_expr = convert_expr_depth(left, depth)?;
390            let right_expr = convert_expr_depth(right, depth)?;
391            // Expand the right-hand side into a list if it is an array literal;
392            // otherwise wrap as a single-element list so InList still evaluates.
393            let list = match right_expr {
394                SqlExpr::ArrayLiteral(elems) => elems,
395                other => vec![other],
396            };
397            Ok(SqlExpr::InList {
398                expr: Box::new(left_expr),
399                list,
400                negated: false,
401            })
402        }
403        _ => Err(SqlError::Unsupported {
404            detail: format!("expression: {expr}"),
405        }),
406    }
407}