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