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