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