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