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    // `current_schemas(bool)` — PostgreSQL catalog function returning TEXT[].
51    // Fold to a literal array at parse time so `= ANY(current_schemas(...))` works
52    // without threading session context into the data-plane evaluator.
53    if (name == "current_schemas" || name == "current_schema")
54        && let Some(expr) = intercept_catalog_function(&name, func, depth)?
55    {
56        return Ok(expr);
57    }
58
59    let args = match &func.args {
60        ast::FunctionArguments::None => Vec::new(),
61        ast::FunctionArguments::Subquery(_) => {
62            return Err(SqlError::Unsupported {
63                detail: "subquery in function args".into(),
64            });
65        }
66        ast::FunctionArguments::List(arg_list) => arg_list
67            .args
68            .iter()
69            .filter_map(|a| match a {
70                ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => {
71                    Some(convert_expr_depth(e, depth))
72                }
73                ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Wildcard) => {
74                    Some(Ok(SqlExpr::Wildcard))
75                }
76                ast::FunctionArg::Named {
77                    arg: ast::FunctionArgExpr::Expr(e),
78                    ..
79                } => Some(convert_expr_depth(e, depth)),
80                _ => None,
81            })
82            .collect::<Result<Vec<_>>>()?,
83    };
84
85    let distinct = match &func.args {
86        ast::FunctionArguments::List(arg_list) => {
87            matches!(
88                arg_list.duplicate_treatment,
89                Some(ast::DuplicateTreatment::Distinct)
90            )
91        }
92        _ => false,
93    };
94
95    Ok(SqlExpr::Function {
96        name,
97        args,
98        distinct,
99    })
100}
101
102/// Intercept PG FTS surface functions and lower them to `pg_*` internal
103/// function-call `SqlExpr` nodes.  Returns `Ok(None)` for non-FTS names.
104///
105/// `ts_rank_cd` is rejected with `SqlError::Unsupported` as per the
106/// approved design.
107fn intercept_fts_function(
108    name: &str,
109    func: &ast::Function,
110    depth: &mut usize,
111) -> Result<Option<SqlExpr>> {
112    use crate::functions::fts_ops::pg_fts_funcs;
113
114    let args = collect_function_args(func, depth)?;
115    match name {
116        "to_tsvector" => Ok(Some(SqlExpr::Function {
117            name: "pg_to_tsvector".into(),
118            args,
119            distinct: false,
120        })),
121        "to_tsquery" => Ok(Some(pg_fts_funcs::lower_pg_to_tsquery(args))),
122        "plainto_tsquery" => Ok(Some(pg_fts_funcs::lower_pg_plainto_tsquery(args))),
123        "phraseto_tsquery" => Ok(Some(pg_fts_funcs::lower_phraseto_tsquery(args))),
124        "websearch_to_tsquery" => Ok(Some(pg_fts_funcs::lower_pg_websearch_to_tsquery(args))),
125        "ts_rank" => Ok(Some(SqlExpr::Function {
126            name: "pg_ts_rank".into(),
127            args,
128            distinct: false,
129        })),
130        "ts_rank_cd" => Err(SqlError::Unsupported {
131            detail: "ts_rank_cd is not supported; use ts_rank or bm25_score instead".into(),
132        }),
133        "ts_headline" => Ok(Some(SqlExpr::Function {
134            name: "pg_ts_headline".into(),
135            args,
136            distinct: false,
137        })),
138        _ => Ok(None),
139    }
140}
141
142/// Intercept PostgreSQL catalog functions and fold them to literal arrays.
143///
144/// `current_schemas(true)` → `ARRAY['pg_catalog', 'public']`
145/// `current_schemas(false)` → `ARRAY['public']`
146/// `current_schema()` → `'public'`
147///
148/// Returns `Ok(Some(expr))` when the function is folded, `Ok(None)` otherwise.
149fn intercept_catalog_function(
150    name: &str,
151    func: &ast::Function,
152    depth: &mut usize,
153) -> Result<Option<SqlExpr>> {
154    let args = collect_function_args(func, depth)?;
155    match name {
156        "current_schemas" => {
157            // Determine whether to include implicit schemas (pg_catalog).
158            let include_implicit = match args.first() {
159                Some(SqlExpr::Literal(SqlValue::Bool(b))) => *b,
160                // Default to false when arg is missing or non-literal.
161                _ => false,
162            };
163            let mut schemas: Vec<SqlExpr> = Vec::new();
164            if include_implicit {
165                schemas.push(SqlExpr::Literal(SqlValue::String("pg_catalog".into())));
166            }
167            schemas.push(SqlExpr::Literal(SqlValue::String("public".into())));
168            Ok(Some(SqlExpr::ArrayLiteral(schemas)))
169        }
170        "current_schema" => Ok(Some(SqlExpr::Literal(SqlValue::String("public".into())))),
171        _ => Ok(None),
172    }
173}
174
175/// Collect function call arguments, converting each `Expr` to `SqlExpr`.
176fn collect_function_args(func: &ast::Function, depth: &mut usize) -> Result<Vec<SqlExpr>> {
177    match &func.args {
178        ast::FunctionArguments::None => Ok(Vec::new()),
179        ast::FunctionArguments::Subquery(_) => Err(SqlError::Unsupported {
180            detail: "subquery in function args".into(),
181        }),
182        ast::FunctionArguments::List(arg_list) => arg_list
183            .args
184            .iter()
185            .filter_map(|a| match a {
186                ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Expr(e)) => {
187                    Some(convert_expr_depth(e, depth))
188                }
189                ast::FunctionArg::Unnamed(ast::FunctionArgExpr::Wildcard) => {
190                    Some(Ok(SqlExpr::Wildcard))
191                }
192                ast::FunctionArg::Named {
193                    arg: ast::FunctionArgExpr::Expr(e),
194                    ..
195                } => Some(convert_expr_depth(e, depth)),
196                _ => None,
197            })
198            .collect::<Result<Vec<_>>>(),
199    }
200}