Skip to main content

uqa_execution/
type_resolution.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Static SQL type propagation and PostgreSQL-compatible common-type rules.
8
9use uqa_core::Value;
10use uqa_sql::ast::{BinaryOp, ColumnType, FunctionBinding};
11use uqa_sql::expr::{TO_HEX_INT4_FUNCTION, TO_HEX_INT8_FUNCTION};
12use uqa_sql::{SQLError, SQLParam};
13
14use crate::{RowSchema, ScalarExpr};
15
16mod containment;
17mod to_hex;
18
19pub trait FunctionTypeResolver: Send + Sync {
20    fn resolve_function_type(
21        &self,
22        name: &str,
23        binding: Option<&FunctionBinding>,
24        argument_names: &[Option<String>],
25        argument_types: &[Option<ColumnType>],
26    ) -> Result<Option<ColumnType>, SQLError>;
27}
28
29pub fn scalar_type(
30    expression: &ScalarExpr,
31    schema: &RowSchema,
32    params: &[SQLParam],
33) -> Result<Option<ColumnType>, SQLError> {
34    scalar_type_inner(expression, schema, params, None)
35}
36
37pub fn scalar_type_with_resolver(
38    expression: &ScalarExpr,
39    schema: &RowSchema,
40    params: &[SQLParam],
41    resolver: &dyn FunctionTypeResolver,
42) -> Result<Option<ColumnType>, SQLError> {
43    scalar_type_inner(expression, schema, params, Some(resolver))
44}
45
46fn scalar_type_inner(
47    expression: &ScalarExpr,
48    schema: &RowSchema,
49    params: &[SQLParam],
50    resolver: Option<&dyn FunctionTypeResolver>,
51) -> Result<Option<ColumnType>, SQLError> {
52    match expression {
53        ScalarExpr::Column(column) => Ok(schema.type_of(column).cloned()),
54        ScalarExpr::Position(position) => Ok(schema.column_type(*position).cloned()),
55        ScalarExpr::QualifiedColumn { qualifier, column } => {
56            Ok(schema.qualified_type(qualifier, column).cloned())
57        }
58        ScalarExpr::Literal(value) => Ok(value_type(value)),
59        ScalarExpr::Param(index) => Ok(index
60            .checked_sub(1)
61            .and_then(|index| params.get(index))
62            .and_then(parameter_type)),
63        ScalarExpr::Cast { expr, ty } => {
64            scalar_type_inner(expr, schema, params, resolver)?;
65            ColumnType::from_sql_name(ty).map(Some)
66        }
67        ScalarExpr::Array(items) => {
68            let mut element = None;
69            for item in items {
70                element = merge_optional_types(
71                    element,
72                    common_context_expression_type(item, schema, params, resolver)?,
73                )?;
74            }
75            Ok(element.map(|element| ColumnType::Array(Box::new(element))))
76        }
77        ScalarExpr::Row(items) => {
78            for item in items {
79                scalar_type_inner(item, schema, params, resolver)?;
80            }
81            Ok(Some(ColumnType::Record))
82        }
83        ScalarExpr::Binary { op, lhs, rhs } => {
84            let left = scalar_type_inner(lhs, schema, params, resolver)?;
85            let right = scalar_type_inner(rhs, schema, params, resolver)?;
86            binary_result_type(*op, left.as_ref(), right.as_ref())
87        }
88        ScalarExpr::UnaryMinus(inner) => scalar_type_inner(inner, schema, params, resolver)?
89            .map_or(Ok(None), |ty| unary_minus_result_type(&ty).map(Some)),
90        ScalarExpr::Not(inner) | ScalarExpr::IsNull { expr: inner, .. } => {
91            scalar_type_inner(inner, schema, params, resolver)?;
92            Ok(Some(ColumnType::Boolean))
93        }
94        ScalarExpr::And(items) | ScalarExpr::Or(items) => {
95            for item in items {
96                scalar_type_inner(item, schema, params, resolver)?;
97            }
98            Ok(Some(ColumnType::Boolean))
99        }
100        ScalarExpr::Between { expr, low, high } => {
101            scalar_type_inner(expr, schema, params, resolver)?;
102            scalar_type_inner(low, schema, params, resolver)?;
103            scalar_type_inner(high, schema, params, resolver)?;
104            Ok(Some(ColumnType::Boolean))
105        }
106        ScalarExpr::InList { expr, list, .. } => {
107            scalar_type_inner(expr, schema, params, resolver)?;
108            for item in list {
109                scalar_type_inner(item, schema, params, resolver)?;
110            }
111            Ok(Some(ColumnType::Boolean))
112        }
113        ScalarExpr::InSubquery { expr, .. } => {
114            scalar_type_inner(expr, schema, params, resolver)?;
115            Ok(Some(ColumnType::Boolean))
116        }
117        ScalarExpr::Exists { .. } => Ok(Some(ColumnType::Boolean)),
118        ScalarExpr::Case {
119            base,
120            when,
121            else_branch,
122        } => {
123            if let Some(base) = base {
124                scalar_type_inner(base, schema, params, resolver)?;
125            }
126            let mut result = None;
127            for (condition, value) in when {
128                scalar_type_inner(condition, schema, params, resolver)?;
129                result = merge_optional_types(
130                    result,
131                    common_context_expression_type(value, schema, params, resolver)?,
132                )?;
133            }
134            if let Some(value) = else_branch {
135                result = merge_optional_types(
136                    result,
137                    common_context_expression_type(value, schema, params, resolver)?,
138                )?;
139            }
140            Ok(result)
141        }
142        ScalarExpr::Func {
143            name,
144            binding,
145            args,
146            order_by,
147            filter,
148            ..
149        } => {
150            if let Some(filter) = filter {
151                scalar_type_inner(filter, schema, params, resolver)?;
152            }
153            builtin_function_type_inner(
154                name,
155                binding.as_ref(),
156                args,
157                order_by,
158                schema,
159                params,
160                resolver,
161            )
162        }
163        ScalarExpr::WindowCall { name, args, spec } => {
164            for expression in &spec.partition_by {
165                scalar_type_inner(expression, schema, params, resolver)?;
166            }
167            for order in &spec.order_by {
168                scalar_type_inner(&order.expr, schema, params, resolver)?;
169            }
170            if let Some(frame) = &spec.frame {
171                for bound in [&frame.start, &frame.end] {
172                    match bound {
173                        crate::ScalarFrameBound::Preceding(expression)
174                        | crate::ScalarFrameBound::Following(expression) => {
175                            scalar_type_inner(expression, schema, params, resolver)?;
176                        }
177                        crate::ScalarFrameBound::UnboundedPreceding
178                        | crate::ScalarFrameBound::UnboundedFollowing
179                        | crate::ScalarFrameBound::CurrentRow => {}
180                    }
181                }
182            }
183            builtin_function_type_inner(name, None, args, &[], schema, params, resolver)
184        }
185        ScalarExpr::ScalarSubquery(_)
186        | ScalarExpr::Star
187        | ScalarExpr::QualifiedStar(_)
188        | ScalarExpr::Default => Ok(None),
189    }
190}
191
192pub fn builtin_function_type(
193    name: &str,
194    args: &[ScalarExpr],
195    order_by: &[crate::ScalarOrder],
196    schema: &RowSchema,
197    params: &[SQLParam],
198) -> Result<Option<ColumnType>, SQLError> {
199    builtin_function_type_inner(name, None, args, order_by, schema, params, None)
200}
201
202fn builtin_function_type_inner(
203    name: &str,
204    binding: Option<&FunctionBinding>,
205    args: &[ScalarExpr],
206    order_by: &[crate::ScalarOrder],
207    schema: &RowSchema,
208    params: &[SQLParam],
209    resolver: Option<&dyn FunctionTypeResolver>,
210) -> Result<Option<ColumnType>, SQLError> {
211    let original_name = name;
212    let lower = name.to_ascii_lowercase();
213    let name = lower.strip_prefix("pg_catalog.").unwrap_or(&lower);
214    if name.contains('.') {
215        return resolve_extension_function_type(
216            resolver,
217            original_name,
218            binding,
219            args,
220            schema,
221            params,
222        );
223    }
224    if name == uqa_sql::expr::NAMED_ARG_FUNCTION {
225        return args.get(1).map_or(Ok(None), |expression| {
226            scalar_type_inner(expression, schema, params, resolver)
227        });
228    }
229    let argument = |position: usize| -> Result<Option<ColumnType>, SQLError> {
230        args.get(position).map_or(Ok(None), |expression| {
231            scalar_type_inner(named_argument_value(expression), schema, params, resolver)
232        })
233    };
234    let ordered_argument = || -> Result<Option<ColumnType>, SQLError> {
235        order_by.first().map_or(Ok(None), |order| {
236            scalar_type_inner(&order.expr, schema, params, resolver)
237        })
238    };
239    let first = || argument(0);
240    for argument in args {
241        scalar_type_inner(named_argument_value(argument), schema, params, resolver)?;
242    }
243    for order in order_by {
244        scalar_type_inner(&order.expr, schema, params, resolver)?;
245    }
246    match name {
247        "pg_typeof" => Ok(Some(ColumnType::Regtype)),
248        "typeof"
249        | "upper"
250        | "lower"
251        | "casefold"
252        | "initcap"
253        | "trim"
254        | "btrim"
255        | "ltrim"
256        | "rtrim"
257        | "concat"
258        | "concat_ws"
259        | "replace"
260        | "substring"
261        | "substr"
262        | "left"
263        | "right"
264        | "chr"
265        | "regexp_replace"
266        | "lpad"
267        | "rpad"
268        | "repeat"
269        | "translate"
270        | "overlay"
271        | "format"
272        | "md5"
273        | "encode"
274        | "split_part"
275        | TO_HEX_INT4_FUNCTION
276        | TO_HEX_INT8_FUNCTION
277        | "quote_ident"
278        | "quote_literal"
279        | "quote_nullable"
280        | "regexp_substr"
281        | "array_to_string"
282        | "array_dims"
283        | "json_typeof"
284        | "jsonb_typeof"
285        | "jsonb_pretty"
286        | "to_char"
287        | "timeofday"
288        | "current_setting"
289        | "merge_action"
290        | "string_to_table"
291        | "regexp_split_to_table"
292        | "json_object_keys"
293        | "jsonb_object_keys"
294        | "json_array_elements_text"
295        | "jsonb_array_elements_text"
296        | "json_extract_path_text"
297        | "jsonb_extract_path_text" => Ok(Some(ColumnType::Text)),
298        "to_hex" => to_hex::resolve_type(original_name, args, schema, params, resolver),
299        "count" | "row_number" | "rank" | "dense_rank" | "crc32" | "crc32c" | "nextval"
300        | "currval" | "setval" => Ok(Some(ColumnType::BigInteger)),
301        "sum" => Ok(first()?.and_then(|ty| aggregate_sum_type(&ty))),
302        "avg" => Ok(first()?.and_then(|ty| aggregate_average_type(&ty))),
303        "stddev" | "stddev_samp" | "stddev_pop" | "variance" | "var_samp" | "var_pop" => {
304            Ok(first()?.and_then(|ty| aggregate_average_type(&ty)))
305        }
306        "min" | "max" | "lag" | "lead" | "first_value" | "last_value" | "nth_value" | "nullif"
307        | "array_cat" | "array_remove" | "array_replace" | "trim_array" | "array_sample"
308        | "array_reverse" | "array_sort" | "__slice" | "__array_slices" | "array_append"
309        | "generate_series" => first(),
310        "mode" | "percentile_disc" => ordered_argument(),
311        "percentile_cont" => Ok(ordered_argument()?.map(|ty| match base_type(&ty) {
312            ColumnType::Interval => ColumnType::Interval,
313            _ => ColumnType::DoublePrecision,
314        })),
315        "array_agg" => Ok(first()?.map(|ty| ColumnType::Array(Box::new(ty)))),
316        "string_agg" => Ok(first()?.map(|ty| {
317            if matches!(ty, ColumnType::Bytea) {
318                ColumnType::Bytea
319            } else {
320                ColumnType::Text
321            }
322        })),
323        "json_agg"
324        | "json_object_agg"
325        | "json_array_elements"
326        | "json_extract_path"
327        | "json_strip_nulls"
328        | "to_json"
329        | "row_to_json"
330        | "json_build_object"
331        | "json_build_array" => Ok(Some(ColumnType::Json)),
332        "jsonb_agg"
333        | "jsonb_object_agg"
334        | "jsonb_array_elements"
335        | "jsonb_extract_path"
336        | "json_delete_path"
337        | "jsonb_set"
338        | "jsonb_insert"
339        | "jsonb_strip_nulls"
340        | "to_jsonb"
341        | "jsonb_build_object"
342        | "jsonb_build_array" => Ok(Some(ColumnType::JsonB)),
343        "json_each" | "jsonb_each" | "json_each_text" | "jsonb_each_text" => {
344            Ok(Some(ColumnType::Record))
345        }
346        "bool_and"
347        | "bool_or"
348        | "every"
349        | "starts_with"
350        | "like"
351        | "ilike"
352        | "similar_to"
353        | "regexp_like"
354        | "isfinite"
355        | "contains_op"
356        | "contained_by_op"
357        | "json_contains"
358        | "json_contained_by"
359        | "json_has_key"
360        | "json_has_any_key"
361        | "json_has_all_keys"
362        | "jsonb_path_exists"
363        | "jsonpath_exists"
364        | "jsonb_path_match"
365        | "jsonpath_match"
366        | "array_overlap"
367        | "__any_op"
368        | "__all_op"
369        | "__is_distinct"
370        | "__between_symmetric"
371        | "st_within"
372        | "st_dwithin"
373        | "overlaps" => {
374            if matches!(name, "contains_op" | "contained_by_op") {
375                return containment::resolve_operator_type(name, args, schema, params, resolver);
376            }
377            Ok(Some(ColumnType::Boolean))
378        }
379        "coalesce" | "greatest" | "least" => common_argument_type(args, schema, params, resolver),
380        "reverse" => Ok(first()?.map(|ty| {
381            if matches!(base_type(&ty), ColumnType::Bytea) {
382                ColumnType::Bytea
383            } else {
384                ColumnType::Text
385            }
386        })),
387        "concat_op" => concat_type(argument(0)?, argument(1)?),
388        "ntile" | "length" | "char_length" | "character_length" | "octet_length" | "position"
389        | "strpos" | "ascii" | "width_bucket" | "bit_length" | "regexp_count" | "regexp_instr"
390        | "num_nulls" | "num_nonnulls" | "array_length" | "array_upper" | "array_lower"
391        | "array_ndims" | "cardinality" | "array_position" | "json_array_length"
392        | "jsonb_array_length" => Ok(Some(ColumnType::Integer)),
393        "abs" => Ok(first()?.map(|ty| base_type(&ty).clone())),
394        "round" | "trunc" | "ceil" | "ceiling" | "floor" | "sign" => {
395            Ok(first()?.map(|ty| numeric_unary_result_type(&ty)))
396        }
397        "mod" | "gcd" | "lcm" => numeric_binary_function_type(argument(0)?, argument(1)?),
398        "div" | "factorial" | "extract" | "to_number" => Ok(Some(numeric_type())),
399        "power" | "pow" => numeric_power_type(args, schema, params, resolver),
400        "sqrt" | "ln" | "log" | "log10" => {
401            numeric_transcendental_type(args, schema, params, resolver)
402        }
403        "sin" | "cos" | "tan" | "asin" | "acos" | "atan" | "atan2" | "sinh" | "cosh" | "tanh"
404        | "exp" | "log2" | "cbrt" | "gamma" | "lgamma" | "degrees" | "radians" | "pi"
405        | "random" | "st_distance" | "date_part" => Ok(Some(ColumnType::DoublePrecision)),
406        "regexp_match" | "regexp_matches" | "string_to_array" => {
407            Ok(Some(ColumnType::Array(Box::new(ColumnType::Text))))
408        }
409        "array_positions" => Ok(Some(ColumnType::Array(Box::new(ColumnType::Integer)))),
410        "decode" => Ok(Some(ColumnType::Bytea)),
411        "array_prepend" => argument(1),
412        "array_fill" => Ok(first()?.map(|ty| ColumnType::Array(Box::new(ty)))),
413        "__subscript" | "__array_subscripts" | "unnest" => {
414            Ok(first()?.and_then(array_element_type))
415        }
416        "now" | "current_timestamp" | "clock_timestamp" | "statement_timestamp" => {
417            Ok(Some(ColumnType::TimestampTz))
418        }
419        "current_date" | "make_date" | "to_date" => Ok(Some(ColumnType::Date)),
420        "to_timestamp" => Ok(Some(ColumnType::TimestampTz)),
421        "age" | "make_interval" | "justify_hours" => Ok(Some(ColumnType::Interval)),
422        "date_trunc" => Ok(argument(1)?.map(|ty| match base_type(&ty) {
423            ColumnType::Interval => ColumnType::Interval,
424            ColumnType::Timestamp => ColumnType::Timestamp,
425            _ => ColumnType::TimestampTz,
426        })),
427        "make_timestamp" => Ok(Some(ColumnType::Timestamp)),
428        "gen_random_uuid" | "uuidv4" | "uuidv7" => Ok(Some(ColumnType::Uuid)),
429        "current_database" | "current_catalog" | "current_schema" | "current_user"
430        | "session_user" => Ok(Some(ColumnType::Name)),
431        "current_schemas" => Ok(Some(ColumnType::Array(Box::new(ColumnType::Name)))),
432        _ => {
433            resolve_extension_function_type(resolver, original_name, binding, args, schema, params)
434        }
435    }
436}
437
438fn resolve_extension_function_type(
439    resolver: Option<&dyn FunctionTypeResolver>,
440    name: &str,
441    binding: Option<&FunctionBinding>,
442    args: &[ScalarExpr],
443    schema: &RowSchema,
444    params: &[SQLParam],
445) -> Result<Option<ColumnType>, SQLError> {
446    let Some(resolver) = resolver else {
447        return Ok(None);
448    };
449    let mut argument_names = Vec::with_capacity(args.len());
450    let mut argument_types = Vec::with_capacity(args.len());
451    for argument in args {
452        let (name, value) = named_argument(argument);
453        argument_names.push(name);
454        argument_types.push(
455            if matches!(value, ScalarExpr::Literal(Value::Str(_) | Value::Null)) {
456                None
457            } else {
458                scalar_type_inner(value, schema, params, Some(resolver))?
459            },
460        );
461    }
462    resolver.resolve_function_type(name, binding, &argument_names, &argument_types)
463}
464
465fn named_argument(expression: &ScalarExpr) -> (Option<String>, &ScalarExpr) {
466    let ScalarExpr::Func { name, args, .. } = expression else {
467        return (None, expression);
468    };
469    if name != uqa_sql::expr::NAMED_ARG_FUNCTION {
470        return (None, expression);
471    }
472    let argument_name = args.first().and_then(|name| match name {
473        ScalarExpr::Literal(Value::Str(name)) => Some(name.clone()),
474        _ => None,
475    });
476    (argument_name, args.get(1).unwrap_or(expression))
477}
478
479fn named_argument_value(expression: &ScalarExpr) -> &ScalarExpr {
480    let ScalarExpr::Func { name, args, .. } = expression else {
481        return expression;
482    };
483    if name != uqa_sql::expr::NAMED_ARG_FUNCTION {
484        return expression;
485    }
486    args.get(1).unwrap_or(expression)
487}
488
489fn numeric_type() -> ColumnType {
490    ColumnType::Numeric {
491        precision: None,
492        scale: None,
493    }
494}
495
496fn base_type(mut ty: &ColumnType) -> &ColumnType {
497    while let ColumnType::Domain { base, .. } = ty {
498        ty = base;
499    }
500    ty
501}
502
503fn aggregate_sum_type(ty: &ColumnType) -> Option<ColumnType> {
504    Some(match base_type(ty) {
505        ColumnType::SmallInteger | ColumnType::Integer => ColumnType::BigInteger,
506        ColumnType::BigInteger | ColumnType::Numeric { .. } => numeric_type(),
507        ColumnType::Real => ColumnType::Real,
508        ColumnType::DoublePrecision => ColumnType::DoublePrecision,
509        _ => return None,
510    })
511}
512
513fn aggregate_average_type(ty: &ColumnType) -> Option<ColumnType> {
514    Some(match base_type(ty) {
515        ColumnType::SmallInteger
516        | ColumnType::Integer
517        | ColumnType::BigInteger
518        | ColumnType::Numeric { .. } => numeric_type(),
519        ColumnType::Real | ColumnType::DoublePrecision => ColumnType::DoublePrecision,
520        _ => return None,
521    })
522}
523
524fn common_argument_type(
525    args: &[ScalarExpr],
526    schema: &RowSchema,
527    params: &[SQLParam],
528    resolver: Option<&dyn FunctionTypeResolver>,
529) -> Result<Option<ColumnType>, SQLError> {
530    let mut result = None;
531    for argument in args {
532        result = merge_optional_types(
533            result,
534            common_context_expression_type(
535                named_argument_value(argument),
536                schema,
537                params,
538                resolver,
539            )?,
540        )?;
541    }
542    Ok(result.or(Some(ColumnType::Text)))
543}
544
545fn concat_type(
546    left: Option<ColumnType>,
547    right: Option<ColumnType>,
548) -> Result<Option<ColumnType>, SQLError> {
549    match (left, right) {
550        (Some(ColumnType::Array(left)), Some(ColumnType::Array(right))) => {
551            common_type(&left, &right).map(|element| Some(ColumnType::Array(Box::new(element))))
552        }
553        (Some(array @ ColumnType::Array(_)), _) | (_, Some(array @ ColumnType::Array(_))) => {
554            Ok(Some(array))
555        }
556        (Some(ColumnType::JsonB), Some(ColumnType::JsonB)) => Ok(Some(ColumnType::JsonB)),
557        _ => Ok(Some(ColumnType::Text)),
558    }
559}
560
561fn numeric_unary_result_type(ty: &ColumnType) -> ColumnType {
562    if matches!(base_type(ty), ColumnType::Numeric { .. }) {
563        numeric_type()
564    } else {
565        ColumnType::DoublePrecision
566    }
567}
568
569fn unary_minus_result_type(ty: &ColumnType) -> Result<ColumnType, SQLError> {
570    match base_type(ty) {
571        ty @ (ColumnType::SmallInteger
572        | ColumnType::Integer
573        | ColumnType::BigInteger
574        | ColumnType::Real
575        | ColumnType::DoublePrecision
576        | ColumnType::Numeric { .. }
577        | ColumnType::Interval) => Ok(ty.clone()),
578        other => Err(SQLError::TypeMismatch(format!(
579            "operator does not exist: - {}",
580            other.sql_name()
581        ))),
582    }
583}
584
585fn numeric_binary_function_type(
586    left: Option<ColumnType>,
587    right: Option<ColumnType>,
588) -> Result<Option<ColumnType>, SQLError> {
589    match (left, right) {
590        (Some(left), Some(right)) => common_numeric_type(base_type(&left), base_type(&right))
591            .map(Some)
592            .ok_or_else(|| {
593                SQLError::TypeMismatch(format!(
594                    "types {} and {} are not numeric",
595                    left.sql_name(),
596                    right.sql_name()
597                ))
598            }),
599        (Some(ty), None) | (None, Some(ty)) => Ok(Some(base_type(&ty).clone())),
600        (None, None) => Ok(None),
601    }
602}
603
604fn numeric_transcendental_type(
605    args: &[ScalarExpr],
606    schema: &RowSchema,
607    params: &[SQLParam],
608    resolver: Option<&dyn FunctionTypeResolver>,
609) -> Result<Option<ColumnType>, SQLError> {
610    let mut saw_argument = false;
611    for argument in args {
612        let Some(ty) = scalar_type_inner(named_argument_value(argument), schema, params, resolver)?
613        else {
614            continue;
615        };
616        saw_argument = true;
617        if !matches!(base_type(&ty), ColumnType::Numeric { .. }) {
618            return Ok(Some(ColumnType::DoublePrecision));
619        }
620    }
621    Ok(saw_argument.then(numeric_type))
622}
623
624fn numeric_power_type(
625    args: &[ScalarExpr],
626    schema: &RowSchema,
627    params: &[SQLParam],
628    resolver: Option<&dyn FunctionTypeResolver>,
629) -> Result<Option<ColumnType>, SQLError> {
630    let mut saw_numeric = false;
631    let mut saw_floating = false;
632    for argument in args {
633        let argument = named_argument_value(argument);
634        if matches!(argument, ScalarExpr::Literal(Value::Str(_) | Value::Null)) {
635            continue;
636        }
637        let Some(ty) = scalar_type_inner(argument, schema, params, resolver)? else {
638            continue;
639        };
640        match base_type(&ty) {
641            ColumnType::Numeric { .. } => saw_numeric = true,
642            ColumnType::SmallInteger | ColumnType::Integer | ColumnType::BigInteger => {}
643            ColumnType::Real | ColumnType::DoublePrecision => saw_floating = true,
644            _ => {
645                return Err(SQLError::Routine {
646                    sqlstate: "42883".into(),
647                    message: "function power with these argument types does not exist".into(),
648                })
649            }
650        }
651    }
652    Ok(if saw_floating {
653        Some(ColumnType::DoublePrecision)
654    } else if saw_numeric {
655        Some(numeric_type())
656    } else if !args.is_empty() {
657        Some(ColumnType::DoublePrecision)
658    } else {
659        None
660    })
661}
662
663fn array_element_type(ty: ColumnType) -> Option<ColumnType> {
664    match ty {
665        ColumnType::Array(element) => Some(*element),
666        ColumnType::Int2Vector => Some(ColumnType::SmallInteger),
667        ColumnType::OidVector => Some(ColumnType::Oid),
668        _ => None,
669    }
670}
671
672fn binary_result_type(
673    op: BinaryOp,
674    left: Option<&ColumnType>,
675    right: Option<&ColumnType>,
676) -> Result<Option<ColumnType>, SQLError> {
677    if matches!(
678        op,
679        BinaryOp::Equal
680            | BinaryOp::NotEqual
681            | BinaryOp::Less
682            | BinaryOp::LessEqual
683            | BinaryOp::Greater
684            | BinaryOp::GreaterEqual
685    ) {
686        return Ok(Some(ColumnType::Boolean));
687    }
688    let (Some(left), Some(right)) = (left, right) else {
689        return merge_optional_types(left.cloned(), right.cloned());
690    };
691    let left = base_type(left);
692    let right = base_type(right);
693    if let Some(ty) = temporal_binary_result_type(op, left, right) {
694        return Ok(Some(ty));
695    }
696    if let Some(ty) = common_numeric_type(left, right) {
697        return Ok(Some(ty));
698    }
699    if matches!(left, ColumnType::JsonB)
700        && matches!(op, BinaryOp::Subtract)
701        && (right.is_character_string()
702            || matches!(right, ColumnType::SmallInteger | ColumnType::Integer)
703            || matches!(right, ColumnType::Array(element) if element.is_character_string()))
704    {
705        return Ok(Some(ColumnType::JsonB));
706    }
707    Err(SQLError::Routine {
708        sqlstate: "42883".into(),
709        message: format!(
710            "operator does not exist: {} {} {}",
711            left.sql_name(),
712            binary_operator_name(op),
713            right.sql_name()
714        ),
715    })
716}
717
718fn temporal_binary_result_type(
719    op: BinaryOp,
720    left: &ColumnType,
721    right: &ColumnType,
722) -> Option<ColumnType> {
723    use ColumnType as T;
724    match (left, right, op) {
725        (T::Date, T::Date, BinaryOp::Subtract) => Some(T::Integer),
726        (T::Date, T::SmallInteger | T::Integer, BinaryOp::Add | BinaryOp::Subtract)
727        | (T::SmallInteger | T::Integer, T::Date, BinaryOp::Add) => Some(T::Date),
728        (T::Date | T::Timestamp, T::Interval, BinaryOp::Add | BinaryOp::Subtract)
729        | (T::Interval, T::Date | T::Timestamp, BinaryOp::Add) => Some(T::Timestamp),
730        (T::TimestampTz, T::Interval, BinaryOp::Add | BinaryOp::Subtract)
731        | (T::Interval, T::TimestampTz, BinaryOp::Add) => Some(T::TimestampTz),
732        (T::Time, T::Interval, BinaryOp::Add | BinaryOp::Subtract)
733        | (T::Interval, T::Time, BinaryOp::Add) => Some(T::Time),
734        (T::TimeTz, T::Interval, BinaryOp::Add | BinaryOp::Subtract)
735        | (T::Interval, T::TimeTz, BinaryOp::Add) => Some(T::TimeTz),
736        (T::Interval, T::Interval, BinaryOp::Add | BinaryOp::Subtract)
737        | (T::Time, T::Time, BinaryOp::Subtract)
738        | (T::TimeTz, T::TimeTz, BinaryOp::Subtract)
739        | (
740            T::Date | T::Timestamp | T::TimestampTz,
741            T::Date | T::Timestamp | T::TimestampTz,
742            BinaryOp::Subtract,
743        ) => Some(T::Interval),
744        (T::Interval, ty, BinaryOp::Multiply | BinaryOp::Divide) if numeric_rank(ty).is_some() => {
745            Some(T::Interval)
746        }
747        (ty, T::Interval, BinaryOp::Multiply) if numeric_rank(ty).is_some() => Some(T::Interval),
748        _ => None,
749    }
750}
751
752fn binary_operator_name(op: BinaryOp) -> &'static str {
753    match op {
754        BinaryOp::Equal => "=",
755        BinaryOp::NotEqual => "<>",
756        BinaryOp::Less => "<",
757        BinaryOp::LessEqual => "<=",
758        BinaryOp::Greater => ">",
759        BinaryOp::GreaterEqual => ">=",
760        BinaryOp::Add => "+",
761        BinaryOp::Subtract => "-",
762        BinaryOp::Multiply => "*",
763        BinaryOp::Divide => "/",
764    }
765}
766
767/// Bind polymorphic type-introspection calls and common-type coercions while
768/// the input schema still carries declared SQL types. Runtime values
769/// deliberately do not encode integer widths, varchar identity, or float
770/// widths, and selector expressions must return the common SQL type rather
771/// than the storage type of the branch selected at runtime.
772pub fn bind_type_introspection(
773    expression: ScalarExpr,
774    schema: &RowSchema,
775    params: &[SQLParam],
776) -> ScalarExpr {
777    bind_type_introspection_inner(expression, schema, params, None)
778}
779
780/// Bind type-introspection calls with access to catalog-backed function and aggregate overloads.
781pub fn bind_type_introspection_with_resolver(
782    expression: ScalarExpr,
783    schema: &RowSchema,
784    params: &[SQLParam],
785    resolver: &dyn FunctionTypeResolver,
786) -> ScalarExpr {
787    bind_type_introspection_inner(expression, schema, params, Some(resolver))
788}
789
790fn bind_type_introspection_inner(
791    expression: ScalarExpr,
792    schema: &RowSchema,
793    params: &[SQLParam],
794    resolver: Option<&dyn FunctionTypeResolver>,
795) -> ScalarExpr {
796    if !requires_type_introspection_binding(&expression) {
797        return expression;
798    }
799    match expression {
800        ScalarExpr::Func {
801            name,
802            binding,
803            mut args,
804            distinct,
805            mut order_by,
806            mut filter,
807        } => {
808            for argument in &mut args {
809                bind_type_introspection_in_place(argument, schema, params, resolver);
810            }
811            for order in &mut order_by {
812                bind_type_introspection_in_place(&mut order.expr, schema, params, resolver);
813            }
814            if let Some(filter) = filter.as_deref_mut() {
815                bind_type_introspection_in_place(filter, schema, params, resolver);
816            }
817            if containment::is_operator(&name) {
818                containment::bind_unknown_arguments(&mut args, schema, params, resolver);
819            }
820            if is_common_type_function(&name) {
821                bind_common_type_expressions(&mut args, schema, params, resolver);
822            }
823            let name =
824                to_hex::bind_overload(name, binding.as_ref(), &args, schema, params, resolver);
825            if is_pg_typeof(&name) && args.len() == 1 {
826                let name = scalar_type_inner(&args[0], schema, params, resolver)
827                    .ok()
828                    .flatten()
829                    .map_or_else(|| "unknown".to_string(), |ty| ty.regtype_name());
830                return ScalarExpr::Cast {
831                    expr: Box::new(ScalarExpr::Literal(Value::Str(name))),
832                    ty: "regtype".into(),
833                };
834            }
835            ScalarExpr::Func {
836                name,
837                binding,
838                args,
839                distinct,
840                order_by,
841                filter,
842            }
843        }
844        ScalarExpr::Array(mut items) => {
845            bind_type_introspection_items(&mut items, schema, params, resolver);
846            bind_common_type_expressions(&mut items, schema, params, resolver);
847            ScalarExpr::Array(items)
848        }
849        ScalarExpr::Row(mut items) => {
850            bind_type_introspection_items(&mut items, schema, params, resolver);
851            ScalarExpr::Row(items)
852        }
853        ScalarExpr::Binary {
854            op,
855            mut lhs,
856            mut rhs,
857        } => {
858            bind_type_introspection_in_place(lhs.as_mut(), schema, params, resolver);
859            bind_type_introspection_in_place(rhs.as_mut(), schema, params, resolver);
860            ScalarExpr::Binary { op, lhs, rhs }
861        }
862        ScalarExpr::UnaryMinus(mut expr) => {
863            let source_type = scalar_type_inner(&expr, schema, params, resolver)
864                .ok()
865                .flatten()
866                .and_then(|ty| unary_minus_result_type(&ty).ok());
867            bind_type_introspection_in_place(expr.as_mut(), schema, params, resolver);
868            if let Some(source_type) = source_type {
869                let source_name = source_type.sql_name();
870                if !matches!(expr.as_ref(), ScalarExpr::Cast { ty, .. } if ty.eq_ignore_ascii_case(&source_name))
871                {
872                    let inner = std::mem::replace(expr.as_mut(), ScalarExpr::Literal(Value::Null));
873                    *expr = ScalarExpr::Cast {
874                        expr: Box::new(inner),
875                        ty: source_name,
876                    };
877                }
878            }
879            ScalarExpr::UnaryMinus(expr)
880        }
881        ScalarExpr::Not(mut inner) => {
882            bind_type_introspection_in_place(inner.as_mut(), schema, params, resolver);
883            ScalarExpr::Not(inner)
884        }
885        ScalarExpr::And(mut items) => {
886            bind_type_introspection_items(&mut items, schema, params, resolver);
887            ScalarExpr::And(items)
888        }
889        ScalarExpr::Or(mut items) => {
890            bind_type_introspection_items(&mut items, schema, params, resolver);
891            ScalarExpr::Or(items)
892        }
893        ScalarExpr::IsNull { mut expr, negated } => {
894            bind_type_introspection_in_place(expr.as_mut(), schema, params, resolver);
895            ScalarExpr::IsNull { expr, negated }
896        }
897        ScalarExpr::Between {
898            mut expr,
899            mut low,
900            mut high,
901        } => {
902            bind_type_introspection_in_place(expr.as_mut(), schema, params, resolver);
903            bind_type_introspection_in_place(low.as_mut(), schema, params, resolver);
904            bind_type_introspection_in_place(high.as_mut(), schema, params, resolver);
905            ScalarExpr::Between { expr, low, high }
906        }
907        ScalarExpr::InList {
908            mut expr,
909            mut list,
910            negated,
911        } => {
912            bind_type_introspection_in_place(expr.as_mut(), schema, params, resolver);
913            bind_type_introspection_items(&mut list, schema, params, resolver);
914            ScalarExpr::InList {
915                expr,
916                list,
917                negated,
918            }
919        }
920        ScalarExpr::WindowCall {
921            name,
922            mut args,
923            mut spec,
924        } => {
925            bind_type_introspection_items(&mut args, schema, params, resolver);
926            bind_type_introspection_items(&mut spec.partition_by, schema, params, resolver);
927            for order in &mut spec.order_by {
928                bind_type_introspection_in_place(&mut order.expr, schema, params, resolver);
929            }
930            if let Some(frame) = spec.frame.as_mut() {
931                bind_frame_bound(&mut frame.start, schema, params, resolver);
932                bind_frame_bound(&mut frame.end, schema, params, resolver);
933            }
934            ScalarExpr::WindowCall { name, args, spec }
935        }
936        ScalarExpr::Case {
937            mut base,
938            mut when,
939            mut else_branch,
940        } => {
941            if let Some(base) = base.as_deref_mut() {
942                bind_type_introspection_in_place(base, schema, params, resolver);
943            }
944            for (condition, result) in &mut when {
945                bind_type_introspection_in_place(condition, schema, params, resolver);
946                bind_type_introspection_in_place(result, schema, params, resolver);
947            }
948            if let Some(else_branch) = else_branch.as_deref_mut() {
949                bind_type_introspection_in_place(else_branch, schema, params, resolver);
950            }
951            if base.is_some() {
952                let comparison_type = common_expression_type(
953                    base.iter()
954                        .map(Box::as_ref)
955                        .chain(when.iter().map(|(condition, _)| condition)),
956                    schema,
957                    params,
958                    resolver,
959                );
960                if let Some(comparison_type) = comparison_type {
961                    if let Some(base) = base.as_deref_mut() {
962                        bind_common_type_cast(base, &comparison_type, schema, params, resolver);
963                    }
964                    for (condition, _) in &mut when {
965                        bind_common_type_cast(
966                            condition,
967                            &comparison_type,
968                            schema,
969                            params,
970                            resolver,
971                        );
972                    }
973                }
974            }
975            let result_type = common_expression_type(
976                when.iter()
977                    .map(|(_, result)| result)
978                    .chain(else_branch.iter().map(Box::as_ref)),
979                schema,
980                params,
981                resolver,
982            );
983            if let Some(result_type) = result_type {
984                for (_, result) in &mut when {
985                    bind_common_type_cast(result, &result_type, schema, params, resolver);
986                }
987                if let Some(else_branch) = else_branch.as_deref_mut() {
988                    bind_common_type_cast(else_branch, &result_type, schema, params, resolver);
989                }
990            }
991            ScalarExpr::Case {
992                base,
993                when,
994                else_branch,
995            }
996        }
997        ScalarExpr::Cast { mut expr, ty } => {
998            let source_type = cast_requires_declared_source(&ty)
999                .then(|| {
1000                    scalar_type_inner(&expr, schema, params, resolver)
1001                        .ok()
1002                        .flatten()
1003                })
1004                .flatten();
1005            bind_type_introspection_in_place(expr.as_mut(), schema, params, resolver);
1006            if let Some(source_type) = source_type {
1007                let source_name = source_type.sql_name();
1008                if !matches!(expr.as_ref(), ScalarExpr::Cast { ty, .. } if ty.eq_ignore_ascii_case(&source_name))
1009                {
1010                    let inner = std::mem::replace(expr.as_mut(), ScalarExpr::Literal(Value::Null));
1011                    *expr = ScalarExpr::Cast {
1012                        expr: Box::new(inner),
1013                        ty: source_name,
1014                    };
1015                }
1016            }
1017            ScalarExpr::Cast { expr, ty }
1018        }
1019        ScalarExpr::InSubquery {
1020            mut expr,
1021            subquery,
1022            negated,
1023        } => {
1024            bind_type_introspection_in_place(expr.as_mut(), schema, params, resolver);
1025            ScalarExpr::InSubquery {
1026                expr,
1027                subquery,
1028                negated,
1029            }
1030        }
1031        other => other,
1032    }
1033}
1034
1035fn requires_type_introspection_binding(expression: &ScalarExpr) -> bool {
1036    match expression {
1037        ScalarExpr::Func {
1038            name,
1039            args,
1040            order_by,
1041            filter,
1042            ..
1043        } => {
1044            is_pg_typeof(name)
1045                || is_common_type_function(name)
1046                || to_hex::is_function(name)
1047                || containment::is_operator(name)
1048                || args.iter().any(requires_type_introspection_binding)
1049                || order_by
1050                    .iter()
1051                    .any(|order| requires_type_introspection_binding(&order.expr))
1052                || filter
1053                    .as_deref()
1054                    .is_some_and(requires_type_introspection_binding)
1055        }
1056        ScalarExpr::Array(_) | ScalarExpr::Case { .. } | ScalarExpr::UnaryMinus(_) => true,
1057        ScalarExpr::Row(items) | ScalarExpr::And(items) | ScalarExpr::Or(items) => {
1058            items.iter().any(requires_type_introspection_binding)
1059        }
1060        ScalarExpr::Binary { lhs, rhs, .. } => {
1061            requires_type_introspection_binding(lhs) || requires_type_introspection_binding(rhs)
1062        }
1063        ScalarExpr::Not(expression)
1064        | ScalarExpr::IsNull {
1065            expr: expression, ..
1066        } => requires_type_introspection_binding(expression),
1067        ScalarExpr::Between { expr, low, high } => {
1068            requires_type_introspection_binding(expr)
1069                || requires_type_introspection_binding(low)
1070                || requires_type_introspection_binding(high)
1071        }
1072        ScalarExpr::InList { expr, list, .. } => {
1073            requires_type_introspection_binding(expr)
1074                || list.iter().any(requires_type_introspection_binding)
1075        }
1076        ScalarExpr::WindowCall { args, spec, .. } => {
1077            args.iter().any(requires_type_introspection_binding)
1078                || spec
1079                    .partition_by
1080                    .iter()
1081                    .any(requires_type_introspection_binding)
1082                || spec
1083                    .order_by
1084                    .iter()
1085                    .any(|order| requires_type_introspection_binding(&order.expr))
1086                || spec.frame.as_ref().is_some_and(|frame| {
1087                    frame_bound_requires_type_introspection_binding(&frame.start)
1088                        || frame_bound_requires_type_introspection_binding(&frame.end)
1089                })
1090        }
1091        ScalarExpr::Cast { expr, ty } => {
1092            cast_requires_declared_source(ty) || requires_type_introspection_binding(expr)
1093        }
1094        ScalarExpr::InSubquery { expr, .. } => requires_type_introspection_binding(expr),
1095        ScalarExpr::Star
1096        | ScalarExpr::QualifiedStar(_)
1097        | ScalarExpr::Default
1098        | ScalarExpr::Column(_)
1099        | ScalarExpr::Position(_)
1100        | ScalarExpr::QualifiedColumn { .. }
1101        | ScalarExpr::Literal(_)
1102        | ScalarExpr::Param(_)
1103        | ScalarExpr::ScalarSubquery(_)
1104        | ScalarExpr::Exists { .. } => false,
1105    }
1106}
1107
1108fn frame_bound_requires_type_introspection_binding(bound: &crate::ScalarFrameBound) -> bool {
1109    match bound {
1110        crate::ScalarFrameBound::Preceding(expression)
1111        | crate::ScalarFrameBound::Following(expression) => {
1112            requires_type_introspection_binding(expression)
1113        }
1114        crate::ScalarFrameBound::UnboundedPreceding
1115        | crate::ScalarFrameBound::UnboundedFollowing
1116        | crate::ScalarFrameBound::CurrentRow => false,
1117    }
1118}
1119
1120fn is_pg_typeof(name: &str) -> bool {
1121    name.eq_ignore_ascii_case("pg_typeof") || name.eq_ignore_ascii_case("pg_catalog.pg_typeof")
1122}
1123
1124fn is_common_type_function(name: &str) -> bool {
1125    matches!(
1126        name.to_ascii_lowercase().as_str(),
1127        "coalesce" | "greatest" | "least"
1128    )
1129}
1130
1131fn bind_common_type_expressions(
1132    expressions: &mut [ScalarExpr],
1133    schema: &RowSchema,
1134    params: &[SQLParam],
1135    resolver: Option<&dyn FunctionTypeResolver>,
1136) {
1137    let Some(target) = common_expression_type(expressions.iter(), schema, params, resolver) else {
1138        return;
1139    };
1140    for expression in expressions {
1141        bind_common_type_cast(expression, &target, schema, params, resolver);
1142    }
1143}
1144
1145fn common_expression_type<'a>(
1146    expressions: impl IntoIterator<Item = &'a ScalarExpr>,
1147    schema: &RowSchema,
1148    params: &[SQLParam],
1149    resolver: Option<&dyn FunctionTypeResolver>,
1150) -> Option<ColumnType> {
1151    let mut common = None;
1152    let mut saw_expression = false;
1153    for expression in expressions {
1154        saw_expression = true;
1155        let expression_type =
1156            common_context_expression_type(expression, schema, params, resolver).ok()?;
1157        common = merge_optional_types(common, expression_type).ok()?;
1158    }
1159    saw_expression.then(|| common.unwrap_or(ColumnType::Text))
1160}
1161
1162fn bind_common_type_cast(
1163    expression: &mut ScalarExpr,
1164    target: &ColumnType,
1165    schema: &RowSchema,
1166    params: &[SQLParam],
1167    resolver: Option<&dyn FunctionTypeResolver>,
1168) {
1169    let target = base_type(target);
1170    let source = common_context_expression_type(expression, schema, params, resolver)
1171        .ok()
1172        .flatten();
1173    if source
1174        .as_ref()
1175        .is_some_and(|source| base_type(source) == target)
1176    {
1177        return;
1178    }
1179    let inner = std::mem::replace(expression, ScalarExpr::Literal(Value::Null));
1180    *expression = ScalarExpr::Cast {
1181        expr: Box::new(inner),
1182        ty: target.sql_name(),
1183    };
1184}
1185
1186fn bind_type_introspection_items(
1187    expressions: &mut [ScalarExpr],
1188    schema: &RowSchema,
1189    params: &[SQLParam],
1190    resolver: Option<&dyn FunctionTypeResolver>,
1191) {
1192    for expression in expressions {
1193        bind_type_introspection_in_place(expression, schema, params, resolver);
1194    }
1195}
1196
1197fn bind_type_introspection_in_place(
1198    expression: &mut ScalarExpr,
1199    schema: &RowSchema,
1200    params: &[SQLParam],
1201    resolver: Option<&dyn FunctionTypeResolver>,
1202) {
1203    let owned = std::mem::replace(expression, ScalarExpr::Literal(Value::Null));
1204    *expression = bind_type_introspection_inner(owned, schema, params, resolver);
1205}
1206
1207fn cast_requires_declared_source(target: &str) -> bool {
1208    let mut target = target.trim().to_ascii_lowercase();
1209    while let Some(element) = target.strip_suffix("[]") {
1210        target = element.trim_end().to_string();
1211    }
1212    matches!(
1213        target.as_str(),
1214        "bytea" | "pg_catalog.bytea" | "oid" | "pg_catalog.oid" | "xid" | "pg_catalog.xid"
1215    )
1216}
1217
1218fn bind_frame_bound(
1219    bound: &mut crate::ScalarFrameBound,
1220    schema: &RowSchema,
1221    params: &[SQLParam],
1222    resolver: Option<&dyn FunctionTypeResolver>,
1223) {
1224    match bound {
1225        crate::ScalarFrameBound::Preceding(expression)
1226        | crate::ScalarFrameBound::Following(expression) => {
1227            bind_type_introspection_in_place(expression.as_mut(), schema, params, resolver);
1228        }
1229        crate::ScalarFrameBound::UnboundedPreceding
1230        | crate::ScalarFrameBound::UnboundedFollowing
1231        | crate::ScalarFrameBound::CurrentRow => {}
1232    }
1233}
1234
1235pub fn values_column_types(
1236    rows: &[Vec<ScalarExpr>],
1237    params: &[SQLParam],
1238) -> Result<Vec<Option<ColumnType>>, SQLError> {
1239    let width = rows.first().map_or(0, Vec::len);
1240    let empty = RowSchema::default();
1241    let mut types = vec![None; width];
1242    for row in rows {
1243        if row.len() != width {
1244            return Err(SQLError::TypeMismatch(
1245                "VALUES lists must all be the same length".into(),
1246            ));
1247        }
1248        for (position, expression) in row.iter().enumerate() {
1249            types[position] = merge_optional_types(
1250                types[position].take(),
1251                common_context_expression_type(expression, &empty, params, None)?,
1252            )?;
1253        }
1254    }
1255    Ok(types
1256        .into_iter()
1257        .map(|ty| ty.or(Some(ColumnType::Text)))
1258        .collect())
1259}
1260
1261/// Resolve an expression participating in `PostgreSQL`'s common-type selection. Bare string and NULL literals retain the parser's `unknown` type until the surrounding VALUES, set operation, CASE, or array context selects a concrete type.
1262pub fn common_context_expression_type(
1263    expression: &ScalarExpr,
1264    schema: &RowSchema,
1265    params: &[SQLParam],
1266    resolver: Option<&dyn FunctionTypeResolver>,
1267) -> Result<Option<ColumnType>, SQLError> {
1268    if matches!(expression, ScalarExpr::Literal(Value::Str(_) | Value::Null)) {
1269        return Ok(None);
1270    }
1271    scalar_type_inner(expression, schema, params, resolver)
1272}
1273
1274fn parameter_type(parameter: &SQLParam) -> Option<ColumnType> {
1275    match parameter {
1276        SQLParam::Scalar(value) => value_type(value),
1277        SQLParam::Vector(values) => u32::try_from(values.len()).ok().map(ColumnType::Vector),
1278        SQLParam::Tensor(values) => values
1279            .first()
1280            .and_then(|values| u32::try_from(values.len()).ok())
1281            .map(ColumnType::Tensor),
1282    }
1283}
1284
1285fn value_type(value: &Value) -> Option<ColumnType> {
1286    match value {
1287        Value::Null | Value::Map(_) => None,
1288        Value::Row(_) | Value::Record(_) => Some(ColumnType::Record),
1289        Value::Bool(_) => Some(ColumnType::Boolean),
1290        Value::Int(value) if i32::try_from(*value).is_ok() => Some(ColumnType::Integer),
1291        Value::Int(_) => Some(ColumnType::BigInteger),
1292        Value::Float(_) => Some(ColumnType::DoublePrecision),
1293        Value::Decimal(_) => Some(ColumnType::Numeric {
1294            precision: None,
1295            scale: None,
1296        }),
1297        Value::Str(_) => Some(ColumnType::Text),
1298        Value::FixedChar(value) => u32::try_from(value.chars().count())
1299            .ok()
1300            .map(ColumnType::Character),
1301        Value::Bytes(_) => Some(ColumnType::Bytea),
1302        Value::Temporal(value) => Some(match value {
1303            uqa_core::TemporalValue::Date { .. } => ColumnType::Date,
1304            uqa_core::TemporalValue::Time { .. } => ColumnType::Time,
1305            uqa_core::TemporalValue::TimeTz { .. } => ColumnType::TimeTz,
1306            uqa_core::TemporalValue::Timestamp { .. } => ColumnType::Timestamp,
1307            uqa_core::TemporalValue::TimestampTz { .. } => ColumnType::TimestampTz,
1308            uqa_core::TemporalValue::Interval { .. } => ColumnType::Interval,
1309        }),
1310        Value::Json(_) => Some(ColumnType::Json),
1311        Value::JsonB(_) => Some(ColumnType::JsonB),
1312        Value::Array(array) => {
1313            let mut element = None;
1314            merge_array_element_types(array.elements(), &mut element)?;
1315            element.map(|element| ColumnType::Array(Box::new(element)))
1316        }
1317        Value::List(values) => {
1318            let mut element = None;
1319            for value in values {
1320                element = merge_optional_types(element, value_type(value)).ok()?;
1321            }
1322            element.map(|element| ColumnType::Array(Box::new(element)))
1323        }
1324    }
1325}
1326
1327fn merge_array_element_types(values: &[Value], element: &mut Option<ColumnType>) -> Option<()> {
1328    for value in values {
1329        if let Value::List(nested) = value {
1330            merge_array_element_types(nested, element)?;
1331        } else {
1332            *element = merge_optional_types(element.take(), value_type(value)).ok()?;
1333        }
1334    }
1335    Some(())
1336}
1337
1338fn merge_optional_types(
1339    left: Option<ColumnType>,
1340    right: Option<ColumnType>,
1341) -> Result<Option<ColumnType>, SQLError> {
1342    match (left, right) {
1343        (None, other) | (other, None) => Ok(other),
1344        (Some(left), Some(right)) => common_type(&left, &right).map(Some),
1345    }
1346}
1347
1348pub fn common_type(left: &ColumnType, right: &ColumnType) -> Result<ColumnType, SQLError> {
1349    if left == right {
1350        return Ok(left.clone());
1351    }
1352    if matches!(left, ColumnType::Domain { .. }) || matches!(right, ColumnType::Domain { .. }) {
1353        return common_type(base_type(left), base_type(right));
1354    }
1355    if let Some(numeric) = common_numeric_type(left, right) {
1356        return Ok(numeric);
1357    }
1358    if matches!(left, ColumnType::Oid) && is_integral_type(right)
1359        || matches!(right, ColumnType::Oid) && is_integral_type(left)
1360    {
1361        return Ok(ColumnType::Oid);
1362    }
1363    if left.is_character_string() && right.is_character_string() {
1364        return Ok(match left {
1365            ColumnType::Bpchar | ColumnType::Character(_) => ColumnType::Bpchar,
1366            ColumnType::Varchar(_) => ColumnType::Varchar(None),
1367            ColumnType::Name => ColumnType::Name,
1368            _ => ColumnType::Text,
1369        });
1370    }
1371    match (left, right) {
1372        (ColumnType::Date, ColumnType::Timestamp) | (ColumnType::Timestamp, ColumnType::Date) => {
1373            Ok(ColumnType::Timestamp)
1374        }
1375        (ColumnType::Date | ColumnType::Timestamp, ColumnType::TimestampTz)
1376        | (ColumnType::TimestampTz, ColumnType::Date | ColumnType::Timestamp) => {
1377            Ok(ColumnType::TimestampTz)
1378        }
1379        (ColumnType::Array(left), ColumnType::Array(right)) => {
1380            common_type(left, right).map(|element| ColumnType::Array(Box::new(element)))
1381        }
1382        _ => Err(SQLError::TypeMismatch(format!(
1383            "types {} and {} cannot be matched",
1384            left.sql_name(),
1385            right.sql_name()
1386        ))),
1387    }
1388}
1389
1390fn is_integral_type(ty: &ColumnType) -> bool {
1391    matches!(
1392        base_type(ty),
1393        ColumnType::SmallInteger | ColumnType::Integer | ColumnType::BigInteger
1394    )
1395}
1396
1397fn common_numeric_type(left: &ColumnType, right: &ColumnType) -> Option<ColumnType> {
1398    let rank = numeric_rank(left)?.max(numeric_rank(right)?);
1399    Some(match rank {
1400        0 => ColumnType::SmallInteger,
1401        1 => ColumnType::Integer,
1402        2 => ColumnType::BigInteger,
1403        3 => ColumnType::Numeric {
1404            precision: None,
1405            scale: None,
1406        },
1407        4 => ColumnType::Real,
1408        _ => ColumnType::DoublePrecision,
1409    })
1410}
1411
1412fn numeric_rank(ty: &ColumnType) -> Option<u8> {
1413    match ty {
1414        ColumnType::SmallInteger => Some(0),
1415        ColumnType::Integer => Some(1),
1416        ColumnType::BigInteger => Some(2),
1417        ColumnType::Numeric { .. } => Some(3),
1418        ColumnType::Real => Some(4),
1419        ColumnType::DoublePrecision => Some(5),
1420        _ => None,
1421    }
1422}
1423
1424pub fn equality_operand_type(
1425    left: &ColumnType,
1426    right: &ColumnType,
1427) -> Result<ColumnType, SQLError> {
1428    if matches!(left, ColumnType::Json) || matches!(right, ColumnType::Json) {
1429        return Err(undefined_equality_operator(left, right));
1430    }
1431    if matches!(left, ColumnType::Array(_)) || matches!(right, ColumnType::Array(_)) {
1432        if left == right {
1433            return Ok(left.clone());
1434        }
1435        return Err(undefined_equality_operator(left, right));
1436    }
1437    if left.is_character_string() && right.is_character_string() {
1438        if matches!(left, ColumnType::Bpchar | ColumnType::Character(_))
1439            || matches!(right, ColumnType::Bpchar | ColumnType::Character(_))
1440        {
1441            return Ok(ColumnType::Bpchar);
1442        }
1443        return Ok(ColumnType::Text);
1444    }
1445    common_type(left, right).map_err(|_| undefined_equality_operator(left, right))
1446}
1447
1448fn undefined_equality_operator(left: &ColumnType, right: &ColumnType) -> SQLError {
1449    SQLError::Routine {
1450        sqlstate: "42883".into(),
1451        message: format!(
1452            "operator does not exist: {} = {}",
1453            left.sql_name(),
1454            right.sql_name()
1455        ),
1456    }
1457}
1458
1459#[cfg(test)]
1460mod tests;