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