Skip to main content

uqa_sql/result/
text.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Typed `PostgreSQL` text output for result consumers.
8
9use std::fmt::Write;
10
11use crate::ast::ColumnType;
12use crate::expr::{format_regtype_value, value_to_string, vector_value_to_string, EngineHook};
13use crate::SQLError;
14use uqa_core::Value;
15
16/// Format a non-NULL result value using its declared type and optional catalog resolver. NULL remains separate from text in the calling result protocol.
17pub fn format_postgres_text(
18    value: &Value,
19    ty: &ColumnType,
20    engine: Option<&dyn EngineHook>,
21) -> Result<String, SQLError> {
22    if let ColumnType::Domain { base, .. } = ty {
23        return format_postgres_text(value, base, engine);
24    }
25    if let Some(text) = format_regtype_value(value, ty, engine)? {
26        return Ok(text);
27    }
28    if matches!(ty, ColumnType::Int2Vector | ColumnType::OidVector) {
29        return vector_value_to_string(value)?
30            .ok_or_else(|| SQLError::Internal("invalid catalog vector result carrier".into()));
31    }
32    if let ColumnType::Array(element) = ty {
33        return format_array(value, element, engine);
34    }
35    Ok(match value {
36        Value::Bool(value) => if *value { "t" } else { "f" }.into(),
37        Value::FixedChar(value) => value.clone(),
38        Value::Float(value) if matches!(ty, ColumnType::Real) => {
39            crate::expr::format_real(*value as f32)
40        }
41        Value::Float(value) => uqa_core::format_float_pg(*value),
42        _ => value_to_string(value)?,
43    })
44}
45
46fn format_array(
47    value: &Value,
48    element: &ColumnType,
49    engine: Option<&dyn EngineHook>,
50) -> Result<String, SQLError> {
51    // SQL array type identity does not constrain value dimensions. Nested
52    // declarations still name the scalar element formatter at every depth.
53    let mut element = element;
54    while let ColumnType::Array(inner) = element {
55        element = inner;
56    }
57    let (values, prefix) = match value {
58        Value::Array(array) => array_parts(array),
59        Value::LegacyVector(vector) => array_parts(vector.as_array()),
60        Value::List(values) => (values.as_slice(), String::new()),
61        _ => return Err(SQLError::Internal("invalid array result carrier".into())),
62    };
63    let mut fields = Vec::with_capacity(values.len());
64    for value in values {
65        fields.push(match value {
66            Value::Null => "NULL".into(),
67            Value::List(_) | Value::Array(_) => format_array(value, element, engine)?,
68            _ => {
69                let text = format_postgres_text(value, element, engine)?;
70                if text.is_empty()
71                    || text.eq_ignore_ascii_case("null")
72                    || text.chars().any(|c| {
73                        c.is_ascii_whitespace() || matches!(c, ',' | '{' | '}' | '"' | '\\')
74                    })
75                {
76                    format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\""))
77                } else {
78                    text
79                }
80            }
81        });
82    }
83    Ok(format!("{prefix}{{{}}}", fields.join(",")))
84}
85
86fn array_parts(array: &uqa_core::ArrayValue) -> (&[Value], String) {
87    let mut prefix = String::new();
88    if !array.elements().is_empty() && array.lower_bounds().iter().any(|lower| *lower != 1) {
89        for (lower, length) in array.lower_bounds().iter().zip(array.dimensions()) {
90            let upper = i64::from(*lower) + *length as i64 - 1;
91            write!(prefix, "[{lower}:{upper}]").expect("writing to String cannot fail");
92        }
93        prefix.push('=');
94    }
95    (array.elements(), prefix)
96}