Skip to main content

uqa_sql/catalog/node_tree/
deparse.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Reconstruct SQL from typed catalog nodes using the caller's catalog names.
8
9use super::{invalid, values, Field, Node};
10use crate::SQLError;
11
12mod bodies;
13mod coercions;
14mod control;
15mod fields;
16mod operators;
17mod partition;
18mod relation;
19mod rows;
20mod subscripts;
21mod types;
22
23pub use bodies::return_body;
24pub use relation::validate_relation;
25
26#[cfg(test)]
27mod tests;
28
29pub trait ExpressionNames {
30    fn column(&self, attribute: i64) -> Result<String, SQLError>;
31    /// The current local relation name used by a whole-row Var.
32    fn relation_name(&self) -> Result<String, SQLError> {
33        Err(invalid("whole-row reference requires its relation name"))
34    }
35    fn routine(&self, oid: i64) -> Result<Vec<String>, SQLError>;
36    fn type_name(&self, oid: i64, modifier: i64) -> Result<String, SQLError>;
37
38    /// Current SQL-visible name of a selected collation identity.
39    fn collation(&self, oid: i64) -> Result<Vec<String>, SQLError> {
40        crate::catalog::collations::builtin_collation_name(oid)
41            .map(|name| vec![name.into()])
42            .ok_or_else(|| invalid("unknown collation identity"))
43    }
44
45    /// Type identities and current output metadata for admitted physical constants.
46    fn value_catalog(&self) -> Option<&dyn crate::expr::SQLValueCatalog> {
47        None
48    }
49
50    /// The invoking session's string-literal rule for reconstructed SQL.
51    fn standard_conforming_strings(&self) -> Result<bool, SQLError> {
52        Ok(true)
53    }
54
55    /// SQL spelling of an external parameter after the renderer validates its positive number.
56    fn parameter(&self, number: i64) -> Result<String, SQLError> {
57        Ok(format!("${number}"))
58    }
59}
60
61pub fn expression(
62    value: &Field,
63    names: &dyn ExpressionNames,
64    pretty: bool,
65) -> Result<String, SQLError> {
66    let renderer = Renderer {
67        names,
68        pretty,
69        indent: 0,
70    };
71    match value {
72        Field::List(values) => values
73            .iter()
74            .map(|value| renderer.field(coercions::implicit_cast_input(value)?, pretty))
75            .collect::<Result<Vec<_>, _>>()
76            .map(|values| values.join(", ")),
77        _ => renderer.field(coercions::implicit_cast_input(value)?, pretty),
78    }
79}
80
81struct Renderer<'a> {
82    names: &'a dyn ExpressionNames,
83    pretty: bool,
84    indent: usize,
85}
86
87impl Renderer<'_> {
88    fn field(&self, value: &Field, outer: bool) -> Result<String, SQLError> {
89        match value {
90            Field::Node(node) => self.node(node, outer),
91            Field::List(values) => values
92                .iter()
93                .map(|value| self.field(value, outer))
94                .collect::<Result<Vec<_>, _>>()
95                .map(|values| values.join(", ")),
96            _ => Err(invalid("expected an expression node")),
97        }
98    }
99
100    fn node(&self, node: &Node, outer: bool) -> Result<String, SQLError> {
101        match node.kind.as_str() {
102            "COERCETODOMAINVALUE" => Ok("VALUE".into()),
103            "VAR" => self.variable(node),
104            "PARAM" => self.parameter(node),
105            "SUBSCRIPTINGREF" => self.array_access(node),
106            "FIELDSELECT" => self.field_select(node),
107            "CONST" => self.constant(node),
108            "PARTITIONBOUNDSPEC" => self.partition_bound(node),
109            "COLLATEEXPR" => self.collate(node, outer),
110            "OPEXPR" | "DISTINCTEXPR" => self.operator(node, outer),
111            "ROWEXPR" => self.row(node),
112            "ROWCOMPAREEXPR" => self.row_comparison(node),
113            "NULLIFEXPR" => {
114                let [left, right] = list(node, "args")? else {
115                    return Err(invalid("invalid NULLIF operands"));
116                };
117                Ok(format!(
118                    "NULLIF({}, {})",
119                    self.field(left, self.pretty)?,
120                    self.field(right, self.pretty)?
121                ))
122            }
123            "FUNCEXPR" => self.function(node),
124            "NAMEDARGEXPR" => Ok(format!(
125                "{} => {}",
126                crate::expr::quote_ident(atom(node, "name")?),
127                self.field(node.field("arg")?, self.pretty)?
128            )),
129            "RELABELTYPE" | "COERCEVIAIO" | "COERCETODOMAIN" | "ARRAYCOERCEEXPR" => {
130                self.coercion(node, outer)
131            }
132            "BOOLEXPR" => self.boolean(node, outer),
133            "BOOLEANTEST" => self.boolean_test(node, outer),
134            "NULLTEST" => self.null_test(node, outer),
135            "COALESCEEXPR" | "MINMAXEXPR" => {
136                let name = match node.kind.as_str() {
137                    "COALESCEEXPR" => "COALESCE",
138                    _ => match node.integer("op")? {
139                        0 => "GREATEST",
140                        1 => "LEAST",
141                        _ => return Err(invalid("invalid minimum/maximum expression")),
142                    },
143                };
144                let arguments = list(node, "args")?
145                    .iter()
146                    .map(|arg| self.field(arg, self.pretty))
147                    .collect::<Result<Vec<_>, _>>()?;
148                Ok(format!("{name}({})", arguments.join(", ")))
149            }
150            "ARRAYEXPR" => {
151                let elements = list(node, "elements")?
152                    .iter()
153                    .map(|arg| self.field(arg, self.pretty))
154                    .collect::<Result<Vec<_>, _>>()?;
155                if elements.is_empty() {
156                    return Ok(format!(
157                        "ARRAY[]::{}",
158                        self.names.type_name(node.integer("array_typeid")?, -1)?
159                    ));
160                }
161                Ok(format!("ARRAY[{}]", elements.join(", ")))
162            }
163            "CASEEXPR" => self.case(node),
164            "SCALARARRAYOPEXPR" => {
165                let operator =
166                    crate::type_resolution::binary_operator_by_oid(node.integer("opno")?)
167                        .ok_or_else(|| invalid("unknown scalar-array operator"))?;
168                let [left, right] = list(node, "args")? else {
169                    return Err(invalid("invalid scalar-array operands"));
170                };
171                let quantifier = if node.boolean("useOr")? { "ANY" } else { "ALL" };
172                Ok(parentheses(
173                    format!(
174                        "{} {} {quantifier} ({})",
175                        self.field(left, false)?,
176                        operator.name,
177                        self.field(right, false)?
178                    ),
179                    !outer,
180                ))
181            }
182            _ => Err(SQLError::Unsupported(format!(
183                "catalog expression deparser for {}",
184                node.kind
185            ))),
186        }
187    }
188
189    fn variable(&self, node: &Node) -> Result<String, SQLError> {
190        if node.integer("varno")? != 1 || node.integer("varlevelsup")? != 0 {
191            return Err(invalid("invalid varno in stored expression"));
192        }
193        match node.integer("varattno")? {
194            0 => self
195                .names
196                .relation_name()
197                .map(|name| format!("{}.*", crate::expr::quote_ident(&name))),
198            -6 => Ok("tableoid".into()),
199            attribute => self
200                .names
201                .column(attribute)
202                .map(|name| crate::expr::quote_ident(&name)),
203        }
204    }
205
206    fn parameter(&self, node: &Node) -> Result<String, SQLError> {
207        let number = node.integer("paramid")?;
208        if node.integer("paramkind")? != 0 || number <= 0 {
209            return Err(invalid("invalid external parameter in stored expression"));
210        }
211        self.names.parameter(number)
212    }
213
214    fn collate(&self, node: &Node, outer: bool) -> Result<String, SQLError> {
215        let name = self
216            .names
217            .collation(node.integer("collOid")?)?
218            .iter()
219            .map(|name| crate::expr::quote_ident(name))
220            .collect::<Vec<_>>()
221            .join(".");
222        Ok(parentheses(
223            format!("{} COLLATE {name}", self.field(node.field("arg")?, false)?),
224            !outer,
225        ))
226    }
227
228    fn constant(&self, node: &Node) -> Result<String, SQLError> {
229        self.constant_labeled(node, true)
230    }
231
232    fn constant_labeled(&self, node: &Node, show_type: bool) -> Result<String, SQLError> {
233        let oid = node.integer("consttype")?;
234        let modifier = node.integer("consttypmod")?;
235        let type_name = || self.names.type_name(oid, modifier);
236        if node.boolean("constisnull")? {
237            return if show_type {
238                Ok(format!("NULL::{}", type_name()?))
239            } else {
240                Ok("NULL".into())
241            };
242        }
243        let Field::Datum { length, bytes } = node.field("constvalue")? else {
244            return Err(invalid("constant has no Datum"));
245        };
246        if node.boolean("constbyval")? && (bytes.len() != 8 || !matches!(*length, 1 | 2 | 4 | 8)) {
247            return Err(invalid("invalid by-value Datum length"));
248        }
249        let quoted = |value: &str| self.constant_literal(value, node, show_type);
250        match oid {
251            705 => self.constant_literal(values::unknown_text(node)?, node, false),
252            16 => match bytes.first() {
253                Some(0) => Ok("false".into()),
254                Some(1) => Ok("true".into()),
255                _ => Err(invalid("invalid boolean Datum")),
256            },
257            20 | 21 | 23 | 26 | 28 => {
258                let value = match oid {
259                    21 => i64::from(i16::from_le_bytes(prefix(bytes)?)),
260                    23 => i64::from(i32::from_le_bytes(prefix(bytes)?)),
261                    26 | 28 => i64::from(u32::from_le_bytes(prefix(bytes)?)),
262                    _ => i64::from_le_bytes(prefix(bytes)?),
263                };
264                if oid == 23 && value >= 0 {
265                    Ok(value.to_string())
266                } else {
267                    quoted(&value.to_string())
268                }
269            }
270            700 => quoted(&crate::expr::format_real(f32::from_le_bytes(prefix(
271                bytes,
272            )?))),
273            701 => quoted(&uqa_core::format_float_pg(f64::from_le_bytes(prefix(
274                bytes,
275            )?))),
276            18 => {
277                let byte = *bytes
278                    .first()
279                    .ok_or_else(|| invalid("empty character Datum"))?;
280                quoted(&crate::expr::internal_char::output_with_control(
281                    byte,
282                    &uqa_core::memory::ProductionControl::uncontrolled(),
283                )?)
284            }
285            19 => {
286                let end = bytes
287                    .iter()
288                    .position(|byte| *byte == 0)
289                    .unwrap_or(bytes.len());
290                quoted(
291                    std::str::from_utf8(&bytes[..end])
292                        .map_err(|_| invalid("invalid name Datum"))?,
293                )
294            }
295            25 | 1042 | 1043 | 1790 => quoted(
296                std::str::from_utf8(values::varlena_payload(*length, bytes)?)
297                    .map_err(|_| invalid("invalid string Datum"))?,
298            ),
299            17 | 114 | 2950 | 3802 => quoted(&values::scalar_text(node)?),
300            1700 => {
301                let text = values::numeric::decode(values::varlena_payload(*length, bytes)?)?;
302                if text.as_bytes().first().is_some_and(u8::is_ascii_digit) && text.contains('.') {
303                    if modifier < 0 || !show_type {
304                        Ok(text)
305                    } else {
306                        Ok(format!("{text}::{}", type_name()?))
307                    }
308                } else {
309                    quoted(&text)
310                }
311            }
312            1082 | 1083 | 1114 | 1184 | 1186 | 1266 => quoted(&crate::expr::value_to_string(
313                &uqa_core::Value::Temporal(values::temporal::decode(bytes, oid)?),
314            )?),
315            _ => match values::catalog_text(node, self.names.value_catalog())? {
316                Some(text) => quoted(&text),
317                None => Err(SQLError::Unsupported(format!(
318                    "catalog expression Datum deparser for type {oid}"
319                ))),
320            },
321        }
322    }
323    fn constant_literal(
324        &self,
325        value: &str,
326        node: &Node,
327        show_type: bool,
328    ) -> Result<String, SQLError> {
329        let mut output = String::new();
330        crate::expr::append_sql_literal(
331            &mut output,
332            value,
333            self.names.standard_conforming_strings()?,
334        );
335        if show_type {
336            output.push_str("::");
337            output.push_str(
338                &self
339                    .names
340                    .type_name(node.integer("consttype")?, node.integer("consttypmod")?)?,
341            );
342        }
343        Ok(output)
344    }
345}
346
347fn list<'a>(node: &'a Node, name: &str) -> Result<&'a [Field], SQLError> {
348    match node.field(name)? {
349        Field::List(values) => Ok(values),
350        Field::Null => Ok(&[]),
351        _ => Err(invalid(format!("expected node list in {name}"))),
352    }
353}
354
355fn atom<'a>(node: &'a Node, name: &str) -> Result<&'a str, SQLError> {
356    match node.field(name)? {
357        Field::Atom(value) | Field::String(value) => Ok(value),
358        _ => Err(invalid(format!("expected token in {name}"))),
359    }
360}
361
362fn prefix<const N: usize>(bytes: &[u8]) -> Result<[u8; N], SQLError> {
363    bytes
364        .get(..N)
365        .and_then(|prefix| prefix.try_into().ok())
366        .ok_or_else(|| invalid("truncated constant Datum"))
367}
368
369fn parentheses(text: String, required: bool) -> String {
370    if required {
371        format!("({text})")
372    } else {
373        text
374    }
375}