1use 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
16pub fn format_postgres_text(
18 value: &Value,
19 ty: &ColumnType,
20 engine: Option<&dyn EngineHook>,
21) -> Result<String, SQLError> {
22 if let Value::Datum(datum) = value {
23 return format_postgres_text(
24 &crate::expr::datums::read_with_catalog(datum, engine)?,
25 ty,
26 engine,
27 );
28 }
29 if let ColumnType::Domain { base, .. } = ty {
30 return format_postgres_text(value, base, engine);
31 }
32 if let Some(text) = format_regtype_value(value, ty, engine)? {
33 return Ok(text);
34 }
35 if matches!(ty, ColumnType::Int2Vector | ColumnType::OidVector) {
36 return vector_value_to_string(value)?
37 .ok_or_else(|| SQLError::Internal("invalid catalog vector result carrier".into()));
38 }
39 if let ColumnType::Array(element) = ty {
40 return format_array(value, element, engine);
41 }
42 if let (ColumnType::Composite(reference), Value::Record(fields)) = (ty, value) {
43 return format_record(fields, fields.type_oid().unwrap_or(reference.oid), engine);
44 }
45 Ok(match value {
46 Value::Bool(value) => if *value { "t" } else { "f" }.into(),
47 Value::FixedChar(value) => value.clone(),
48 Value::Float(value) if matches!(ty, ColumnType::Real) => {
49 crate::expr::format_real(*value as f32)
50 }
51 Value::Float(value) => uqa_core::format_float_pg(*value),
52 Value::Enum(label) => {
53 crate::expr::enums::enum_label_text(engine.and_then(EngineHook::enum_labels), label)?
54 }
55 _ if crate::expr::enums::contains_enum_carrier(value) => {
57 value_to_string(&crate::expr::enums::render_enum_labels(
58 engine.and_then(EngineHook::enum_labels),
59 value,
60 )?)?
61 }
62 _ => value_to_string(value)?,
63 })
64}
65
66fn format_array(
67 value: &Value,
68 element: &ColumnType,
69 engine: Option<&dyn EngineHook>,
70) -> Result<String, SQLError> {
71 let mut element = element;
74 while let ColumnType::Array(inner) = element {
75 element = inner;
76 }
77 let catalog_element;
78 if let Value::Array(array) = value {
79 if let Some(oid) = array.element_type_oid() {
80 if let Some(actual) = crate::catalog::type_metadata::builtin_scalar_type(oid) {
81 element = actual;
82 } else if crate::catalog::type_metadata::pg_type_oid(element) != i64::from(oid) {
83 catalog_element = engine
84 .map(|engine| engine.resolve_type_oid(oid))
85 .transpose()
86 .map_err(SQLError::Internal)?
87 .flatten()
88 .ok_or_else(|| SQLError::Routine {
89 sqlstate: "XX000".into(),
90 message: format!("cache lookup failed for type {oid}"),
91 })?;
92 element = &catalog_element;
93 }
94 }
95 }
96 let (values, prefix) = match value {
97 Value::Array(array) => array_parts(array),
98 Value::LegacyVector(vector) => array_parts(vector.as_array()),
99 Value::List(values) => (values.as_slice(), String::new()),
100 _ => return Err(SQLError::Internal("invalid array result carrier".into())),
101 };
102 let mut fields = Vec::with_capacity(values.len());
103 for value in values {
104 fields.push(match value {
105 Value::Null => "NULL".into(),
106 Value::List(_) | Value::Array(_) => format_array(value, element, engine)?,
107 _ => {
108 let text = format_postgres_text(value, element, engine)?;
109 if text.is_empty()
110 || text.eq_ignore_ascii_case("null")
111 || text.chars().any(|c| {
112 c.is_ascii_whitespace() || matches!(c, ',' | '{' | '}' | '"' | '\\')
113 })
114 {
115 format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\""))
116 } else {
117 text
118 }
119 }
120 });
121 }
122 Ok(format!("{prefix}{{{}}}", fields.join(",")))
123}
124
125fn format_record(
127 fields: &[(String, Value)],
128 type_oid: u32,
129 engine: Option<&dyn EngineHook>,
130) -> Result<String, SQLError> {
131 let descriptor = crate::expr::composites::descriptor(
132 engine.and_then(EngineHook::composite_types),
133 type_oid,
134 )?;
135 let mut text = String::from("(");
136 for (index, (name, field)) in fields.iter().enumerate() {
137 if index != 0 {
138 text.push(',');
139 }
140 if matches!(field, Value::Null) {
141 continue;
142 }
143 let rendered = match descriptor.attribute(name) {
144 Some((_, attribute)) => format_postgres_text(field, &attribute.ty, engine)?,
145 None => format_postgres_text(
146 field,
147 &crate::type_resolution::value_type(field).unwrap_or(ColumnType::Text),
148 engine,
149 )?,
150 };
151 let quoted = rendered.is_empty()
152 || rendered.chars().any(|character| {
153 character.is_ascii_whitespace() || matches!(character, ',' | '(' | ')' | '"' | '\\')
154 });
155 if quoted {
156 text.push('"');
157 for character in rendered.chars() {
158 if matches!(character, '"' | '\\') {
159 text.push(character);
160 }
161 text.push(character);
162 }
163 text.push('"');
164 } else {
165 text.push_str(&rendered);
166 }
167 }
168 text.push(')');
169 Ok(text)
170}
171
172fn array_parts(array: &uqa_core::ArrayValue) -> (&[Value], String) {
173 let mut prefix = String::new();
174 if !array.elements().is_empty() && array.lower_bounds().iter().any(|lower| *lower != 1) {
175 for (lower, length) in array.lower_bounds().iter().zip(array.dimensions()) {
176 let upper = i64::from(*lower) + *length as i64 - 1;
177 write!(prefix, "[{lower}:{upper}]").expect("writing to String cannot fail");
178 }
179 prefix.push('=');
180 }
181 (array.elements(), prefix)
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use std::cell::Cell;
188 use uqa_core::ArrayValue;
189
190 struct Catalog {
191 lookups: Cell<usize>,
192 }
193
194 fn domain() -> ColumnType {
195 ColumnType::Domain {
196 schema: "public".into(),
197 name: "real_value".into(),
198 oid: 16_500,
199 array_oid: Some(16_501),
200 base: Box::new(ColumnType::Real),
201 }
202 }
203
204 impl EngineHook for Catalog {
205 fn nextval(&self, _: &str) -> Result<i64, SQLError> {
206 unreachable!()
207 }
208
209 fn currval(&self, _: &str) -> Result<i64, SQLError> {
210 unreachable!()
211 }
212
213 fn setval(&self, _: &str, _: i64, _: bool) -> Result<i64, SQLError> {
214 unreachable!()
215 }
216
217 fn resolve_type_oid(&self, oid: u32) -> Result<Option<ColumnType>, String> {
218 self.lookups.set(self.lookups.get() + 1);
219 Ok((oid == 16_500).then(domain))
220 }
221 }
222
223 #[test]
224 fn array_output_uses_original_catalog_element_and_reports_missing_identity() {
225 let catalog = Catalog {
226 lookups: Cell::new(0),
227 };
228 let value = Value::Array(
229 ArrayValue::try_new(vec![Value::Float(f64::from(0.1_f32)), Value::Null])
230 .unwrap()
231 .with_element_type_oid(Some(16_500)),
232 );
233 let changed = ColumnType::Array(Box::new(ColumnType::Text));
234 assert_eq!(
235 format_postgres_text(&value, &changed, Some(&catalog)).unwrap(),
236 "{0.1,NULL}"
237 );
238 assert_eq!(catalog.lookups.get(), 1);
239 assert_eq!(
240 format_postgres_text(
241 &value,
242 &ColumnType::Array(Box::new(domain())),
243 Some(&catalog),
244 )
245 .unwrap(),
246 "{0.1,NULL}"
247 );
248 assert_eq!(catalog.lookups.get(), 1);
249 let error = format_postgres_text(&value, &changed, None).unwrap_err();
250 assert_eq!(error.sqlstate(), Some("XX000"));
251 assert_eq!(error.to_string(), "cache lookup failed for type 16500");
252 }
253}