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::schema::ScalarTypeSchema;
13use crate::{RowSchema, ScalarExpr};
14use uqa_core::Value;
15
16mod array_transform;
17pub(crate) mod between;
18pub(crate) mod boolean;
19pub(crate) use array_transform::resolve_array_transform_call;
20mod call;
21mod cast_compatibility;
22mod checksum;
23mod common;
24pub(crate) mod concatenation;
25pub(crate) use common::literal_input_type_with_control;
26pub(crate) use common::select_common_input_type_in_with_control;
27pub(crate) use common::value_type;
28pub(crate) use common::value_type_with_control;
29pub(crate) use common::{array_element_type, base_type};
30mod containment;
31pub(crate) mod enums;
32mod equality;
33pub(crate) mod field_selection;
34mod fixed_builtin;
35pub(crate) use fixed_builtin::overloads as fixed_builtin_overloads;
36mod function_inputs;
37mod functions;
38pub(crate) use function_inputs::{call_type_from_observations, CallInputTypes};
39mod gamma;
40mod inference;
41use inference::scalar_type_inner_with_control;
42mod introspection;
43pub(crate) mod json_path;
44mod json_strip;
45mod length;
46mod md5;
47pub(crate) mod membership;
48mod operator_errors;
49pub(crate) mod row_comparison;
50pub(crate) mod row_distinct;
51pub use operator_errors::{
52    ambiguous_binary_operator, ambiguous_prefix_operator, undefined_binary_operator,
53    undefined_binary_operator_named, undefined_prefix_operator,
54};
55mod operators;
56pub(crate) use operators::comparison::ComparisonSupport;
57pub use operators::comparison::{
58    comparison_key_is_constant, type_supports_hashing, type_supports_ordering, ComparisonKeySupport,
59};
60mod overload_resolution;
61pub(crate) use overload_resolution::resolve_local_builtin_call_with_control;
62mod qualified_column;
63mod range;
64mod range_offsets;
65pub use range_offsets::range_frame_offset_type;
66mod reverse;
67mod routine_signature;
68mod scalar_input;
69mod stored_constants;
70pub use scalar_input::{
71    scalar_array_element_oid_with_control, scalar_cast_source_type_name_with_control,
72    scalar_integer_operation_width, scalar_integer_operation_width_with_control,
73    scalar_operand_type_name, scalar_operand_type_name_with_control,
74    scalar_record_field_type_with_control,
75};
76pub(crate) use stored_constants::store_syntax_operand_coercions;
77pub use stored_constants::{
78    contains_unknown_literal, fold_stored_enum_constants, store_operand_coercions,
79    stored_enum_constant,
80};
81pub(crate) mod pattern;
82mod string_binary;
83
84pub use cast_compatibility::{
85    assignment_type_compatible, cast_catalog_entry, cast_volatility, explicit_type_compatible,
86    CastCatalogEntry, CastMethod,
87};
88pub(crate) use cast_compatibility::{cast_catalog_entry_with_control, preserves_record_identity};
89#[doc(hidden)]
90pub use checksum::{resolve_checksum_overload, ResolvedChecksumOverload};
91pub use common::{
92    common_context_expression_type, common_type, common_type_in, effective_overload_argument_type,
93    effective_overload_argument_type_with_params, function_call_argument_signature,
94    select_common_input_type, values_column_types, CommonTypeContext,
95    FunctionCallArgumentSignature,
96};
97pub use equality::{
98    equality_operand_type, equality_operand_type_with_control, foreign_key_operand_type,
99};
100#[doc(hidden)]
101pub use fixed_builtin::{
102    fixed_builtin_return_type, fixed_builtin_return_type_with_control,
103    is_function as is_fixed_builtin, resolve_fixed_builtin_call, ResolvedFixedBuiltinCall,
104};
105pub use functions::{
106    builtin_function_argument_targets, builtin_function_type, builtin_function_type_with_resolver,
107};
108#[doc(hidden)]
109pub use gamma::{resolve_gamma_overload, ResolvedGammaOverload};
110pub use introspection::{
111    bind_type_introspection, bind_type_introspection_with_control,
112    bind_type_introspection_with_resolver, validate_catalog_literals,
113};
114#[doc(hidden)]
115pub use json_strip::{resolve_json_strip_overload, ResolvedJsonStripOverload};
116#[doc(hidden)]
117pub use length::{resolve_length_overload, ResolvedLengthOverload};
118#[doc(hidden)]
119pub use md5::{resolve_md5_overload, ResolvedMd5Overload};
120#[doc(hidden)]
121pub use operators::{
122    binary_operator_by_oid, binary_operator_catalog_entry, binary_operator_types,
123    binary_operator_types_with_control, binary_operator_volatility, binary_result_type,
124    binary_result_type_with_control, numeric_operator_types, numeric_operator_types_with_control,
125    require_equality_operator, require_ordering_operator, unary_minus_catalog_entry,
126    unary_operator_by_oid, BinaryOperatorCatalogEntry, NumericOperatorTypes,
127    UnaryOperatorCatalogEntry,
128};
129pub(crate) use operators::{named_binary_operator_catalog_entry, named_binary_operator_types};
130#[doc(hidden)]
131pub use overload_resolution::{
132    builtin_binding_matches, builtin_name_matches, canonical_column_type_name,
133    canonical_routine_type_name, function_resolution_error, match_builtin_function_call,
134    match_builtin_function_overload, match_function_signature, parse_enum_type_identity,
135    rank_function_matches, resolve_local_builtin_overload, routine_type_accepts_implicit_cast,
136    routine_type_category, routine_type_is_preferred, FunctionParameterDescriptor,
137    MatchedBuiltinFunction, MatchedFunctionSignature, RankedFunctionMatch,
138};
139#[doc(hidden)]
140pub use reverse::{resolve_reverse_overload, ResolvedReverseOverload};
141#[doc(hidden)]
142pub(crate) use routine_signature::match_routine_candidate;
143pub use routine_signature::{
144    match_routine_signature, routine_polymorphic_type, MatchedRoutineSignature,
145    RoutineCallDescriptor, RoutineCoercionTarget, RoutineParameterDescriptor,
146    RoutinePolymorphicFamily, RoutinePolymorphicType, RoutineSignatureMatchError,
147    RoutineTypeSubstitutions, RoutineVariadicMode, RoutineVariadicPlan,
148};
149#[doc(hidden)]
150pub use string_binary::{ResolvedStringBinaryOverload, ResolvedTextByteaOverload};
151
152pub trait FunctionTypeResolver: Send + Sync {
153    /// Format a selected type using the binding catalog's namespace visibility.
154    fn format_type_name(&self, ty: &ColumnType) -> Result<String, SQLError> {
155        Ok(ty.sql_name())
156    }
157
158    /// Return whether an external runtime callback claims this unbound function
159    /// name without exposing a declared SQL return type. Such callbacks must
160    /// retain dispatch precedence instead of being rebound to a same-named
161    /// built-in overload.
162    fn has_untyped_function(&self, _name: &str) -> bool {
163        false
164    }
165
166    /// Resolve a catalog-owned SQL type name that is not represented by the
167    /// built-in [`ColumnType::from_sql_name`] mapping, such as a domain.
168    fn resolve_type_name(&self, _name: &str) -> Result<Option<ColumnType>, SQLError> {
169        Ok(None)
170    }
171
172    /// Resolve an explicit collation name with the binding catalog's namespace visibility and privileges.
173    fn resolve_collation_name(&self, _name: &str) -> Result<Option<u32>, SQLError> {
174        Ok(None)
175    }
176
177    /// Enum labels of the binding catalog. Binding converts `unknown` literals coerced to an enum type with them, as `PostgreSQL` parse analysis calls the type's input function.
178    fn enum_labels(&self) -> Option<&dyn crate::expr::enums::EnumLabelCatalog> {
179        None
180    }
181
182    /// Input functions whose catalog constraints must run when an unknown literal is analyzed. Callers retain the returned constant; metadata-only inference does not invoke this capability.
183    fn catalog_input_functions(&self) -> Option<&dyn crate::expr::CatalogInputFunctions> {
184        None
185    }
186
187    /// Composite type attributes of the binding catalog, which type field selections and row coercions.
188    fn composite_types(&self) -> Option<&dyn crate::expr::composites::CompositeTypeCatalog> {
189        None
190    }
191
192    /// Require the current user's `USAGE` privilege on a type that a relation column, domain or routine declares, as `object_aclcheck(TypeRelationId, ..., ACL_USAGE)` and `aclcheck_error_type` require it; see [`crate::catalog::security::type_inquiry::usage_governing_type`]. A resolver without roles and type privileges has nothing to deny.
193    fn require_type_usage(&self, _ty: &ColumnType) -> Result<(), SQLError> {
194        Ok(())
195    }
196
197    fn resolve_function_type(
198        &self,
199        name: &str,
200        binding: Option<&FunctionBinding>,
201        argument_names: &[Option<String>],
202        argument_types: &[Option<ColumnType>],
203        explicit_variadic: bool,
204    ) -> Result<Option<ColumnType>, SQLError>;
205
206    /// Resolve a catalog-backed overload together with the stable binding needed to execute it after built-in and user-defined candidates have been ranked.
207    fn resolve_function_overload(
208        &self,
209        _name: &str,
210        _binding: Option<&FunctionBinding>,
211        _argument_names: &[Option<String>],
212        _argument_types: &[Option<ColumnType>],
213        _explicit_variadic: bool,
214    ) -> Result<Option<ResolvedFunctionOverload>, SQLError> {
215        Ok(None)
216    }
217
218    /// 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`].
219    fn is_scalar_function_binding(&self, _binding: &FunctionBinding) -> Result<bool, SQLError> {
220        Ok(false)
221    }
222
223    /// Resolve catalog-backed routines and the supplied built-in overloads as
224    /// one `PostgreSQL` candidate set. Implementations with catalog visibility
225    /// should override this so search-path shadowing and unknown-category
226    /// selection happen before a winner is chosen.
227    fn resolve_function_overload_with_builtins(
228        &self,
229        name: &str,
230        binding: Option<&FunctionBinding>,
231        argument_names: &[Option<String>],
232        argument_types: &[Option<ColumnType>],
233        explicit_variadic: bool,
234        _builtins: &[BuiltinFunctionOverload],
235    ) -> Result<Option<ResolvedFunctionOverload>, SQLError> {
236        self.resolve_function_overload(
237            name,
238            binding,
239            argument_names,
240            argument_types,
241            explicit_variadic,
242        )
243    }
244
245    /// Resolve the declared first-column type of a physical scalar-subquery slot when the owning execution context carries its plan arena.
246    fn resolve_scalar_subquery_type(
247        &self,
248        _subquery: crate::SubqueryId,
249        _outer_schema: &RowSchema,
250        _params: &[SQLParam],
251    ) -> Result<Option<ColumnType>, SQLError> {
252        Ok(None)
253    }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct BuiltinFunctionOverload {
258    /// The final declared array parameter accepts expanded elements or an explicit VARIADIC array.
259    pub variadic: bool,
260    pub name: String,
261    pub argument_names: Vec<Option<String>>,
262    pub argument_types: Vec<ColumnType>,
263    pub default_arguments: usize,
264    pub return_type: ColumnType,
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct ResolvedFunctionOverload {
269    pub binding: FunctionBinding,
270    pub return_type: ColumnType,
271    pub exact_matches: usize,
272    pub known_arguments: usize,
273    pub preferred_matches: usize,
274    pub precedes_pg_catalog: bool,
275}
276
277impl ResolvedFunctionOverload {
278    #[must_use]
279    pub fn is_exact_for_known_arguments(&self) -> bool {
280        self.known_arguments > 0 && self.exact_matches == self.known_arguments
281    }
282}
283
284pub fn scalar_type(
285    expression: &ScalarExpr,
286    schema: &dyn ScalarTypeSchema,
287    params: &[SQLParam],
288) -> Result<Option<ColumnType>, SQLError> {
289    scalar_type_inner(expression, schema, params, None)
290}
291
292/// Infer a scalar type from borrowed schema and parameter metadata while retaining every constructed type and temporary buffer under the supplied allowance.
293pub fn scalar_type_with_control(
294    expression: &ScalarExpr,
295    schema: &dyn ScalarTypeSchema,
296    params: &[SQLParam],
297    control: &uqa_core::memory::ProductionControl<'_>,
298) -> Result<Option<uqa_core::memory::Produced<ColumnType>>, SQLError> {
299    scalar_type_inner_with_control(expression, schema, params, None, control)
300}
301
302/// Preserve unknown-literal and domain rules when selecting an operator's common type without a catalog callback.
303pub fn common_context_type_with_control(
304    expression: &ScalarExpr,
305    schema: &dyn ScalarTypeSchema,
306    params: &[SQLParam],
307    control: &uqa_core::memory::ProductionControl<'_>,
308) -> Result<Option<uqa_core::memory::Produced<ColumnType>>, SQLError> {
309    common::common_context_expression_type_with_control(expression, schema, params, None, control)
310}
311
312pub fn scalar_type_with_resolver(
313    expression: &ScalarExpr,
314    schema: &dyn ScalarTypeSchema,
315    params: &[SQLParam],
316    resolver: &dyn FunctionTypeResolver,
317) -> Result<Option<ColumnType>, SQLError> {
318    scalar_type_inner(expression, schema, params, Some(resolver))
319}
320
321/// A bare string or NULL literal, which `PostgreSQL` types as `unknown` until its context resolves it.
322pub fn is_unknown_literal(expression: &ScalarExpr) -> bool {
323    matches!(expression, ScalarExpr::Literal(Value::Str(_) | Value::Null))
324}
325
326/// Whether a type's input function consults the catalog: the OID alias types and `aclitem` look names up, and an enum, a composite type, a record or a type known by name is read through its catalog definition. Analysis without the catalog leaves such a literal in the cast form that binding or evaluation reads, and reads every other literal with the type's input function.
327pub fn catalog_input_type(ty: &ColumnType) -> bool {
328    match ty {
329        ColumnType::Regproc
330        | ColumnType::Regprocedure
331        | ColumnType::Regclass
332        | ColumnType::Regcollation
333        | ColumnType::Regnamespace
334        | ColumnType::Regrole
335        | ColumnType::Regtype
336        | ColumnType::AclItem
337        | ColumnType::PgNodeTree
338        | ColumnType::Named(_)
339        | ColumnType::Enum(_)
340        | ColumnType::Composite(_)
341        | ColumnType::Record
342        | ColumnType::AnyArray
343        | ColumnType::Vector(_)
344        | ColumnType::Tensor(_) => true,
345        ColumnType::Array(inner) | ColumnType::Domain { base: inner, .. } => {
346            catalog_input_type(inner)
347        }
348        _ => false,
349    }
350}
351
352/// Whether value operations can require live catalog metadata: enum identities, record fields and their array/domain containers cannot be evaluated by a catalog-free scalar implementation. OID aliases have already decoded their names and operate on ordinary integer values.
353#[must_use]
354pub fn catalog_value_type(ty: &ColumnType) -> bool {
355    match ty {
356        ColumnType::Named(_)
357        | ColumnType::Enum(_)
358        | ColumnType::Composite(_)
359        | ColumnType::Record
360        | ColumnType::AnyArray => true,
361        ColumnType::Array(inner) | ColumnType::Domain { base: inner, .. } => {
362            catalog_value_type(inner)
363        }
364        _ => false,
365    }
366}
367
368/// The type of an assignment's source before coercion to its destination. A bare string or NULL literal has none: `transformAssignedExpr` and `transformAssignmentIndirection` convert it with the destination type's input function instead of checking a source type, so `'7'` assigns to an integer array element and `'abc'` fails as integer input.
369pub fn assignment_source_type(
370    expression: &ScalarExpr,
371    schema: &dyn ScalarTypeSchema,
372    params: &[SQLParam],
373    resolver: &dyn FunctionTypeResolver,
374) -> Result<Option<ColumnType>, SQLError> {
375    if is_unknown_literal(expression) {
376        return Ok(None);
377    }
378    scalar_type_with_resolver(expression, schema, params, resolver)
379}
380
381pub(super) fn scalar_type_inner(
382    expression: &ScalarExpr,
383    schema: &dyn ScalarTypeSchema,
384    params: &[SQLParam],
385    resolver: Option<&dyn FunctionTypeResolver>,
386) -> Result<Option<ColumnType>, SQLError> {
387    scalar_type_inner_with_control(
388        expression,
389        schema,
390        params,
391        resolver,
392        &uqa_core::memory::ProductionControl::uncontrolled(),
393    )
394    .map(|ty| {
395        ty.map(|ty| {
396            ty.into_uncontrolled()
397                .expect("ordinary scalar inference has no reservation")
398        })
399    })
400}
401
402#[cfg(test)]
403mod tests;
404
405mod declaration;
406pub use declaration::{resolve_alter_column_type, resolve_declared_column_type};
407
408mod coercion;
409pub mod composite_rows;
410pub use coercion::coerce_common_context_value;