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 "has_table_privilege" if matches!(argument_count, 2 | 3) => Some(true),
58 "has_column_privilege" if matches!(argument_count, 3 | 4) => Some(true),
59 "has_database_privilege" if matches!(argument_count, 2 | 3) => Some(true),
60 "has_schema_privilege" if matches!(argument_count, 2 | 3) => Some(true),
61 "has_sequence_privilege" | "has_function_privilege" if matches!(argument_count, 2 | 3) => {
62 Some(true)
63 }
64 "pg_get_sequence_data" | "pg_sequence_last_value" | "pg_sequence_parameters"
65 if argument_count == 1 =>
66 {
67 Some(true)
68 }
69 "nextval" | "currval" if argument_count == 1 => Some(true),
70 "lastval" if argument_count == 0 => Some(true),
71 "setval" if matches!(argument_count, 2 | 3) => Some(true),
72 "overlaps" if argument_count == 4 => Some(false),
73 "abs"
74 | "acos"
75 | "array_dims"
76 | "array_ndims"
77 | "array_reverse"
78 | "ascii"
79 | "asin"
80 | "atan"
81 | "bit_length"
82 | "cardinality"
83 | "casefold"
84 | "cbrt"
85 | "ceil"
86 | "ceiling"
87 | "char_length"
88 | "character_length"
89 | "chr"
90 | "cos"
91 | "cosh"
92 | "current_schemas"
93 | "degrees"
94 | "exp"
95 | "factorial"
96 | "floor"
97 | "gamma"
98 | "initcap"
99 | "isfinite"
100 | "json_array_length"
101 | "jsonb_array_length"
102 | "json_typeof"
103 | "jsonb_typeof"
104 | "jsonb_pretty"
105 | "justify_hours"
106 | "length"
107 | "lgamma"
108 | "ln"
109 | "log10"
110 | "log2"
111 | "lower"
112 | "md5"
113 | "octet_length"
114 | "quote_ident"
115 | "quote_literal"
116 | "radians"
117 | "reverse"
118 | "row_to_json"
119 | "sign"
120 | "sin"
121 | "sinh"
122 | "sqrt"
123 | "tan"
124 | "tanh"
125 | "to_bin"
126 | "to_hex"
127 | "to_oct"
128 | "to_json"
129 | "to_jsonb"
130 | "to_regclass"
131 | "to_regnamespace"
132 | "to_regproc"
133 | "to_regprocedure"
134 | "to_regrole"
135 | "to_regtype"
136 | "to_timestamp"
137 | "upper"
138 | "uuid_extract_timestamp"
139 | "uuid_extract_version"
140 if argument_count == 1 =>
141 {
142 Some(true)
143 }
144 "random" if argument_count == 2 => Some(true),
145 "age" | "btrim" | "ltrim" | "rtrim" | "trim" | "log" | "round" | "trunc"
146 | "json_strip_nulls" | "jsonb_strip_nulls"
147 if matches!(argument_count, 1 | 2) =>
148 {
149 Some(true)
150 }
151 "array_sort" if matches!(argument_count, 1..=3) => Some(true),
152 "array_length" | "array_lower" | "array_upper" | "atan2" | "date_part" | "date_trunc"
153 | "decode" | "encode" | "extract" | "gcd" | "lcm" | "left" | "mod" | "power" | "pow"
154 | "repeat" | "right" | "starts_with" | "position" | "strpos" | "to_char" | "to_date"
155 | "to_number" | "trim_array" | "point" | "st_distance" | "st_within"
156 if argument_count == 2 =>
157 {
158 Some(true)
159 }
160 "like" | "ilike" | "similar_to" if argument_count == 2 => Some(true),
161 "like" | "ilike" | "similar_to" if argument_count == 3 => Some(false),
162 "array_to_string" if argument_count == 2 => Some(true),
163 "substring" | "substr" | "lpad" | "rpad" if matches!(argument_count, 2 | 3) => Some(true),
164 "regexp_count" if matches!(argument_count, 2..=4) => Some(true),
165 "regexp_instr" if matches!(argument_count, 2..=7) => Some(true),
166 "regexp_like" | "regexp_match" | "regexp_matches" if matches!(argument_count, 2 | 3) => {
167 Some(true)
168 }
169 "regexp_replace" if matches!(argument_count, 3..=6) => Some(true),
170 "regexp_substr" if matches!(argument_count, 2..=6) => Some(true),
171 "replace" | "split_part" | "translate" | "make_date" if argument_count == 3 => Some(true),
172 "overlay" | "jsonb_set" | "jsonb_insert" if matches!(argument_count, 3 | 4) => Some(true),
173 "json_extract_path"
174 | "jsonb_extract_path"
175 | "json_extract_path_text"
176 | "jsonb_extract_path_text"
177 if argument_count >= 2 =>
178 {
179 Some(true)
180 }
181 "json_contains" | "json_contained_by" | "json_delete_path" | "json_has_key"
182 | "json_has_any_key" | "json_has_all_keys" | "jsonb_path_exists" | "jsonpath_exists"
183 | "jsonb_path_match" | "jsonpath_match"
184 if argument_count == 2 =>
185 {
186 Some(true)
187 }
188 "make_timestamp" if matches!(argument_count, 6 | 7) => Some(true),
189 "make_interval" if argument_count <= 7 => Some(true),
190 "width_bucket" if argument_count == 4 => Some(true),
191 "st_dwithin" if matches!(argument_count, 2 | 3) => Some(true),
192 _ => None,
193 }
194}
195
196#[must_use]
198pub fn bound_scalar_function_strictness(
199 name: &str,
200 binding: Option<&FunctionBinding>,
201 argument_count: usize,
202) -> Option<bool> {
203 let Some(binding) = binding else {
204 return builtin_scalar_function_strictness(name, argument_count);
205 };
206 if let Some(dispatch) = binding.dispatch {
207 return match dispatch {
208 FunctionDispatch::NumericOperator(_) => Some(true),
209 FunctionDispatch::ArraySubscripts
210 | FunctionDispatch::Subscript
211 | FunctionDispatch::BetweenSymmetric
212 | FunctionDispatch::ToBinInt4
213 | FunctionDispatch::ToBinInt8
214 | FunctionDispatch::ToHexInt4
215 | FunctionDispatch::ToHexInt8
216 | FunctionDispatch::ToOctInt4
217 | FunctionDispatch::ToOctInt8
218 | FunctionDispatch::RandomInt4Range
219 | FunctionDispatch::RandomInt8Range
220 | FunctionDispatch::RandomNumericRange
221 | FunctionDispatch::ArraySortJson
222 | FunctionDispatch::JsonExtract { .. }
223 | FunctionDispatch::Range { .. } => Some(true),
224 FunctionDispatch::ArraySlices
225 | FunctionDispatch::Slice
226 | FunctionDispatch::AnyOperator
227 | FunctionDispatch::AllOperator
228 | FunctionDispatch::IsDistinct => Some(false),
229 FunctionDispatch::NamedArgument | FunctionDispatch::VariadicArgument => None,
230 };
231 }
232 binding
233 .builtin
234 .then(|| builtin_scalar_function_strictness(&binding.name, argument_count))
235 .flatten()
236}
237
238pub fn eval_bound_builtin_function_call(
240 binding: &FunctionBinding,
241 call_args: Vec<(Option<String>, Value)>,
242 ctx: &EvalContext<'_>,
243) -> Result<Value> {
244 if let Some(error) = &binding.resolution_error {
245 return Err(error.sql_error());
246 }
247 let Some(dispatch) = binding.dispatch else {
248 let value = eval_builtin_function_call(&binding.name, call_args, ctx)?;
249 if matches!(value, Value::Int(_) | Value::Float(_)) {
251 if let Some(
252 ty @ (crate::ColumnType::SmallInteger
253 | crate::ColumnType::Integer
254 | crate::ColumnType::Real),
255 ) = crate::fixed_builtin_return_type(binding)
256 {
257 return super::cast_value(&value, &ty.sql_name());
258 }
259 }
260 return Ok(value);
261 };
262 if let Some(result) = random::eval_dispatched_random_function(dispatch, &call_args, ctx) {
263 return result;
264 }
265 if call_args.iter().any(|(name, _)| name.is_some()) {
266 return Err(SQLError::Internal(format!(
267 "bound {} expression retained a named argument",
268 dispatch.label()
269 )));
270 }
271 let evaluated = call_args
272 .into_iter()
273 .map(|(_, value)| value)
274 .collect::<Vec<_>>();
275 eval_dispatched_builtin_with_control(
276 binding,
277 dispatch,
278 &evaluated,
279 &uqa_core::memory::ProductionControl::uncontrolled(),
280 )
281 .map(|value| {
282 value
283 .into_uncontrolled()
284 .expect("ordinary dispatched builtin result")
285 })
286}
287
288pub(super) fn eval_dispatched_builtin_with_control(
289 binding: &FunctionBinding,
290 dispatch: FunctionDispatch,
291 evaluated: &[Value],
292 control: &uqa_core::memory::ProductionControl<'_>,
293) -> Result<uqa_core::memory::Produced<Value>> {
294 if let Some(result) = scalar_postgres::eval_dispatched_postgres_function_with_control(
295 dispatch, evaluated, control,
296 ) {
297 return result;
298 }
299 match dispatch {
300 FunctionDispatch::NumericOperator(operator) => {
301 super::numeric_operator::eval_bound_operator_with_control(
302 operator, binding, evaluated, control,
303 )
304 }
305 FunctionDispatch::JsonExtract { as_text, path } => {
306 super::json::json_extract_operator_with_control(evaluated, as_text, path, control)
307 }
308 FunctionDispatch::ArraySortJson => {
309 scalar_array::eval_dispatched_json_array_sort_with_control(evaluated, control)
310 }
311 FunctionDispatch::Range {
312 operation,
313 subtype,
314 multirange,
315 } => scalar_range::eval_dispatched_range_function_with_control(
316 operation, subtype, multirange, evaluated, control,
317 ),
318 FunctionDispatch::NamedArgument | FunctionDispatch::VariadicArgument => Err(
319 SQLError::Internal("call-argument syntax marker reached scalar execution".into()),
320 ),
321 _ => Err(SQLError::Internal(format!(
322 "{} has no scalar executor",
323 dispatch.label()
324 ))),
325 }
326}