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::expr::{value_to_tensor, value_to_vector};
11use crate::{ColumnType, SQLError};
12use uqa_core::{ArrayValue, DecimalValue, TemporalValue, Value};
13
14pub fn coerce_assignment_value(
15    context: &dyn AssignmentContext,
16    value: Value,
17    target: &ColumnType,
18    source: Option<&ColumnType>,
19) -> Result<Value, SQLError> {
20    if source.is_some_and(|source| same_domain_identity(source, target)) {
21        return Ok(value);
22    }
23    let value = if target.is_character_string() {
24        source
25            .map(|source| crate::expr::format_regtype_value(&value, source, Some(context)))
26            .transpose()?
27            .flatten()
28            .map(Value::Str)
29            .unwrap_or(value)
30    } else {
31        value
32    };
33    convert_value_to_column_type_with_context(context, value, target)
34}
35
36fn same_domain_identity(source: &ColumnType, target: &ColumnType) -> bool {
37    match (source, target) {
38        (ColumnType::Domain { oid: source, .. }, ColumnType::Domain { oid: target, .. }) => {
39            source == target
40        }
41        (ColumnType::Array(source), ColumnType::Array(target)) => {
42            same_domain_identity(source, target)
43        }
44        _ => false,
45    }
46}
47
48pub fn coerce_json_value(value: Value, jsonb: bool) -> Result<Value, SQLError> {
49    crate::expr::cast_value(&value, if jsonb { "jsonb" } else { "json" })
50}
51
52pub fn convert_declared_value_to_column_type(
53    context: &dyn AssignmentContext,
54    value: Value,
55    source_ty: &ColumnType,
56    target_ty: &ColumnType,
57) -> Result<Value, SQLError> {
58    match (source_ty, target_ty) {
59        (ColumnType::Domain { base, .. }, target) => {
60            convert_declared_value_to_column_type(context, value, base, target)
61        }
62        (source, ColumnType::Domain { base, .. }) => {
63            convert_declared_value_to_column_type(context, value, source, base)
64        }
65        (ColumnType::Array(source), ColumnType::Array(target)) => {
66            let Value::Array(array) = value else {
67                return Err(SQLError::TypeMismatch(format!(
68                    "cannot cast a non-array value to {}[]",
69                    column_type_name(target)
70                )));
71            };
72            let source = array_scalar_type(source);
73            let target = array_scalar_type(target);
74            let converted =
75                convert_declared_array_elements(context, array.elements(), source, target)?;
76            ArrayValue::with_lower_bounds(converted, array.lower_bounds().to_vec())
77                .map(Value::Array)
78                .ok_or_else(|| {
79                    SQLError::TypeMismatch(
80                        "multidimensional arrays must have matching dimensions".into(),
81                    )
82                })
83        }
84        (ColumnType::Range(source), ColumnType::Range(target)) if source == target => {
85            crate::expr::cast_value_from(&value, target.range_name(), Some(source.range_name()))
86        }
87        (ColumnType::Range(source), ColumnType::Multirange(target)) if source == target => {
88            crate::expr::cast_value_from(
89                &value,
90                target.multirange_name(),
91                Some(source.range_name()),
92            )
93        }
94        (ColumnType::Multirange(source), ColumnType::Multirange(target)) if source == target => {
95            crate::expr::cast_value_from(
96                &value,
97                target.multirange_name(),
98                Some(source.multirange_name()),
99            )
100        }
101        (_, ColumnType::Range(_) | ColumnType::Multirange(_)) => {
102            Err(SQLError::TypeMismatch(format!(
103                "column cannot be cast automatically from type {} to type {}",
104                column_type_name(source_ty),
105                column_type_name(target_ty)
106            )))
107        }
108        (source, ColumnType::Oid)
109            if matches!(
110                source,
111                ColumnType::SmallInteger
112                    | ColumnType::Integer
113                    | ColumnType::BigInteger
114                    | ColumnType::Oid
115                    | ColumnType::Regproc
116                    | ColumnType::Regprocedure
117                    | ColumnType::Regclass
118                    | ColumnType::Regnamespace
119                    | ColumnType::Regrole
120                    | ColumnType::Regtype
121            ) =>
122        {
123            crate::expr::cast_value_from(&value, "oid", Some(column_type_name(source)))
124        }
125        (ColumnType::Xid, ColumnType::Xid) => Ok(value),
126        (ColumnType::Bytea, ColumnType::Bytea) => Ok(value),
127        (_, ColumnType::Oid | ColumnType::Xid | ColumnType::Bytea) => {
128            Err(SQLError::TypeMismatch(format!(
129                "column cannot be cast automatically from type {} to type {}",
130                column_type_name(source_ty),
131                column_type_name(target_ty)
132            )))
133        }
134        _ => convert_value_to_column_type_with_context(context, value, target_ty),
135    }
136}
137
138fn type_requires_catalog_resolution(ty: &ColumnType) -> bool {
139    match ty {
140        ColumnType::Regrole | ColumnType::Domain { .. } => true,
141        ColumnType::Array(element) => type_requires_catalog_resolution(element),
142        _ => false,
143    }
144}
145
146pub fn convert_value_to_column_type_with_context(
147    context: &dyn AssignmentContext,
148    value: Value,
149    ty: &ColumnType,
150) -> Result<Value, SQLError> {
151    if let Some(value) = super::domain::assign_domain_value(context, &value, ty)? {
152        return Ok(value);
153    }
154    if matches!(value, Value::Null) {
155        return Ok(Value::Null);
156    }
157    if let ColumnType::Array(element) = ty {
158        if type_requires_catalog_resolution(element) {
159            return convert_catalog_array(context, value, element);
160        }
161    }
162    if type_requires_catalog_resolution(ty) {
163        return crate::expr::cast_value_with_type_resolution(
164            &value,
165            None,
166            &ty.sql_name(),
167            Some(context),
168        );
169    }
170    convert_value_to_column_type(value, ty)
171}
172
173fn convert_catalog_array(
174    context: &dyn AssignmentContext,
175    value: Value,
176    element: &ColumnType,
177) -> Result<Value, SQLError> {
178    let array = match value {
179        Value::Array(array) => array,
180        Value::Str(text) => crate::expr::parse_pg_array_literal(&text)?,
181        other => {
182            return Err(SQLError::TypeMismatch(format!(
183                "expected an array, got {other:?}"
184            )))
185        }
186    };
187    let values = convert_catalog_array_elements(context, array.elements(), element)?;
188    ArrayValue::with_lower_bounds(values, array.lower_bounds().to_vec())
189        .map(Value::Array)
190        .ok_or_else(|| {
191            SQLError::TypeMismatch("multidimensional arrays must have matching dimensions".into())
192        })
193}
194
195fn convert_catalog_array_elements(
196    context: &dyn AssignmentContext,
197    values: &[Value],
198    element: &ColumnType,
199) -> Result<Vec<Value>, SQLError> {
200    let mut element = element;
201    while let ColumnType::Array(nested) = element {
202        element = nested;
203    }
204    values
205        .iter()
206        .map(|value| match value {
207            Value::List(values) => {
208                convert_catalog_array_elements(context, values, element).map(Value::List)
209            }
210            value => convert_value_to_column_type_with_context(context, value.clone(), element),
211        })
212        .collect()
213}
214
215#[expect(
216    clippy::too_many_lines,
217    reason = "preserves DDL dependency and action order"
218)]
219pub fn convert_value_to_column_type(value: Value, ty: &ColumnType) -> Result<Value, SQLError> {
220    if matches!(value, Value::Null) {
221        return Ok(Value::Null);
222    }
223    match ty {
224        ColumnType::Named(name) => Err(SQLError::Routine {
225            sqlstate: "42704".into(),
226            message: format!("type \"{name}\" does not exist"),
227        }),
228        ColumnType::SmallInteger => crate::expr::cast_value(&value, "smallint"),
229        ColumnType::Integer => crate::expr::cast_value(&value, "integer"),
230        ColumnType::BigInteger => crate::expr::cast_value(&value, "bigint"),
231        ColumnType::Oid | ColumnType::Xid => {
232            let Value::Int(value) = crate::expr::cast_value(&value, "bigint")? else {
233                unreachable!("bigint cast returned a non-integer value")
234            };
235            u32::try_from(value)
236                .map(|value| Value::Int(i64::from(value)))
237                .map_err(|_| {
238                    SQLError::TypeMismatch(format!(
239                        "value {value} is out of range for type {}",
240                        column_type_name(ty)
241                    ))
242                })
243        }
244        ColumnType::Boolean => match value {
245            Value::Bool(_) => Ok(value),
246            Value::Str(text) => parse_boolean_text(&text)
247                .map(Value::Bool)
248                .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast `{text}` to boolean"))),
249            other => Err(SQLError::TypeMismatch(format!(
250                "cannot cast {other:?} to boolean"
251            ))),
252        },
253        ColumnType::Void => Ok(Value::Void),
254        ColumnType::Text | ColumnType::RefCursor => Ok(Value::Str(value_to_text(&value))),
255        ColumnType::Name => crate::expr::cast_value(&value, "name"),
256        ColumnType::Uuid => crate::expr::cast_value(&value, "uuid"),
257        ColumnType::Varchar(None) => Ok(Value::Str(value_to_text(&value))),
258        ColumnType::Varchar(Some(length)) => convert_varying_character(value, *length),
259        ColumnType::Bpchar => Ok(Value::FixedChar(value_to_text(&value))),
260        ColumnType::Character(length) => {
261            let length = usize::try_from(*length).map_err(|_| {
262                SQLError::TypeMismatch(format!(
263                    "character length {length} exceeds the platform addressable range"
264                ))
265            })?;
266            let text = value_to_text(&value);
267            let char_count = text.chars().count();
268            let significant = if char_count > length {
269                let retained = text.chars().take(length).collect::<String>();
270                let discarded = text.chars().skip(length).collect::<String>();
271                if !discarded.chars().all(|character| character == ' ') {
272                    return Err(SQLError::Routine {
273                        sqlstate: "22001".into(),
274                        message: format!("value too long for type character({length})"),
275                    });
276                }
277                retained
278            } else {
279                text
280            };
281            let padding = length.saturating_sub(significant.chars().count());
282            let mut padded = significant;
283            padded.extend(std::iter::repeat_n(' ', padding));
284            Ok(Value::FixedChar(padded))
285        }
286        ColumnType::Real | ColumnType::DoublePrecision => {
287            crate::expr::cast_value(&value, &ty.sql_name())
288        }
289        ColumnType::Numeric { precision, scale } => {
290            let decimal = match value {
291                Value::Decimal(d) => d,
292                Value::Int(i) => DecimalValue::from_i64(i),
293                Value::Float(f) => DecimalValue::from_f64_lossy(f).ok_or_else(|| {
294                    SQLError::TypeMismatch(format!("cannot cast {f:?} to numeric"))
295                })?,
296                Value::Bool(b) => DecimalValue::from_bool(b),
297                Value::Str(s) => DecimalValue::parse(&s).ok_or_else(|| {
298                    SQLError::TypeMismatch(format!("cannot cast `{s}` to numeric"))
299                })?,
300                other => {
301                    return Err(SQLError::TypeMismatch(format!(
302                        "cannot cast {other:?} to numeric"
303                    )));
304                }
305            };
306            let rounded = match scale {
307                Some(s) => decimal.round_to_scale(*s).ok_or_else(|| {
308                    SQLError::TypeMismatch(format!("cannot round numeric to scale {s}"))
309                })?,
310                None => decimal,
311            };
312            if let Some(precision) = precision {
313                let scale = scale.unwrap_or(0);
314                if !rounded.fits_precision(*precision, scale) {
315                    return Err(SQLError::TypeMismatch(format!(
316                        "numeric field overflow: value {} exceeds precision {precision}, scale {scale}",
317                        rounded.to_sql_string()
318                    )));
319                }
320            }
321            Ok(Value::Decimal(rounded))
322        }
323        ColumnType::Json => coerce_json_value(value, false),
324        ColumnType::JsonB => coerce_json_value(value, true),
325        ColumnType::Bytea => Ok(match value {
326            Value::Bytes(_) => value,
327            Value::Str(s) => Value::Bytes(s.into_bytes()),
328            other => Value::Bytes(value_to_text(&other).into_bytes()),
329        }),
330        ColumnType::InternalChar => {
331            let text = value_to_text(&value);
332            if text.len() == 1 {
333                Ok(Value::Str(text))
334            } else {
335                Err(SQLError::TypeMismatch(format!(
336                    "value `{text}` must be exactly one byte for type \"char\""
337                )))
338            }
339        }
340        ColumnType::Regproc
341        | ColumnType::Regprocedure
342        | ColumnType::Regclass
343        | ColumnType::Regnamespace
344        | ColumnType::Regtype
345        | ColumnType::PgNodeTree
346        | ColumnType::AclItem => Ok(match value {
347            Value::Int(_) | Value::Str(_) => value,
348            other => Value::Str(value_to_text(&other)),
349        }),
350        ColumnType::Regrole => match value {
351            Value::Int(value) => u32::try_from(value)
352                .map(|value| Value::Int(i64::from(value)))
353                .map_err(|_| {
354                    SQLError::TypeMismatch(format!(
355                        "value {value} is out of range for type regrole"
356                    ))
357                }),
358            Value::Str(_) | Value::FixedChar(_) => Err(SQLError::Internal(
359                "regrole name conversion requires catalog resolution".into(),
360            )),
361            other => Err(SQLError::TypeMismatch(format!(
362                "cannot cast {other:?} to regrole"
363            ))),
364        },
365        ColumnType::Int2Vector => convert_value_to_column_type(
366            value,
367            &ColumnType::Array(Box::new(ColumnType::SmallInteger)),
368        ),
369        ColumnType::OidVector => {
370            convert_value_to_column_type(value, &ColumnType::Array(Box::new(ColumnType::Oid)))
371        }
372        ColumnType::AnyArray => match value {
373            Value::Array(_) => Ok(value),
374            other => Err(SQLError::TypeMismatch(format!(
375                "cannot cast {other:?} to anyarray"
376            ))),
377        },
378        ColumnType::Record => match value {
379            Value::Record(_) => Ok(value),
380            Value::Row(values) => Ok(Value::Record(
381                values
382                    .into_iter()
383                    .enumerate()
384                    .map(|(index, value)| (format!("f{}", index + 1), value))
385                    .collect(),
386            )),
387            other => Err(SQLError::TypeMismatch(format!(
388                "cannot cast {other:?} to record"
389            ))),
390        },
391        ColumnType::Array(element_type) => {
392            let array = match value {
393                Value::Array(array) => array,
394                Value::List(elements) => ArrayValue::try_new(elements).ok_or_else(|| {
395                    SQLError::TypeMismatch(
396                        "multidimensional arrays must have matching dimensions".into(),
397                    )
398                })?,
399                Value::Str(text) => crate::expr::parse_pg_array_literal(&text)?,
400                other => {
401                    return Err(SQLError::TypeMismatch(format!(
402                        "cannot cast {other:?} to {}[]",
403                        column_type_name(element_type)
404                    )))
405                }
406            };
407            let converted = convert_array_elements(array.elements(), element_type)?;
408            ArrayValue::with_lower_bounds(converted, array.lower_bounds().to_vec())
409                .map(Value::Array)
410                .ok_or_else(|| {
411                    SQLError::TypeMismatch(
412                        "multidimensional arrays must have matching dimensions".into(),
413                    )
414                })
415        }
416        ColumnType::Date
417        | ColumnType::Time
418        | ColumnType::TimePrecision(_)
419        | ColumnType::TimeTz
420        | ColumnType::TimeTzPrecision(_)
421        | ColumnType::Timestamp
422        | ColumnType::TimestampPrecision(_)
423        | ColumnType::TimestampTz
424        | ColumnType::TimestampTzPrecision(_)
425        | ColumnType::Interval
426        | ColumnType::IntervalWithFields { .. } => convert_temporal_value(value, ty),
427        ColumnType::Range(subtype) => crate::expr::cast_value(&value, subtype.range_name()),
428        ColumnType::Multirange(subtype) => {
429            crate::expr::cast_value(&value, subtype.multirange_name())
430        }
431        ColumnType::Vector(dim) => {
432            let vector = value_to_vector(&value)?;
433            validate_vector_dimensions(*dim, vector.len())?;
434            Ok(vector_to_value(vector))
435        }
436        ColumnType::Tensor(dim) => {
437            let tensor = value_to_tensor(&value)?;
438            for vector in &tensor {
439                validate_vector_dimensions(*dim, vector.len())?;
440            }
441            Ok(Value::List(
442                tensor.into_iter().map(vector_to_value).collect(),
443            ))
444        }
445        ColumnType::Domain { base, .. } => convert_value_to_column_type(value, base),
446    }
447}
448
449fn convert_array_elements(
450    elements: &[Value],
451    element_type: &ColumnType,
452) -> Result<Vec<Value>, SQLError> {
453    let element_type = array_scalar_type(element_type);
454    elements
455        .iter()
456        .cloned()
457        .map(|element| match element {
458            Value::List(nested) => convert_array_elements(&nested, element_type).map(Value::List),
459            scalar => convert_value_to_column_type(scalar, element_type),
460        })
461        .collect()
462}
463
464fn convert_declared_array_elements(
465    context: &dyn AssignmentContext,
466    elements: &[Value],
467    source_type: &ColumnType,
468    target_type: &ColumnType,
469) -> Result<Vec<Value>, SQLError> {
470    elements
471        .iter()
472        .cloned()
473        .map(|element| match element {
474            Value::List(nested) => {
475                convert_declared_array_elements(context, &nested, source_type, target_type)
476                    .map(Value::List)
477            }
478            scalar => {
479                convert_declared_value_to_column_type(context, scalar, source_type, target_type)
480            }
481        })
482        .collect()
483}
484
485fn array_scalar_type(mut ty: &ColumnType) -> &ColumnType {
486    while let ColumnType::Array(element) = ty {
487        ty = element;
488    }
489    ty
490}
491
492fn convert_varying_character(value: Value, length: u32) -> Result<Value, SQLError> {
493    let length = usize::try_from(length).map_err(|_| {
494        SQLError::TypeMismatch(format!(
495            "character varying length {length} exceeds the platform addressable range"
496        ))
497    })?;
498    let text = value_to_text(&value);
499    if text.chars().count() <= length {
500        return Ok(Value::Str(text));
501    }
502    let retained = text.chars().take(length).collect::<String>();
503    let discarded = text.chars().skip(length).collect::<String>();
504    if discarded.chars().all(|character| character == ' ') {
505        Ok(Value::Str(retained))
506    } else {
507        Err(SQLError::Routine {
508            sqlstate: "22001".into(),
509            message: format!("value too long for type character varying({length})"),
510        })
511    }
512}
513
514fn vector_to_value(vector: Vec<f32>) -> Value {
515    Value::List(
516        vector
517            .into_iter()
518            .map(|value| Value::Float(f64::from(value)))
519            .collect(),
520    )
521}
522
523pub fn validate_vector_dimensions(expected: u32, actual: usize) -> Result<(), SQLError> {
524    let expected = usize::try_from(expected).map_err(|_| {
525        SQLError::TypeMismatch(format!(
526            "declared vector dimension {expected} exceeds the platform usize range"
527        ))
528    })?;
529    if actual == expected {
530        Ok(())
531    } else {
532        Err(SQLError::VectorDimMismatch { expected, actual })
533    }
534}
535
536pub use crate::catalog::type_metadata::column_type_name;
537
538fn parse_boolean_text(text: &str) -> Option<bool> {
539    match text.trim().to_ascii_lowercase().as_str() {
540        "true" | "t" | "yes" | "y" | "on" | "1" => Some(true),
541        "false" | "f" | "no" | "n" | "off" | "0" => Some(false),
542        _ => None,
543    }
544}
545
546fn convert_temporal_value(value: Value, ty: &ColumnType) -> Result<Value, SQLError> {
547    crate::expr::cast_value(&value, &ty.sql_name())
548}
549
550pub use crate::expr::value_to_text;
551
552pub fn json_to_core_value(json: serde_json::Value) -> Value {
553    match json {
554        serde_json::Value::Null => Value::Null,
555        serde_json::Value::Bool(b) => Value::Bool(b),
556        serde_json::Value::Number(n) => {
557            if let Some(i) = n.as_i64() {
558                Value::Int(i)
559            } else if let Some(d) = DecimalValue::parse(&n.to_string()) {
560                Value::Decimal(d)
561            } else if let Some(f) = n.as_f64() {
562                Value::Float(f)
563            } else {
564                Value::Null
565            }
566        }
567        serde_json::Value::String(s) => Value::Str(s),
568        serde_json::Value::Array(items) => {
569            Value::List(items.into_iter().map(json_to_core_value).collect())
570        }
571        serde_json::Value::Object(obj) => {
572            if let Ok(temporal) =
573                serde_json::from_value::<TemporalValue>(serde_json::Value::Object(obj.clone()))
574            {
575                return Value::Temporal(temporal);
576            }
577            Value::Map(
578                obj.into_iter()
579                    .map(|(k, v)| (k, json_to_core_value(v)))
580                    .collect(),
581            )
582        }
583    }
584}
585
586pub use crate::expr::core_value_to_json;
587
588pub fn json_table_value_to_text(value: &serde_json::Value) -> Value {
589    match value {
590        serde_json::Value::Null => Value::Null,
591        serde_json::Value::String(s) => Value::Str(s.clone()),
592        serde_json::Value::Bool(b) => Value::Str(b.to_string()),
593        serde_json::Value::Number(n) => Value::Str(n.to_string()),
594        serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Str(value.to_string()),
595    }
596}
597
598pub fn json_table_arg(value: &Value, name: &str) -> Result<serde_json::Value, SQLError> {
599    match value {
600        Value::Json(s) | Value::JsonB(s) | Value::Str(s) => {
601            serde_json::from_str::<serde_json::Value>(s)
602                .map_err(|e| SQLError::TypeMismatch(format!("{name}: invalid JSON: {e}")))
603        }
604        other => Ok(core_value_to_json(other)),
605    }
606}