Skip to main content

nodedb_sql/resolver/expr/
functions.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use sqlparser::ast;
4
5use crate::error::{Result, SqlError};
6use crate::parser::normalize::{SCHEMA_QUALIFIED_MSG, normalize_ident};
7use crate::types::*;
8
9use super::convert::convert_expr_depth;
10
11pub(super) fn convert_function_depth(func: &ast::Function, depth: &mut usize) -> Result<SqlExpr> {
12    // Intercept PG FTS surface functions and lower them to pg_* internal names
13    // before the generic path runs.
14    if func.name.0.len() == 1 {
15        let raw_name = match &func.name.0[0] {
16            ast::ObjectNamePart::Identifier(ident) => ident.value.to_ascii_lowercase(),
17            _ => String::new(),
18        };
19        if let Some(expr) = intercept_fts_function(&raw_name, func, depth)? {
20            return Ok(expr);
21        }
22    }
23
24    if func.name.0.len() > 1 {
25        let qualified: String = func
26            .name
27            .0
28            .iter()
29            .map(|p| match p {
30                ast::ObjectNamePart::Identifier(ident) => ident.value.clone(),
31                _ => String::new(),
32            })
33            .collect::<Vec<_>>()
34            .join(".");
35        return Err(SqlError::Unsupported {
36            detail: format!("schema-qualified function name '{qualified}': {SCHEMA_QUALIFIED_MSG}"),
37        });
38    }
39    let name = func
40        .name
41        .0
42        .iter()
43        .map(|p| match p {
44            ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
45            _ => String::new(),
46        })
47        .collect::<Vec<_>>()
48        .join(".");
49
50    let args = match &func.args {
51        ast::FunctionArguments::None => Vec::new(),
52        ast::FunctionArguments::Subquery(_) => {
53            return Err(SqlError::Unsupported {
54                detail: "subquery in function args".into(),
55            });
56        }
57        ast::FunctionArguments::List(arg_list) => arg_list
58            .args
59            .iter()
60            .filter_map(|a| match a {
61                ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => {
62                    Some(convert_expr_depth(e, depth))
63                }
64                ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Wildcard) => {
65                    Some(Ok(SqlExpr::Wildcard))
66                }
67                ast::FunctionArg::Named {
68                    arg: ast::FunctionArgExpr::Expr(e),
69                    ..
70                } => Some(convert_expr_depth(e, depth)),
71                _ => None,
72            })
73            .collect::<Result<Vec<_>>>()?,
74    };
75
76    let distinct = match &func.args {
77        ast::FunctionArguments::List(arg_list) => {
78            matches!(
79                arg_list.duplicate_treatment,
80                Some(ast::DuplicateTreatment::Distinct)
81            )
82        }
83        _ => false,
84    };
85
86    Ok(SqlExpr::Function {
87        name,
88        args,
89        distinct,
90    })
91}
92
93/// Intercept PG FTS surface functions and lower them to `pg_*` internal
94/// function-call `SqlExpr` nodes.  Returns `Ok(None)` for non-FTS names.
95///
96/// `ts_rank_cd` is rejected with `SqlError::Unsupported` as per the
97/// approved design.
98fn intercept_fts_function(
99    name: &str,
100    func: &ast::Function,
101    depth: &mut usize,
102) -> Result<Option<SqlExpr>> {
103    use crate::functions::fts_ops::pg_fts_funcs;
104
105    let args = collect_function_args(func, depth)?;
106    match name {
107        "to_tsvector" => Ok(Some(SqlExpr::Function {
108            name: "pg_to_tsvector".into(),
109            args,
110            distinct: false,
111        })),
112        "to_tsquery" => Ok(Some(pg_fts_funcs::lower_pg_to_tsquery(args))),
113        "plainto_tsquery" => Ok(Some(pg_fts_funcs::lower_pg_plainto_tsquery(args))),
114        "phraseto_tsquery" => Ok(Some(pg_fts_funcs::lower_phraseto_tsquery(args))),
115        "websearch_to_tsquery" => Ok(Some(pg_fts_funcs::lower_pg_websearch_to_tsquery(args))),
116        "ts_rank" => Ok(Some(SqlExpr::Function {
117            name: "pg_ts_rank".into(),
118            args,
119            distinct: false,
120        })),
121        "ts_rank_cd" => Err(SqlError::Unsupported {
122            detail: "ts_rank_cd is not supported; use ts_rank or bm25_score instead".into(),
123        }),
124        "ts_headline" => Ok(Some(SqlExpr::Function {
125            name: "pg_ts_headline".into(),
126            args,
127            distinct: false,
128        })),
129        _ => Ok(None),
130    }
131}
132
133/// Collect function call arguments, converting each `Expr` to `SqlExpr`.
134fn collect_function_args(func: &ast::Function, depth: &mut usize) -> Result<Vec<SqlExpr>> {
135    match &func.args {
136        ast::FunctionArguments::None => Ok(Vec::new()),
137        ast::FunctionArguments::Subquery(_) => Err(SqlError::Unsupported {
138            detail: "subquery in function args".into(),
139        }),
140        ast::FunctionArguments::List(arg_list) => arg_list
141            .args
142            .iter()
143            .filter_map(|a| match a {
144                ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => {
145                    Some(convert_expr_depth(e, depth))
146                }
147                ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Wildcard) => {
148                    Some(Ok(SqlExpr::Wildcard))
149                }
150                ast::FunctionArg::Named {
151                    arg: ast::FunctionArgExpr::Expr(e),
152                    ..
153                } => Some(convert_expr_depth(e, depth)),
154                _ => None,
155            })
156            .collect::<Result<Vec<_>>>(),
157    }
158}