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_value_with_control, EngineHook};
16use crate::{
17    ast::ColumnType,
18    error::{Result, SQLError},
19};
20
21#[must_use]
22pub fn coercion_type_name(ty: &ColumnType) -> String {
23    coercion_type_name_with_control(ty, &ProductionControl::uncontrolled())
24        .expect("ordinary coercion type name")
25        .into_uncontrolled()
26        .expect("ordinary coercion type name owner")
27}
28
29fn coercion_type_name_with_control(
30    ty: &ColumnType,
31    control: &ProductionControl<'_>,
32) -> Result<Produced<String>> {
33    control.check()?;
34    match ty {
35        ColumnType::Domain { base, .. } => coercion_type_name_with_control(base, control),
36        ColumnType::Array(element) => {
37            let element = coercion_type_name_with_control(element, control)?;
38            let mut name = ProductionString::new(*control);
39            name.push_str(&element)?;
40            name.push_str("[]")?;
41            Ok(name.finish()?)
42        }
43        _ => Ok(ty.sql_name_with_control(control)?),
44    }
45}
46
47fn regrole_array_type(ty: &ColumnType) -> bool {
48    match ty {
49        ColumnType::Array(element) => {
50            matches!(element.as_ref(), ColumnType::Regrole) || regrole_array_type(element)
51        }
52        _ => false,
53    }
54}
55
56fn array_leaf_type(ty: &ColumnType) -> &ColumnType {
57    match ty {
58        ColumnType::Array(element) => array_leaf_type(element),
59        _ => ty,
60    }
61}
62
63fn optional_type_name(
64    name: &str,
65    control: &ProductionControl<'_>,
66) -> Result<Option<Produced<ColumnType>>> {
67    match ColumnType::from_sql_name_with_control(name, control) {
68        Ok(ty) => Ok(Some(ty)),
69        Err(error) if matches!(error.sqlstate(), Some("53200" | "57014")) => Err(error),
70        Err(_) => Ok(None),
71    }
72}
73
74/// Cast a value after resolving catalog-owned types and enforcing domain constraints before exposing a base-type carrier.
75pub fn cast_value_with_type_resolution(
76    value: &Value,
77    source_ty: Option<&str>,
78    target_ty: &str,
79    engine: Option<&dyn EngineHook>,
80) -> Result<Value> {
81    cast_value_with_type_resolution_with_control(
82        value,
83        source_ty,
84        target_ty,
85        engine,
86        &ProductionControl::uncontrolled(),
87    )?
88    .into_uncontrolled()
89    .map_err(|_| SQLError::Internal("ordinary catalog cast owner".into()))
90}
91
92/// Resolve catalog inputs at their external handoff, then admit SQL-owned names, element conversions and output before constructing them.
93pub fn cast_value_with_type_resolution_with_control(
94    value: &Value,
95    source_ty: Option<&str>,
96    target_ty: &str,
97    engine: Option<&dyn EngineHook>,
98    control: &ProductionControl<'_>,
99) -> Result<Produced<Value>> {
100    control.check()?;
101    let resolved_target = engine
102        .map(|engine| engine.resolve_type_name(target_ty))
103        .transpose()
104        .map_err(SQLError::Internal)?
105        .flatten()
106        .map(|ty| ty.retain_external_with_control(control))
107        .transpose()?;
108    control.check()?;
109    if let (Some(engine), Some(target)) = (engine, resolved_target.as_deref()) {
110        if let Some(value) = engine.cast_domain(value, source_ty, target)? {
111            return Ok(control.retain_external_value(value)?);
112        }
113        control.check()?;
114        if matches!(target, ColumnType::Array(_)) && requires_catalog_array_cast(target) {
115            return cast_catalog_array(value, source_ty, target, engine, control);
116        }
117    }
118    let resolved_source = match (engine, source_ty) {
119        (Some(engine), Some(source_ty)) => engine
120            .resolve_type_name(source_ty)
121            .map_err(SQLError::Internal)?
122            .map(|ty| {
123                let ty = ty.retain_external_with_control(control)?;
124                coercion_type_name_with_control(&ty, control)
125            })
126            .transpose()?,
127        _ => None,
128    };
129    let source_ty = resolved_source
130        .as_ref()
131        .map(|name| name.as_str())
132        .or(source_ty);
133    let target_name = resolved_target
134        .as_ref()
135        .map(|ty| coercion_type_name_with_control(ty, control))
136        .transpose()?;
137    let target_ty = target_name.as_ref().map_or(target_ty, |name| name.as_str());
138    let parsed_target = if resolved_target.is_some() {
139        None
140    } else {
141        optional_type_name(target_ty, control)?
142    };
143    let target_column_type = resolved_target.as_deref().or(parsed_target.as_deref());
144    cast_resolved_value(
145        value,
146        source_ty,
147        target_ty,
148        target_column_type,
149        engine,
150        control,
151    )
152}
153
154fn cast_resolved_value(
155    value: &Value,
156    source_ty: Option<&str>,
157    target_ty: &str,
158    target_column_type: Option<&ColumnType>,
159    engine: Option<&dyn EngineHook>,
160    control: &ProductionControl<'_>,
161) -> Result<Produced<Value>> {
162    if target_column_type.is_some_and(regrole_array_type) {
163        let source_column_type = source_ty
164            .map(|name| optional_type_name(name, control))
165            .transpose()?
166            .flatten();
167        let source_name = source_column_type
168            .as_deref()
169            .map(array_leaf_type)
170            .map(|ty| ty.sql_name_with_control(control))
171            .transpose()?;
172        return cast_array(
173            value,
174            source_name.as_ref().map(|name| name.as_str()),
175            "regrole",
176            "regrole[]",
177            engine,
178            control,
179        );
180    }
181    if target_ty.eq_ignore_ascii_case("text") {
182        if let Some(source_ty) = source_ty
183            .map(|source| optional_type_name(source, control))
184            .transpose()?
185            .flatten()
186        {
187            if let Some(text) =
188                format_regtype_value_with_control(value, &source_ty, engine, control)?
189            {
190                let (text, memory) = text.into_parts();
191                return Ok(control.finish(Value::Str(text), memory)?);
192            }
193        }
194    }
195    if let (Some(engine), Value::Str(name) | Value::FixedChar(name)) = (engine, value) {
196        let oid = resolve_regobject_input(name, target_ty, target_column_type, engine, control)?;
197        if let Some(oid) = oid {
198            return Ok(control.finish(Value::Int(oid), control.empty_reservation())?);
199        }
200    }
201    cast_value_from_with_control(value, target_ty, source_ty, control)
202}
203
204fn resolve_regobject_input(
205    name: &str,
206    target_ty: &str,
207    target_column_type: Option<&ColumnType>,
208    engine: &dyn EngineHook,
209    control: &ProductionControl<'_>,
210) -> Result<Option<i64>> {
211    enum ObjectKind {
212        Relation,
213        Routine,
214        Role,
215        Namespace,
216        Type,
217    }
218    let kind = if target_ty.eq_ignore_ascii_case("regclass") {
219        ObjectKind::Relation
220    } else if target_ty.eq_ignore_ascii_case("regprocedure") {
221        ObjectKind::Routine
222    } else if target_ty.eq_ignore_ascii_case("regrole") {
223        ObjectKind::Role
224    } else if matches!(target_column_type, Some(ColumnType::Regnamespace)) {
225        ObjectKind::Namespace
226    } else if matches!(target_column_type, Some(ColumnType::Regtype)) {
227        ObjectKind::Type
228    } else {
229        return Ok(None);
230    };
231    let oid = match kind {
232        ObjectKind::Relation => engine.resolve_regclass_input(name)?,
233        ObjectKind::Routine => engine
234            .resolve_regprocedure(name)
235            .map_err(SQLError::Internal)?,
236        ObjectKind::Role => engine.resolve_regrole(name)?,
237        ObjectKind::Namespace => engine.resolve_regnamespace(name)?,
238        ObjectKind::Type => engine.resolve_regtype_input(name)?,
239    };
240    control.check()?;
241    if oid.is_some() || matches!(kind, ObjectKind::Type) {
242        return Ok(oid);
243    }
244    let (sqlstate, message) = match kind {
245        ObjectKind::Relation => ("42P01", format!("relation \"{name}\" does not exist")),
246        ObjectKind::Routine => ("42883", format!("function {name} does not exist")),
247        ObjectKind::Role => ("42704", format!("role \"{name}\" does not exist")),
248        ObjectKind::Namespace => ("3F000", format!("schema \"{name}\" does not exist")),
249        ObjectKind::Type => unreachable!("unresolved regtype uses ordinary conversion"),
250    };
251    Err(SQLError::Routine {
252        sqlstate: sqlstate.into(),
253        message,
254    })
255}
256
257fn requires_catalog_array_cast(ty: &ColumnType) -> bool {
258    match ty {
259        ColumnType::Domain { .. } | ColumnType::Regtype => true,
260        ColumnType::Array(element) => requires_catalog_array_cast(element),
261        _ => false,
262    }
263}
264
265fn cast_catalog_array(
266    value: &Value,
267    source: Option<&str>,
268    target: &ColumnType,
269    engine: &dyn EngineHook,
270    control: &ProductionControl<'_>,
271) -> Result<Produced<Value>> {
272    if matches!(value, Value::Null) {
273        return Ok(control.finish(Value::Null, control.empty_reservation())?);
274    }
275    let source_element = source.map(|name| name.trim_end_matches("[]"));
276    let target_element = array_leaf_type(target).sql_name_with_control(control)?;
277    let target_name = target.sql_name_with_control(control)?;
278    cast_array(
279        value,
280        source_element,
281        &target_element,
282        &target_name,
283        Some(engine),
284        control,
285    )
286}
287
288fn cast_array(
289    value: &Value,
290    source: Option<&str>,
291    target_element: &str,
292    target_name: &str,
293    engine: Option<&dyn EngineHook>,
294    control: &ProductionControl<'_>,
295) -> Result<Produced<Value>> {
296    let parsed;
297    let array = match value {
298        Value::Array(array) => array,
299        Value::Str(text) => {
300            parsed = parse_pg_array_literal_with_control(text, control)?;
301            &parsed
302        }
303        other => {
304            return Err(SQLError::TypeMismatch(format!(
305                "CAST AS {target_name}: expected array, got {other:?}"
306            )))
307        }
308    };
309    let elements = cast_array_elements(array.elements(), source, target_element, engine, control)?;
310    let array = rebuild_array(array, elements, control)?
311        .ok_or_else(|| SQLError::TypeMismatch("array dimensions changed during cast".into()))?;
312    let (array, memory) = array.into_parts();
313    Ok(control.finish(Value::Array(array), memory)?)
314}
315
316fn cast_array_elements(
317    values: &[Value],
318    source: Option<&str>,
319    target: &str,
320    engine: Option<&dyn EngineHook>,
321    control: &ProductionControl<'_>,
322) -> Result<Produced<Vec<Value>>> {
323    let mut output = ProductionVec::new(*control);
324    output.reserve(values.len())?;
325    for value in values {
326        let value = match value {
327            Value::List(values) => {
328                let (values, memory) =
329                    cast_array_elements(values, source, target, engine, control)?.into_parts();
330                control.finish(Value::List(values), memory)?
331            }
332            value => cast_value_with_type_resolution_with_control(
333                value, source, target, engine, control,
334            )?,
335        };
336        output.push_produced(value)?;
337    }
338    Ok(output.finish()?)
339}
340
341pub(super) fn rebuild_array(
342    source: &ArrayValue,
343    elements: Produced<Vec<Value>>,
344    control: &ProductionControl<'_>,
345) -> Result<Option<Produced<ArrayValue>>> {
346    let mut bounds = ProductionVec::new(*control);
347    bounds.reserve(source.lower_bounds().len())?;
348    for bound in source.lower_bounds() {
349        bounds.push_copy(*bound)?;
350    }
351    Ok(ArrayValue::with_lower_bounds_with_control(
352        elements,
353        bounds.finish()?,
354        control,
355    )?)
356}
357
358#[cfg(test)]
359mod tests;