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 control;
13mod operators;
14
15pub trait ExpressionNames {
16    fn column(&self, attribute: i64) -> Result<String, SQLError>;
17    fn routine(&self, oid: i64) -> Result<Vec<String>, SQLError>;
18    fn type_name(&self, oid: i64, modifier: i64) -> Result<String, SQLError>;
19}
20
21pub fn expression(
22    value: &Field,
23    names: &dyn ExpressionNames,
24    pretty: bool,
25) -> Result<String, SQLError> {
26    Renderer {
27        names,
28        pretty,
29        indent: 0,
30    }
31    .field(value, pretty)
32}
33
34struct Renderer<'a> {
35    names: &'a dyn ExpressionNames,
36    pretty: bool,
37    indent: usize,
38}
39
40impl Renderer<'_> {
41    fn field(&self, value: &Field, outer: bool) -> Result<String, SQLError> {
42        match value {
43            Field::Node(node) => self.node(node, outer),
44            Field::List(values) => values
45                .iter()
46                .map(|value| self.field(value, outer))
47                .collect::<Result<Vec<_>, _>>()
48                .map(|values| values.join(", ")),
49            _ => Err(invalid("expected an expression node")),
50        }
51    }
52
53    fn node(&self, node: &Node, outer: bool) -> Result<String, SQLError> {
54        match node.kind.as_str() {
55            "COERCETODOMAINVALUE" => Ok("VALUE".into()),
56            "VAR" => {
57                if node.integer("varno")? != 1 || node.integer("varlevelsup")? != 0 {
58                    return Err(invalid("invalid varno in stored expression"));
59                }
60                self.names
61                    .column(node.integer("varattno")?)
62                    .map(|name| crate::expr::quote_ident(&name))
63            }
64            "CONST" => self.constant(node),
65            "OPEXPR" | "DISTINCTEXPR" => self.operator(node, outer),
66            "FUNCEXPR" => self.function(node),
67            "RELABELTYPE" => self.cast(
68                node.field("arg")?,
69                node.integer("resulttype")?,
70                node.integer("resulttypmod")?,
71            ),
72            "COERCEVIAIO" => self.cast(node.field("arg")?, node.integer("resulttype")?, -1),
73            "COERCETODOMAIN" => self.cast(
74                node.field("arg")?,
75                node.integer("resulttype")?,
76                node.integer("resulttypmod")?,
77            ),
78            "BOOLEXPR" => self.boolean(node, outer),
79            "NULLTEST" => {
80                let operator = match node.integer("nulltesttype")? {
81                    0 => "IS NULL",
82                    1 => "IS NOT NULL",
83                    _ => return Err(invalid("invalid null test")),
84                };
85                Ok(parentheses(
86                    format!("{} {operator}", self.field(node.field("arg")?, false)?),
87                    !outer,
88                ))
89            }
90            "COALESCEEXPR" | "MINMAXEXPR" => {
91                let name = match node.kind.as_str() {
92                    "COALESCEEXPR" => "COALESCE",
93                    _ => match node.integer("op")? {
94                        0 => "GREATEST",
95                        1 => "LEAST",
96                        _ => return Err(invalid("invalid minimum/maximum expression")),
97                    },
98                };
99                let arguments = list(node, "args")?
100                    .iter()
101                    .map(|arg| self.field(arg, false))
102                    .collect::<Result<Vec<_>, _>>()?;
103                Ok(format!("{name}({})", arguments.join(", ")))
104            }
105            "ARRAYEXPR" => {
106                let elements = list(node, "elements")?
107                    .iter()
108                    .map(|arg| self.field(arg, self.pretty))
109                    .collect::<Result<Vec<_>, _>>()?;
110                Ok(format!("ARRAY[{}]", elements.join(", ")))
111            }
112            "CASEEXPR" => self.case(node),
113            "SCALARARRAYOPEXPR" => {
114                let operator =
115                    crate::type_resolution::binary_operator_by_oid(node.integer("opno")?)
116                        .ok_or_else(|| invalid("unknown scalar-array operator"))?;
117                let [left, right] = list(node, "args")? else {
118                    return Err(invalid("invalid scalar-array operands"));
119                };
120                let quantifier = if node.boolean("useOr")? { "ANY" } else { "ALL" };
121                Ok(parentheses(
122                    format!(
123                        "{} {} {quantifier} ({})",
124                        self.field(left, false)?,
125                        operator.name,
126                        self.field(right, false)?
127                    ),
128                    !outer,
129                ))
130            }
131            _ => Err(SQLError::Unsupported(format!(
132                "catalog expression deparser for {}",
133                node.kind
134            ))),
135        }
136    }
137
138    fn cast(&self, argument: &Field, oid: i64, modifier: i64) -> Result<String, SQLError> {
139        let text = self.field(argument, false)?;
140        let atomic = matches!(argument, Field::Node(node) if matches!(node.kind.as_str(), "VAR" | "COERCETODOMAINVALUE" | "CONST" | "FUNCEXPR"));
141        Ok(format!(
142            "{}::{}",
143            parentheses(text, !self.pretty || !atomic),
144            self.names.type_name(oid, modifier)?
145        ))
146    }
147
148    fn constant(&self, node: &Node) -> Result<String, SQLError> {
149        let oid = node.integer("consttype")?;
150        let modifier = node.integer("consttypmod")?;
151        let type_name = || self.names.type_name(oid, modifier);
152        if node.boolean("constisnull")? {
153            return Ok(format!("NULL::{}", type_name()?));
154        }
155        let Field::Datum { length, bytes } = node.field("constvalue")? else {
156            return Err(invalid("constant has no Datum"));
157        };
158        if node.boolean("constbyval")? && (bytes.len() != 8 || !matches!(*length, 1 | 2 | 4 | 8)) {
159            return Err(invalid("invalid by-value Datum length"));
160        }
161        let quoted = |value: &str| -> Result<String, SQLError> {
162            Ok(format!("{}::{}", literal(value), type_name()?))
163        };
164        match oid {
165            16 => match bytes.first() {
166                Some(0) => Ok("false".into()),
167                Some(1) => Ok("true".into()),
168                _ => Err(invalid("invalid boolean Datum")),
169            },
170            20 | 21 | 23 | 26 | 28 => {
171                let value = match oid {
172                    21 => i64::from(i16::from_le_bytes(prefix(bytes)?)),
173                    23 => i64::from(i32::from_le_bytes(prefix(bytes)?)),
174                    26 | 28 => i64::from(u32::from_le_bytes(prefix(bytes)?)),
175                    _ => i64::from_le_bytes(prefix(bytes)?),
176                };
177                if oid == 23 && value >= 0 {
178                    Ok(value.to_string())
179                } else {
180                    quoted(&value.to_string())
181                }
182            }
183            700 | 701 => {
184                let value = if oid == 700 {
185                    f64::from(f32::from_le_bytes(prefix(bytes)?))
186                } else {
187                    f64::from_le_bytes(prefix(bytes)?)
188                };
189                quoted(&uqa_core::format_float_pg(value))
190            }
191            18 => quoted(
192                std::str::from_utf8(
193                    bytes
194                        .get(..1)
195                        .ok_or_else(|| invalid("empty character Datum"))?,
196                )
197                .map_err(|_| invalid("invalid character Datum"))?,
198            ),
199            19 => {
200                let end = bytes
201                    .iter()
202                    .position(|byte| *byte == 0)
203                    .unwrap_or(bytes.len());
204                quoted(
205                    std::str::from_utf8(&bytes[..end])
206                        .map_err(|_| invalid("invalid name Datum"))?,
207                )
208            }
209            25 | 1042 | 1043 | 1790 => quoted(
210                std::str::from_utf8(values::varlena_payload(*length, bytes)?)
211                    .map_err(|_| invalid("invalid string Datum"))?,
212            ),
213            1700 => {
214                let text = values::numeric::decode(values::varlena_payload(*length, bytes)?)?;
215                if text.as_bytes().first().is_some_and(u8::is_ascii_digit) && text.contains('.') {
216                    if modifier < 0 {
217                        Ok(text)
218                    } else {
219                        Ok(format!("{text}::{}", type_name()?))
220                    }
221                } else {
222                    quoted(&text)
223                }
224            }
225            1082 | 1083 | 1114 | 1184 | 1186 | 1266 => {
226                quoted(&values::temporal::decode(bytes, oid)?.to_sql_string())
227            }
228            _ => Err(SQLError::Unsupported(format!(
229                "catalog expression Datum deparser for type {oid}"
230            ))),
231        }
232    }
233}
234
235fn list<'a>(node: &'a Node, name: &str) -> Result<&'a [Field], SQLError> {
236    match node.field(name)? {
237        Field::List(values) => Ok(values),
238        Field::Null => Ok(&[]),
239        _ => Err(invalid(format!("expected node list in {name}"))),
240    }
241}
242
243fn atom<'a>(node: &'a Node, name: &str) -> Result<&'a str, SQLError> {
244    match node.field(name)? {
245        Field::Atom(value) | Field::String(value) => Ok(value),
246        _ => Err(invalid(format!("expected token in {name}"))),
247    }
248}
249
250fn prefix<const N: usize>(bytes: &[u8]) -> Result<[u8; N], SQLError> {
251    bytes
252        .get(..N)
253        .and_then(|prefix| prefix.try_into().ok())
254        .ok_or_else(|| invalid("truncated constant Datum"))
255}
256
257fn parentheses(text: String, required: bool) -> String {
258    if required {
259        format!("({text})")
260    } else {
261        text
262    }
263}
264
265fn literal(value: &str) -> String {
266    let value = value.replace('\'', "''");
267    if value.contains('\\') {
268        format!("E'{}'", value.replace('\\', "\\\\"))
269    } else {
270        format!("'{value}'")
271    }
272}