Skip to main content

uqa_sql/catalog/
node_tree.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! `PostgreSQL` node-tree text used by stored catalog expressions.
8
9use std::fmt;
10
11use crate::SQLError;
12
13mod read;
14pub use read::parse;
15pub mod deparse;
16pub mod expressions;
17mod values;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Node {
21    pub kind: String,
22    pub fields: Vec<(String, Field)>,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum Field {
27    Null,
28    Atom(String),
29    String(String),
30    Node(Node),
31    List(Vec<Field>),
32    /// `PostgreSQL` writes by-value Datums at machine-word width even when the declared type is narrower.
33    Datum {
34        length: usize,
35        bytes: Vec<u8>,
36    },
37}
38
39impl Node {
40    pub fn new(
41        kind: impl Into<String>,
42        fields: impl IntoIterator<Item = (&'static str, Field)>,
43    ) -> Self {
44        Self {
45            kind: kind.into(),
46            fields: fields
47                .into_iter()
48                .map(|(name, value)| (name.into(), value))
49                .collect(),
50        }
51    }
52
53    pub fn field(&self, name: &str) -> Result<&Field, SQLError> {
54        self.fields
55            .iter()
56            .find_map(|(field, value)| (field == name).then_some(value))
57            .ok_or_else(|| invalid(format!("missing {name} in {} node", self.kind)))
58    }
59
60    pub fn integer(&self, name: &str) -> Result<i64, SQLError> {
61        match self.field(name)? {
62            Field::Atom(value) => value
63                .parse()
64                .map_err(|_| invalid(format!("invalid integer field {name}"))),
65            _ => Err(invalid(format!("invalid integer field {name}"))),
66        }
67    }
68
69    pub fn boolean(&self, name: &str) -> Result<bool, SQLError> {
70        match self.field(name)? {
71            Field::Atom(value) if value == "true" => Ok(true),
72            Field::Atom(value) if value == "false" => Ok(false),
73            _ => Err(invalid(format!("invalid boolean field {name}"))),
74        }
75    }
76}
77
78impl From<i64> for Field {
79    fn from(value: i64) -> Self {
80        Self::Atom(value.to_string())
81    }
82}
83
84impl From<bool> for Field {
85    fn from(value: bool) -> Self {
86        Self::Atom(value.to_string())
87    }
88}
89
90impl From<Node> for Field {
91    fn from(value: Node) -> Self {
92        Self::Node(value)
93    }
94}
95
96impl fmt::Display for Node {
97    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
98        write!(output, "{{{}", self.kind)?;
99        for (name, value) in &self.fields {
100            write!(output, " :{name} {value}")?;
101        }
102        output.write_str("}")
103    }
104}
105
106impl fmt::Display for Field {
107    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
108        match self {
109            Self::Null => output.write_str("<>"),
110            Self::Atom(value) => output.write_str(value),
111            Self::String(value) => write_token(output, value),
112            Self::Node(node) => node.fmt(output),
113            Self::List(values) => {
114                output.write_str("(")?;
115                for (index, value) in values.iter().enumerate() {
116                    if index != 0 {
117                        output.write_str(" ")?;
118                    }
119                    value.fmt(output)?;
120                }
121                output.write_str(")")
122            }
123            Self::Datum { length, bytes } => {
124                write!(output, "{length} [")?;
125                for byte in bytes {
126                    // PostgreSQL promotes native C char, whose signedness is platform-specific.
127                    write!(output, " {}", std::ffi::c_char::from_ne_bytes([*byte]))?;
128                }
129                output.write_str(" ]")
130            }
131        }
132    }
133}
134
135fn write_token(output: &mut fmt::Formatter<'_>, value: &str) -> fmt::Result {
136    if value.is_empty() {
137        return output.write_str("\"\"");
138    }
139    let mut bytes = value.bytes();
140    let first = bytes.next().expect("nonempty token");
141    if matches!(first, b'<' | b'"')
142        || first.is_ascii_digit()
143        || (matches!(first, b'+' | b'-')
144            && bytes
145                .next()
146                .is_some_and(|byte| byte.is_ascii_digit() || byte == b'.'))
147    {
148        output.write_str("\\")?;
149    }
150    for character in value.chars() {
151        if matches!(character, ' ' | '\n' | '\t' | '(' | ')' | '{' | '}' | '\\') {
152            output.write_str("\\")?;
153        }
154        write!(output, "{character}")?;
155    }
156    Ok(())
157}
158
159pub(super) fn invalid(message: impl Into<String>) -> SQLError {
160    SQLError::Routine {
161        sqlstate: "XX000".into(),
162        message: message.into(),
163    }
164}
165
166#[cfg(test)]
167mod tests;