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