Skip to main content

uqa_sql/type_resolution/
mod.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 crate::ast::{ColumnType, FunctionBinding};
10use crate::{SQLError, SQLParam};
11
12use crate::{RowSchema, ScalarExpr};
13#[cfg(test)]
14use uqa_core::Value;
15
16mod array_transform;
17mod cast_compatibility;
18mod checksum;
19mod common;
20mod containment;
21mod equality;
22mod fixed_builtin;
23mod functions;
24mod gamma;
25mod introspection;
26mod json_strip;
27mod length;
28mod md5;
29mod operators;
30mod overload_resolution;
31mod qualified_column;
32mod range;
33mod reverse;
34mod routine_signature;
35mod string_binary;
36
37pub use cast_compatibility::{assignment_type_compatible, explicit_type_compatible};
38#[doc(hidden)]
39pub use checksum::{resolve_checksum_overload, ResolvedChecksumOverload};
40pub use common::{
41    common_context_expression_type, common_type, effective_overload_argument_type,
42    effective_overload_argument_type_with_params, function_call_argument_signature,
43    values_column_types, FunctionCallArgumentSignature,
44};
45pub use equality::{equality_operand_type, foreign_key_operand_type};
46#[doc(hidden)]
47pub use fixed_builtin::{
48    fixed_builtin_return_type, is_function as is_fixed_builtin, resolve_fixed_builtin_call,
49    ResolvedFixedBuiltinCall,
50};
51pub use functions::{builtin_function_argument_targets, builtin_function_type};
52#[doc(hidden)]
53pub use gamma::{resolve_gamma_overload, ResolvedGammaOverload};
54pub use introspection::{bind_type_introspection, bind_type_introspection_with_resolver};
55#[doc(hidden)]
56pub use json_strip::{resolve_json_strip_overload, ResolvedJsonStripOverload};
57#[doc(hidden)]
58pub use length::{resolve_length_overload, ResolvedLengthOverload};
59#[doc(hidden)]
60pub use md5::{resolve_md5_overload, ResolvedMd5Overload};
61#[doc(hidden)]
62pub use operators::{
63    binary_operator_types, binary_result_type, require_equality_operator, require_ordering_operator,
64};
65#[doc(hidden)]
66pub use overload_resolution::{
67    builtin_binding_matches, builtin_name_matches, canonical_column_type_name,
68    canonical_routine_type_name, function_resolution_error, match_builtin_function_overload,
69    match_function_signature, rank_function_matches, resolve_local_builtin_overload,
70    routine_type_accepts_implicit_cast, routine_type_category, routine_type_is_preferred,
71    FunctionParameterDescriptor, MatchedBuiltinFunction, MatchedFunctionSignature,
72    RankedFunctionMatch,
73};
74#[doc(hidden)]
75pub use reverse::{resolve_reverse_overload, ResolvedReverseOverload};
76#[doc(hidden)]
77pub use routine_signature::{
78    match_routine_signature, routine_polymorphic_type, MatchedRoutineSignature,
79    RoutineCallDescriptor, RoutineCoercionTarget, RoutineParameterDescriptor,
80    RoutinePolymorphicFamily, RoutinePolymorphicType, RoutineSignatureMatchError,
81    RoutineTypeSubstitutions, RoutineVariadicMode, RoutineVariadicPlan,
82};
83#[doc(hidden)]
84pub use string_binary::{ResolvedStringBinaryOverload, ResolvedTextByteaOverload};
85
86pub trait FunctionTypeResolver: Send + Sync {
87    /// Return whether an external runtime callback claims this unbound function
88    /// name without exposing a declared SQL return type. Such callbacks must
89    /// retain dispatch precedence instead of being rebound to a same-named
90    /// built-in overload.
91    fn has_untyped_function(&self, _name: &str) -> bool {
92        false
93    }
94
95    /// Resolve a catalog-owned SQL type name that is not represented by the
96    /// built-in [`ColumnType::from_sql_name`] mapping, such as a domain.
97    fn resolve_type_name(&self, _name: &str) -> Result<Option<ColumnType>, SQLError> {
98        Ok(None)
99    }
100
101    fn resolve_function_type(
102        &self,
103        name: &str,
104        binding: Option<&FunctionBinding>,
105        argument_names: &[Option<String>],
106        argument_types: &[Option<ColumnType>],
107        explicit_variadic: bool,
108    ) -> Result<Option<ColumnType>, SQLError>;
109
110    /// Resolve a catalog-backed overload together with the stable binding needed to execute it after built-in and user-defined candidates have been ranked.
111    fn resolve_function_overload(
112        &self,
113        _name: &str,
114        _binding: Option<&FunctionBinding>,
115        _argument_names: &[Option<String>],
116        _argument_types: &[Option<ColumnType>],
117        _explicit_variadic: bool,
118    ) -> Result<Option<ResolvedFunctionOverload>, SQLError> {
119        Ok(None)
120    }
121
122    /// Return whether an exact catalog-selected binding can execute in a scalar expression. The conservative default prevents aggregate, procedure, and set-returning routines from being attached to [`ScalarExpr::Func`].
123    fn is_scalar_function_binding(&self, _binding: &FunctionBinding) -> Result<bool, SQLError> {
124        Ok(false)
125    }
126
127    /// Resolve catalog-backed routines and the supplied built-in overloads as
128    /// one `PostgreSQL` candidate set. Implementations with catalog visibility
129    /// should override this so search-path shadowing and unknown-category
130    /// selection happen before a winner is chosen.
131    fn resolve_function_overload_with_builtins(
132        &self,
133        name: &str,
134        binding: Option<&FunctionBinding>,
135        argument_names: &[Option<String>],
136        argument_types: &[Option<ColumnType>],
137        explicit_variadic: bool,
138        _builtins: &[BuiltinFunctionOverload],
139    ) -> Result<Option<ResolvedFunctionOverload>, SQLError> {
140        self.resolve_function_overload(
141            name,
142            binding,
143            argument_names,
144            argument_types,
145            explicit_variadic,
146        )
147    }
148
149    /// Resolve the declared first-column type of a physical scalar-subquery slot when the owning execution context carries its plan arena.
150    fn resolve_scalar_subquery_type(
151        &self,
152        _subquery: crate::SubqueryId,
153        _outer_schema: &RowSchema,
154        _params: &[SQLParam],
155    ) -> Result<Option<ColumnType>, SQLError> {
156        Ok(None)
157    }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct BuiltinFunctionOverload {
162    pub name: String,
163    pub argument_names: Vec<Option<String>>,
164    pub argument_types: Vec<ColumnType>,
165    pub default_arguments: usize,
166    pub return_type: ColumnType,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct ResolvedFunctionOverload {
171    pub binding: FunctionBinding,
172    pub return_type: ColumnType,
173    pub exact_matches: usize,
174    pub known_arguments: usize,
175    pub preferred_matches: usize,
176    pub precedes_pg_catalog: bool,
177}
178
179impl ResolvedFunctionOverload {
180    #[must_use]
181    pub fn is_exact_for_known_arguments(&self) -> bool {
182        self.known_arguments > 0 && self.exact_matches == self.known_arguments
183    }
184}
185
186pub fn scalar_type(
187    expression: &ScalarExpr,
188    schema: &RowSchema,
189    params: &[SQLParam],
190) -> Result<Option<ColumnType>, SQLError> {
191    scalar_type_inner(expression, schema, params, None)
192}
193
194pub fn scalar_type_with_resolver(
195    expression: &ScalarExpr,
196    schema: &RowSchema,
197    params: &[SQLParam],
198    resolver: &dyn FunctionTypeResolver,
199) -> Result<Option<ColumnType>, SQLError> {
200    scalar_type_inner(expression, schema, params, Some(resolver))
201}
202
203#[expect(
204    clippy::too_many_lines,
205    reason = "type resolution preserves candidate order and ambiguity diagnostics atomically"
206)]
207pub(super) fn scalar_type_inner(
208    expression: &ScalarExpr,
209    schema: &RowSchema,
210    params: &[SQLParam],
211    resolver: Option<&dyn FunctionTypeResolver>,
212) -> Result<Option<ColumnType>, SQLError> {
213    if matches!(
214        expression,
215        ScalarExpr::Func { binding, .. }
216            if binding.as_ref().and_then(|binding| binding.dispatch).is_some_and(
217                crate::ast::FunctionDispatch::is_call_argument_marker
218            )
219    ) {
220        let argument = crate::scalar_call_argument(expression)?;
221        return scalar_type_inner(argument.value, schema, params, resolver);
222    }
223    match expression {
224        ScalarExpr::Column(column) => {
225            if schema.has_unqualified_column(column) || schema.column_is_ambiguous(column) {
226                Ok(schema.type_of(column).cloned())
227            } else if schema.has_qualifier(column) {
228                Ok(Some(ColumnType::Record))
229            } else {
230                Ok(None)
231            }
232        }
233        ScalarExpr::Position(position) => Ok(schema.column_type(*position).cloned()),
234        ScalarExpr::InternalColumn(column) => Ok(schema.internal_type(*column).cloned()),
235        ScalarExpr::QualifiedColumn { qualifier, column } => {
236            qualified_column::resolve(schema, qualifier, column)
237        }
238        ScalarExpr::Literal(value) => Ok(common::value_type(value)),
239        ScalarExpr::TypedLiteral {
240            bound_type: Some(ty),
241            ..
242        } => Ok(Some(ty.clone())),
243        ScalarExpr::TypedLiteral { ty, .. } => {
244            let target = match ColumnType::from_sql_name(ty) {
245                Ok(ty) => Ok(Some(ty)),
246                Err(error @ SQLError::Unsupported(_)) => match resolver {
247                    Some(resolver) => resolver
248                        .resolve_type_name(ty)?
249                        .map_or(Err(error), |ty| Ok(Some(ty))),
250                    None => Err(error),
251                },
252                Err(error) => Err(error),
253            }?;
254            Ok(target)
255        }
256        ScalarExpr::Param(index) => Ok(index
257            .checked_sub(1)
258            .and_then(|index| params.get(index))
259            .and_then(common::parameter_type)),
260        ScalarExpr::Cast { expr, ty } => {
261            let source = scalar_type_inner(expr, schema, params, resolver)?;
262            let target = match ColumnType::from_sql_name(ty) {
263                Ok(ty) => Ok(Some(ty)),
264                Err(error @ SQLError::Unsupported(_)) => match resolver {
265                    Some(resolver) => resolver
266                        .resolve_type_name(ty)?
267                        .map_or(Err(error), |ty| Ok(Some(ty))),
268                    None => Err(error),
269                },
270                Err(error) => Err(error),
271            }?;
272            if let Some(target) = target.as_ref() {
273                cast_compatibility::validate_explicit_cast(source.as_ref(), target)?;
274            }
275            Ok(target)
276        }
277        ScalarExpr::Array(items) => {
278            if items.is_empty() {
279                return Ok(None);
280            }
281            let mut element = None;
282            for item in items {
283                element = common::merge_optional_types(
284                    element,
285                    common::common_context_expression_type(item, schema, params, resolver)?,
286                )?;
287            }
288            Ok(Some(ColumnType::Array(Box::new(
289                element.unwrap_or(ColumnType::Text),
290            ))))
291        }
292        ScalarExpr::Row(items) => {
293            for item in items {
294                scalar_type_inner(item, schema, params, resolver)?;
295            }
296            Ok(Some(ColumnType::Record))
297        }
298        ScalarExpr::Binary { op, lhs, rhs } => {
299            let left = common_context_expression_type(lhs, schema, params, resolver)?;
300            let right = common_context_expression_type(rhs, schema, params, resolver)?;
301            operators::binary_result_type(*op, left.as_ref(), right.as_ref())
302        }
303        ScalarExpr::UnaryMinus(inner) => scalar_type_inner(inner, schema, params, resolver)?
304            .map_or(Ok(None), |ty| {
305                operators::unary_minus_result_type(&ty).map(Some)
306            }),
307        ScalarExpr::Not(inner) | ScalarExpr::IsNull { expr: inner, .. } => {
308            scalar_type_inner(inner, schema, params, resolver)?;
309            Ok(Some(ColumnType::Boolean))
310        }
311        ScalarExpr::And(items) | ScalarExpr::Or(items) => {
312            for item in items {
313                scalar_type_inner(item, schema, params, resolver)?;
314            }
315            Ok(Some(ColumnType::Boolean))
316        }
317        ScalarExpr::Between { expr, low, high } => {
318            let value = scalar_type_inner(expr, schema, params, resolver)?;
319            let low = scalar_type_inner(low, schema, params, resolver)?;
320            let high = scalar_type_inner(high, schema, params, resolver)?;
321            operators::binary_result_type(
322                crate::ast::BinaryOp::GreaterEqual,
323                value.as_ref(),
324                low.as_ref(),
325            )?;
326            operators::binary_result_type(
327                crate::ast::BinaryOp::LessEqual,
328                value.as_ref(),
329                high.as_ref(),
330            )?;
331            Ok(Some(ColumnType::Boolean))
332        }
333        ScalarExpr::InList { expr, list, .. } => {
334            let needle = scalar_type_inner(expr, schema, params, resolver)?;
335            for item in list {
336                let candidate = scalar_type_inner(item, schema, params, resolver)?;
337                operators::binary_result_type(
338                    crate::ast::BinaryOp::Equal,
339                    needle.as_ref(),
340                    candidate.as_ref(),
341                )?;
342            }
343            Ok(Some(ColumnType::Boolean))
344        }
345        ScalarExpr::InSubquery { expr, subquery, .. } => {
346            let needle = scalar_type_inner(expr, schema, params, resolver)?;
347            let candidate = resolver
348                .map(|resolver| resolver.resolve_scalar_subquery_type(*subquery, schema, params))
349                .transpose()?
350                .flatten();
351            operators::binary_result_type(
352                crate::ast::BinaryOp::Equal,
353                needle.as_ref(),
354                candidate.as_ref(),
355            )?;
356            Ok(Some(ColumnType::Boolean))
357        }
358        ScalarExpr::Exists { .. } => Ok(Some(ColumnType::Boolean)),
359        ScalarExpr::Case {
360            base,
361            when,
362            else_branch,
363        } => {
364            let simple = base.is_some();
365            let base_type = base
366                .as_deref()
367                .map(|base| common::common_context_expression_type(base, schema, params, resolver))
368                .transpose()?
369                .flatten();
370            let mut result = None;
371            for (condition, value) in when {
372                let condition_type = if simple {
373                    common::common_context_expression_type(condition, schema, params, resolver)?
374                } else {
375                    scalar_type_inner(condition, schema, params, resolver)?
376                };
377                if simple {
378                    operators::binary_result_type(
379                        crate::ast::BinaryOp::Equal,
380                        base_type.as_ref(),
381                        condition_type.as_ref(),
382                    )?;
383                }
384                result = common::merge_optional_types(
385                    result,
386                    common::common_context_expression_type(value, schema, params, resolver)?,
387                )?;
388            }
389            if let Some(value) = else_branch {
390                result = common::merge_optional_types(
391                    result,
392                    common::common_context_expression_type(value, schema, params, resolver)?,
393                )?;
394            }
395            match result {
396                Some(result) => {
397                    common::case_output_type(expression, &result, schema, params, resolver)
398                        .map(Some)
399                }
400                result => Ok(result),
401            }
402        }
403        ScalarExpr::Func {
404            name,
405            binding,
406            args,
407            distinct,
408            order_by,
409            filter,
410        } => {
411            if let Some(crate::ast::FunctionResolutionError::UndefinedFunction { signature }) =
412                binding
413                    .as_ref()
414                    .and_then(|binding| binding.resolution_error.as_ref())
415            {
416                return Err(SQLError::Routine {
417                    sqlstate: "42883".into(),
418                    message: format!("function {signature} does not exist"),
419                });
420            }
421            if let Some(filter) = filter {
422                scalar_type_inner(filter, schema, params, resolver)?;
423            }
424            if *distinct {
425                for argument in args {
426                    if let Some(ty) = scalar_type_inner(argument, schema, params, resolver)? {
427                        require_equality_operator(&ty)?;
428                    }
429                }
430            }
431            for order in order_by {
432                if let Some(ty) = scalar_type_inner(&order.expr, schema, params, resolver)? {
433                    require_ordering_operator(&ty)?;
434                }
435            }
436            functions::builtin_function_type_inner(
437                name,
438                binding.as_ref(),
439                args,
440                order_by,
441                schema,
442                params,
443                resolver,
444            )
445        }
446        ScalarExpr::WindowCall { name, args, spec } => {
447            for expression in &spec.partition_by {
448                if let Some(ty) = scalar_type_inner(expression, schema, params, resolver)? {
449                    require_equality_operator(&ty)?;
450                }
451            }
452            for order in &spec.order_by {
453                if let Some(ty) = scalar_type_inner(&order.expr, schema, params, resolver)? {
454                    require_ordering_operator(&ty)?;
455                }
456            }
457            if let Some(frame) = &spec.frame {
458                for bound in [&frame.start, &frame.end] {
459                    match bound {
460                        crate::ScalarFrameBound::Preceding(expression)
461                        | crate::ScalarFrameBound::Following(expression) => {
462                            scalar_type_inner(expression, schema, params, resolver)?;
463                        }
464                        crate::ScalarFrameBound::UnboundedPreceding
465                        | crate::ScalarFrameBound::UnboundedFollowing
466                        | crate::ScalarFrameBound::CurrentRow => {}
467                    }
468                }
469            }
470            functions::builtin_function_type_inner(name, None, args, &[], schema, params, resolver)
471        }
472        ScalarExpr::ScalarSubquery(subquery) => resolver.map_or(Ok(None), |resolver| {
473            resolver.resolve_scalar_subquery_type(*subquery, schema, params)
474        }),
475        ScalarExpr::QualifiedStar(qualifier) if schema.has_qualifier(qualifier) => {
476            Ok(Some(ColumnType::Record))
477        }
478        ScalarExpr::Star | ScalarExpr::QualifiedStar(_) | ScalarExpr::Default => Ok(None),
479    }
480}
481
482#[cfg(test)]
483mod tests;
484
485mod declaration;
486pub use declaration::resolve_declared_column_type;
487
488mod coercion;
489pub use coercion::coerce_common_context_value;