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