Skip to main content

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