1use uqa_core::Value;
10
11use crate::ast::{FunctionBinding, FunctionDispatch};
12use crate::error::{Result, SQLError};
13
14use super::call_arguments::normalized_function_name;
15use super::call_dispatch::eval_builtin_function_call;
16use super::context::EvalContext;
17use super::{random, scalar_array, scalar_postgres, scalar_range};
18
19#[expect(
20 clippy::too_many_lines,
21 reason = "builtin dispatch preserves arity, NULL, and error precedence"
22)]
23pub fn builtin_scalar_function_strictness(name: &str, argument_count: usize) -> Option<bool> {
24 let normalized = normalized_function_name(name);
25 match normalized.as_ref() {
26 "int4range" | "int8range" | "numrange" | "daterange" | "tsrange" | "tstzrange"
27 if matches!(argument_count, 2 | 3) =>
28 {
29 Some(false)
30 }
31 "int4multirange" | "int8multirange" | "nummultirange" | "datemultirange"
32 | "tsmultirange" | "tstzmultirange"
33 if argument_count <= 1 =>
34 {
35 Some(true)
36 }
37 "multirange" if argument_count == 1 => Some(true),
38 "coalesce" | "greatest" | "least" if argument_count >= 1 => Some(false),
39 "nullif" | "concat_op" if argument_count == 2 => Some(false),
40 "concat" | "format" | "json_build_array" | "jsonb_build_array" | "json_build_object"
41 | "jsonb_build_object" | "num_nulls" | "num_nonnulls" => Some(false),
42 "concat_ws" if argument_count >= 1 => Some(false),
43 "quote_nullable" | "pg_typeof" | "typeof" if argument_count == 1 => Some(false),
44 "array_cat" | "array_append" | "array_prepend" | "array_remove" | "array_positions"
45 if argument_count == 2 =>
46 {
47 Some(false)
48 }
49 "array_position" if matches!(argument_count, 2 | 3) => Some(false),
50 "array_replace" if argument_count == 3 => Some(false),
51 "array_fill" if matches!(argument_count, 2 | 3) => Some(false),
52 "array_to_string" if argument_count == 3 => Some(false),
53 "string_to_array" | "string_to_table" if matches!(argument_count, 2 | 3) => Some(false),
54 "pg_has_role" if matches!(argument_count, 2 | 3) => Some(true),
55 "pg_get_userbyid" if argument_count == 1 => Some(true),
56 "current_setting" if matches!(argument_count, 1 | 2) => Some(true),
57 "set_config" if argument_count == 3 => Some(false),
58 "pg_sleep" | "pg_sleep_for" | "pg_sleep_until" if argument_count == 1 => Some(true),
59 "has_table_privilege" if matches!(argument_count, 2 | 3) => Some(true),
60 "has_column_privilege" if matches!(argument_count, 3 | 4) => Some(true),
61 "has_database_privilege" if matches!(argument_count, 2 | 3) => Some(true),
62 "has_schema_privilege" if matches!(argument_count, 2 | 3) => Some(true),
63 "has_sequence_privilege" | "has_function_privilege" | "has_type_privilege"
64 if matches!(argument_count, 2 | 3) =>
65 {
66 Some(true)
67 }
68 "pg_get_constraintdef" if matches!(argument_count, 1 | 2) => Some(true),
69 "pg_get_function_arguments"
70 | "pg_get_function_identity_arguments"
71 | "pg_get_function_result"
72 | "pg_get_functiondef"
73 | "pg_get_function_sqlbody"
74 if argument_count == 1 =>
75 {
76 Some(true)
77 }
78 "pg_get_sequence_data" | "pg_sequence_last_value" | "pg_sequence_parameters"
79 if argument_count == 1 =>
80 {
81 Some(true)
82 }
83 "nextval" | "currval" if argument_count == 1 => Some(true),
84 "lastval" if argument_count == 0 => Some(true),
85 "setval" if matches!(argument_count, 2 | 3) => Some(true),
86 "overlaps" if argument_count == 4 => Some(false),
87 "abs"
88 | "acos"
89 | "array_dims"
90 | "array_ndims"
91 | "array_reverse"
92 | "ascii"
93 | "asin"
94 | "atan"
95 | "bit_length"
96 | "cardinality"
97 | "casefold"
98 | "cbrt"
99 | "ceil"
100 | "ceiling"
101 | "char_length"
102 | "character_length"
103 | "chr"
104 | "cos"
105 | "cosh"
106 | "current_schemas"
107 | "degrees"
108 | "exp"
109 | "factorial"
110 | "floor"
111 | "gamma"
112 | "initcap"
113 | "isfinite"
114 | "json_array_length"
115 | "jsonb_array_length"
116 | "json_typeof"
117 | "jsonb_typeof"
118 | "jsonb_pretty"
119 | "justify_days"
120 | "justify_hours"
121 | "justify_interval"
122 | "length"
123 | "lgamma"
124 | "ln"
125 | "log10"
126 | "log2"
127 | "lower"
128 | "md5"
129 | "octet_length"
130 | "quote_ident"
131 | "quote_literal"
132 | "radians"
133 | "reverse"
134 | "row_to_json"
135 | "sign"
136 | "sin"
137 | "sinh"
138 | "sqrt"
139 | "tan"
140 | "tanh"
141 | "to_bin"
142 | "to_hex"
143 | "to_oct"
144 | "to_json"
145 | "to_jsonb"
146 | "to_regclass"
147 | "to_regcollation"
148 | "to_regnamespace"
149 | "to_regproc"
150 | "to_regprocedure"
151 | "to_regrole"
152 | "to_regtype"
153 | "to_timestamp"
154 | "upper"
155 | "uuid_extract_timestamp"
156 | "uuid_extract_version"
157 if argument_count == 1 =>
158 {
159 Some(true)
160 }
161 "random" if argument_count == 2 => Some(true),
162 "age" | "btrim" | "ltrim" | "rtrim" | "trim" | "log" | "round" | "trunc"
163 | "json_strip_nulls" | "jsonb_strip_nulls"
164 if matches!(argument_count, 1 | 2) =>
165 {
166 Some(true)
167 }
168 "array_sort" if matches!(argument_count, 1..=3) => Some(true),
169 "date_trunc" if matches!(argument_count, 2 | 3) => Some(true),
170 "array_length" | "array_lower" | "array_upper" | "atan2" | "date_part" | "decode"
171 | "encode" | "extract" | "gcd" | "get_byte" | "lcm" | "left" | "mod" | "power" | "pow"
172 | "repeat" | "right" | "starts_with" | "position" | "strpos" | "to_char" | "to_date"
173 | "to_number" | "trim_array" | "point" | "st_distance" | "st_within"
174 if argument_count == 2 =>
175 {
176 Some(true)
177 }
178 "like" | "ilike" | "similar_to" | "regex_match_op" | "regex_imatch_op"
179 if argument_count == 2 =>
180 {
181 Some(true)
182 }
183 "like" | "ilike" | "similar_to" if argument_count == 3 => Some(false),
184 "array_to_string" if argument_count == 2 => Some(true),
185 "substring" | "substr" | "lpad" | "rpad" if matches!(argument_count, 2 | 3) => Some(true),
186 "regexp_count" if matches!(argument_count, 2..=4) => Some(true),
187 "regexp_instr" if matches!(argument_count, 2..=7) => Some(true),
188 "regexp_like" | "regexp_match" | "regexp_matches" if matches!(argument_count, 2 | 3) => {
189 Some(true)
190 }
191 "regexp_replace" if matches!(argument_count, 3..=6) => Some(true),
192 "regexp_substr" if matches!(argument_count, 2..=6) => Some(true),
193 "replace" | "split_part" | "translate" | "make_date" if argument_count == 3 => Some(true),
194 "overlay" | "jsonb_set" | "jsonb_insert" if matches!(argument_count, 3 | 4) => Some(true),
195 "json_extract_path"
196 | "jsonb_extract_path"
197 | "json_extract_path_text"
198 | "jsonb_extract_path_text"
199 if argument_count >= 2 =>
200 {
201 Some(true)
202 }
203 "json_contains" | "json_contained_by" | "json_delete_path" | "json_has_key"
204 | "json_has_any_key" | "json_has_all_keys" | "jsonb_path_exists" | "jsonpath_exists"
205 | "jsonb_path_match" | "jsonpath_match"
206 if argument_count == 2 =>
207 {
208 Some(true)
209 }
210 "make_timestamp" if matches!(argument_count, 6 | 7) => Some(true),
211 "make_interval" if argument_count <= 7 => Some(true),
212 "width_bucket" if argument_count == 4 => Some(true),
213 "st_dwithin" if matches!(argument_count, 2 | 3) => Some(true),
214 _ => None,
215 }
216}
217
218#[must_use]
220pub fn bound_scalar_function_strictness(
221 name: &str,
222 binding: Option<&FunctionBinding>,
223 argument_count: usize,
224) -> Option<bool> {
225 let Some(binding) = binding else {
226 return builtin_scalar_function_strictness(name, argument_count);
227 };
228 if let Some(dispatch) = binding.dispatch {
229 return match dispatch {
230 FunctionDispatch::NumericOperator(_) => Some(true),
231 FunctionDispatch::ArraySubscripts
232 | FunctionDispatch::Subscript
233 | FunctionDispatch::FieldSelect
234 | FunctionDispatch::BetweenSymmetric
235 | FunctionDispatch::ToBinInt4
236 | FunctionDispatch::ToBinInt8
237 | FunctionDispatch::ToHexInt4
238 | FunctionDispatch::ToHexInt8
239 | FunctionDispatch::ToOctInt4
240 | FunctionDispatch::ToOctInt8
241 | FunctionDispatch::RandomInt4Range
242 | FunctionDispatch::RandomInt8Range
243 | FunctionDispatch::RandomNumericRange
244 | FunctionDispatch::ArraySortJson
245 | FunctionDispatch::JsonExtract { .. }
246 | FunctionDispatch::Range { .. } => Some(true),
247 FunctionDispatch::ArraySlices
248 | FunctionDispatch::ArrayConcat
249 | FunctionDispatch::ArrayAppend
250 | FunctionDispatch::ArrayPrepend
251 | FunctionDispatch::Slice
252 | FunctionDispatch::AnyOperator
253 | FunctionDispatch::AllOperator
254 | FunctionDispatch::IsDistinct => Some(false),
255 FunctionDispatch::Enum { operation, .. } => Some(operation.is_strict()),
256 FunctionDispatch::NamedArgument | FunctionDispatch::VariadicArgument => None,
257 };
258 }
259 binding
260 .builtin
261 .then(|| builtin_scalar_function_strictness(&binding.name, argument_count))
262 .flatten()
263}
264
265pub fn eval_bound_builtin_function_call(
267 binding: &FunctionBinding,
268 call_args: Vec<(Option<String>, Value)>,
269 ctx: &EvalContext<'_>,
270) -> Result<Value> {
271 if let Some(error) = &binding.resolution_error {
272 return Err(error.sql_error());
273 }
274 if let Some(hook) = ctx.engine {
275 hook.require_builtin_execute(binding)?;
276 }
277 let Some(dispatch) = binding.dispatch else {
278 let value = eval_builtin_function_call(&binding.name, call_args, ctx)?;
279 if matches!(value, Value::Int(_) | Value::Float(_)) {
281 if let Some(
282 ty @ (crate::ColumnType::SmallInteger
283 | crate::ColumnType::Integer
284 | crate::ColumnType::Real),
285 ) = crate::fixed_builtin_return_type(binding)
286 {
287 return super::cast_value(&value, &ty.sql_name());
288 }
289 }
290 return Ok(value);
291 };
292 if let Some(result) = random::eval_dispatched_random_function(dispatch, &call_args, ctx) {
293 return result;
294 }
295 if call_args.iter().any(|(name, _)| name.is_some()) {
296 return Err(SQLError::Internal(format!(
297 "bound {} expression retained a named argument",
298 dispatch.label()
299 )));
300 }
301 let evaluated = call_args
302 .into_iter()
303 .map(|(_, value)| value)
304 .collect::<Vec<_>>();
305 if let FunctionDispatch::Enum {
306 operation,
307 type_oid,
308 } = dispatch
309 {
310 return super::enums::enum_function_value_with_state(
311 ctx.engine.and_then(super::EngineHook::enum_labels),
312 operation,
313 type_oid,
314 &evaluated,
315 ctx.enum_comparison_state(),
316 );
317 }
318 if let Some(result) = scalar_postgres::eval_comparison_with_context(
319 dispatch,
320 &evaluated,
321 &uqa_core::memory::ProductionControl::uncontrolled(),
322 ctx,
323 ) {
324 return result.map(|value| {
325 value
326 .into_uncontrolled()
327 .expect("ordinary comparison result")
328 });
329 }
330 eval_dispatched_builtin_with_control(
331 binding,
332 dispatch,
333 &evaluated,
334 &uqa_core::memory::ProductionControl::uncontrolled(),
335 ctx,
336 )
337 .map(|value| {
338 value
339 .into_uncontrolled()
340 .expect("ordinary dispatched builtin result")
341 })
342}
343
344pub(super) fn eval_dispatched_builtin_with_control(
345 binding: &FunctionBinding,
346 dispatch: FunctionDispatch,
347 evaluated: &[Value],
348 control: &uqa_core::memory::ProductionControl<'_>,
349 context: &EvalContext<'_>,
350) -> Result<uqa_core::memory::Produced<Value>> {
351 if dispatch == FunctionDispatch::FieldSelect {
352 if let Some(field) = &binding.composite_field {
353 return super::composites::fields::select_with_control(
355 field,
356 evaluated,
357 context.engine,
358 control,
359 );
360 }
361 }
362 if let Some(result) = scalar_postgres::eval_dispatched_postgres_function_with_control(
363 dispatch, evaluated, control,
364 ) {
365 return result;
366 }
367 match dispatch {
368 FunctionDispatch::NumericOperator(operator) => {
369 super::numeric_operator::eval_bound_operator_with_control(
370 operator, binding, evaluated, control,
371 )
372 }
373 FunctionDispatch::JsonExtract { as_text, path } => {
374 super::json::json_extract_operator_with_control(evaluated, as_text, path, control)
375 }
376 FunctionDispatch::ArraySortJson => {
377 scalar_array::eval_dispatched_json_array_sort_with_control(evaluated, control)
378 }
379 FunctionDispatch::ArrayConcat
380 | FunctionDispatch::ArrayAppend
381 | FunctionDispatch::ArrayPrepend => {
382 let name = match dispatch {
383 FunctionDispatch::ArrayConcat => "array_cat",
384 FunctionDispatch::ArrayAppend => "array_append",
385 _ => "array_prepend",
386 };
387 scalar_array::eval_array_functions_with_control(name, evaluated, control)
388 .expect("registered array concatenation operation")
389 }
390 FunctionDispatch::Range {
391 operation,
392 subtype,
393 multirange,
394 } => scalar_range::eval_dispatched_range_function_with_control(
395 operation, subtype, multirange, evaluated, control,
396 ),
397 FunctionDispatch::Enum {
399 operation,
400 type_oid,
401 } => super::enums::enum_function_value_with_control(
402 None, operation, type_oid, evaluated, None, control,
403 ),
404 FunctionDispatch::NamedArgument | FunctionDispatch::VariadicArgument => Err(
405 SQLError::Internal("call-argument syntax marker reached scalar execution".into()),
406 ),
407 _ => Err(SQLError::Internal(format!(
408 "{} has no scalar executor",
409 dispatch.label()
410 ))),
411 }
412}