Skip to main content

uqa_sql/type_resolution/
common.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7use crate::ast::ColumnType;
8use crate::{SQLError, SQLParam};
9use uqa_core::{
10    memory::{Produced, ProductionControl},
11    Value,
12};
13
14use crate::schema::ScalarTypeSchema;
15use crate::{scalar_call_arguments, RowSchema, ScalarExpr};
16
17use super::FunctionTypeResolver;
18
19mod inputs;
20pub use inputs::select_common_input_type;
21pub(crate) use inputs::select_common_input_type_in_with_control;
22pub(super) use inputs::{select_common_input_type_with_control, CommonInputType};
23
24/// Inputs whose absent type means `PostgreSQL` `unknown`, rather than an unresolved UQA runtime value.
25pub(super) fn unknown_input(mut expression: &ScalarExpr) -> bool {
26    while let Ok(Some(clause)) = crate::catalog::collations::expression::scalar_clause(expression) {
27        expression = clause.argument;
28    }
29    matches!(
30        expression,
31        ScalarExpr::Literal(Value::Null | Value::Str(_)) | ScalarExpr::Param(_)
32    )
33}
34
35/// Decoded function-call argument names, effective overload types, and whether the call used explicit `VARIADIC` syntax.
36#[doc(hidden)]
37pub type FunctionCallArgumentSignature = (Vec<Option<String>>, Vec<Option<ColumnType>>, bool);
38
39/// Build the PostgreSQL-compatible overload signature for one physical function call using the shared common-context typing rule.
40#[doc(hidden)]
41pub fn function_call_argument_signature(
42    arguments: &[ScalarExpr],
43    schema: &dyn ScalarTypeSchema,
44    params: &[SQLParam],
45    resolver: Option<&dyn FunctionTypeResolver>,
46) -> Result<FunctionCallArgumentSignature, SQLError> {
47    let call_arguments = scalar_call_arguments(arguments)?;
48    let explicit_variadic = call_arguments
49        .iter()
50        .any(|argument| argument.explicit_variadic);
51    let mut argument_names = Vec::with_capacity(call_arguments.len());
52    let mut argument_types = Vec::with_capacity(call_arguments.len());
53    for argument in call_arguments {
54        argument_names.push(argument.name.map(str::to_string));
55        let argument_type =
56            common_context_expression_type(argument.value, schema, params, resolver)?;
57        argument_types.push(effective_overload_argument_type_with_params(
58            argument.value,
59            argument_type,
60            params,
61        ));
62    }
63    Ok((argument_names, argument_types, explicit_variadic))
64}
65
66pub(super) fn local_routine_name(name: &str) -> String {
67    let lower = name.to_ascii_lowercase();
68    lower
69        .strip_prefix("pg_catalog.")
70        .unwrap_or(&lower)
71        .to_string()
72}
73
74pub(super) fn numeric_type() -> ColumnType {
75    ColumnType::Numeric {
76        precision: None,
77        scale: None,
78    }
79}
80
81pub(crate) fn base_type(mut ty: &ColumnType) -> &ColumnType {
82    while let ColumnType::Domain { base, .. } = ty {
83        ty = base;
84    }
85    ty.without_temporal_modifiers()
86}
87
88/// `coerce_type` reads a scalar domain through its base input, but array input keeps each element's domain checks. Input functions receive interval modifiers; other modifiers remain on the outer coercion.
89pub(crate) fn literal_input_type_with_control(
90    mut ty: &ColumnType,
91    control: &ProductionControl<'_>,
92) -> Result<Produced<ColumnType>, SQLError> {
93    fn input_type(
94        ty: &ColumnType,
95        control: &ProductionControl<'_>,
96    ) -> Result<Produced<ColumnType>, SQLError> {
97        Ok(match ty {
98            ColumnType::IntervalWithFields { .. } | ColumnType::Domain { .. } => {
99                ty.clone_with_control(control)?
100            }
101            ColumnType::Array(element) => {
102                ColumnType::array_with_control(input_type(element, control)?, control)?
103            }
104            _ => ty.without_type_modifiers_with_control(control)?,
105        })
106    }
107    while let ColumnType::Domain { base, .. } = ty {
108        ty = base;
109    }
110    input_type(ty, control)
111}
112
113pub(crate) fn array_element_type(ty: &ColumnType) -> Option<&ColumnType> {
114    match base_type(ty) {
115        ColumnType::Array(element) => {
116            let mut element = element.as_ref();
117            while let ColumnType::Array(inner) = element {
118                element = inner;
119            }
120            Some(element)
121        }
122        ColumnType::Int2Vector => Some(&ColumnType::SmallInteger),
123        ColumnType::OidVector => Some(&ColumnType::Oid),
124        _ => None,
125    }
126}
127
128pub fn values_column_types(
129    rows: &[Vec<ScalarExpr>],
130    params: &[SQLParam],
131) -> Result<Vec<Option<ColumnType>>, SQLError> {
132    let width = rows.first().map_or(0, Vec::len);
133    let empty = RowSchema::default();
134    let control = ProductionControl::uncontrolled();
135    let mut types = (0..width)
136        .map(|_| CommonInputType::new())
137        .collect::<Vec<_>>();
138    for row in rows {
139        if row.len() != width {
140            return Err(SQLError::TypeMismatch(
141                "VALUES lists must all be the same length".into(),
142            ));
143        }
144        for (position, expression) in row.iter().enumerate() {
145            let ty = common_context_expression_type_with_control(
146                expression, &empty, params, None, &control,
147            )?;
148            if ty.is_some() || unknown_input(expression) {
149                types[position].include_owned(CommonTypeContext::Values, ty, &control)?;
150            }
151        }
152    }
153    types
154        .into_iter()
155        .map(|ty| {
156            ty.finish(&control).map(|ty| {
157                Some(
158                    ty.into_uncontrolled()
159                        .expect("ordinary VALUES type has no reservation"),
160                )
161            })
162        })
163        .collect()
164}
165
166/// 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.
167pub fn common_context_expression_type(
168    expression: &ScalarExpr,
169    schema: &dyn ScalarTypeSchema,
170    params: &[SQLParam],
171    resolver: Option<&dyn FunctionTypeResolver>,
172) -> Result<Option<ColumnType>, SQLError> {
173    common_context_expression_type_with_control(
174        expression,
175        schema,
176        params,
177        resolver,
178        &ProductionControl::uncontrolled(),
179    )
180    .map(|ty| {
181        ty.map(|ty| {
182            ty.into_uncontrolled()
183                .expect("ordinary common-context inference has no reservation")
184        })
185    })
186}
187
188pub(super) fn common_context_expression_type_with_control(
189    expression: &ScalarExpr,
190    schema: &dyn ScalarTypeSchema,
191    params: &[SQLParam],
192    resolver: Option<&dyn FunctionTypeResolver>,
193    control: &ProductionControl<'_>,
194) -> Result<Option<Produced<ColumnType>>, SQLError> {
195    control.check()?;
196    if let Some(ty) = schema.observed_expression_type(expression) {
197        return super::inference::copy_type(ty, control);
198    }
199    if let Some(clause) = crate::catalog::collations::expression::scalar_clause(expression)? {
200        let ty = common_context_expression_type_with_control(
201            clause.argument,
202            schema,
203            params,
204            resolver,
205            control,
206        )?;
207        crate::catalog::collations::expression::select_with_control(
208            clause.name,
209            clause.oid,
210            ty.as_deref(),
211            resolver,
212            control,
213        )?;
214        return Ok(ty);
215    }
216    if matches!(expression, ScalarExpr::Literal(Value::Str(_) | Value::Null)) {
217        return Ok(None);
218    }
219    super::scalar_type_inner_with_control(expression, schema, params, resolver, control)
220}
221
222/// Preserve parser-level `unknown` identity for fixed built-in overload selection.
223#[doc(hidden)]
224pub fn effective_overload_argument_type(
225    expression: &ScalarExpr,
226    resolved: Option<ColumnType>,
227) -> Option<ColumnType> {
228    if effective_overload_argument_type_ref_with_params(expression, resolved.as_ref(), &[])
229        .is_some()
230    {
231        resolved
232    } else {
233        None
234    }
235}
236
237/// Preserve an explicitly typed scalar parameter while retaining the legacy `unknown` treatment of untyped text-valued [`SQLParam::Scalar`] parameters.
238#[doc(hidden)]
239pub fn effective_overload_argument_type_with_params(
240    expression: &ScalarExpr,
241    resolved: Option<ColumnType>,
242    params: &[SQLParam],
243) -> Option<ColumnType> {
244    if effective_overload_argument_type_ref_with_params(expression, resolved.as_ref(), params)
245        .is_some()
246    {
247        resolved
248    } else {
249        None
250    }
251}
252
253/// Borrow the same effective argument type before a controlled caller decides whether a payload copy is needed.
254pub(super) fn effective_overload_argument_type_ref_with_params<'a>(
255    expression: &ScalarExpr,
256    resolved: Option<&'a ColumnType>,
257    params: &[SQLParam],
258) -> Option<&'a ColumnType> {
259    if let ScalarExpr::Param(index) = expression {
260        if index
261            .checked_sub(1)
262            .and_then(|index| params.get(index))
263            .is_some_and(|parameter| parameter.declared_scalar_type().is_some())
264        {
265            return resolved;
266        }
267    }
268    if matches!(expression, ScalarExpr::Literal(Value::Str(_) | Value::Null))
269        || matches!(expression, ScalarExpr::Param(_)) && matches!(resolved, Some(ColumnType::Text))
270    {
271        None
272    } else {
273        resolved
274    }
275}
276
277pub(super) fn parameter_type_with_control(
278    parameter: &SQLParam,
279    control: &ProductionControl<'_>,
280) -> Result<Option<Produced<ColumnType>>, SQLError> {
281    control.check()?;
282    let scalar = match parameter {
283        SQLParam::Scalar(value) => return value_type_with_control(value, control),
284        SQLParam::TypedScalar { ty, .. } | SQLParam::DeferredError { ty, .. } => {
285            return Ok(Some(ty.clone_with_control(control)?))
286        }
287        SQLParam::Vector(values) => u32::try_from(values.len()).ok().map(ColumnType::Vector),
288        SQLParam::Tensor(values) => values
289            .first()
290            .and_then(|values| u32::try_from(values.len()).ok())
291            .map(ColumnType::Tensor),
292    };
293    scalar
294        .map(|ty| {
295            control
296                .finish(ty, control.empty_reservation())
297                .map_err(Into::into)
298        })
299        .transpose()
300}
301
302pub(crate) fn value_type(value: &Value) -> Option<ColumnType> {
303    value_type_with_control(value, &ProductionControl::uncontrolled())
304        .expect("ordinary value type inference cannot be cancelled or limited")
305        .map(|value| {
306            value
307                .into_uncontrolled()
308                .expect("ordinary value type has no reservation")
309        })
310}
311
312pub(crate) fn value_type_with_control(
313    value: &Value,
314    control: &ProductionControl<'_>,
315) -> Result<Option<Produced<ColumnType>>, SQLError> {
316    control.check()?;
317    let scalar = match value {
318        // A runtime enum carrier knows only its type OID; declared expression types supply the enum's identity.
319        Value::Null | Value::Map(_) | Value::Enum(_) | Value::Datum(_) => None,
320        Value::Void => Some(ColumnType::Void),
321        Value::Row(_) | Value::Record(_) => Some(ColumnType::Record),
322        Value::Bool(_) => Some(ColumnType::Boolean),
323        Value::Int(value) if i32::try_from(*value).is_ok() => Some(ColumnType::Integer),
324        Value::Int(_) => Some(ColumnType::BigInteger),
325        Value::Float(_) => Some(ColumnType::DoublePrecision),
326        Value::Decimal(_) => Some(numeric_type()),
327        Value::Str(_) => Some(ColumnType::Text),
328        Value::FixedChar(value) => {
329            let mut count = 0_usize;
330            for _ in value.chars() {
331                control.check()?;
332                count += 1;
333            }
334            u32::try_from(count).ok().map(ColumnType::Character)
335        }
336        Value::Bytes(_) => Some(ColumnType::Bytea),
337        Value::Temporal(value) => Some(match value {
338            uqa_core::TemporalValue::Date { .. } => ColumnType::Date,
339            uqa_core::TemporalValue::Time { .. } => ColumnType::Time,
340            uqa_core::TemporalValue::TimeTz { .. } => ColumnType::TimeTz,
341            uqa_core::TemporalValue::Timestamp { .. } => ColumnType::Timestamp,
342            uqa_core::TemporalValue::TimestampTz { .. } => ColumnType::TimestampTz,
343            uqa_core::TemporalValue::Interval { .. } => ColumnType::Interval,
344        }),
345        Value::Json(_) => Some(ColumnType::Json),
346        Value::JsonB(_) => Some(ColumnType::JsonB),
347        Value::LegacyVector(vector) => Some(match vector.kind() {
348            uqa_core::LegacyVectorKind::SmallInteger => ColumnType::Int2Vector,
349            uqa_core::LegacyVectorKind::Oid => ColumnType::OidVector,
350        }),
351        Value::Array(array) => {
352            let mut element = None;
353            if !merge_array_element_types(array.elements(), &mut element, control)? {
354                return Ok(None);
355            }
356            return element
357                .map(|element| ColumnType::array_with_control(element, control).map_err(Into::into))
358                .transpose();
359        }
360        Value::List(values) => {
361            let mut element = None;
362            for value in values {
363                let next = value_type_with_control(value, control)?;
364                match merge_value_types(element, next, control) {
365                    Ok(merged) => element = merged,
366                    Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => {
367                        return Err(error)
368                    }
369                    Err(_) => return Ok(None),
370                }
371            }
372            return element
373                .map(|element| ColumnType::array_with_control(element, control).map_err(Into::into))
374                .transpose();
375        }
376    };
377    scalar
378        .map(|ty| {
379            control
380                .finish(ty, control.empty_reservation())
381                .map_err(Into::into)
382        })
383        .transpose()
384}
385
386fn merge_array_element_types(
387    values: &[Value],
388    element: &mut Option<Produced<ColumnType>>,
389    control: &ProductionControl<'_>,
390) -> Result<bool, SQLError> {
391    for value in values {
392        control.check()?;
393        if let Value::List(nested) = value {
394            if !merge_array_element_types(nested, element, control)? {
395                return Ok(false);
396            }
397        } else {
398            match merge_value_types(
399                element.take(),
400                value_type_with_control(value, control)?,
401                control,
402            ) {
403                Ok(merged) => *element = merged,
404                Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => {
405                    return Err(error)
406                }
407                Err(_) => return Ok(false),
408            }
409        }
410    }
411    Ok(true)
412}
413
414pub(super) fn merge_value_types(
415    left: Option<Produced<ColumnType>>,
416    right: Option<Produced<ColumnType>>,
417    control: &ProductionControl<'_>,
418) -> Result<Option<Produced<ColumnType>>, SQLError> {
419    control.check()?;
420    match (left, right) {
421        (None, other) | (other, None) => Ok(other),
422        (Some(left), Some(right)) if *left == *right => Ok(Some(left)),
423        (Some(left), Some(right)) => common_type_with_control(&left, &right, control).map(Some),
424    }
425}
426
427/// `coerce_to_common_type` reads an `unknown` string constant with the selected type's input function, which reports what the type rejects, before the statement runs. A type whose input function consults the catalog is read when the expression is bound to it.
428pub(super) fn read_unknown_literals<'a>(
429    expressions: impl IntoIterator<Item = &'a ScalarExpr>,
430    target: &ColumnType,
431) -> Result<(), SQLError> {
432    if super::catalog_input_type(target) {
433        return Ok(());
434    }
435    // `coerce_type` hands the literal to the target type's input function, so the diagnostic is the input function's.
436    let target = base_type(target).without_type_modifiers().catalog_name();
437    for expression in expressions {
438        if let ScalarExpr::Literal(Value::Str(text)) = expression {
439            crate::expr::cast_value_from(&Value::Str(text.clone()), &target, None)?;
440        }
441    }
442    Ok(())
443}
444
445pub fn common_type(left: &ColumnType, right: &ColumnType) -> Result<ColumnType, SQLError> {
446    common_type_with_control(left, right, &ProductionControl::uncontrolled()).map(|value| {
447        value
448            .into_uncontrolled()
449            .expect("ordinary common type has no reservation")
450    })
451}
452
453/// The common type of `left` and `right` for the construct `context`, which reports a conflict as `select_common_type` and `coerce_to_common_type` report it: `42804` when the types are of different categories and `42846` when the other type has no implicit cast to the selected one.
454pub fn common_type_in(
455    context: CommonTypeContext,
456    left: &ColumnType,
457    right: &ColumnType,
458) -> Result<ColumnType, SQLError> {
459    select_pair_with_control(left, right, &ProductionControl::uncontrolled())
460        .map(|value| {
461            value
462                .into_uncontrolled()
463                .expect("ordinary common type has no reservation")
464        })
465        .map_err(|failure| failure.in_context(context))
466}
467
468/// The construct selecting a common type, as `select_common_type` and `coerce_to_common_type` name it in their diagnostics.
469#[derive(Clone, Copy, Debug, PartialEq, Eq)]
470pub enum CommonTypeContext {
471    Union,
472    Intersect,
473    Except,
474    Values,
475    Case,
476    Coalesce,
477    Array,
478    Greatest,
479    Least,
480    In,
481    JoinUsing,
482    Cycle,
483}
484
485impl CommonTypeContext {
486    pub const fn label(self) -> &'static str {
487        match self {
488            Self::Union => "UNION",
489            Self::Intersect => "INTERSECT",
490            Self::Except => "EXCEPT",
491            Self::Values => "VALUES",
492            Self::Case => "CASE",
493            Self::Coalesce => "COALESCE",
494            Self::Array => "ARRAY",
495            Self::Greatest => "GREATEST",
496            Self::Least => "LEAST",
497            Self::In => "IN",
498            Self::JoinUsing => "JOIN/USING",
499            Self::Cycle => "CYCLE",
500        }
501    }
502
503    /// The name `coerce_to_common_type` reports: `transformCaseExpr` coerces the results in a `CASE/WHEN` context.
504    pub const fn coercion_label(self) -> &'static str {
505        match self {
506            Self::Case => "CASE/WHEN",
507            other => other.label(),
508        }
509    }
510
511    pub const fn set_operation(kind: crate::ast::SetOpKind) -> Self {
512        match kind {
513            crate::ast::SetOpKind::Union => Self::Union,
514            crate::ast::SetOpKind::Intersect => Self::Intersect,
515            crate::ast::SetOpKind::Except => Self::Except,
516        }
517    }
518
519    /// The context of a function whose arguments share a common type.
520    pub fn function(name: &str) -> Option<Self> {
521        match local_routine_name(name).as_str() {
522            "coalesce" => Some(Self::Coalesce),
523            "greatest" => Some(Self::Greatest),
524            "least" => Some(Self::Least),
525            _ => None,
526        }
527    }
528}
529
530/// Why two types have no common type: `select_common_type` finds them in different categories, or `coerce_to_common_type` finds no implicit cast from the other type to the selected one. `Failed` carries an error the selection itself met.
531#[derive(Debug)]
532pub(super) enum CommonTypeFailure {
533    /// The two types, in the order they met.
534    Unmatched(Box<(ColumnType, ColumnType)>),
535    /// The type without an implicit cast, and the selected type it would convert to.
536    Inconvertible(Box<(ColumnType, ColumnType)>),
537    Failed(Box<SQLError>),
538}
539
540impl CommonTypeFailure {
541    fn failed(error: impl Into<SQLError>) -> Self {
542        Self::Failed(Box::new(error.into()))
543    }
544
545    /// The diagnostic the construct `context` reports.
546    fn in_context(self, context: CommonTypeContext) -> SQLError {
547        match self {
548            Self::Unmatched(types) => SQLError::Routine {
549                sqlstate: "42804".into(),
550                message: format!(
551                    "{} types {} and {} cannot be matched",
552                    context.label(),
553                    types.0.sql_name(),
554                    types.1.sql_name()
555                ),
556            },
557            Self::Inconvertible(types) => SQLError::Routine {
558                sqlstate: "42846".into(),
559                message: format!(
560                    "{} could not convert type {} to {}",
561                    context.coercion_label(),
562                    types.0.sql_name(),
563                    types.1.sql_name()
564                ),
565            },
566            Self::Failed(error) => *error,
567        }
568    }
569
570    /// The diagnostic of a selection without a construct: a type mismatch naming the two types.
571    fn without_context(self, left: &ColumnType, right: &ColumnType) -> SQLError {
572        match self {
573            Self::Failed(error) => *error,
574            Self::Unmatched(_) | Self::Inconvertible(_) => SQLError::TypeMismatch(format!(
575                "types {} and {} cannot be matched",
576                left.sql_name(),
577                right.sql_name()
578            )),
579        }
580    }
581}
582
583/// Preserve the existing common-type rules while the selected type owns its copied names and array boxes; a conflict is a type mismatch naming the two types.
584pub(super) fn common_type_with_control(
585    left: &ColumnType,
586    right: &ColumnType,
587    control: &ProductionControl<'_>,
588) -> Result<Produced<ColumnType>, SQLError> {
589    select_pair_with_control(left, right, control)
590        .map_err(|failure| failure.without_context(left, right))
591}
592
593/// `select_common_type` over two types: equal types, temporal modifiers and domains reduce first, then the numeric, OID, character, temporal and array rules, and then the category rule.
594fn select_pair_with_control(
595    left: &ColumnType,
596    right: &ColumnType,
597    control: &ProductionControl<'_>,
598) -> Result<Produced<ColumnType>, CommonTypeFailure> {
599    control.check().map_err(CommonTypeFailure::failed)?;
600    if left == right {
601        return left
602            .clone_with_control(control)
603            .map_err(CommonTypeFailure::failed);
604    }
605    if left != left.without_temporal_modifiers() || right != right.without_temporal_modifiers() {
606        return select_pair_with_control(
607            left.without_temporal_modifiers(),
608            right.without_temporal_modifiers(),
609            control,
610        );
611    }
612    if matches!(left, ColumnType::Domain { .. }) || matches!(right, ColumnType::Domain { .. }) {
613        return select_pair_with_control(base_type(left), base_type(right), control);
614    }
615    let scalar = if let Some(numeric) = common_numeric_type(left, right) {
616        numeric
617    } else if matches!(left, ColumnType::Oid) && is_integral_type(right)
618        || matches!(right, ColumnType::Oid) && is_integral_type(left)
619    {
620        ColumnType::Oid
621    } else if left.is_character_string() && right.is_character_string() {
622        match left {
623            ColumnType::Bpchar | ColumnType::Character(_) => ColumnType::Bpchar,
624            ColumnType::Varchar(_) => ColumnType::Varchar(None),
625            ColumnType::Name => ColumnType::Name,
626            _ => ColumnType::Text,
627        }
628    } else {
629        match (left, right) {
630            (ColumnType::Date, ColumnType::Timestamp)
631            | (ColumnType::Timestamp, ColumnType::Date) => ColumnType::Timestamp,
632            (ColumnType::Date | ColumnType::Timestamp, ColumnType::TimestampTz)
633            | (ColumnType::TimestampTz, ColumnType::Date | ColumnType::Timestamp) => {
634                ColumnType::TimestampTz
635            }
636            (ColumnType::Array(_), ColumnType::Array(_)) => {
637                // An array type does not fix its number of dimensions: `integer[][]` is `integer[]`, so arrays meet at their element types, and each value keeps its own dimensions.
638                let element = select_pair_with_control(
639                    innermost_array_element(left),
640                    innermost_array_element(right),
641                    control,
642                )?;
643                return ColumnType::array_with_control(element, control)
644                    .map_err(CommonTypeFailure::failed);
645            }
646            _ => same_category_common_type(left, right)?,
647        }
648    };
649    control
650        .finish(scalar, control.empty_reservation())
651        .map_err(CommonTypeFailure::failed)
652}
653
654/// `select_common_type` for two types of one category that the rules above do not cover: the first type stays unless it is not its category's preferred type and coerces implicitly to the second, which does not coerce back to it. The other type must then coerce implicitly to the selected one, as `coerce_to_common_type` requires.
655fn same_category_common_type(
656    left: &ColumnType,
657    right: &ColumnType,
658) -> Result<ColumnType, CommonTypeFailure> {
659    use super::overload_resolution::{
660        routine_type_accepts_implicit_cast as implicit, routine_type_category,
661        routine_type_is_preferred,
662    };
663    let left_name = super::canonical_column_type_name(left);
664    let right_name = super::canonical_column_type_name(right);
665    if routine_type_category(&left_name) != routine_type_category(&right_name) {
666        return Err(CommonTypeFailure::Unmatched(Box::new((
667            left.clone(),
668            right.clone(),
669        ))));
670    }
671    let switch = !routine_type_is_preferred(&left_name)
672        && implicit(&left_name, &right_name)
673        && !implicit(&right_name, &left_name);
674    let (selected, other, selected_name, other_name) = if switch {
675        (right, left, &right_name, &left_name)
676    } else {
677        (left, right, &left_name, &right_name)
678    };
679    if implicit(other_name, selected_name) {
680        Ok(selected.clone())
681    } else {
682        Err(CommonTypeFailure::Inconvertible(Box::new((
683            other.clone(),
684            selected.clone(),
685        ))))
686    }
687}
688
689pub(super) mod case;
690
691/// The element type below every array level of `ty`.
692fn innermost_array_element(mut ty: &ColumnType) -> &ColumnType {
693    while let ColumnType::Array(element) = ty {
694        ty = element;
695    }
696    ty
697}
698
699fn is_integral_type(ty: &ColumnType) -> bool {
700    matches!(
701        base_type(ty),
702        ColumnType::SmallInteger | ColumnType::Integer | ColumnType::BigInteger
703    )
704}
705
706pub(super) fn common_numeric_type(left: &ColumnType, right: &ColumnType) -> Option<ColumnType> {
707    let rank = numeric_rank(left)?.max(numeric_rank(right)?);
708    Some(match rank {
709        0 => ColumnType::SmallInteger,
710        1 => ColumnType::Integer,
711        2 => ColumnType::BigInteger,
712        3 => numeric_type(),
713        4 => ColumnType::Real,
714        _ => ColumnType::DoublePrecision,
715    })
716}
717
718pub(super) fn numeric_rank(ty: &ColumnType) -> Option<u8> {
719    match ty {
720        ColumnType::SmallInteger => Some(0),
721        ColumnType::Integer => Some(1),
722        ColumnType::BigInteger => Some(2),
723        ColumnType::Numeric { .. } => Some(3),
724        ColumnType::Real => Some(4),
725        ColumnType::DoublePrecision => Some(5),
726        _ => None,
727    }
728}
729
730/// Array dimensions belong to values; `PostgreSQL` operator signatures identify an array by its scalar element type, including an element domain's identity.
731pub(super) fn same_operator_type_with_control(
732    left: &ColumnType,
733    right: &ColumnType,
734    control: &ProductionControl<'_>,
735) -> Result<bool, uqa_core::ValueRetentionError> {
736    fn element(mut ty: &ColumnType) -> &ColumnType {
737        while let ColumnType::Array(inner) = ty {
738            ty = inner;
739        }
740        ty
741    }
742    let left = base_type(left);
743    let right = base_type(right);
744    let (left, right) = match (left, right) {
745        (ColumnType::Array(left), ColumnType::Array(right)) => (element(left), element(right)),
746        _ => (left, right),
747    };
748    let left = left.without_type_modifiers_with_control(control)?;
749    let right = right.without_type_modifiers_with_control(control)?;
750    Ok(*left == *right)
751}
752
753#[cfg(test)]
754mod production_tests;