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