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_domain_identity(source, target)) {
24        return normalize_existing(value, target);
25    }
26    let value = if target.is_character_string() {
27        source
28            .map(|source| crate::expr::format_regtype_value(&value, source, Some(context)))
29            .transpose()?
30            .flatten()
31            .map(Value::Str)
32            .unwrap_or(value)
33    } else {
34        value
35    };
36    convert_value_to_column_type_with_context(context, value, target)
37}
38
39fn same_domain_identity(source: &ColumnType, target: &ColumnType) -> bool {
40    match (source, target) {
41        (ColumnType::Domain { oid: source, .. }, ColumnType::Domain { oid: target, .. }) => {
42            source == target
43        }
44        (ColumnType::Array(source), ColumnType::Array(target)) => {
45            same_domain_identity(source, target)
46        }
47        _ => false,
48    }
49}
50
51pub fn coerce_json_value(value: Value, jsonb: bool) -> Result<Value, SQLError> {
52    crate::expr::cast_value(&value, if jsonb { "jsonb" } else { "json" })
53}
54
55pub fn convert_declared_value_to_column_type(
56    context: &dyn AssignmentContext,
57    value: Value,
58    source_ty: &ColumnType,
59    target_ty: &ColumnType,
60) -> Result<Value, SQLError> {
61    match (source_ty, target_ty) {
62        (ColumnType::Domain { base, .. }, target) => {
63            convert_declared_value_to_column_type(context, value, base, target)
64        }
65        (source, ColumnType::Domain { base, .. }) => {
66            convert_declared_value_to_column_type(context, value, source, base)
67        }
68        (ColumnType::Array(source), ColumnType::Array(target)) => {
69            let Value::Array(array) = value else {
70                return Err(SQLError::TypeMismatch(format!(
71                    "cannot cast a non-array value to {}[]",
72                    column_type_name(target)
73                )));
74            };
75            let source = array_scalar_type(source);
76            let target = array_scalar_type(target);
77            let converted =
78                convert_declared_array_elements(context, array.elements(), source, target)?;
79            ArrayValue::with_lower_bounds(converted, array.lower_bounds().to_vec())
80                .map(Value::Array)
81                .ok_or_else(|| {
82                    SQLError::TypeMismatch(
83                        "multidimensional arrays must have matching dimensions".into(),
84                    )
85                })
86        }
87        (ColumnType::Range(source), ColumnType::Range(target)) if source == target => {
88            crate::expr::cast_value_from(&value, target.range_name(), Some(source.range_name()))
89        }
90        (ColumnType::Range(source), ColumnType::Multirange(target)) if source == target => {
91            crate::expr::cast_value_from(
92                &value,
93                target.multirange_name(),
94                Some(source.range_name()),
95            )
96        }
97        (ColumnType::Multirange(source), ColumnType::Multirange(target)) if source == target => {
98            crate::expr::cast_value_from(
99                &value,
100                target.multirange_name(),
101                Some(source.multirange_name()),
102            )
103        }
104        (_, ColumnType::Range(_) | ColumnType::Multirange(_)) => {
105            Err(SQLError::TypeMismatch(format!(
106                "column cannot be cast automatically from type {} to type {}",
107                column_type_name(source_ty),
108                column_type_name(target_ty)
109            )))
110        }
111        (source, ColumnType::Oid)
112            if matches!(
113                source,
114                ColumnType::SmallInteger
115                    | ColumnType::Integer
116                    | ColumnType::BigInteger
117                    | ColumnType::Oid
118                    | ColumnType::Regproc
119                    | ColumnType::Regprocedure
120                    | ColumnType::Regclass
121                    | ColumnType::Regnamespace
122                    | ColumnType::Regrole
123                    | ColumnType::Regtype
124            ) =>
125        {
126            crate::expr::cast_value_from(&value, "oid", Some(column_type_name(source)))
127        }
128        (ColumnType::Xid, ColumnType::Xid) => Ok(value),
129        (ColumnType::Bytea, ColumnType::Bytea) => Ok(value),
130        (_, ColumnType::Oid | ColumnType::Xid | ColumnType::Bytea) => {
131            Err(SQLError::TypeMismatch(format!(
132                "column cannot be cast automatically from type {} to type {}",
133                column_type_name(source_ty),
134                column_type_name(target_ty)
135            )))
136        }
137        _ => convert_value_to_column_type_with_context(context, value, target_ty),
138    }
139}
140
141fn type_requires_catalog_resolution(ty: &ColumnType) -> bool {
142    match ty {
143        ColumnType::Regrole | ColumnType::Domain { .. } => true,
144        ColumnType::Array(element) => type_requires_catalog_resolution(element),
145        _ => false,
146    }
147}
148
149pub fn convert_value_to_column_type_with_context(
150    context: &dyn AssignmentContext,
151    value: Value,
152    ty: &ColumnType,
153) -> Result<Value, SQLError> {
154    if let Some(value) = super::domain::assign_domain_value(context, &value, ty)? {
155        return Ok(value);
156    }
157    if matches!(value, Value::Null) {
158        return Ok(Value::Null);
159    }
160    if let ColumnType::Array(element) = ty {
161        if type_requires_catalog_resolution(element) {
162            return convert_catalog_array(context, value, element);
163        }
164    }
165    if type_requires_catalog_resolution(ty) {
166        return crate::expr::cast_value_with_type_resolution(
167            &value,
168            None,
169            &ty.sql_name(),
170            Some(context),
171        );
172    }
173    convert_value_to_column_type(value, ty)
174}
175
176fn convert_catalog_array(
177    context: &dyn AssignmentContext,
178    value: Value,
179    element: &ColumnType,
180) -> Result<Value, SQLError> {
181    let array = match value {
182        Value::Array(array) => array,
183        Value::Str(text) => crate::expr::parse_pg_array_literal(&text)?,
184        other => {
185            return Err(SQLError::TypeMismatch(format!(
186                "expected an array, got {other:?}"
187            )))
188        }
189    };
190    let values = convert_catalog_array_elements(context, array.elements(), element)?;
191    ArrayValue::with_lower_bounds(values, array.lower_bounds().to_vec())
192        .map(Value::Array)
193        .ok_or_else(|| {
194            SQLError::TypeMismatch("multidimensional arrays must have matching dimensions".into())
195        })
196}
197
198fn convert_catalog_array_elements(
199    context: &dyn AssignmentContext,
200    values: &[Value],
201    element: &ColumnType,
202) -> Result<Vec<Value>, SQLError> {
203    let mut element = element;
204    while let ColumnType::Array(nested) = element {
205        element = nested;
206    }
207    values
208        .iter()
209        .map(|value| match value {
210            Value::List(values) => {
211                convert_catalog_array_elements(context, values, element).map(Value::List)
212            }
213            value => convert_value_to_column_type_with_context(context, value.clone(), element),
214        })
215        .collect()
216}
217
218mod production;
219pub use production::convert_value_to_column_type_with_control;
220
221pub fn convert_value_to_column_type(value: Value, ty: &ColumnType) -> Result<Value, SQLError> {
222    let control = uqa_core::memory::ProductionControl::uncontrolled();
223    convert_value_to_column_type_with_control(control.finish(value, None)?, ty, &control)?
224        .into_uncontrolled()
225        .map_err(|_| SQLError::Internal("ordinary assignment production owner".into()))
226}
227
228fn convert_declared_array_elements(
229    context: &dyn AssignmentContext,
230    elements: &[Value],
231    source_type: &ColumnType,
232    target_type: &ColumnType,
233) -> Result<Vec<Value>, SQLError> {
234    elements
235        .iter()
236        .cloned()
237        .map(|element| match element {
238            Value::List(nested) => {
239                convert_declared_array_elements(context, &nested, source_type, target_type)
240                    .map(Value::List)
241            }
242            scalar => {
243                convert_declared_value_to_column_type(context, scalar, source_type, target_type)
244            }
245        })
246        .collect()
247}
248
249fn array_scalar_type(mut ty: &ColumnType) -> &ColumnType {
250    while let ColumnType::Array(element) = ty {
251        ty = element;
252    }
253    ty
254}
255
256pub fn validate_vector_dimensions(expected: u32, actual: usize) -> Result<(), SQLError> {
257    let expected = usize::try_from(expected).map_err(|_| {
258        SQLError::TypeMismatch(format!(
259            "declared vector dimension {expected} exceeds the platform usize range"
260        ))
261    })?;
262    if actual == expected {
263        Ok(())
264    } else {
265        Err(SQLError::VectorDimMismatch { expected, actual })
266    }
267}
268
269pub use crate::catalog::type_metadata::column_type_name;
270
271pub use crate::expr::value_to_text;
272
273pub fn json_to_core_value(json: serde_json::Value) -> Value {
274    match json {
275        serde_json::Value::Null => Value::Null,
276        serde_json::Value::Bool(b) => Value::Bool(b),
277        serde_json::Value::Number(n) => {
278            if let Some(i) = n.as_i64() {
279                Value::Int(i)
280            } else if let Some(d) = DecimalValue::parse(&n.to_string()) {
281                Value::Decimal(d)
282            } else if let Some(f) = n.as_f64() {
283                Value::Float(f)
284            } else {
285                Value::Null
286            }
287        }
288        serde_json::Value::String(s) => Value::Str(s),
289        serde_json::Value::Array(items) => {
290            Value::List(items.into_iter().map(json_to_core_value).collect())
291        }
292        serde_json::Value::Object(obj) => {
293            if let Ok(temporal) =
294                serde_json::from_value::<TemporalValue>(serde_json::Value::Object(obj.clone()))
295            {
296                return Value::Temporal(temporal);
297            }
298            Value::Map(
299                obj.into_iter()
300                    .map(|(k, v)| (k, json_to_core_value(v)))
301                    .collect(),
302            )
303        }
304    }
305}
306
307pub use crate::expr::core_value_to_json;
308
309pub fn json_table_value_to_text(value: &serde_json::Value) -> Value {
310    match value {
311        serde_json::Value::Null => Value::Null,
312        serde_json::Value::String(s) => Value::Str(s.clone()),
313        serde_json::Value::Bool(b) => Value::Str(b.to_string()),
314        serde_json::Value::Number(n) => Value::Str(n.to_string()),
315        serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Str(value.to_string()),
316    }
317}
318
319pub fn json_table_arg(value: &Value, name: &str) -> Result<serde_json::Value, SQLError> {
320    match value {
321        Value::Json(s) | Value::JsonB(s) | Value::Str(s) => {
322            serde_json::from_str::<serde_json::Value>(s)
323                .map_err(|e| SQLError::TypeMismatch(format!("{name}: invalid JSON: {e}")))
324        }
325        other => Ok(core_value_to_json(other)),
326    }
327}