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