Skip to main content

uqa_sql/assignment/
conversion.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Declared-column assignment coercion for scalar, array, vector, and temporal values.
8
9use super::AssignmentContext;
10use crate::{ColumnType, SQLError};
11use uqa_core::{ArrayValue, DecimalValue, TemporalValue, Value};
12
13mod carrier;
14pub(super) use carrier::normalize_existing;
15pub use carrier::{contains_legacy_vectors, normalize_legacy_vector_carrier_with_control};
16
17pub fn coerce_assignment_value(
18    context: &dyn AssignmentContext,
19    value: Value,
20    target: &ColumnType,
21    source: Option<&ColumnType>,
22) -> Result<Value, SQLError> {
23    if source.is_some_and(|source| same_catalog_value_type(source, target)) {
24        return normalize_existing(value, target);
25    }
26    if matches!(target, ColumnType::Domain { .. }) {
27        let source = source.map(ColumnType::catalog_name);
28        if let Some(value) =
29            super::domain::assign_domain_value_from(context, &value, source.as_deref(), target)?
30        {
31            return Ok(value);
32        }
33    }
34    let value = if target.is_character_string() {
35        source
36            .map(|source| crate::expr::format_regtype_value(&value, source, Some(context)))
37            .transpose()?
38            .flatten()
39            .map(Value::Str)
40            .unwrap_or(value)
41    } else {
42        value
43    };
44    convert_value_to_column_type_with_context(context, value, target)
45}
46
47fn same_catalog_value_type(source: &ColumnType, target: &ColumnType) -> bool {
48    match (source, target) {
49        (ColumnType::Domain { oid: source, .. }, ColumnType::Domain { oid: target, .. }) => {
50            source == target
51        }
52        (ColumnType::Composite(source), ColumnType::Composite(target)) => source.oid == target.oid,
53        (ColumnType::Domain { base, .. }, ColumnType::Composite(_)) => {
54            same_catalog_value_type(base, target)
55        }
56        (ColumnType::Array(source), ColumnType::Array(target)) => {
57            same_catalog_value_type(source, target)
58        }
59        _ => false,
60    }
61}
62
63pub fn coerce_json_value(value: Value, jsonb: bool) -> Result<Value, SQLError> {
64    crate::expr::cast_value(&value, if jsonb { "jsonb" } else { "json" })
65}
66
67pub fn convert_declared_value_to_column_type(
68    context: &dyn AssignmentContext,
69    value: Value,
70    source_ty: &ColumnType,
71    target_ty: &ColumnType,
72) -> Result<Value, SQLError> {
73    match (source_ty, target_ty) {
74        (ColumnType::Domain { base, .. }, target) => {
75            convert_declared_value_to_column_type(context, value, base, target)
76        }
77        (source, ColumnType::Domain { base, .. }) => {
78            convert_declared_value_to_column_type(context, value, source, base)
79        }
80        (ColumnType::Array(source), ColumnType::Array(target)) => {
81            let Value::Array(array) = value else {
82                return Err(SQLError::TypeMismatch(format!(
83                    "cannot cast a non-array value to {}[]",
84                    column_type_name(target)
85                )));
86            };
87            let source = array_scalar_type(source);
88            let target = array_scalar_type(target);
89            let converted =
90                convert_declared_array_elements(context, array.elements(), source, target)?;
91            ArrayValue::with_lower_bounds(converted, array.lower_bounds().to_vec())
92                .map(|array| {
93                    array.with_element_type_oid(Some(crate::catalog::type_metadata::pg_type_oid(
94                        target,
95                    ) as u32))
96                })
97                .map(Value::Array)
98                .ok_or_else(|| {
99                    SQLError::TypeMismatch(
100                        "multidimensional arrays must have matching dimensions".into(),
101                    )
102                })
103        }
104        (ColumnType::Range(source), ColumnType::Range(target)) if source == target => {
105            crate::expr::cast_value_from(&value, target.range_name(), Some(source.range_name()))
106        }
107        (ColumnType::Range(source), ColumnType::Multirange(target)) if source == target => {
108            crate::expr::cast_value_from(
109                &value,
110                target.multirange_name(),
111                Some(source.range_name()),
112            )
113        }
114        (ColumnType::Multirange(source), ColumnType::Multirange(target)) if source == target => {
115            crate::expr::cast_value_from(
116                &value,
117                target.multirange_name(),
118                Some(source.multirange_name()),
119            )
120        }
121        (_, ColumnType::Range(_) | ColumnType::Multirange(_)) => {
122            Err(SQLError::TypeMismatch(format!(
123                "column cannot be cast automatically from type {} to type {}",
124                column_type_name(source_ty),
125                column_type_name(target_ty)
126            )))
127        }
128        (source, ColumnType::Oid)
129            if matches!(
130                source,
131                ColumnType::SmallInteger
132                    | ColumnType::Integer
133                    | ColumnType::BigInteger
134                    | ColumnType::Oid
135                    | ColumnType::Regproc
136                    | ColumnType::Regprocedure
137                    | ColumnType::Regclass
138                    | ColumnType::Regcollation
139                    | ColumnType::Regnamespace
140                    | ColumnType::Regrole
141                    | ColumnType::Regtype
142            ) =>
143        {
144            crate::expr::cast_value_from(&value, "oid", Some(column_type_name(source)))
145        }
146        (ColumnType::Xid, ColumnType::Xid) => Ok(value),
147        (ColumnType::Bytea, ColumnType::Bytea) => Ok(value),
148        (_, ColumnType::Oid | ColumnType::Xid | ColumnType::Bytea) => {
149            Err(SQLError::TypeMismatch(format!(
150                "column cannot be cast automatically from type {} to type {}",
151                column_type_name(source_ty),
152                column_type_name(target_ty)
153            )))
154        }
155        _ => convert_value_to_column_type_with_context(context, value, target_ty),
156    }
157}
158
159/// Whether converting a value to `ty` needs the statement's catalog or session: enum labels, domain constraints, composite attributes, role names or the current time zone.
160pub(crate) fn type_requires_catalog_resolution(ty: &ColumnType) -> bool {
161    match ty {
162        // The OID alias types read a name through the catalog, so an assigned text value becomes the object's OID, as `regclassin` and its siblings read it.
163        ColumnType::Regrole
164        | ColumnType::Regclass
165        | ColumnType::Regtype
166        | ColumnType::Regproc
167        | ColumnType::Regprocedure
168        | ColumnType::Regcollation
169        | ColumnType::Regnamespace
170        | ColumnType::Domain { .. }
171        | ColumnType::Enum(_)
172        | ColumnType::Composite(_)
173        | ColumnType::TimestampTz
174        | ColumnType::TimestampTzPrecision(_) => true,
175        ColumnType::Array(element) => type_requires_catalog_resolution(element),
176        _ => false,
177    }
178}
179
180pub fn convert_value_to_column_type_with_context(
181    context: &dyn AssignmentContext,
182    value: Value,
183    ty: &ColumnType,
184) -> Result<Value, SQLError> {
185    // Assignment to a string type calls the enum output function, which reads the current label.
186    let value = if textual_target(ty) && crate::expr::enums::contains_enum_carrier(&value) {
187        crate::expr::enums::render_enum_labels(
188            crate::expr::EngineHook::enum_labels(context),
189            &value,
190        )?
191    } else {
192        value
193    };
194    if let Some(value) = super::domain::assign_domain_value(context, &value, ty)? {
195        return Ok(value);
196    }
197    if matches!(value, Value::Null) {
198        return Ok(Value::Null);
199    }
200    if let ColumnType::Array(element) = ty {
201        if type_requires_catalog_resolution(element) {
202            return convert_catalog_array(context, value, element);
203        }
204    }
205    if type_requires_catalog_resolution(ty) {
206        return crate::expr::cast_value_with_type_resolution(
207            &value,
208            None,
209            &ty.catalog_name(),
210            Some(context),
211        );
212    }
213    convert_value_to_column_type(value, ty)
214}
215
216fn textual_target(ty: &ColumnType) -> bool {
217    match ty {
218        ColumnType::Array(element) => textual_target(element),
219        other => other.is_character_string(),
220    }
221}
222
223fn convert_catalog_array(
224    context: &dyn AssignmentContext,
225    value: Value,
226    element: &ColumnType,
227) -> Result<Value, SQLError> {
228    let array = match value {
229        Value::Array(array) => array,
230        Value::Str(text) => crate::expr::parse_pg_array_literal(&text)?,
231        other => {
232            return Err(SQLError::TypeMismatch(format!(
233                "expected an array, got {other:?}"
234            )))
235        }
236    };
237    let values = convert_catalog_array_elements(context, array.elements(), element)?;
238    ArrayValue::with_lower_bounds(values, array.lower_bounds().to_vec())
239        .map(|array| {
240            array.with_element_type_oid(Some(crate::catalog::type_metadata::pg_type_oid(
241                array_scalar_type(element),
242            ) as u32))
243        })
244        .map(Value::Array)
245        .ok_or_else(|| {
246            SQLError::TypeMismatch("multidimensional arrays must have matching dimensions".into())
247        })
248}
249
250fn convert_catalog_array_elements(
251    context: &dyn AssignmentContext,
252    values: &[Value],
253    element: &ColumnType,
254) -> Result<Vec<Value>, SQLError> {
255    let mut element = element;
256    while let ColumnType::Array(nested) = element {
257        element = nested;
258    }
259    values
260        .iter()
261        .map(|value| match value {
262            Value::List(values) => {
263                convert_catalog_array_elements(context, values, element).map(Value::List)
264            }
265            value => convert_value_to_column_type_with_context(context, value.clone(), element),
266        })
267        .collect()
268}
269
270mod production;
271pub use production::{convert_value_to_column_type_with_control, numeric_field_overflow};
272
273pub fn convert_value_to_column_type(value: Value, ty: &ColumnType) -> Result<Value, SQLError> {
274    let control = uqa_core::memory::ProductionControl::uncontrolled();
275    convert_value_to_column_type_with_control(control.finish(value, None)?, ty, &control)?
276        .into_uncontrolled()
277        .map_err(|_| SQLError::Internal("ordinary assignment production owner".into()))
278}
279
280fn convert_declared_array_elements(
281    context: &dyn AssignmentContext,
282    elements: &[Value],
283    source_type: &ColumnType,
284    target_type: &ColumnType,
285) -> Result<Vec<Value>, SQLError> {
286    elements
287        .iter()
288        .cloned()
289        .map(|element| match element {
290            Value::List(nested) => {
291                convert_declared_array_elements(context, &nested, source_type, target_type)
292                    .map(Value::List)
293            }
294            scalar => {
295                convert_declared_value_to_column_type(context, scalar, source_type, target_type)
296            }
297        })
298        .collect()
299}
300
301fn array_scalar_type(mut ty: &ColumnType) -> &ColumnType {
302    while let ColumnType::Array(element) = ty {
303        ty = element;
304    }
305    ty
306}
307
308pub fn validate_vector_dimensions(expected: u32, actual: usize) -> Result<(), SQLError> {
309    let expected = usize::try_from(expected).map_err(|_| {
310        SQLError::TypeMismatch(format!(
311            "declared vector dimension {expected} exceeds the platform usize range"
312        ))
313    })?;
314    if actual == expected {
315        Ok(())
316    } else {
317        Err(SQLError::VectorDimMismatch { expected, actual })
318    }
319}
320
321pub use crate::catalog::type_metadata::column_type_name;
322
323pub use crate::expr::value_to_text;
324
325pub fn json_to_core_value(json: serde_json::Value) -> Value {
326    match json {
327        serde_json::Value::Null => Value::Null,
328        serde_json::Value::Bool(b) => Value::Bool(b),
329        serde_json::Value::Number(n) => {
330            if let Some(i) = n.as_i64() {
331                Value::Int(i)
332            } else if let Some(d) = DecimalValue::parse(&n.to_string()) {
333                Value::Decimal(d)
334            } else if let Some(f) = n.as_f64() {
335                Value::Float(f)
336            } else {
337                Value::Null
338            }
339        }
340        serde_json::Value::String(s) => Value::Str(s),
341        serde_json::Value::Array(items) => {
342            Value::List(items.into_iter().map(json_to_core_value).collect())
343        }
344        serde_json::Value::Object(obj) => {
345            if let Ok(temporal) =
346                serde_json::from_value::<TemporalValue>(serde_json::Value::Object(obj.clone()))
347            {
348                return Value::Temporal(temporal);
349            }
350            Value::Map(
351                obj.into_iter()
352                    .map(|(k, v)| (k, json_to_core_value(v)))
353                    .collect(),
354            )
355        }
356    }
357}
358
359pub use crate::expr::core_value_to_json;
360
361pub fn json_table_value_to_text(value: &serde_json::Value) -> Value {
362    match value {
363        serde_json::Value::Null => Value::Null,
364        serde_json::Value::String(s) => Value::Str(s.clone()),
365        serde_json::Value::Bool(b) => Value::Str(b.to_string()),
366        serde_json::Value::Number(n) => Value::Str(n.to_string()),
367        serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Str(value.to_string()),
368    }
369}
370
371pub fn json_table_arg(value: &Value, name: &str) -> Result<serde_json::Value, SQLError> {
372    match value {
373        Value::Json(s) | Value::JsonB(s) | Value::Str(s) => {
374            serde_json::from_str::<serde_json::Value>(s)
375                .map_err(|e| SQLError::TypeMismatch(format!("{name}: invalid JSON: {e}")))
376        }
377        other => core_value_to_json(other),
378    }
379}