1use uqa_core::{ArrayValue, Value};
10
11use crate::error::{Result, SQLError};
12
13use super::call_arguments::normalized_function_name;
14use super::context::EvalContext;
15use super::conversion::to_f64;
16use super::diagnostics::{unknown_function_error, value_type_name};
17use super::json::{jsonpath_candidate, jsonpath_match};
18use super::scalar_dispatch::{eval_scalar_function, eval_sequence_function};
19use super::{array_transform, json_strip, random};
20
21pub fn eval_function_call(
25 name: &str,
26 call_args: Vec<(Option<String>, Value)>,
27 ctx: &EvalContext<'_>,
28) -> Result<Value> {
29 eval_function_call_inner(name, call_args, ctx, true)
30}
31
32pub fn eval_builtin_function_call(
35 name: &str,
36 call_args: Vec<(Option<String>, Value)>,
37 ctx: &EvalContext<'_>,
38) -> Result<Value> {
39 eval_function_call_inner(name, call_args, ctx, false)
40}
41
42#[expect(
43 clippy::too_many_lines,
44 reason = "builtin dispatch preserves arity, NULL, and error precedence"
45)]
46fn eval_function_call_inner(
47 name: &str,
48 call_args: Vec<(Option<String>, Value)>,
49 ctx: &EvalContext<'_>,
50 allow_dynamic_dispatch: bool,
51) -> Result<Value> {
52 let lower = normalized_function_name(name);
53 let lower = lower.as_ref();
54 let evaluated: Vec<Value> = call_args.iter().map(|(_, value)| value.clone()).collect();
55
56 if let Some(result) = random::eval_random_function(lower, &call_args, ctx) {
57 return result;
58 }
59 if lower == "random" && !evaluated.is_empty() {
60 return Err(SQLError::TypeMismatch("random takes no arguments".into()));
61 }
62 if lower == "setseed" {
63 let [value] = evaluated.as_slice() else {
64 return Err(SQLError::TypeMismatch("setseed takes 1 arg".into()));
65 };
66 let seed = to_f64(value)?;
67 if !seed.is_finite() || !(-1.0..=1.0).contains(&seed) {
68 return Err(SQLError::Routine {
69 sqlstate: "22023".into(),
70 message: format!("setseed parameter {seed} is out of allowed range [-1,1]"),
71 });
72 }
73 let engine = ctx.engine.ok_or_else(|| {
74 SQLError::Unsupported("setseed requires a logical engine session".into())
75 })?;
76 if !engine.set_random_seed(seed).map_err(SQLError::Internal)? {
77 return Err(SQLError::Unsupported(
78 "engine hook does not provide a session random stream".into(),
79 ));
80 }
81 return Ok(Value::Str(String::new()));
82 }
83
84 if lower == "current_schema" {
85 if !evaluated.is_empty() {
86 return Err(SQLError::TypeMismatch(
87 "current_schema takes no arguments".into(),
88 ));
89 }
90 let schema = ctx
91 .engine
92 .map(|engine| engine.current_schema())
93 .transpose()
94 .map_err(SQLError::Internal)?
95 .flatten()
96 .unwrap_or_else(|| "public".to_string());
97 return Ok(Value::Str(schema));
98 }
99 if lower == "current_schemas" {
100 let [Value::Bool(include_implicit)] = evaluated.as_slice() else {
101 return Err(SQLError::TypeMismatch(
102 "current_schemas takes one boolean argument".into(),
103 ));
104 };
105 let schemas = ctx
106 .engine
107 .map(|engine| engine.current_schemas(*include_implicit))
108 .transpose()
109 .map_err(SQLError::Internal)?
110 .flatten()
111 .unwrap_or_else(|| {
112 let mut schemas = Vec::new();
113 if *include_implicit {
114 schemas.push("pg_catalog".to_string());
115 }
116 schemas.push("public".to_string());
117 schemas
118 });
119 return ArrayValue::try_new(schemas.into_iter().map(Value::Str).collect())
120 .map(Value::Array)
121 .ok_or_else(|| SQLError::TypeMismatch("invalid current_schemas result".into()));
122 }
123 if matches!(lower, "current_user" | "session_user") {
124 if !evaluated.is_empty() {
125 return Err(SQLError::TypeMismatch(format!(
126 "{lower} takes no arguments"
127 )));
128 }
129 let user = ctx
130 .engine
131 .map(|engine| {
132 if lower == "current_user" {
133 engine.current_user()
134 } else {
135 engine.session_user()
136 }
137 })
138 .transpose()
139 .map_err(SQLError::Internal)?
140 .flatten()
141 .unwrap_or_else(|| "uqa".to_string());
142 return Ok(Value::Str(user));
143 }
144 let regobject_type = match lower {
145 "to_regproc" => Some(crate::ast::ColumnType::Regproc),
146 "to_regprocedure" => Some(crate::ast::ColumnType::Regprocedure),
147 "to_regclass" => Some(crate::ast::ColumnType::Regclass),
148 "to_regnamespace" => Some(crate::ast::ColumnType::Regnamespace),
149 "to_regrole" => Some(crate::ast::ColumnType::Regrole),
150 "to_regtype" => Some(crate::ast::ColumnType::Regtype),
151 _ => None,
152 };
153 if let Some(regobject_type) = regobject_type {
154 let [value] = evaluated.as_slice() else {
155 return Err(SQLError::BadArity {
156 name: lower.into(),
157 expected: "1".into(),
158 actual: evaluated.len(),
159 });
160 };
161 let name = match value {
162 Value::Null => return Ok(Value::Null),
163 Value::Str(name) | Value::FixedChar(name) => name,
164 value => {
165 return Err(SQLError::TypeMismatch(format!(
166 "{lower} requires text, got {}",
167 value_type_name(value)
168 )));
169 }
170 };
171 let oid = ctx
172 .engine
173 .map(|engine| engine.resolve_regobject(®object_type, name))
174 .transpose()?
175 .flatten();
176 return Ok(oid.map_or(Value::Null, Value::Int));
177 }
178
179 if crate::registry::is_registered(lower) {
183 if lower == "fts_match" && jsonpath_candidate(&evaluated) {
184 return jsonpath_match(&evaluated);
185 }
186 return Err(SQLError::Unsupported(format!(
187 "scalar evaluation of `{name}` is not supported (use the function registry)"
188 )));
189 }
190
191 if call_args.iter().any(|(name, _)| name.is_some()) {
192 if let Some(positional) = builtin_named_args(lower, &call_args) {
193 return eval_scalar_function(lower, &positional);
194 }
195 if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
196 if let Some(result) = engine.call_user_function(lower, &call_args) {
197 return result;
198 }
199 }
200 return Err(unknown_function_error(lower, &call_args));
201 }
202
203 if matches!(lower, "nextval" | "currval" | "lastval" | "setval") {
205 return eval_sequence_function(lower, &evaluated, ctx);
206 }
207 if let Some(engine) = ctx
208 .engine
209 .filter(|engine| allow_dynamic_dispatch && engine.has_scalar_functions())
210 {
211 if let Some(result) = engine.call_scalar_function(lower, &evaluated) {
212 return result;
213 }
214 }
215 match eval_scalar_function(lower, &evaluated) {
216 Err(SQLError::UnknownFunction(_)) => {
219 if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
220 if let Some(result) = engine.call_user_function(lower, &call_args) {
221 return result;
222 }
223 }
224 Err(unknown_function_error(lower, &call_args))
225 }
226 other => other,
227 }
228}
229
230fn builtin_named_args(function: &str, call_args: &[(Option<String>, Value)]) -> Option<Vec<Value>> {
231 if matches!(function, "array_sort" | "array_reverse") {
232 return array_transform::reorder_named_values(function, call_args);
233 }
234 if matches!(function, "json_strip_nulls" | "jsonb_strip_nulls") {
235 return json_strip::reorder_named_values(function, call_args);
236 }
237 let names: &[&str] = match function {
238 "regexp_count" => match call_args.len() {
239 2 => &["string", "pattern"],
240 3 => &["string", "pattern", "start"],
241 4 => &["string", "pattern", "start", "flags"],
242 _ => return None,
243 },
244 "regexp_like" => match call_args.len() {
245 2 => &["string", "pattern"],
246 3 => &["string", "pattern", "flags"],
247 _ => return None,
248 },
249 "regexp_substr" => match call_args.len() {
250 2 => &["string", "pattern"],
251 3 => &["string", "pattern", "start"],
252 4 => &["string", "pattern", "start", "N"],
253 5 => &["string", "pattern", "start", "N", "flags"],
254 6 => &["string", "pattern", "start", "N", "flags", "subexpr"],
255 _ => return None,
256 },
257 "regexp_instr" => match call_args.len() {
258 2 => &["string", "pattern"],
259 3 => &["string", "pattern", "start"],
260 4 => &["string", "pattern", "start", "N"],
261 5 => &["string", "pattern", "start", "N", "endoption"],
262 6 => &["string", "pattern", "start", "N", "endoption", "flags"],
263 7 => &[
264 "string",
265 "pattern",
266 "start",
267 "N",
268 "endoption",
269 "flags",
270 "subexpr",
271 ],
272 _ => return None,
273 },
274 "regexp_replace" => match call_args.len() {
275 3 => &["string", "pattern", "replacement"],
276 4 if call_args
277 .iter()
278 .any(|(name, _)| name.as_deref() == Some("flags")) =>
279 {
280 &["string", "pattern", "replacement", "flags"]
281 }
282 4 => &["string", "pattern", "replacement", "start"],
283 5 => &["string", "pattern", "replacement", "start", "N"],
284 6 => &["string", "pattern", "replacement", "start", "N", "flags"],
285 _ => return None,
286 },
287 "make_interval" => return make_interval_named_args(call_args),
288 _ => return None,
289 };
290 reorder_named_args(call_args, names)
291}
292
293fn reorder_named_args(
294 call_args: &[(Option<String>, Value)],
295 parameter_names: &[&str],
296) -> Option<Vec<Value>> {
297 if call_args.len() != parameter_names.len() {
298 return None;
299 }
300 let mut slots = vec![None; parameter_names.len()];
301 let mut positional_index = 0;
302 let mut saw_named = false;
303 for (name, value) in call_args {
304 let slot = if let Some(name) = name {
305 saw_named = true;
306 parameter_names
307 .iter()
308 .position(|candidate| candidate == name)?
309 } else {
310 if saw_named {
311 return None;
312 }
313 let slot = positional_index;
314 positional_index += 1;
315 slot
316 };
317 if slots.get(slot)?.is_some() {
318 return None;
319 }
320 slots[slot] = Some(value.clone());
321 }
322 slots.into_iter().collect()
323}
324
325fn make_interval_named_args(call_args: &[(Option<String>, Value)]) -> Option<Vec<Value>> {
329 const NAMES: [&str; 7] = ["years", "months", "weeks", "days", "hours", "mins", "secs"];
330 let mut positional = vec![Value::Int(0); NAMES.len()];
331 let mut positional_index = 0;
332 let mut saw_named = false;
333 let mut assigned = [false; NAMES.len()];
334 for (name, value) in call_args {
335 let slot = if let Some(name) = name {
336 saw_named = true;
337 NAMES.iter().position(|candidate| candidate == name)?
338 } else {
339 if saw_named {
340 return None;
341 }
342 let slot = positional_index;
343 positional_index += 1;
344 slot
345 };
346 if slot >= NAMES.len() || assigned[slot] {
347 return None;
348 }
349 assigned[slot] = true;
350 positional[slot] = value.clone();
351 }
352 Some(positional)
353}