Skip to main content

uqa_sql/assignment/conversion/
production.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Assignment conversion preserves admitted inputs while their replacements are produced.
8
9use super::{
10    array_scalar_type, column_type_name, validate_vector_dimensions, ColumnType, SQLError,
11};
12use crate::expr::{
13    cast_value_from_with_control, parse_pg_array_literal_with_control,
14    value_to_tensor_with_control, value_to_text_with_control, value_to_vector_with_control,
15};
16use uqa_core::{
17    memory::{MemoryReservation, Produced, ProductionControl, ProductionString, ProductionVec},
18    ArrayValue, DecimalValue, Value,
19};
20
21type Result<T> = std::result::Result<T, SQLError>;
22
23#[expect(
24    clippy::too_many_lines,
25    reason = "assignment matrix preserves conversion and validation order"
26)]
27pub fn convert_value_to_column_type_with_control(
28    value: Produced<Value>,
29    ty: &ColumnType,
30    control: &ProductionControl<'_>,
31) -> Result<Produced<Value>> {
32    let (value, memory) = value.into_parts();
33    let value = control.finish(value, memory)?;
34    if matches!(&*value, Value::Null) {
35        return Ok(value);
36    }
37    match ty {
38        ColumnType::Named(name) => Err(SQLError::Routine {
39            sqlstate: "42704".into(),
40            message: format!("type \"{name}\" does not exist"),
41        }),
42        // Label input needs the catalog; catalog-aware assignment converts text before this context-free step.
43        ColumnType::Enum(reference) => match &*value {
44            Value::Enum(label) if label.type_oid() == reference.oid => Ok(value),
45            _ => Err(SQLError::Internal(format!(
46                "enum input for type OID {} requires catalog-aware conversion",
47                reference.oid
48            ))),
49        },
50        // Composite input and coercion need the attributes; catalog-aware assignment converts rows and text before this context-free step.
51        ColumnType::Composite(reference) => match &*value {
52            Value::Record(_) => Ok(value),
53            _ => Err(SQLError::Internal(format!(
54                "composite input for type OID {} requires catalog-aware conversion",
55                reference.oid
56            ))),
57        },
58        ColumnType::SmallInteger => cast_value_from_with_control(&value, "smallint", None, control),
59        ColumnType::Integer => cast_value_from_with_control(&value, "integer", None, control),
60        ColumnType::BigInteger => cast_value_from_with_control(&value, "bigint", None, control),
61        // `oidin` reads the text, so its diagnostics name `oid`.
62        ColumnType::Oid => cast_value_from_with_control(&value, "oid", None, control),
63        ColumnType::Xid => {
64            let converted = cast_value_from_with_control(&value, "bigint", None, control)?;
65            let Value::Int(number) = &*converted else {
66                unreachable!("bigint cast returned a non-integer value");
67            };
68            u32::try_from(*number).map_err(|_| {
69                SQLError::TypeMismatch(format!(
70                    "value {number} is out of range for type {}",
71                    column_type_name(ty)
72                ))
73            })?;
74            Ok(converted)
75        }
76        ColumnType::Boolean => match &*value {
77            Value::Bool(_) => Ok(value),
78            Value::Str(text) => {
79                let boolean = parse_boolean_text(text)
80                    .ok_or_else(|| crate::expr::invalid_boolean_input(text))?;
81                Ok(control.finish(Value::Bool(boolean), control.empty_reservation())?)
82            }
83            other => Err(SQLError::TypeMismatch(format!(
84                "cannot cast {other:?} to boolean"
85            ))),
86        },
87        ColumnType::Void => Ok(control.finish(Value::Void, control.empty_reservation())?),
88        ColumnType::Text | ColumnType::RefCursor | ColumnType::Varchar(None) => {
89            text_value(value_to_text_with_control(&value, control)?, false, control)
90        }
91        ColumnType::Name => cast_value_from_with_control(&value, "name", None, control),
92        ColumnType::Uuid => cast_value_from_with_control(&value, "uuid", None, control),
93        ColumnType::Varchar(Some(length)) => character_value(&value, *length, false, control),
94        ColumnType::Bpchar => {
95            text_value(value_to_text_with_control(&value, control)?, true, control)
96        }
97        ColumnType::Character(length) => character_value(&value, *length, true, control),
98        ColumnType::Real => cast_value_from_with_control(&value, "real", None, control),
99        ColumnType::DoublePrecision => {
100            cast_value_from_with_control(&value, "double precision", None, control)
101        }
102        ColumnType::Numeric { precision, scale } => {
103            numeric_value(value, *precision, *scale, control)
104        }
105        ColumnType::Json => cast_value_from_with_control(&value, "json", None, control),
106        ColumnType::JsonB => cast_value_from_with_control(&value, "jsonb", None, control),
107        // Text reaches a bytea column through its input function, `byteain`, which decodes the hex and escape formats; no other type has an assignment cast to bytea.
108        ColumnType::Bytea => match &*value {
109            Value::Bytes(_) => Ok(value),
110            Value::Str(_) | Value::FixedChar(_) => {
111                cast_value_from_with_control(&value, "bytea", Some("text"), control)
112            }
113            other => Err(SQLError::TypeMismatch(format!(
114                "cannot cast {other:?} to bytea"
115            ))),
116        },
117        ColumnType::InternalChar => {
118            let text = value_to_text_with_control(&value, control)?;
119            if text.len() != 1 {
120                return Err(SQLError::TypeMismatch(format!(
121                    "value `{}` must be exactly one byte for type \"char\"",
122                    text.as_str()
123                )));
124            }
125            text_value(text, false, control)
126        }
127        ColumnType::Regproc
128        | ColumnType::Regprocedure
129        | ColumnType::Regclass
130        | ColumnType::Regcollation
131        | ColumnType::Regnamespace
132        | ColumnType::Regtype
133        | ColumnType::PgNodeTree
134        | ColumnType::AclItem => {
135            if matches!(&*value, Value::Int(_) | Value::Str(_)) {
136                Ok(value)
137            } else {
138                text_value(value_to_text_with_control(&value, control)?, false, control)
139            }
140        }
141        ColumnType::Regrole => match &*value {
142            Value::Int(number) => {
143                u32::try_from(*number).map_err(|_| {
144                    SQLError::TypeMismatch(format!(
145                        "value {number} is out of range for type regrole"
146                    ))
147                })?;
148                Ok(value)
149            }
150            Value::Str(_) | Value::FixedChar(_) => Err(SQLError::Internal(
151                "regrole name conversion requires catalog resolution".into(),
152            )),
153            other => Err(SQLError::TypeMismatch(format!(
154                "cannot cast {other:?} to regrole"
155            ))),
156        },
157        ColumnType::Int2Vector | ColumnType::OidVector => {
158            let name = column_type_name(ty);
159            if matches!(&*value, Value::LegacyVector(vector) if vector.kind().type_name() == name) {
160                Ok(value)
161            } else {
162                cast_value_from_with_control(&value, name, None, control)
163            }
164        }
165        ColumnType::AnyArray => {
166            if value.array_view().is_some() {
167                Ok(value)
168            } else {
169                Err(SQLError::TypeMismatch(format!(
170                    "cannot cast {:?} to anyarray",
171                    *value
172                )))
173            }
174        }
175        ColumnType::Record => match &*value {
176            Value::Record(_) => Ok(value),
177            Value::Row(_) => record_value(value, control),
178            other => Err(SQLError::TypeMismatch(format!(
179                "cannot cast {other:?} to record"
180            ))),
181        },
182        ColumnType::Array(element) => array_value(value, element, control),
183        ColumnType::Date
184        | ColumnType::Time
185        | ColumnType::TimePrecision(_)
186        | ColumnType::TimeTz
187        | ColumnType::TimeTzPrecision(_)
188        | ColumnType::Timestamp
189        | ColumnType::TimestampPrecision(_)
190        | ColumnType::TimestampTz
191        | ColumnType::TimestampTzPrecision(_)
192        | ColumnType::Interval
193        | ColumnType::IntervalWithFields { .. } => {
194            let name = ty.sql_name_with_control(control)?;
195            cast_value_from_with_control(&value, &name, None, control)
196        }
197        ColumnType::Range(subtype) => {
198            cast_value_from_with_control(&value, subtype.range_name(), None, control)
199        }
200        ColumnType::Multirange(subtype) => {
201            cast_value_from_with_control(&value, subtype.multirange_name(), None, control)
202        }
203        ColumnType::Vector(dimensions) => {
204            let vector = value_to_vector_with_control(&value, control)?;
205            validate_vector_dimensions(*dimensions, vector.len())?;
206            vector_value(&vector, control)
207        }
208        ColumnType::Tensor(dimensions) => {
209            let tensor = value_to_tensor_with_control(&value, control)?;
210            for vector in &*tensor {
211                control.check()?;
212                validate_vector_dimensions(*dimensions, vector.len())?;
213            }
214            let mut output = ProductionVec::new(*control);
215            output.reserve(tensor.len())?;
216            for vector in &*tensor {
217                output.push_produced(vector_value(vector, control)?)?;
218            }
219            let (values, memory) = output.finish()?.into_parts();
220            Ok(control.finish(Value::List(values), memory)?)
221        }
222        ColumnType::Domain { base, .. } => {
223            convert_value_to_column_type_with_control(value, base, control)
224        }
225    }
226}
227
228fn numeric_value(
229    value: Produced<Value>,
230    precision: Option<u32>,
231    scale: Option<i32>,
232    control: &ProductionControl<'_>,
233) -> Result<Produced<Value>> {
234    let decimal = match &*value {
235        Value::Decimal(_) => {
236            let (Value::Decimal(value), memory) = value.into_parts() else {
237                unreachable!();
238            };
239            control.finish(value, memory)?
240        }
241        Value::Int(number) => DecimalValue::from_i64_with_control(*number, control)?,
242        Value::Bool(boolean) => DecimalValue::from_i64_with_control(i64::from(*boolean), control)?,
243        Value::Float(number) => DecimalValue::from_f64_lossy_with_control(*number, control)?
244            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast {number:?} to numeric")))?,
245        Value::Str(text) => DecimalValue::parse_with_control(text, control)?
246            .ok_or_else(|| crate::expr::invalid_numeric_input(text))?,
247        other => {
248            return Err(SQLError::TypeMismatch(format!(
249                "cannot cast {other:?} to numeric"
250            )))
251        }
252    };
253    let rounded = match scale {
254        Some(scale) => decimal
255            .round_to_scale_with_control(scale, control)?
256            .ok_or_else(|| {
257                SQLError::TypeMismatch(format!("cannot round numeric to scale {scale}"))
258            })?,
259        None => decimal,
260    };
261    if let Some(precision) = precision {
262        let scale = scale.unwrap_or(0);
263        if !rounded.fits_precision_with_control(precision, scale, control)? {
264            return Err(numeric_field_overflow(precision, scale));
265        }
266    }
267    let (decimal, memory) = rounded.into_parts();
268    Ok(control.finish(Value::Decimal(decimal), memory)?)
269}
270
271fn text_value(
272    text: Produced<String>,
273    fixed: bool,
274    control: &ProductionControl<'_>,
275) -> Result<Produced<Value>> {
276    let (text, memory) = text.into_parts();
277    Ok(control.finish(
278        if fixed {
279            Value::FixedChar(text)
280        } else {
281            Value::Str(text)
282        },
283        memory,
284    )?)
285}
286
287fn character_value(
288    value: &Value,
289    length: u32,
290    fixed: bool,
291    control: &ProductionControl<'_>,
292) -> Result<Produced<Value>> {
293    let name = if fixed {
294        "character"
295    } else {
296        "character varying"
297    };
298    let length = usize::try_from(length).map_err(|_| {
299        SQLError::TypeMismatch(format!(
300            "{name} length {length} exceeds the platform addressable range"
301        ))
302    })?;
303    let text = value_to_text_with_control(value, control)?;
304    let mut count = 0;
305    let mut end = 0;
306    for (index, character) in text.char_indices() {
307        control.check()?;
308        if count < length {
309            count += 1;
310            end = index + character.len_utf8();
311        } else if character != ' ' {
312            return Err(SQLError::Routine {
313                sqlstate: "22001".into(),
314                message: format!("value too long for type {name}({length})"),
315            });
316        }
317    }
318    let mut output = ProductionString::from_produced(text, *control)?;
319    output.truncate(end)?;
320    if fixed {
321        for _ in count..length {
322            output.push(' ')?;
323        }
324    }
325    text_value(output.finish()?, fixed, control)
326}
327
328fn array_value(
329    value: Produced<Value>,
330    element: &ColumnType,
331    control: &ProductionControl<'_>,
332) -> Result<Produced<Value>> {
333    let array = match &*value {
334        Value::Array(_) => {
335            let (Value::Array(array), memory) = value.into_parts() else {
336                unreachable!();
337            };
338            control.finish(array, memory)?
339        }
340        Value::LegacyVector(_) => {
341            let (Value::LegacyVector(vector), memory) = value.into_parts() else {
342                unreachable!();
343            };
344            control.finish(vector.into_array(), memory)?
345        }
346        Value::List(_) => {
347            let (Value::List(elements), memory) = value.into_parts() else {
348                unreachable!();
349            };
350            ArrayValue::try_new_with_control(control.finish(elements, memory)?, control)?
351                .ok_or_else(array_shape_error)?
352        }
353        Value::Str(text) => parse_pg_array_literal_with_control(text, control)?,
354        other => {
355            return Err(SQLError::TypeMismatch(format!(
356                "cannot cast {other:?} to {}[]",
357                column_type_name(element)
358            )))
359        }
360    };
361    let converted = array_elements(array.elements(), array_scalar_type(element), control)?;
362    let mut bounds = ProductionVec::new(*control);
363    bounds.reserve(array.lower_bounds().len())?;
364    for bound in array.lower_bounds() {
365        bounds.push_copy(*bound)?;
366    }
367    let array = ArrayValue::with_lower_bounds_with_control(converted, bounds.finish()?, control)?
368        .ok_or_else(array_shape_error)?;
369    let (array, memory) = array.into_parts();
370    let oid = crate::catalog::type_metadata::pg_type_oid(array_scalar_type(element)) as u32;
371    Ok(control.finish(Value::Array(array.with_element_type_oid(Some(oid))), memory)?)
372}
373
374fn array_elements(
375    values: &[Value],
376    element: &ColumnType,
377    control: &ProductionControl<'_>,
378) -> Result<Produced<Vec<Value>>> {
379    let mut output = ProductionVec::new(*control);
380    output.reserve(values.len())?;
381    for value in values {
382        let value = match value {
383            Value::List(values) => {
384                let (values, memory) = array_elements(values, element, control)?.into_parts();
385                control.finish(Value::List(values), memory)?
386            }
387            scalar => convert_value_to_column_type_with_control(
388                control.copy_value(scalar)?,
389                element,
390                control,
391            )?,
392        };
393        output.push_produced(value)?;
394    }
395    Ok(output.finish()?)
396}
397
398fn array_shape_error() -> SQLError {
399    SQLError::TypeMismatch("multidimensional arrays must have matching dimensions".into())
400}
401
402fn record_value(
403    value: Produced<Value>,
404    control: &ProductionControl<'_>,
405) -> Result<Produced<Value>> {
406    let Value::Row(source) = &*value else {
407        unreachable!();
408    };
409    // A record identity conversion must retain the producing expression's descriptor.
410    if source.field_types().is_some() {
411        return Ok(value);
412    }
413    let mut records = ProductionVec::new(*control);
414    records.reserve(source.len())?;
415    for index in 0..source.len() {
416        let (name, memory) = control.format(format_args!("f{}", index + 1))?.into_parts();
417        records.push_produced(control.finish((name, Value::Null), memory)?)?;
418    }
419    let (records, records_memory) = records.finish()?.into_parts();
420    let (Value::Row(source), source_memory) = value.into_parts() else {
421        unreachable!();
422    };
423    let old_buffer_bytes =
424        source.capacity() * size_of::<Value>() + uqa_core::RowValue::retained_header_bytes();
425    let mut parts = RecordParts {
426        source: source.into_values(),
427        records,
428        memory: control.combine(source_memory, records_memory),
429    };
430    for ((_, destination), source) in parts.records.iter_mut().zip(parts.source) {
431        control.check()?;
432        *destination = source;
433    }
434    if let Some(memory) = &mut parts.memory {
435        drop(memory.split(old_buffer_bytes));
436    }
437    let (record, memory) = uqa_core::RecordValue::with_control(
438        control.finish(parts.records, parts.memory)?,
439        None,
440        control,
441    )?
442    .into_parts();
443    Ok(control.finish(Value::Record(record), memory)?)
444}
445
446struct RecordParts {
447    source: Vec<Value>,
448    records: Vec<(String, Value)>,
449    memory: Option<MemoryReservation>,
450}
451
452fn vector_value(vector: &[f32], control: &ProductionControl<'_>) -> Result<Produced<Value>> {
453    let mut output = ProductionVec::new(*control);
454    output.reserve(vector.len())?;
455    for value in vector {
456        output.push_produced(
457            control.finish(Value::Float(f64::from(*value)), control.empty_reservation())?,
458        )?;
459    }
460    let (values, memory) = output.finish()?.into_parts();
461    Ok(control.finish(Value::List(values), memory)?)
462}
463
464fn parse_boolean_text(text: &str) -> Option<bool> {
465    let text = text.trim();
466    if ["true", "t", "yes", "y", "on", "1"]
467        .iter()
468        .any(|candidate| text.eq_ignore_ascii_case(candidate))
469    {
470        Some(true)
471    } else if ["false", "f", "no", "n", "off", "0"]
472        .iter()
473        .any(|candidate| text.eq_ignore_ascii_case(candidate))
474    {
475        Some(false)
476    } else {
477        None
478    }
479}
480
481#[cfg(test)]
482mod tests;
483
484/// `apply_typmod`: a value that does not fit the declared precision reports `22003` with the magnitude the field admits, `10^(precision - scale)`, or `1` when the precision equals the scale.
485#[must_use]
486pub fn numeric_field_overflow(precision: u32, scale: i32) -> SQLError {
487    let maxdigits = i64::from(precision) - i64::from(scale);
488    SQLError::Diagnostic {
489        sqlstate: "22003".into(),
490        message: "numeric field overflow".into(),
491        detail: Some(format!(
492            "A field with precision {precision}, scale {scale} must round to an absolute value less than {}.",
493            if maxdigits == 0 {
494                "1".to_string()
495            } else {
496                format!("10^{maxdigits}")
497            }
498        )),
499        hint: None,
500    }
501}