Skip to main content

uqa_sql/expr/context/
casting.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Catalog-aware cast selection shares controlled type, array and value constructors.
8
9use uqa_core::{
10    memory::{Produced, ProductionControl, ProductionString, ProductionVec},
11    ArrayValue, Value,
12};
13
14use super::super::casting::{cast_value_from_with_control, parse_pg_array_literal_with_control};
15use super::{format_regtype_elements_with_control, format_regtype_value_with_control, EngineHook};
16use crate::{
17    ast::ColumnType,
18    error::{Result, SQLError},
19};
20
21mod datestyle;
22mod timezone;
23
24/// Catalog input needed after analysis selects a type for an unknown literal. Domain array input includes each element's constraints, so the caller must retain this value rather than read the text again at execution.
25pub trait CatalogInputFunctions {
26    fn read_unknown_input(&self, text: &str, target: &ColumnType) -> Result<Value>;
27}
28
29pub(crate) fn requires_domain_array_input(target: &ColumnType) -> bool {
30    matches!(target, ColumnType::Array(element) if matches!(array_leaf_type(element), ColumnType::Domain { .. }))
31}
32
33/// Input constants containing named composites or domain array elements must be read while the definition's catalog descriptor is available.
34pub(crate) fn requires_catalog_constant_input(target: &ColumnType) -> bool {
35    requires_domain_array_input(target)
36        || match target {
37            ColumnType::Composite(_) => true,
38            ColumnType::Array(element) | ColumnType::Domain { base: element, .. } => {
39                requires_catalog_constant_input(element)
40            }
41            _ => false,
42        }
43}
44
45/// Read a catalog-dependent constant once, retaining its typed datum in stored syntax.
46pub fn read_catalog_input(
47    text: &str,
48    target: &ColumnType,
49    engine: &dyn EngineHook,
50) -> Result<Value> {
51    if requires_domain_array_input(target) {
52        return read_catalog_array_input(text, target, engine);
53    }
54    if !requires_catalog_constant_input(target) {
55        return Err(SQLError::Internal(
56            "catalog input requires a composite or domain array".into(),
57        ));
58    }
59    super::cast_value_with_type_resolution(
60        &Value::Str(text.into()),
61        Some("unknown"),
62        &target.catalog_name(),
63        Some(engine),
64    )
65}
66
67/// Read an array whose elements are domains with the same input and constraint path as a catalog-aware cast. The resolved target preserves domain identity; a scalar outer domain remains the surrounding expression's runtime coercion.
68pub fn read_catalog_array_input(
69    text: &str,
70    target: &ColumnType,
71    engine: &dyn EngineHook,
72) -> Result<Value> {
73    if !requires_domain_array_input(target) {
74        return Err(SQLError::Internal(
75            "catalog array input requires domain elements".into(),
76        ));
77    }
78    let control = ProductionControl::uncontrolled();
79    let parsed = parse_pg_array_literal_with_control(text, &control)?
80        .into_uncontrolled()
81        .map_err(|_| SQLError::Internal("ordinary catalog array input owner".into()))?;
82    cast_catalog_array(&Value::Array(parsed), None, target, engine, &control)?
83        .into_uncontrolled()
84        .map_err(|_| SQLError::Internal("ordinary catalog array input owner".into()))
85}
86
87#[must_use]
88pub fn coercion_type_name(ty: &ColumnType) -> String {
89    coercion_type_name_with_control(ty, &ProductionControl::uncontrolled())
90        .expect("ordinary coercion type name")
91        .into_uncontrolled()
92        .expect("ordinary coercion type name owner")
93}
94
95fn coercion_type_name_with_control(
96    ty: &ColumnType,
97    control: &ProductionControl<'_>,
98) -> Result<Produced<String>> {
99    control.check()?;
100    match ty {
101        ColumnType::Domain { base, .. } => coercion_type_name_with_control(base, control),
102        ColumnType::Array(element) => {
103            let element = coercion_type_name_with_control(element, control)?;
104            let mut name = ProductionString::new(*control);
105            name.push_str(&element)?;
106            name.push_str("[]")?;
107            Ok(name.finish()?)
108        }
109        _ => Ok(ty.sql_name_with_control(control)?),
110    }
111}
112
113/// The types of `PostgreSQL`'s string category, which any type casts to through its output function.
114fn is_string_type(ty: &ColumnType) -> bool {
115    matches!(
116        ty,
117        ColumnType::Text | ColumnType::Name | ColumnType::Varchar(_) | ColumnType::Character(_)
118    )
119}
120
121fn regrole_array_type(ty: &ColumnType) -> bool {
122    match ty {
123        ColumnType::Array(element) => {
124            matches!(element.as_ref(), ColumnType::Regrole) || regrole_array_type(element)
125        }
126        _ => false,
127    }
128}
129
130fn array_leaf_type(ty: &ColumnType) -> &ColumnType {
131    match ty {
132        ColumnType::Array(element) => array_leaf_type(element),
133        _ => ty,
134    }
135}
136
137fn optional_type_name(
138    name: &str,
139    control: &ProductionControl<'_>,
140) -> Result<Option<Produced<ColumnType>>> {
141    match ColumnType::from_sql_name_with_control(name, control) {
142        Ok(ty) => Ok(Some(ty)),
143        Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => Err(error),
144        Err(_) => Ok(None),
145    }
146}
147
148/// Cast a value after resolving catalog-owned types and enforcing domain constraints before exposing a base-type carrier.
149pub fn cast_value_with_type_resolution(
150    value: &Value,
151    source_ty: Option<&str>,
152    target_ty: &str,
153    engine: Option<&dyn EngineHook>,
154) -> Result<Value> {
155    cast_value_with_type_resolution_with_control(
156        value,
157        source_ty,
158        target_ty,
159        engine,
160        &ProductionControl::uncontrolled(),
161    )?
162    .into_uncontrolled()
163    .map_err(|_| SQLError::Internal("ordinary catalog cast owner".into()))
164}
165
166/// The text a value's output function spells, and the type to cast it from, when a cast goes through output: an enum label, `record_out` of a composite record cast to a string type, and a container whose enum fields and elements spell their current labels.
167fn output_for_cast<'a>(
168    engine: &dyn EngineHook,
169    value: &Value,
170    source_ty: Option<&'a str>,
171    resolved_source_type: Option<&ColumnType>,
172    target: &ColumnType,
173) -> Result<Option<(Value, Option<&'a str>)>> {
174    if let Value::Enum(label) = value {
175        let output =
176            super::super::enums::enum_output_for_cast(engine.enum_labels(), label, target)?;
177        return Ok(Some((output, Some("text"))));
178    }
179    // `record_out` spells each field through its attribute type's output function.
180    if let (Value::Record(_), Some(source @ ColumnType::Composite(_))) =
181        (value, resolved_source_type)
182    {
183        if is_string_type(target) {
184            let text = crate::result::format_postgres_text(value, source, Some(engine))?;
185            return Ok(Some((Value::Str(text), Some("text"))));
186        }
187    }
188    if matches!(value, Value::Record(_) | Value::Row(_))
189        && is_string_type(target)
190        && super::super::enums::contains_enum_carrier(value)
191    {
192        let rendered = super::super::enums::render_enum_labels(engine.enum_labels(), value)?;
193        return Ok(Some((rendered, source_ty)));
194    }
195    Ok(None)
196}
197
198/// Array output borrows the input and retains both intermediate labels and final text under the caller's production budget.
199fn enum_array_output(
200    value: &Value,
201    source_ty: Option<&str>,
202    target_ty: &str,
203    target: &ColumnType,
204    engine: &dyn EngineHook,
205    control: &ProductionControl<'_>,
206) -> Result<Option<Produced<Value>>> {
207    if !matches!(value, Value::Array(_))
208        || !is_string_type(target)
209        || !super::super::enums::contains_enum_carrier(value)
210    {
211        return Ok(None);
212    }
213    let text_array = ColumnType::Array(Box::new(ColumnType::Text));
214    let labels = cast_catalog_array(value, source_ty, &text_array, engine, control)?;
215    cast_value_from_with_control(&labels, target_ty, Some("text[]"), control).map(Some)
216}
217
218/// Resolve catalog inputs at their external handoff, then admit SQL-owned names, element conversions and output before constructing them.
219pub fn cast_value_with_type_resolution_with_control(
220    value: &Value,
221    source_ty: Option<&str>,
222    target_ty: &str,
223    engine: Option<&dyn EngineHook>,
224    control: &ProductionControl<'_>,
225) -> Result<Produced<Value>> {
226    control.check()?;
227    let resolved_target = engine
228        .map(|engine| engine.resolve_type_name(target_ty))
229        .transpose()
230        .map_err(SQLError::Internal)?
231        .flatten()
232        .map(|ty| ty.retain_external_with_control(control))
233        .transpose()?;
234    control.check()?;
235    let resolved_source_type = match (engine, source_ty) {
236        (Some(engine), Some(source)) => engine
237            .resolve_type_name(source)
238            .map_err(SQLError::Internal)?,
239        _ => None,
240    };
241    let parsed_target = if resolved_target.is_some() {
242        None
243    } else {
244        optional_type_name(target_ty, control)?
245    };
246    let target_column_type = resolved_target.as_deref().or(parsed_target.as_deref());
247    let _date_order = datestyle::input_scope(value, target_column_type, engine, control)?;
248    if let (Some(engine), Some(target)) = (engine, resolved_target.as_deref()) {
249        if let Some(value) = engine.cast_domain(value, source_ty, target)? {
250            return Ok(control.retain_external_value(value)?);
251        }
252        if let Some(value) = super::super::enums::cast_to_enum(
253            engine.enum_labels(),
254            value,
255            resolved_source_type.as_ref(),
256            target,
257        )? {
258            return Ok(control.retain_external_value(value)?);
259        }
260        if let Some(value) = super::super::composites::cast_to_composite(
261            engine,
262            value,
263            resolved_source_type.as_ref(),
264            target,
265        )? {
266            return Ok(control.retain_external_value(value)?);
267        }
268        if let Some(output) =
269            enum_array_output(value, source_ty, target_ty, target, engine, control)?
270        {
271            return Ok(output);
272        }
273        if let Some((output, output_source)) = output_for_cast(
274            engine,
275            value,
276            source_ty,
277            resolved_source_type.as_ref(),
278            target,
279        )? {
280            return cast_value_with_type_resolution_with_control(
281                &output,
282                output_source,
283                target_ty,
284                Some(engine),
285                control,
286            );
287        }
288        control.check()?;
289        if matches!(target, ColumnType::Array(_))
290            && (requires_catalog_array_cast(target)
291                || super::super::enums::contains_enum_carrier(value)
292                || resolved_source_type
293                    .as_ref()
294                    .is_some_and(super::super::enums::is_enum_bearing))
295        {
296            return cast_catalog_array(value, source_ty, target, engine, control);
297        }
298    }
299    let resolved_source = match (engine, source_ty) {
300        (Some(engine), Some(source_ty)) => engine
301            .resolve_type_name(source_ty)
302            .map_err(SQLError::Internal)?
303            .map(|ty| {
304                let ty = ty.retain_external_with_control(control)?;
305                coercion_type_name_with_control(&ty, control)
306            })
307            .transpose()?,
308        _ => None,
309    };
310    let source_ty = resolved_source.as_deref().map(String::as_str).or(source_ty);
311    let target_name = resolved_target
312        .as_ref()
313        .map(|ty| coercion_type_name_with_control(ty, control))
314        .transpose()?;
315    let target_ty = target_name.as_ref().map_or(target_ty, |name| name.as_str());
316    cast_resolved_value(
317        value,
318        source_ty,
319        target_ty,
320        target_column_type,
321        engine,
322        control,
323    )
324}
325
326fn cast_resolved_value(
327    value: &Value,
328    source_ty: Option<&str>,
329    target_ty: &str,
330    target_column_type: Option<&ColumnType>,
331    engine: Option<&dyn EngineHook>,
332    control: &ProductionControl<'_>,
333) -> Result<Produced<Value>> {
334    if target_column_type.is_some_and(regrole_array_type) {
335        let source_column_type = source_ty
336            .map(|name| optional_type_name(name, control))
337            .transpose()?
338            .flatten();
339        let source_name = source_column_type
340            .as_deref()
341            .map(array_leaf_type)
342            .map(|ty| ty.sql_name_with_control(control))
343            .transpose()?;
344        return cast_array(
345            value,
346            source_name.as_ref().map(|name| name.as_str()),
347            "regrole",
348            "regrole[]",
349            engine,
350            control,
351        );
352    }
353    // `find_coercion_pathway` casts an OID alias to a string type through its output function, as `CoerceViaIO`, and the string type then applies its own modifier; an array does so element by element.
354    if let Some(ColumnType::Array(element)) = target_column_type {
355        if is_string_type(element) {
356            if let Some(source_ty) = source_ty
357                .map(|source| optional_type_name(source, control))
358                .transpose()?
359                .flatten()
360            {
361                if let Some(elements) =
362                    format_regtype_elements_with_control(value, &source_ty, engine, control)?
363                {
364                    let (elements, _memory) = elements.into_parts();
365                    return cast_value_from_with_control(
366                        &Value::Array(elements),
367                        target_ty,
368                        Some("text[]"),
369                        control,
370                    );
371                }
372            }
373        }
374    }
375    let text_target = target_ty.eq_ignore_ascii_case("text");
376    if text_target || target_column_type.is_some_and(is_string_type) {
377        if let Some(source_ty) = source_ty
378            .map(|source| optional_type_name(source, control))
379            .transpose()?
380            .flatten()
381        {
382            if let Some(text) =
383                format_regtype_value_with_control(value, &source_ty, engine, control)?
384            {
385                let (text, memory) = text.into_parts();
386                if text_target {
387                    return Ok(control.finish(Value::Str(text), memory)?);
388                }
389                return cast_value_from_with_control(
390                    &Value::Str(text),
391                    target_ty,
392                    Some("text"),
393                    control,
394                );
395            }
396        }
397    }
398    if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
399        let oid = resolve_regobject_input(name, target_ty, target_column_type, engine, control)?;
400        if let Some(oid) = oid {
401            return Ok(control.finish(Value::Int(oid), control.empty_reservation())?);
402        }
403    }
404    let zoned = timezone::cast_local_timestamp(value, target_column_type, engine, control)?;
405    cast_value_from_with_control(
406        zoned.as_ref().unwrap_or(value),
407        target_ty,
408        source_ty,
409        control,
410    )
411}
412
413fn resolve_regobject_input(
414    name: &str,
415    target_ty: &str,
416    target_column_type: Option<&ColumnType>,
417    engine: &dyn EngineHook,
418    control: &ProductionControl<'_>,
419) -> Result<Option<i64>> {
420    enum ObjectKind {
421        Relation,
422        RoutineName,
423        Routine,
424        Role,
425        Namespace,
426        Type,
427    }
428    let kind = if target_ty.eq_ignore_ascii_case("regclass") {
429        ObjectKind::Relation
430    } else if matches!(target_column_type, Some(ColumnType::Regproc))
431        || target_ty.eq_ignore_ascii_case("regproc")
432    {
433        ObjectKind::RoutineName
434    } else if target_ty.eq_ignore_ascii_case("regprocedure") {
435        ObjectKind::Routine
436    } else if target_ty.eq_ignore_ascii_case("regrole") {
437        ObjectKind::Role
438    } else if matches!(target_column_type, Some(ColumnType::Regnamespace)) {
439        ObjectKind::Namespace
440    } else if matches!(target_column_type, Some(ColumnType::Regtype)) {
441        ObjectKind::Type
442    } else {
443        return Ok(None);
444    };
445    let oid = match kind {
446        ObjectKind::Relation => engine.resolve_regclass_input(name)?,
447        // `regprocin` reports its own missing and ambiguous names.
448        ObjectKind::RoutineName => return engine.resolve_regproc(name),
449        ObjectKind::Routine => engine.resolve_regprocedure_input(name)?,
450        ObjectKind::Role => engine.resolve_regrole(name)?,
451        ObjectKind::Namespace => engine.resolve_regnamespace(name)?,
452        ObjectKind::Type => engine.resolve_regtype_input(name)?,
453    };
454    control.check()?;
455    if oid.is_some() || matches!(kind, ObjectKind::Type) {
456        return Ok(oid);
457    }
458    let (sqlstate, message) = match kind {
459        ObjectKind::Relation => ("42P01", format!("relation \"{name}\" does not exist")),
460        ObjectKind::Routine => ("42883", format!("function \"{name}\" does not exist")),
461        ObjectKind::Role => ("42704", format!("role \"{name}\" does not exist")),
462        ObjectKind::Namespace => ("3F000", format!("schema \"{name}\" does not exist")),
463        ObjectKind::Type | ObjectKind::RoutineName => {
464            unreachable!("regtype and regproc input return before reporting a missing object")
465        }
466    };
467    Err(SQLError::Routine {
468        sqlstate: sqlstate.into(),
469        message,
470    })
471}
472
473/// Whether an array cast needs the catalog for its elements: domains, enums, composites and the OID alias types whose input functions read catalog names. `regrole[]` has its own path above.
474fn requires_catalog_array_cast(ty: &ColumnType) -> bool {
475    match ty {
476        ColumnType::Domain { .. }
477        | ColumnType::Regtype
478        | ColumnType::Regclass
479        | ColumnType::Regproc
480        | ColumnType::Regprocedure
481        | ColumnType::Regnamespace
482        | ColumnType::Enum(_)
483        | ColumnType::Composite(_)
484        | ColumnType::TimestampTz
485        | ColumnType::TimestampTzPrecision(_) => true,
486        ColumnType::Array(element) => requires_catalog_array_cast(element),
487        _ => false,
488    }
489}
490
491fn cast_catalog_array(
492    value: &Value,
493    source: Option<&str>,
494    target: &ColumnType,
495    engine: &dyn EngineHook,
496    control: &ProductionControl<'_>,
497) -> Result<Produced<Value>> {
498    if matches!(value, Value::Null) {
499        return Ok(control.finish(Value::Null, control.empty_reservation())?);
500    }
501    let source_element = source.map(|name| name.trim_end_matches("[]"));
502    let leaf = array_leaf_type(target);
503    let target_element = control.copy_text(&leaf.catalog_name())?;
504    let target_name = target.sql_name_with_control(control)?;
505    cast_array(
506        value,
507        source_element,
508        &target_element,
509        &target_name,
510        Some(engine),
511        control,
512    )
513}
514
515fn cast_array(
516    value: &Value,
517    source: Option<&str>,
518    target_element: &str,
519    target_name: &str,
520    engine: Option<&dyn EngineHook>,
521    control: &ProductionControl<'_>,
522) -> Result<Produced<Value>> {
523    let parsed;
524    let array = match value {
525        Value::Array(array) => array,
526        Value::Str(text) => {
527            parsed = parse_pg_array_literal_with_control(text, control)?;
528            &parsed
529        }
530        other => {
531            return Err(SQLError::TypeMismatch(format!(
532                "CAST AS {target_name}: expected array, got {other:?}"
533            )))
534        }
535    };
536    let elements = cast_array_elements(array.elements(), source, target_element, engine, control)?;
537    let array = rebuild_array(array, elements, control)?
538        .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()))?;
539    let (array, memory) = array.into_parts();
540    Ok(control.finish(Value::Array(array), memory)?)
541}
542
543fn cast_array_elements(
544    values: &[Value],
545    source: Option<&str>,
546    target: &str,
547    engine: Option<&dyn EngineHook>,
548    control: &ProductionControl<'_>,
549) -> Result<Produced<Vec<Value>>> {
550    let mut output = ProductionVec::new(*control);
551    output.reserve(values.len())?;
552    for value in values {
553        let value = match value {
554            Value::List(values) => {
555                let (values, memory) =
556                    cast_array_elements(values, source, target, engine, control)?.into_parts();
557                control.finish(Value::List(values), memory)?
558            }
559            value => cast_value_with_type_resolution_with_control(
560                value, source, target, engine, control,
561            )?,
562        };
563        output.push_produced(value)?;
564    }
565    Ok(output.finish()?)
566}
567
568pub(super) fn rebuild_array(
569    source: &ArrayValue,
570    elements: Produced<Vec<Value>>,
571    control: &ProductionControl<'_>,
572) -> Result<Option<Produced<ArrayValue>>> {
573    let mut bounds = ProductionVec::new(*control);
574    bounds.reserve(source.lower_bounds().len())?;
575    for bound in source.lower_bounds() {
576        bounds.push_copy(*bound)?;
577    }
578    Ok(ArrayValue::with_lower_bounds_with_control(
579        elements,
580        bounds.finish()?,
581        control,
582    )?)
583}
584
585#[cfg(test)]
586mod tests;