nodedb_sql/resolver/expr/
functions.rs1use std::sync::LazyLock;
4
5use sqlparser::ast;
6
7use crate::error::{Result, SqlError};
8use crate::functions::registry::FunctionRegistry;
9use crate::parser::normalize::{SCHEMA_QUALIFIED_MSG, normalize_ident};
10use crate::resolver::ColumnScope;
11use crate::types::*;
12
13use super::convert::convert_expr_depth;
14
15static FUNCTION_REGISTRY: LazyLock<FunctionRegistry> = LazyLock::new(FunctionRegistry::new);
22
23pub(super) fn convert_function_depth(
24 func: &ast::Function,
25 depth: &mut usize,
26 scope: &ColumnScope<'_>,
27) -> Result<SqlExpr> {
28 if func.name.0.len() == 1 {
31 let raw_name = match &func.name.0[0] {
32 ast::ObjectNamePart::Identifier(ident) => ident.value.to_ascii_lowercase(),
33 _ => String::new(),
34 };
35 if let Some(expr) = intercept_fts_function(&raw_name, func, depth, scope)? {
36 return Ok(expr);
37 }
38 }
39
40 if func.name.0.len() > 1 {
41 let qualified: String = func
42 .name
43 .0
44 .iter()
45 .map(|p| match p {
46 ast::ObjectNamePart::Identifier(ident) => ident.value.clone(),
47 _ => String::new(),
48 })
49 .collect::<Vec<_>>()
50 .join(".");
51 return Err(SqlError::Unsupported {
52 detail: format!("schema-qualified function name '{qualified}': {SCHEMA_QUALIFIED_MSG}"),
53 });
54 }
55 let name = func
56 .name
57 .0
58 .iter()
59 .map(|p| match p {
60 ast::ObjectNamePart::Identifier(ident) => normalize_ident(ident),
61 _ => String::new(),
62 })
63 .collect::<Vec<_>>()
64 .join(".");
65
66 if (name == "current_schemas" || name == "current_schema")
70 && let Some(expr) = intercept_catalog_function(&name, func, depth, scope)?
71 {
72 return Ok(expr);
73 }
74
75 if FUNCTION_REGISTRY.lookup(&name).is_none() {
86 return Err(SqlError::UndefinedFunction { name });
87 }
88
89 if scope.is_row_scope()
98 && !scope.allows_cp_functions()
99 && crate::functions::sequence_accessor::is_sequence_accessor(&name)
100 {
101 return Err(SqlError::SequencePerRowUnsupported { name });
102 }
103
104 let args = collect_function_args(func, depth, scope)?;
105
106 let distinct = match &func.args {
107 ast::FunctionArguments::List(arg_list) => {
108 matches!(
109 arg_list.duplicate_treatment,
110 Some(ast::DuplicateTreatment::Distinct)
111 )
112 }
113 _ => false,
114 };
115
116 Ok(SqlExpr::Function {
117 name,
118 args,
119 distinct,
120 })
121}
122
123fn intercept_fts_function(
129 name: &str,
130 func: &ast::Function,
131 depth: &mut usize,
132 scope: &ColumnScope<'_>,
133) -> Result<Option<SqlExpr>> {
134 use crate::functions::fts_ops::pg_fts_funcs;
135
136 if !matches!(
137 name,
138 "to_tsvector"
139 | "to_tsquery"
140 | "plainto_tsquery"
141 | "phraseto_tsquery"
142 | "websearch_to_tsquery"
143 | "ts_rank"
144 | "ts_rank_cd"
145 | "ts_headline"
146 ) {
147 return Ok(None);
148 }
149 let args = collect_function_args(func, depth, scope)?;
150 match name {
151 "to_tsvector" => Ok(Some(SqlExpr::Function {
152 name: "pg_to_tsvector".into(),
153 args,
154 distinct: false,
155 })),
156 "to_tsquery" => Ok(Some(pg_fts_funcs::lower_pg_to_tsquery(args))),
157 "plainto_tsquery" => Ok(Some(pg_fts_funcs::lower_pg_plainto_tsquery(args))),
158 "phraseto_tsquery" => Ok(Some(pg_fts_funcs::lower_phraseto_tsquery(args))),
159 "websearch_to_tsquery" => Ok(Some(pg_fts_funcs::lower_pg_websearch_to_tsquery(args))),
160 "ts_rank" => Ok(Some(SqlExpr::Function {
161 name: "pg_ts_rank".into(),
162 args,
163 distinct: false,
164 })),
165 "ts_rank_cd" => Err(SqlError::Unsupported {
166 detail: "ts_rank_cd is not supported; use ts_rank or bm25_score instead".into(),
167 }),
168 "ts_headline" => Ok(Some(SqlExpr::Function {
169 name: "pg_ts_headline".into(),
170 args,
171 distinct: false,
172 })),
173 _ => Ok(None),
174 }
175}
176
177fn intercept_catalog_function(
185 name: &str,
186 func: &ast::Function,
187 depth: &mut usize,
188 scope: &ColumnScope<'_>,
189) -> Result<Option<SqlExpr>> {
190 let args = collect_function_args(func, depth, scope)?;
191 match name {
192 "current_schemas" => {
193 let include_implicit = match args.first() {
195 Some(SqlExpr::Literal(SqlValue::Bool(b))) => *b,
196 _ => false,
198 };
199 let mut schemas: Vec<SqlExpr> = Vec::new();
200 if include_implicit {
201 schemas.push(SqlExpr::Literal(SqlValue::String("pg_catalog".into())));
202 }
203 schemas.push(SqlExpr::Literal(SqlValue::String("public".into())));
204 Ok(Some(SqlExpr::ArrayLiteral(schemas)))
205 }
206 "current_schema" => Ok(Some(SqlExpr::Literal(SqlValue::String("public".into())))),
207 _ => Ok(None),
208 }
209}
210
211fn collect_function_args(
213 func: &ast::Function,
214 depth: &mut usize,
215 scope: &ColumnScope<'_>,
216) -> Result<Vec<SqlExpr>> {
217 match &func.args {
218 ast::FunctionArguments::None => Ok(Vec::new()),
219 ast::FunctionArguments::Subquery(_) => Err(SqlError::Unsupported {
220 detail: "subquery in function args".into(),
221 }),
222 ast::FunctionArguments::List(arg_list) => arg_list
223 .args
224 .iter()
225 .filter_map(|a| match a {
226 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => {
227 Some(convert_expr_depth(e, depth, scope))
228 }
229 ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Wildcard) => {
230 Some(Ok(SqlExpr::Wildcard))
231 }
232 ast::FunctionArg::Named {
233 arg: ast::FunctionArgExpr::Expr(e),
234 ..
235 } => Some(convert_expr_depth(e, depth, scope)),
236 _ => None,
237 })
238 .collect::<Result<Vec<_>>>(),
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use sqlparser::dialect::GenericDialect;
245 use sqlparser::parser::Parser;
246
247 use super::*;
248
249 fn parse_expr(sql: &str) -> ast::Expr {
251 let stmts = Parser::parse_sql(&GenericDialect {}, sql).unwrap();
252 match stmts.into_iter().next().unwrap() {
253 ast::Statement::Query(q) => match *q.body {
254 ast::SetExpr::Select(s) => match s.projection.into_iter().next().unwrap() {
255 ast::SelectItem::UnnamedExpr(e) => e,
256 ast::SelectItem::ExprWithAlias { expr, .. } => expr,
257 other => panic!("expected a bare/aliased expr projection, got {other:?}"),
258 },
259 _ => panic!("expected SELECT"),
260 },
261 _ => panic!("expected query"),
262 }
263 }
264
265 fn function_ast(sql: &str) -> ast::Function {
267 match parse_expr(sql) {
268 ast::Expr::Function(f) => f,
269 other => panic!("expected a function call expr, got {other:?}"),
270 }
271 }
272
273 #[test]
274 fn unknown_function_name_is_rejected() {
275 let func = function_ast("SELECT totally_bogus_fn(1, 2)");
276 let mut depth = 0;
277 let err = convert_function_depth(&func, &mut depth, &ColumnScope::Unchecked).unwrap_err();
278 match err {
279 SqlError::UndefinedFunction { name } => assert_eq!(name, "totally_bogus_fn"),
280 other => panic!("expected SqlError::UndefinedFunction, got {other:?}"),
281 }
282 }
283
284 #[test]
285 fn known_scalar_function_name_resolves() {
286 let func = function_ast("SELECT upper('x')");
287 let mut depth = 0;
288 let expr = convert_function_depth(&func, &mut depth, &ColumnScope::Unchecked).unwrap();
289 match expr {
290 SqlExpr::Function { name, .. } => assert_eq!(name, "upper"),
291 other => panic!("expected SqlExpr::Function, got {other:?}"),
292 }
293 }
294
295 #[test]
302 fn function_lookup_is_case_insensitive() {
303 let mut depth = 0;
304 let lower = function_ast("SELECT upper('x')");
305 let upper_quoted = function_ast(r#"SELECT "UPPER"('x')"#);
306 assert!(
307 convert_function_depth(&lower, &mut depth, &ColumnScope::Unchecked).is_ok(),
308 "lowercase 'upper' must resolve"
309 );
310 assert!(
311 convert_function_depth(&upper_quoted, &mut depth, &ColumnScope::Unchecked).is_ok(),
312 "quoted 'UPPER' must still resolve via case-insensitive registry lookup"
313 );
314 }
315
316 #[test]
319 fn a_sequence_accessor_resolves_without_a_relation() {
320 let func = function_ast("SELECT nextval('s')");
321 let mut depth = 0;
322 let scope = crate::resolver::columns::TableScope::new();
323 let expr = convert_function_depth(&func, &mut depth, &ColumnScope::Relations(&scope))
324 .expect("a FROM-less sequence accessor must resolve");
325 match expr {
326 SqlExpr::Function { name, .. } => assert_eq!(name, "nextval"),
327 other => panic!("expected SqlExpr::Function, got {other:?}"),
328 }
329 }
330
331 #[test]
334 fn a_sequence_accessor_over_a_relation_is_refused() {
335 let mut depth = 0;
336 let outer = crate::resolver::columns::TableScope::new();
337 let scope = crate::resolver::columns::TableScope::new().nested_in(outer);
338 for call in [
339 "SELECT nextval('s')",
340 "SELECT currval('s')",
341 "SELECT setval('s', 1)",
342 ] {
343 let func = function_ast(call);
344 let err = convert_function_depth(&func, &mut depth, &ColumnScope::Relations(&scope))
345 .unwrap_err();
346 assert!(
347 matches!(err, SqlError::SequencePerRowUnsupported { .. }),
348 "expected SqlError::SequencePerRowUnsupported for {call}, got {err:?}"
349 );
350 }
351 }
352
353 #[test]
357 fn a_cp_allowing_scope_resolves_the_accessor_and_a_nested_scope_refuses() {
358 let mut depth = 0;
359 let func = function_ast("SELECT nextval('s')");
360 let allowed =
361 crate::resolver::columns::test_support::open_scope("t").allowing_cp_functions();
362 assert!(allowed.is_row_scope());
363 let expr = convert_function_depth(&func, &mut depth, &ColumnScope::Relations(&allowed))
364 .expect("a SELECT-list accessor over a relation must resolve");
365 assert!(matches!(expr, SqlExpr::Function { ref name, .. } if name == "nextval"));
366
367 let nested = crate::resolver::columns::TableScope::new().nested_in(allowed);
368 let err = convert_function_depth(&func, &mut depth, &ColumnScope::Relations(&nested))
369 .unwrap_err();
370 assert!(matches!(err, SqlError::SequencePerRowUnsupported { .. }));
371 }
372}