Skip to main content

uqa_sql/expr/
conversion.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Scalar coercion, checked numeric conversion, and vector/tensor decoding.
8
9use super::{out_of_range, ArrayValue, DecimalValue, Result, SQLError, Value};
10
11use uqa_core::memory::{Produced, ProductionControl, ProductionString, ProductionVec};
12
13/// A value whose text is owned by the SQL catalog reached a context-free conversion. Catalog-aware callers render these carriers before delegating here.
14pub fn catalog_output_required(value: &uqa_core::EnumValue) -> SQLError {
15    SQLError::Internal(format!(
16        "enum value of type OID {} requires catalog-aware output",
17        value.type_oid()
18    ))
19}
20
21/// Produce SQL text, including errors from type output functions.
22pub fn value_to_string(value: &Value) -> Result<String> {
23    value_to_string_with_control(value, &ProductionControl::uncontrolled())
24        .map(|text| text.into_uncontrolled().expect("ordinary value text"))
25}
26
27pub fn value_to_string_with_control(
28    value: &Value,
29    control: &ProductionControl<'_>,
30) -> Result<Produced<String>> {
31    control.check()?;
32    Ok(match value {
33        Value::Null | Value::Void => control.copy_text("")?,
34        Value::Int(value) => control.format(format_args!("{value}"))?,
35        Value::Float(value) => uqa_core::format_float_pg_with_control(*value, control)?,
36        Value::Decimal(value) => value.to_sql_string_with_control(control)?,
37        Value::Str(value) | Value::Json(value) | Value::JsonB(value) => control.copy_text(value)?,
38        Value::FixedChar(value) => control.copy_text(value.trim_end_matches(' '))?,
39        Value::Bool(value) => control.copy_text(if *value { "true" } else { "false" })?,
40        Value::Temporal(value) => value.to_sql_string_in_interval_style_with_control(
41            super::temporal_interval_style(),
42            control,
43        )?,
44        Value::Array(value) => array_value_to_string_with_control(value, control)?,
45        Value::LegacyVector(_) => {
46            vector_value_to_string_with_control(value, control)?.expect("validated legacy vector")
47        }
48        Value::List(_) | Value::Map(_) => {
49            return super::json::format_value_as_json_with_control(value, control)
50        }
51        Value::Row(values) => composite_value_to_string(values.iter(), control)?,
52        Value::Record(fields) => {
53            composite_value_to_string(fields.iter().map(|(_, value)| value), control)?
54        }
55        Value::Enum(value) => return Err(catalog_output_required(value)),
56        Value::Datum(value) => {
57            return value_to_string_with_control(
58                &*super::datums::read_with_control(value, control)?,
59                control,
60            )
61        }
62        Value::Bytes(values) => bytea_to_string_with_control(values, control)?,
63    })
64}
65
66pub(crate) fn bytea_to_string_with_control(
67    values: &[u8],
68    control: &ProductionControl<'_>,
69) -> Result<Produced<String>> {
70    const HEX: &[u8; 16] = b"0123456789abcdef";
71    let mut text = ProductionString::new(*control);
72    text.push_str("\\x")?;
73    for byte in values {
74        text.push(char::from(HEX[usize::from(byte >> 4)]))?;
75        text.push(char::from(HEX[usize::from(byte & 0xf)]))?;
76    }
77    Ok(text.finish()?)
78}
79
80/// `PostgreSQL`'s legacy vector text format separates values with spaces.
81pub fn vector_value_to_string(value: &Value) -> Result<Option<String>> {
82    vector_value_to_string_with_control(value, &ProductionControl::uncontrolled())
83        .map(|text| text.map(|text| text.into_uncontrolled().expect("ordinary vector text")))
84}
85
86pub(super) fn vector_value_to_string_with_control(
87    value: &Value,
88    control: &ProductionControl<'_>,
89) -> Result<Option<Produced<String>>> {
90    control.check()?;
91    let elements = match value {
92        Value::LegacyVector(vector) => {
93            if !vector.has_vector_layout() {
94                return Err(SQLError::Routine {
95                    sqlstate: "42804".into(),
96                    message: format!("array is not a valid {}", vector.kind().type_name()),
97                });
98            }
99            vector.elements()
100        }
101        Value::List(elements) => elements.as_slice(),
102        Value::Array(array) if array.dimensions().len() <= 1 => array.elements(),
103        _ => return Ok(None),
104    };
105    let mut text = ProductionString::new(*control);
106    for (index, value) in elements.iter().enumerate() {
107        if index != 0 {
108            text.push(' ')?;
109        }
110        text.push_str(&value_to_string_with_control(value, control)?)?;
111    }
112    Ok(Some(text.finish()?))
113}
114
115pub fn array_value_to_string(array: &ArrayValue) -> Result<String> {
116    array_value_to_string_with_control(array, &ProductionControl::uncontrolled())
117        .map(|text| text.into_uncontrolled().expect("ordinary array text"))
118}
119
120pub(super) fn array_value_to_string_with_control(
121    array: &ArrayValue,
122    control: &ProductionControl<'_>,
123) -> Result<Produced<String>> {
124    let mut text = ProductionString::new(*control);
125    append_array(&mut text, array, control)?;
126    Ok(text.finish()?)
127}
128
129fn append_array(
130    text: &mut ProductionString<'_>,
131    array: &ArrayValue,
132    control: &ProductionControl<'_>,
133) -> Result<()> {
134    if !array.elements().is_empty() && array.lower_bounds().iter().any(|lower| *lower != 1) {
135        for (lower, length) in array.lower_bounds().iter().zip(array.dimensions()) {
136            let upper = i64::from(*lower) + i64::try_from(*length).unwrap_or(i64::MAX) - 1;
137            text.push_str(&control.format(format_args!("[{lower}:{upper}]"))?)?;
138        }
139        text.push('=')?;
140    }
141    append_array_elements(text, array.elements(), control)
142}
143
144fn append_array_elements(
145    text: &mut ProductionString<'_>,
146    elements: &[Value],
147    control: &ProductionControl<'_>,
148) -> Result<()> {
149    text.push('{')?;
150    for (index, value) in elements.iter().enumerate() {
151        if index != 0 {
152            text.push(',')?;
153        }
154        match value {
155            Value::Null => text.push_str("NULL")?,
156            Value::Bool(value) => text.push_str(if *value { "t" } else { "f" })?,
157            Value::List(values) => append_array_elements(text, values, control)?,
158            Value::Array(array) => append_array(text, array, control)?,
159            other => {
160                let value = value_to_string_with_control(other, control)?;
161                let mut quoted = value.is_empty() || value.eq_ignore_ascii_case("null");
162                for character in value.chars() {
163                    control.check()?;
164                    quoted |= character.is_whitespace()
165                        || matches!(character, ',' | '{' | '}' | '"' | '\\');
166                }
167                append_escaped(text, &value, quoted, false)?;
168            }
169        }
170    }
171    text.push('}')?;
172    Ok(())
173}
174
175fn composite_value_to_string<'a>(
176    values: impl IntoIterator<Item = &'a Value>,
177    control: &ProductionControl<'_>,
178) -> Result<Produced<String>> {
179    let mut text = ProductionString::new(*control);
180    text.push('(')?;
181    for (index, value) in values.into_iter().enumerate() {
182        if index != 0 {
183            text.push(',')?;
184        }
185        if matches!(value, Value::Null) {
186            continue;
187        }
188        let value = match value {
189            Value::Bool(value) => control.copy_text(if *value { "t" } else { "f" })?,
190            other => value_to_string_with_control(other, control)?,
191        };
192        let mut quoted = value.is_empty();
193        for byte in value.bytes() {
194            control.check()?;
195            quoted |=
196                matches!(byte, b',' | b'(' | b')' | b'"' | b'\\') || byte.is_ascii_whitespace();
197        }
198        append_escaped(&mut text, &value, quoted, true)?;
199    }
200    text.push(')')?;
201    Ok(text.finish()?)
202}
203
204fn append_escaped(
205    text: &mut ProductionString<'_>,
206    value: &str,
207    quoted: bool,
208    composite: bool,
209) -> Result<()> {
210    if !quoted {
211        text.push_str(value)?;
212        return Ok(());
213    }
214    text.push('"')?;
215    for character in value.chars() {
216        if character == '\\' || (character == '"' && !composite) {
217            text.push('\\')?;
218        }
219        if character == '"' && composite {
220            text.push('"')?;
221        }
222        text.push(character)?;
223    }
224    text.push('"')?;
225    Ok(())
226}
227
228pub(super) fn float1_with_control<F: FnOnce(f64) -> f64>(
229    args: &[Value],
230    name: &str,
231    f: F,
232    control: &ProductionControl<'_>,
233) -> Result<Value> {
234    control.check()?;
235    if args.len() != 1 {
236        return Err(SQLError::TypeMismatch(format!("{name} takes 1 arg")));
237    }
238    if matches!(args[0], Value::Null) {
239        return Ok(Value::Null);
240    }
241    Ok(Value::Float(f(to_f64_with_control(&args[0], control)?)))
242}
243
244pub(super) fn to_i64(v: &Value) -> Result<i64> {
245    to_i64_with_control(v, &ProductionControl::uncontrolled())
246}
247
248pub(super) fn to_i64_with_control(v: &Value, control: &ProductionControl<'_>) -> Result<i64> {
249    if let Value::Datum(datum) = v {
250        return to_i64_with_control(&*super::datums::read_with_control(datum, control)?, control);
251    }
252    control.check()?;
253    match v {
254        Value::Int(n) => Ok(*n),
255        Value::Float(f) => float_to_i64_trunc(*f),
256        Value::Decimal(d) => d
257            .to_i64_trunc_with_control(control)?
258            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast {v:?} to integer"))),
259        Value::Bool(b) => Ok(i64::from(*b)),
260        Value::Str(s) | Value::FixedChar(s) => s
261            .trim()
262            .parse()
263            .map_err(|_| SQLError::TypeMismatch(format!("cannot parse {s:?} as integer"))),
264        other => Err(SQLError::TypeMismatch(format!(
265            "expected integer, got {other:?}"
266        ))),
267    }
268}
269
270pub(super) fn nonnegative_usize(value: i64, label: &str) -> Result<usize> {
271    usize::try_from(value).map_err(|_| SQLError::Routine {
272        sqlstate: "22003".into(),
273        message: format!("{label} exceeds the platform addressable range"),
274    })
275}
276
277pub(super) fn allocation_error(label: &str) -> SQLError {
278    SQLError::Routine {
279        sqlstate: "53200".into(),
280        message: format!("{label} result exceeds available memory"),
281    }
282}
283
284pub(crate) fn to_f64(v: &Value) -> Result<f64> {
285    to_f64_with_control(v, &ProductionControl::uncontrolled())
286}
287
288pub(crate) fn to_f64_with_control(v: &Value, control: &ProductionControl<'_>) -> Result<f64> {
289    if let Value::Datum(datum) = v {
290        return to_f64_with_control(&*super::datums::read_with_control(datum, control)?, control);
291    }
292    super::floating::to_float_with_control(v, super::FloatWidth::DoublePrecision, control)
293}
294
295/// The error `numeric_in` reports for text it cannot read.
296#[must_use]
297pub fn invalid_numeric_input(text: &str) -> SQLError {
298    SQLError::Routine {
299        sqlstate: "22P02".into(),
300        message: format!("invalid input syntax for type numeric: \"{text}\""),
301    }
302}
303
304pub(super) fn to_decimal(value: &Value) -> Result<DecimalValue> {
305    to_decimal_with_control(value, &ProductionControl::uncontrolled())?
306        .into_uncontrolled()
307        .map_err(|_| SQLError::Internal("ordinary numeric production owner".into()))
308}
309
310pub(super) fn to_decimal_with_control(
311    value: &Value,
312    control: &ProductionControl<'_>,
313) -> Result<Produced<DecimalValue>> {
314    control.check()?;
315    match value {
316        Value::Datum(datum) => {
317            to_decimal_with_control(&*super::datums::read_with_control(datum, control)?, control)
318        }
319        Value::Decimal(value) => Ok(value.clone_with_control(control)?),
320        Value::Int(value) => Ok(DecimalValue::from_i64_with_control(*value, control)?),
321        Value::Bool(value) => Ok(DecimalValue::from_i64_with_control(
322            i64::from(*value),
323            control,
324        )?),
325        Value::Float(number) => DecimalValue::from_f64_lossy_with_control(*number, control)?
326            .ok_or_else(|| SQLError::TypeMismatch(format!("cannot cast {value:?} to numeric"))),
327        Value::Str(text) | Value::FixedChar(text) => {
328            DecimalValue::parse_with_control(text, control)?
329                .ok_or_else(|| invalid_numeric_input(text))
330        }
331        other => Err(SQLError::TypeMismatch(format!(
332            "expected number, got {other:?}"
333        ))),
334    }
335}
336
337pub(super) fn float_to_i64_trunc(value: f64) -> Result<i64> {
338    if !value.is_finite() || value < i64::MIN as f64 || value >= 9_223_372_036_854_775_808.0 {
339        return Err(out_of_range("bigint"));
340    }
341    Ok(value.trunc() as i64)
342}
343
344pub(super) fn float_to_i64_rounded(value: f64, type_name: &str) -> Result<i64> {
345    let rounded = value.round();
346    if !rounded.is_finite() || rounded < i64::MIN as f64 || rounded >= 9_223_372_036_854_775_808.0 {
347        return Err(out_of_range(type_name));
348    }
349    Ok(rounded as i64)
350}
351
352pub(super) fn gcd_i64(a: i64, b: i64) -> Result<i64> {
353    let mut a = a.unsigned_abs();
354    let mut b = b.unsigned_abs();
355    while b != 0 {
356        let r = a % b;
357        a = b;
358        b = r;
359    }
360    i64::try_from(a).map_err(|_| out_of_range("bigint"))
361}
362
363/// Coerce a [`Value`] into a `Vec<f32>` if it is a homogeneous numeric
364/// list (used to read vector literals from `ARRAY[...]` or `$N` Vector
365/// params).
366pub fn value_to_vector(v: &Value) -> Result<Vec<f32>> {
367    value_to_vector_with_control(v, &ProductionControl::uncontrolled())?
368        .into_uncontrolled()
369        .map_err(|_| SQLError::Internal("ordinary vector owner".into()))
370}
371
372pub fn value_to_vector_with_control(
373    v: &Value,
374    control: &ProductionControl<'_>,
375) -> Result<Produced<Vec<f32>>> {
376    let items = vector_items(v)?;
377    let mut out = ProductionVec::new(*control);
378    out.reserve(items.len())?;
379    for item in items {
380        out.push_copy(vector_element_with_control(item, control)?)?;
381    }
382    Ok(out.finish()?)
383}
384
385pub(crate) fn vector_items(v: &Value) -> Result<&[Value]> {
386    match v {
387        Value::List(items) => Ok(items.as_slice()),
388        Value::Array(array) if array.dimensions().len() <= 1 => Ok(array.elements()),
389        Value::Array(array) => Err(SQLError::TypeMismatch(format!(
390            "expected one-dimensional vector input, got {} dimensions",
391            array.dimensions().len()
392        ))),
393        other => Err(SQLError::TypeMismatch(format!(
394            "expected vector (numeric array), got {other:?}"
395        ))),
396    }
397}
398
399pub(crate) fn vector_element(item: &Value) -> Result<f32> {
400    vector_element_with_control(item, &ProductionControl::uncontrolled())
401}
402
403pub(crate) fn vector_element_with_control(
404    item: &Value,
405    control: &ProductionControl<'_>,
406) -> Result<f32> {
407    control.check()?;
408    match item {
409        Value::Float(f) => numeric_f64_to_f32(*f, item),
410        Value::Int(i) => Ok(*i as f32),
411        Value::Decimal(d) => numeric_f64_to_f32(
412            d.to_f64_with_control(control)?.ok_or_else(|| {
413                SQLError::TypeMismatch(format!("vector element must fit f32, got {item:?}"))
414            })?,
415            item,
416        ),
417        other => Err(SQLError::TypeMismatch(format!(
418            "vector element must be numeric, got {other:?}"
419        ))),
420    }
421}
422
423pub(super) fn numeric_f64_to_f32(value: f64, source: &Value) -> Result<f32> {
424    if !value.is_finite() || value < -(f32::MAX as f64) || value > f32::MAX as f64 {
425        return Err(SQLError::TypeMismatch(format!(
426            "vector element must be finite and fit f32, got {source:?}"
427        )));
428    }
429    Ok(value as f32)
430}
431
432/// Coerce a [`Value`] into a tensor: an array of homogeneous numeric
433/// vectors. Used by `TENSOR(N)` columns to store chunk embeddings for one
434/// row while still indexing each vector element.
435pub fn value_to_tensor(v: &Value) -> Result<Vec<Vec<f32>>> {
436    value_to_tensor_with_control(v, &ProductionControl::uncontrolled())?
437        .into_uncontrolled()
438        .map_err(|_| SQLError::Internal("ordinary tensor owner".into()))
439}
440
441pub fn value_to_tensor_with_control(
442    v: &Value,
443    control: &ProductionControl<'_>,
444) -> Result<Produced<Vec<Vec<f32>>>> {
445    let items = tensor_items(v)?;
446    let mut out = ProductionVec::new(*control);
447    out.reserve(items.len())?;
448    for item in items {
449        out.push_produced(value_to_vector_with_control(item, control)?)?;
450    }
451    Ok(out.finish()?)
452}
453
454pub(crate) fn tensor_items(v: &Value) -> Result<&[Value]> {
455    match v {
456        Value::List(items) => Ok(items.as_slice()),
457        Value::Array(array) if array.dimensions().is_empty() || array.dimensions().len() == 2 => {
458            Ok(array.elements())
459        }
460        Value::Array(array) => Err(SQLError::TypeMismatch(format!(
461            "expected two-dimensional tensor input, got {} dimensions",
462            array.dimensions().len()
463        ))),
464        other => Err(SQLError::TypeMismatch(format!(
465            "expected tensor (array of numeric arrays), got {other:?}"
466        ))),
467    }
468}