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