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::random;
19use super::scalar_dispatch::{eval_scalar_function, eval_sequence_function};
20
21mod named;
22mod production;
23mod temporal;
24use named::builtin_named_args;
25pub use production::{
26 eval_builtin_function_call_with_control, eval_generated_function_call_with_control,
27};
28
29pub fn eval_function_call(
33 name: &str,
34 call_args: Vec<(Option<String>, Value)>,
35 ctx: &EvalContext<'_>,
36) -> Result<Value> {
37 eval_function_call_inner(name, call_args, ctx, true)
38}
39
40pub fn eval_builtin_function_call(
43 name: &str,
44 call_args: Vec<(Option<String>, Value)>,
45 ctx: &EvalContext<'_>,
46) -> Result<Value> {
47 eval_function_call_inner(name, call_args, ctx, false)
48}
49
50#[expect(
51 clippy::too_many_lines,
52 reason = "builtin dispatch preserves arity, NULL, and error precedence"
53)]
54fn eval_function_call_inner(
55 name: &str,
56 call_args: Vec<(Option<String>, Value)>,
57 ctx: &EvalContext<'_>,
58 allow_dynamic_dispatch: bool,
59) -> Result<Value> {
60 let lower = normalized_function_name(name);
61 let lower = lower.as_ref();
62 let call_args = if super::enums::call_applies_output_functions(lower, &call_args)
63 && call_args
64 .iter()
65 .any(|(_, value)| super::enums::contains_enum_carrier(value))
66 {
67 super::enums::render_call_arguments(
68 ctx.engine.and_then(super::EngineHook::enum_labels),
69 call_args,
70 )?
71 } else {
72 call_args
73 };
74 let evaluated: Vec<Value> = call_args.iter().map(|(_, value)| value.clone()).collect();
75
76 if let Some(result) = super::current_time::eval_current_time(lower, &evaluated, Some(ctx)) {
77 return result;
78 }
79
80 if lower == "current_setting" {
81 return super::session_settings::current_setting(&evaluated, ctx);
82 }
83 if lower == "set_config" {
84 return super::session_settings::set_config(&evaluated, ctx);
85 }
86 if let Some(result) = super::session_sleep::eval_session_sleep(lower, &evaluated, ctx) {
87 return result;
88 }
89
90 if let Some(result) = random::eval_random_function(lower, &call_args, ctx) {
91 return result;
92 }
93 if lower == "random" && !evaluated.is_empty() {
94 return Err(SQLError::TypeMismatch("random takes no arguments".into()));
95 }
96 if lower == "setseed" {
97 let [value] = evaluated.as_slice() else {
98 return Err(SQLError::TypeMismatch("setseed takes 1 arg".into()));
99 };
100 let seed = to_f64(value)?;
101 if !seed.is_finite() || !(-1.0..=1.0).contains(&seed) {
102 return Err(SQLError::Routine {
103 sqlstate: "22023".into(),
104 message: format!("setseed parameter {seed} is out of allowed range [-1,1]"),
105 });
106 }
107 let engine = ctx.engine.ok_or_else(|| {
108 SQLError::Unsupported("setseed requires a logical engine session".into())
109 })?;
110 if !engine.set_random_seed(seed).map_err(SQLError::Internal)? {
111 return Err(SQLError::Unsupported(
112 "engine hook does not provide a session random stream".into(),
113 ));
114 }
115 return Ok(Value::Str(String::new()));
116 }
117
118 if lower == "current_schema" {
119 if !evaluated.is_empty() {
120 return Err(SQLError::TypeMismatch(
121 "current_schema takes no arguments".into(),
122 ));
123 }
124 let schema = match ctx.engine {
125 Some(engine) => engine.current_schema().map_err(SQLError::Internal)?,
126 None => Some("public".to_string()),
127 };
128 return Ok(schema.map_or(Value::Null, Value::Str));
129 }
130 if lower == "current_schemas" {
131 let [Value::Bool(include_implicit)] = evaluated.as_slice() else {
132 return Err(SQLError::TypeMismatch(
133 "current_schemas takes one boolean argument".into(),
134 ));
135 };
136 let schemas = ctx
137 .engine
138 .map(|engine| engine.current_schemas(*include_implicit))
139 .transpose()
140 .map_err(SQLError::Internal)?
141 .flatten()
142 .unwrap_or_else(|| {
143 let mut schemas = Vec::new();
144 if *include_implicit {
145 schemas.push("pg_catalog".to_string());
146 }
147 schemas.push("public".to_string());
148 schemas
149 });
150 return ArrayValue::try_new(schemas.into_iter().map(Value::Str).collect())
151 .map(Value::Array)
152 .ok_or_else(|| SQLError::TypeMismatch("invalid current_schemas result".into()));
153 }
154 if matches!(lower, "current_user" | "session_user") {
155 if !evaluated.is_empty() {
156 return Err(SQLError::TypeMismatch(format!(
157 "{lower} takes no arguments"
158 )));
159 }
160 let user = ctx
161 .engine
162 .map(|engine| {
163 if lower == "current_user" {
164 engine.current_user()
165 } else {
166 engine.session_user()
167 }
168 })
169 .transpose()?
170 .flatten()
171 .unwrap_or_else(|| "uqa".to_string());
172 return Ok(Value::Str(user));
173 }
174 let regobject_type = match lower {
175 "to_regproc" => Some(crate::ast::ColumnType::Regproc),
176 "to_regprocedure" => Some(crate::ast::ColumnType::Regprocedure),
177 "to_regclass" => Some(crate::ast::ColumnType::Regclass),
178 "to_regcollation" => Some(crate::ast::ColumnType::Regcollation),
179 "to_regnamespace" => Some(crate::ast::ColumnType::Regnamespace),
180 "to_regrole" => Some(crate::ast::ColumnType::Regrole),
181 "to_regtype" => Some(crate::ast::ColumnType::Regtype),
182 _ => None,
183 };
184 if let Some(regobject_type) = regobject_type {
185 let [value] = evaluated.as_slice() else {
186 return Err(SQLError::BadArity {
187 name: lower.into(),
188 expected: "1".into(),
189 actual: evaluated.len(),
190 });
191 };
192 let name = match value {
193 Value::Null => return Ok(Value::Null),
194 Value::Str(name) | Value::FixedChar(name) => name,
195 value => {
196 return Err(SQLError::TypeMismatch(format!(
197 "{lower} requires text, got {}",
198 value_type_name(value)
199 )));
200 }
201 };
202 let oid = ctx
203 .engine
204 .map(|engine| engine.resolve_regobject(®object_type, name))
205 .transpose()?
206 .flatten();
207 return Ok(oid.map_or(Value::Null, Value::Int));
208 }
209
210 if crate::registry::is_registered(lower) {
214 if lower == "fts_match" && jsonpath_candidate(&evaluated) {
215 return jsonpath_match(&evaluated);
216 }
217 return Err(SQLError::Unsupported(format!(
218 "scalar evaluation of `{name}` is not supported (use the function registry)"
219 )));
220 }
221
222 if call_args.iter().any(|(name, _)| name.is_some()) {
223 if let Some(positional) = builtin_named_args(
224 lower,
225 &call_args,
226 &uqa_core::memory::ProductionControl::uncontrolled(),
227 )? {
228 return eval_scalar_function(lower, &positional);
229 }
230 if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
231 if let Some(result) = engine.call_user_function(lower, &call_args) {
232 return result;
233 }
234 }
235 return Err(unknown_function_error(lower, &call_args));
236 }
237
238 if matches!(lower, "nextval" | "currval" | "lastval" | "setval") {
240 return eval_sequence_function(lower, &evaluated, ctx);
241 }
242 if let Some(engine) = ctx
243 .engine
244 .filter(|engine| allow_dynamic_dispatch && engine.has_scalar_functions())
245 {
246 if let Some(result) = engine.call_scalar_function(lower, &evaluated) {
247 return result;
248 }
249 }
250 if lower == "date_trunc" {
251 if let Some(result) = super::time::truncate_session_zone(&evaluated, ctx) {
252 return result;
253 }
254 }
255 if let Some(result) = temporal::extract_session_zone(lower, &evaluated, ctx) {
256 return result;
257 }
258 if matches!(lower, "coalesce" | "nullif" | "greatest" | "least") {
259 return super::scalar_core::selection_with_context(
260 lower,
261 &evaluated,
262 &uqa_core::memory::ProductionControl::uncontrolled(),
263 ctx,
264 )
265 .map(|value| {
266 value
267 .into_uncontrolled()
268 .expect("ordinary selection result")
269 });
270 }
271 match eval_scalar_function(lower, &evaluated) {
272 Err(SQLError::UnknownFunction(_)) => {
275 if let Some(engine) = ctx.engine.filter(|_| allow_dynamic_dispatch) {
276 if let Some(result) = engine.call_user_function(lower, &call_args) {
277 return result;
278 }
279 }
280 Err(unknown_function_error(lower, &call_args))
281 }
282 other => other,
283 }
284}