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
19/// Decoded function-call argument names, effective overload types, and whether the call used explicit `VARIADIC` syntax.
20#[doc(hidden)]
21pub type FunctionCallArgumentSignature = (Vec<Option<String>>, Vec<Option<ColumnType>>, bool);
22
23/// Build the PostgreSQL-compatible overload signature for one physical function call using the shared common-context typing rule.
24#[doc(hidden)]
25pub fn function_call_argument_signature(
26    arguments: &[ScalarExpr],
27    schema: &dyn ScalarTypeSchema,
28    params: &[SQLParam],
29    resolver: Option<&dyn FunctionTypeResolver>,
30) -> Result<FunctionCallArgumentSignature, SQLError> {
31    let call_arguments = scalar_call_arguments(arguments)?;
32    let explicit_variadic = call_arguments
33        .iter()
34        .any(|argument| argument.explicit_variadic);
35    let mut argument_names = Vec::with_capacity(call_arguments.len());
36    let mut argument_types = Vec::with_capacity(call_arguments.len());
37    for argument in call_arguments {
38        argument_names.push(argument.name.map(str::to_string));
39        let argument_type =
40            common_context_expression_type(argument.value, schema, params, resolver)?;
41        argument_types.push(effective_overload_argument_type_with_params(
42            argument.value,
43            argument_type,
44            params,
45        ));
46    }
47    Ok((argument_names, argument_types, explicit_variadic))
48}
49
50pub(super) fn local_routine_name(name: &str) -> String {
51    let lower = name.to_ascii_lowercase();
52    lower
53        .strip_prefix("pg_catalog.")
54        .unwrap_or(&lower)
55        .to_string()
56}
57
58pub(super) fn numeric_type() -> ColumnType {
59    ColumnType::Numeric {
60        precision: None,
61        scale: None,
62    }
63}
64
65pub(super) fn base_type(mut ty: &ColumnType) -> &ColumnType {
66    while let ColumnType::Domain { base, .. } = ty {
67        ty = base;
68    }
69    ty.without_temporal_modifiers()
70}
71
72pub(crate) fn array_element_type(ty: &ColumnType) -> Option<&ColumnType> {
73    match base_type(ty) {
74        ColumnType::Array(element) => Some(element),
75        ColumnType::Int2Vector => Some(&ColumnType::SmallInteger),
76        ColumnType::OidVector => Some(&ColumnType::Oid),
77        _ => None,
78    }
79}
80
81pub fn values_column_types(
82    rows: &[Vec<ScalarExpr>],
83    params: &[SQLParam],
84) -> Result<Vec<Option<ColumnType>>, SQLError> {
85    let width = rows.first().map_or(0, Vec::len);
86    let empty = RowSchema::default();
87    let mut types = vec![None; width];
88    for row in rows {
89        if row.len() != width {
90            return Err(SQLError::TypeMismatch(
91                "VALUES lists must all be the same length".into(),
92            ));
93        }
94        for (position, expression) in row.iter().enumerate() {
95            types[position] = merge_optional_types(
96                types[position].take(),
97                common_context_expression_type(expression, &empty, params, None)?,
98            )?;
99        }
100    }
101    Ok(types
102        .into_iter()
103        .map(|ty| ty.or(Some(ColumnType::Text)))
104        .collect())
105}
106
107/// 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.
108pub fn common_context_expression_type(
109    expression: &ScalarExpr,
110    schema: &dyn ScalarTypeSchema,
111    params: &[SQLParam],
112    resolver: Option<&dyn FunctionTypeResolver>,
113) -> Result<Option<ColumnType>, SQLError> {
114    common_context_expression_type_with_control(
115        expression,
116        schema,
117        params,
118        resolver,
119        &ProductionControl::uncontrolled(),
120    )
121    .map(|ty| {
122        ty.map(|ty| {
123            ty.into_uncontrolled()
124                .expect("ordinary common-context inference has no reservation")
125        })
126    })
127}
128
129pub(super) fn common_context_expression_type_with_control(
130    expression: &ScalarExpr,
131    schema: &dyn ScalarTypeSchema,
132    params: &[SQLParam],
133    resolver: Option<&dyn FunctionTypeResolver>,
134    control: &ProductionControl<'_>,
135) -> Result<Option<Produced<ColumnType>>, SQLError> {
136    control.check()?;
137    if matches!(expression, ScalarExpr::Literal(Value::Str(_) | Value::Null)) {
138        return Ok(None);
139    }
140    super::scalar_type_inner_with_control(expression, schema, params, resolver, control)
141}
142
143/// Preserve parser-level `unknown` identity for fixed built-in overload selection.
144#[doc(hidden)]
145pub fn effective_overload_argument_type(
146    expression: &ScalarExpr,
147    resolved: Option<ColumnType>,
148) -> Option<ColumnType> {
149    if effective_overload_argument_type_ref_with_params(expression, resolved.as_ref(), &[])
150        .is_some()
151    {
152        resolved
153    } else {
154        None
155    }
156}
157
158/// Preserve an explicitly typed scalar parameter while retaining the legacy `unknown` treatment of untyped text-valued [`SQLParam::Scalar`] parameters.
159#[doc(hidden)]
160pub fn effective_overload_argument_type_with_params(
161    expression: &ScalarExpr,
162    resolved: Option<ColumnType>,
163    params: &[SQLParam],
164) -> Option<ColumnType> {
165    if effective_overload_argument_type_ref_with_params(expression, resolved.as_ref(), params)
166        .is_some()
167    {
168        resolved
169    } else {
170        None
171    }
172}
173
174/// Borrow the same effective argument type before a controlled caller decides whether a payload copy is needed.
175pub(super) fn effective_overload_argument_type_ref_with_params<'a>(
176    expression: &ScalarExpr,
177    resolved: Option<&'a ColumnType>,
178    params: &[SQLParam],
179) -> Option<&'a ColumnType> {
180    if let ScalarExpr::Param(index) = expression {
181        if index
182            .checked_sub(1)
183            .and_then(|index| params.get(index))
184            .is_some_and(|parameter| parameter.declared_scalar_type().is_some())
185        {
186            return resolved;
187        }
188    }
189    if matches!(expression, ScalarExpr::Literal(Value::Str(_) | Value::Null))
190        || matches!(expression, ScalarExpr::Param(_)) && matches!(resolved, Some(ColumnType::Text))
191    {
192        None
193    } else {
194        resolved
195    }
196}
197
198pub(super) fn parameter_type_with_control(
199    parameter: &SQLParam,
200    control: &ProductionControl<'_>,
201) -> Result<Option<Produced<ColumnType>>, SQLError> {
202    control.check()?;
203    let scalar = match parameter {
204        SQLParam::Scalar(value) => return value_type_with_control(value, control),
205        SQLParam::TypedScalar { ty, .. } => return Ok(Some(ty.clone_with_control(control)?)),
206        SQLParam::Vector(values) => u32::try_from(values.len()).ok().map(ColumnType::Vector),
207        SQLParam::Tensor(values) => values
208            .first()
209            .and_then(|values| u32::try_from(values.len()).ok())
210            .map(ColumnType::Tensor),
211    };
212    scalar
213        .map(|ty| {
214            control
215                .finish(ty, control.empty_reservation())
216                .map_err(Into::into)
217        })
218        .transpose()
219}
220
221pub(crate) fn value_type(value: &Value) -> Option<ColumnType> {
222    value_type_with_control(value, &ProductionControl::uncontrolled())
223        .expect("ordinary value type inference cannot be cancelled or limited")
224        .map(|value| {
225            value
226                .into_uncontrolled()
227                .expect("ordinary value type has no reservation")
228        })
229}
230
231pub(crate) fn value_type_with_control(
232    value: &Value,
233    control: &ProductionControl<'_>,
234) -> Result<Option<Produced<ColumnType>>, SQLError> {
235    control.check()?;
236    let scalar = match value {
237        Value::Null | Value::Map(_) => None,
238        Value::Void => Some(ColumnType::Void),
239        Value::Row(_) | Value::Record(_) => Some(ColumnType::Record),
240        Value::Bool(_) => Some(ColumnType::Boolean),
241        Value::Int(value) if i32::try_from(*value).is_ok() => Some(ColumnType::Integer),
242        Value::Int(_) => Some(ColumnType::BigInteger),
243        Value::Float(_) => Some(ColumnType::DoublePrecision),
244        Value::Decimal(_) => Some(numeric_type()),
245        Value::Str(_) => Some(ColumnType::Text),
246        Value::FixedChar(value) => {
247            let mut count = 0_usize;
248            for _ in value.chars() {
249                control.check()?;
250                count += 1;
251            }
252            u32::try_from(count).ok().map(ColumnType::Character)
253        }
254        Value::Bytes(_) => Some(ColumnType::Bytea),
255        Value::Temporal(value) => Some(match value {
256            uqa_core::TemporalValue::Date { .. } => ColumnType::Date,
257            uqa_core::TemporalValue::Time { .. } => ColumnType::Time,
258            uqa_core::TemporalValue::TimeTz { .. } => ColumnType::TimeTz,
259            uqa_core::TemporalValue::Timestamp { .. } => ColumnType::Timestamp,
260            uqa_core::TemporalValue::TimestampTz { .. } => ColumnType::TimestampTz,
261            uqa_core::TemporalValue::Interval { .. } => ColumnType::Interval,
262        }),
263        Value::Json(_) => Some(ColumnType::Json),
264        Value::JsonB(_) => Some(ColumnType::JsonB),
265        Value::LegacyVector(vector) => Some(match vector.kind() {
266            uqa_core::LegacyVectorKind::SmallInteger => ColumnType::Int2Vector,
267            uqa_core::LegacyVectorKind::Oid => ColumnType::OidVector,
268        }),
269        Value::Array(array) => {
270            let mut element = None;
271            if !merge_array_element_types(array.elements(), &mut element, control)? {
272                return Ok(None);
273            }
274            return element
275                .map(|element| ColumnType::array_with_control(element, control).map_err(Into::into))
276                .transpose();
277        }
278        Value::List(values) => {
279            let mut element = None;
280            for value in values {
281                let next = value_type_with_control(value, control)?;
282                match merge_value_types(element, next, control) {
283                    Ok(merged) => element = merged,
284                    Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => {
285                        return Err(error)
286                    }
287                    Err(_) => return Ok(None),
288                }
289            }
290            return element
291                .map(|element| ColumnType::array_with_control(element, control).map_err(Into::into))
292                .transpose();
293        }
294    };
295    scalar
296        .map(|ty| {
297            control
298                .finish(ty, control.empty_reservation())
299                .map_err(Into::into)
300        })
301        .transpose()
302}
303
304fn merge_array_element_types(
305    values: &[Value],
306    element: &mut Option<Produced<ColumnType>>,
307    control: &ProductionControl<'_>,
308) -> Result<bool, SQLError> {
309    for value in values {
310        control.check()?;
311        if let Value::List(nested) = value {
312            if !merge_array_element_types(nested, element, control)? {
313                return Ok(false);
314            }
315        } else {
316            match merge_value_types(
317                element.take(),
318                value_type_with_control(value, control)?,
319                control,
320            ) {
321                Ok(merged) => *element = merged,
322                Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => {
323                    return Err(error)
324                }
325                Err(_) => return Ok(false),
326            }
327        }
328    }
329    Ok(true)
330}
331
332pub(super) fn merge_value_types(
333    left: Option<Produced<ColumnType>>,
334    right: Option<Produced<ColumnType>>,
335    control: &ProductionControl<'_>,
336) -> Result<Option<Produced<ColumnType>>, SQLError> {
337    control.check()?;
338    match (left, right) {
339        (None, other) | (other, None) => Ok(other),
340        (Some(left), Some(right)) if *left == *right => Ok(Some(left)),
341        (Some(left), Some(right)) => common_type_with_control(&left, &right, control).map(Some),
342    }
343}
344
345pub(super) fn merge_optional_types(
346    left: Option<ColumnType>,
347    right: Option<ColumnType>,
348) -> Result<Option<ColumnType>, SQLError> {
349    match (left, right) {
350        (None, other) | (other, None) => Ok(other),
351        (Some(left), Some(right)) => common_type(&left, &right).map(Some),
352    }
353}
354
355pub fn common_type(left: &ColumnType, right: &ColumnType) -> Result<ColumnType, SQLError> {
356    common_type_with_control(left, right, &ProductionControl::uncontrolled()).map(|value| {
357        value
358            .into_uncontrolled()
359            .expect("ordinary common type has no reservation")
360    })
361}
362
363/// Preserve the existing common-type rules while the selected type owns its copied names and array boxes.
364pub(super) fn common_type_with_control(
365    left: &ColumnType,
366    right: &ColumnType,
367    control: &ProductionControl<'_>,
368) -> Result<Produced<ColumnType>, SQLError> {
369    control.check()?;
370    if left == right {
371        return left.clone_with_control(control).map_err(Into::into);
372    }
373    if left != left.without_temporal_modifiers() || right != right.without_temporal_modifiers() {
374        return common_type_with_control(
375            left.without_temporal_modifiers(),
376            right.without_temporal_modifiers(),
377            control,
378        );
379    }
380    if matches!(left, ColumnType::Domain { .. }) || matches!(right, ColumnType::Domain { .. }) {
381        return common_type_with_control(base_type(left), base_type(right), control);
382    }
383    let scalar = if let Some(numeric) = common_numeric_type(left, right) {
384        numeric
385    } else if matches!(left, ColumnType::Oid) && is_integral_type(right)
386        || matches!(right, ColumnType::Oid) && is_integral_type(left)
387    {
388        ColumnType::Oid
389    } else if left.is_character_string() && right.is_character_string() {
390        match left {
391            ColumnType::Bpchar | ColumnType::Character(_) => ColumnType::Bpchar,
392            ColumnType::Varchar(_) => ColumnType::Varchar(None),
393            ColumnType::Name => ColumnType::Name,
394            _ => ColumnType::Text,
395        }
396    } else {
397        match (left, right) {
398            (ColumnType::Date, ColumnType::Timestamp)
399            | (ColumnType::Timestamp, ColumnType::Date) => ColumnType::Timestamp,
400            (ColumnType::Date | ColumnType::Timestamp, ColumnType::TimestampTz)
401            | (ColumnType::TimestampTz, ColumnType::Date | ColumnType::Timestamp) => {
402                ColumnType::TimestampTz
403            }
404            (ColumnType::Array(left), ColumnType::Array(right)) => {
405                return ColumnType::array_with_control(
406                    common_type_with_control(left, right, control)?,
407                    control,
408                )
409                .map_err(Into::into)
410            }
411            _ => {
412                return Err(SQLError::TypeMismatch(format!(
413                    "types {} and {} cannot be matched",
414                    left.sql_name(),
415                    right.sql_name()
416                )))
417            }
418        }
419    };
420    control
421        .finish(scalar, control.empty_reservation())
422        .map_err(Into::into)
423}
424
425pub(super) mod case;
426
427fn is_integral_type(ty: &ColumnType) -> bool {
428    matches!(
429        base_type(ty),
430        ColumnType::SmallInteger | ColumnType::Integer | ColumnType::BigInteger
431    )
432}
433
434pub(super) fn common_numeric_type(left: &ColumnType, right: &ColumnType) -> Option<ColumnType> {
435    let rank = numeric_rank(left)?.max(numeric_rank(right)?);
436    Some(match rank {
437        0 => ColumnType::SmallInteger,
438        1 => ColumnType::Integer,
439        2 => ColumnType::BigInteger,
440        3 => numeric_type(),
441        4 => ColumnType::Real,
442        _ => ColumnType::DoublePrecision,
443    })
444}
445
446pub(super) fn numeric_rank(ty: &ColumnType) -> Option<u8> {
447    match ty {
448        ColumnType::SmallInteger => Some(0),
449        ColumnType::Integer => Some(1),
450        ColumnType::BigInteger => Some(2),
451        ColumnType::Numeric { .. } => Some(3),
452        ColumnType::Real => Some(4),
453        ColumnType::DoublePrecision => Some(5),
454        _ => None,
455    }
456}
457
458/// Array dimensions belong to values; `PostgreSQL` operator signatures identify an array by its scalar element type, including an element domain's identity.
459pub(super) fn same_operator_type_with_control(
460    left: &ColumnType,
461    right: &ColumnType,
462    control: &ProductionControl<'_>,
463) -> Result<bool, uqa_core::ValueRetentionError> {
464    fn element(mut ty: &ColumnType) -> &ColumnType {
465        while let ColumnType::Array(inner) = ty {
466            ty = inner;
467        }
468        ty
469    }
470    let left = base_type(left);
471    let right = base_type(right);
472    let (left, right) = match (left, right) {
473        (ColumnType::Array(left), ColumnType::Array(right)) => (element(left), element(right)),
474        _ => (left, right),
475    };
476    let left = left.without_type_modifiers_with_control(control)?;
477    let right = right.without_type_modifiers_with_control(control)?;
478    Ok(*left == *right)
479}
480
481#[cfg(test)]
482mod production_tests;