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_from(context, value, target, source)
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::InternalChar, ColumnType::InternalChar)
54        | (ColumnType::PgNodeTree, ColumnType::PgNodeTree) => true,
55        (ColumnType::Domain { base, .. }, _) => same_catalog_value_type(base, target),
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    if same_catalog_value_type(source_ty, target_ty) {
74        return normalize_existing(value, target_ty);
75    }
76    match (source_ty, target_ty) {
77        (ColumnType::Domain { base, .. }, target) => {
78            convert_declared_value_to_column_type(context, value, base, target)
79        }
80        (source, ColumnType::Domain { base, .. }) => {
81            convert_declared_value_to_column_type(context, value, source, base)
82        }
83        (ColumnType::Array(source), ColumnType::Array(target)) => {
84            let Value::Array(array) = value else {
85                return Err(SQLError::TypeMismatch(format!(
86                    "cannot cast a non-array value to {}[]",
87                    column_type_name(target)
88                )));
89            };
90            let source = array_scalar_type(source);
91            let target = array_scalar_type(target);
92            let converted =
93                convert_declared_array_elements(context, array.elements(), source, target)?;
94            ArrayValue::with_lower_bounds(converted, array.lower_bounds().to_vec())
95                .map(|array| {
96                    array.with_element_type_oid(Some(crate::catalog::type_metadata::pg_type_oid(
97                        target,
98                    ) as u32))
99                })
100                .map(Value::Array)
101                .ok_or_else(|| {
102                    SQLError::TypeMismatch(
103                        "multidimensional arrays must have matching dimensions".into(),
104                    )
105                })
106        }
107        (ColumnType::Range(source), ColumnType::Range(target)) if source == target => {
108            crate::expr::cast_value_from(&value, target.range_name(), Some(source.range_name()))
109        }
110        (ColumnType::Range(source), ColumnType::Multirange(target)) if source == target => {
111            crate::expr::cast_value_from(
112                &value,
113                target.multirange_name(),
114                Some(source.range_name()),
115            )
116        }
117        (ColumnType::Multirange(source), ColumnType::Multirange(target)) if source == target => {
118            crate::expr::cast_value_from(
119                &value,
120                target.multirange_name(),
121                Some(source.multirange_name()),
122            )
123        }
124        (_, ColumnType::Range(_) | ColumnType::Multirange(_)) => {
125            Err(SQLError::TypeMismatch(format!(
126                "column cannot be cast automatically from type {} to type {}",
127                column_type_name(source_ty),
128                column_type_name(target_ty)
129            )))
130        }
131        (source, ColumnType::Oid)
132            if matches!(
133                source,
134                ColumnType::SmallInteger
135                    | ColumnType::Integer
136                    | ColumnType::BigInteger
137                    | ColumnType::Oid
138                    | ColumnType::Regproc
139                    | ColumnType::Regprocedure
140                    | ColumnType::Regclass
141                    | ColumnType::Regcollation
142                    | ColumnType::Regnamespace
143                    | ColumnType::Regrole
144                    | ColumnType::Regtype
145            ) =>
146        {
147            crate::expr::cast_value_from(&value, "oid", Some(column_type_name(source)))
148        }
149        (ColumnType::Xid, ColumnType::Xid) => Ok(value),
150        (ColumnType::Bytea, ColumnType::Bytea) => Ok(value),
151        (_, ColumnType::Oid | ColumnType::Xid | ColumnType::Bytea) => {
152            Err(SQLError::TypeMismatch(format!(
153                "column cannot be cast automatically from type {} to type {}",
154                column_type_name(source_ty),
155                column_type_name(target_ty)
156            )))
157        }
158        _ => convert_value_to_column_type_with_context_from(
159            context,
160            value,
161            target_ty,
162            Some(source_ty),
163        ),
164    }
165}
166
167/// 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.
168pub(crate) fn type_requires_catalog_resolution(ty: &ColumnType) -> bool {
169    match ty {
170        // 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.
171        ColumnType::Regrole
172        | ColumnType::Regclass
173        | ColumnType::Regtype
174        | ColumnType::Regproc
175        | ColumnType::Regprocedure
176        | ColumnType::Regcollation
177        | ColumnType::Regnamespace
178        | ColumnType::Domain { .. }
179        | ColumnType::Enum(_)
180        | ColumnType::Composite(_)
181        | ColumnType::TimestampTz
182        | ColumnType::TimestampTzPrecision(_) => true,
183        ColumnType::Array(element) => type_requires_catalog_resolution(element),
184        _ => false,
185    }
186}
187
188pub fn convert_value_to_column_type_with_context(
189    context: &dyn AssignmentContext,
190    value: Value,
191    ty: &ColumnType,
192) -> Result<Value, SQLError> {
193    convert_value_to_column_type_with_context_from(context, value, ty, None)
194}
195
196fn convert_value_to_column_type_with_context_from(
197    context: &dyn AssignmentContext,
198    value: Value,
199    ty: &ColumnType,
200    source: Option<&ColumnType>,
201) -> Result<Value, SQLError> {
202    // Assignment to a string type calls the enum output function, which reads the current label.
203    let value = if textual_target(ty) && crate::expr::enums::contains_enum_carrier(&value) {
204        crate::expr::enums::render_enum_labels(
205            crate::expr::EngineHook::enum_labels(context),
206            &value,
207        )?
208    } else {
209        value
210    };
211    if let Some(value) = super::domain::assign_domain_value(context, &value, ty)? {
212        return Ok(value);
213    }
214    if matches!(value, Value::Null) {
215        return Ok(Value::Null);
216    }
217    if let ColumnType::Array(element) = ty {
218        if type_requires_catalog_resolution(element) {
219            return convert_catalog_array(context, value, element);
220        }
221    }
222    if type_requires_catalog_resolution(ty) {
223        return crate::expr::cast_value_with_type_resolution(
224            &value,
225            None,
226            &ty.catalog_name(),
227            Some(context),
228        );
229    }
230    convert_value_to_column_type_from(value, ty, source)
231}
232
233fn textual_target(ty: &ColumnType) -> bool {
234    match ty {
235        ColumnType::Array(element) => textual_target(element),
236        other => other.is_character_string(),
237    }
238}
239
240fn convert_catalog_array(
241    context: &dyn AssignmentContext,
242    value: Value,
243    element: &ColumnType,
244) -> Result<Value, SQLError> {
245    let array = match value {
246        Value::Array(array) => array,
247        Value::Str(text) => crate::expr::parse_pg_array_literal(&text)?,
248        other => {
249            return Err(SQLError::TypeMismatch(format!(
250                "expected an array, got {other:?}"
251            )))
252        }
253    };
254    let values = convert_catalog_array_elements(context, array.elements(), element)?;
255    ArrayValue::with_lower_bounds(values, array.lower_bounds().to_vec())
256        .map(|array| {
257            array.with_element_type_oid(Some(crate::catalog::type_metadata::pg_type_oid(
258                array_scalar_type(element),
259            ) as u32))
260        })
261        .map(Value::Array)
262        .ok_or_else(|| {
263            SQLError::TypeMismatch("multidimensional arrays must have matching dimensions".into())
264        })
265}
266
267fn convert_catalog_array_elements(
268    context: &dyn AssignmentContext,
269    values: &[Value],
270    element: &ColumnType,
271) -> Result<Vec<Value>, SQLError> {
272    let mut element = element;
273    while let ColumnType::Array(nested) = element {
274        element = nested;
275    }
276    values
277        .iter()
278        .map(|value| match value {
279            Value::List(values) => {
280                convert_catalog_array_elements(context, values, element).map(Value::List)
281            }
282            value => convert_value_to_column_type_with_context(context, value.clone(), element),
283        })
284        .collect()
285}
286
287mod production;
288pub use production::{
289    convert_value_to_column_type_from_with_control, convert_value_to_column_type_with_control,
290    numeric_field_overflow,
291};
292
293pub fn convert_value_to_column_type(value: Value, ty: &ColumnType) -> Result<Value, SQLError> {
294    convert_value_to_column_type_from(value, ty, None)
295}
296
297/// Convert a catalog-free assignment while retaining the analyzed source identity.
298pub fn convert_value_to_column_type_from(
299    value: Value,
300    ty: &ColumnType,
301    source: Option<&ColumnType>,
302) -> Result<Value, SQLError> {
303    let control = uqa_core::memory::ProductionControl::uncontrolled();
304    convert_value_to_column_type_from_with_control(
305        control.finish(value, None)?,
306        ty,
307        source,
308        &control,
309    )?
310    .into_uncontrolled()
311    .map_err(|_| SQLError::Internal("ordinary assignment production owner".into()))
312}
313
314fn convert_declared_array_elements(
315    context: &dyn AssignmentContext,
316    elements: &[Value],
317    source_type: &ColumnType,
318    target_type: &ColumnType,
319) -> Result<Vec<Value>, SQLError> {
320    elements
321        .iter()
322        .cloned()
323        .map(|element| match element {
324            Value::List(nested) => {
325                convert_declared_array_elements(context, &nested, source_type, target_type)
326                    .map(Value::List)
327            }
328            scalar => {
329                convert_declared_value_to_column_type(context, scalar, source_type, target_type)
330            }
331        })
332        .collect()
333}
334
335fn array_scalar_type(mut ty: &ColumnType) -> &ColumnType {
336    while let ColumnType::Array(element) = ty {
337        ty = element;
338    }
339    ty
340}
341
342pub fn validate_vector_dimensions(expected: u32, actual: usize) -> Result<(), SQLError> {
343    let expected = usize::try_from(expected).map_err(|_| {
344        SQLError::TypeMismatch(format!(
345            "declared vector dimension {expected} exceeds the platform usize range"
346        ))
347    })?;
348    if actual == expected {
349        Ok(())
350    } else {
351        Err(SQLError::VectorDimMismatch { expected, actual })
352    }
353}
354
355pub use crate::catalog::type_metadata::column_type_name;
356
357pub use crate::expr::value_to_text;
358
359pub fn json_to_core_value(json: serde_json::Value) -> Value {
360    match json {
361        serde_json::Value::Null => Value::Null,
362        serde_json::Value::Bool(b) => Value::Bool(b),
363        serde_json::Value::Number(n) => {
364            if let Some(i) = n.as_i64() {
365                Value::Int(i)
366            } else if let Some(d) = DecimalValue::parse(&n.to_string()) {
367                Value::Decimal(d)
368            } else if let Some(f) = n.as_f64() {
369                Value::Float(f)
370            } else {
371                Value::Null
372            }
373        }
374        serde_json::Value::String(s) => Value::Str(s),
375        serde_json::Value::Array(items) => {
376            Value::List(items.into_iter().map(json_to_core_value).collect())
377        }
378        serde_json::Value::Object(obj) => {
379            if let Ok(temporal) =
380                serde_json::from_value::<TemporalValue>(serde_json::Value::Object(obj.clone()))
381            {
382                return Value::Temporal(temporal);
383            }
384            Value::Map(
385                obj.into_iter()
386                    .map(|(k, v)| (k, json_to_core_value(v)))
387                    .collect(),
388            )
389        }
390    }
391}
392
393pub use crate::expr::core_value_to_json;
394
395pub fn json_table_value_to_text(value: &serde_json::Value) -> Value {
396    match value {
397        serde_json::Value::Null => Value::Null,
398        serde_json::Value::String(s) => Value::Str(s.clone()),
399        serde_json::Value::Bool(b) => Value::Str(b.to_string()),
400        serde_json::Value::Number(n) => Value::Str(n.to_string()),
401        serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Str(value.to_string()),
402    }
403}
404
405pub fn json_table_arg(value: &Value, name: &str) -> Result<serde_json::Value, SQLError> {
406    match value {
407        Value::Json(s) | Value::JsonB(s) | Value::Str(s) => {
408            serde_json::from_str::<serde_json::Value>(s)
409                .map_err(|e| SQLError::TypeMismatch(format!("{name}: invalid JSON: {e}")))
410        }
411        other => core_value_to_json(other),
412    }
413}