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;
18pub use values::constant_with_source as constant;
19pub(crate) use values::numeric::{
20    decode_with_control as decode_numeric_datum_with_control, encode as encode_numeric_datum,
21};
22pub(crate) use values::temporal::{
23    decode as decode_temporal_datum, encode as encode_temporal_datum,
24};
25
26#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct Node {
29    pub kind: String,
30    pub fields: Vec<(String, Field)>,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
34#[serde(deny_unknown_fields)]
35pub enum Field {
36    Null,
37    Atom(String),
38    String(String),
39    /// A quoted `PostgreSQL` String value in a node list, rather than a bare field token.
40    StringValue(String),
41    Node(Node),
42    List(Vec<Field>),
43    /// `PostgreSQL` writes by-value Datums at machine-word width even when the declared type is narrower.
44    Datum {
45        length: usize,
46        bytes: Vec<u8>,
47    },
48}
49
50impl Node {
51    pub fn new(
52        kind: impl Into<String>,
53        fields: impl IntoIterator<Item = (&'static str, Field)>,
54    ) -> Self {
55        Self {
56            kind: kind.into(),
57            fields: fields
58                .into_iter()
59                .map(|(name, value)| (name.into(), value))
60                .collect(),
61        }
62    }
63
64    pub fn field(&self, name: &str) -> Result<&Field, SQLError> {
65        self.fields
66            .iter()
67            .find_map(|(field, value)| (field == name).then_some(value))
68            .ok_or_else(|| invalid(format!("missing {name} in {} node", self.kind)))
69    }
70
71    pub fn integer(&self, name: &str) -> Result<i64, SQLError> {
72        match self.field(name)? {
73            Field::Atom(value) => value
74                .parse()
75                .map_err(|_| invalid(format!("invalid integer field {name}"))),
76            _ => Err(invalid(format!("invalid integer field {name}"))),
77        }
78    }
79
80    pub fn boolean(&self, name: &str) -> Result<bool, SQLError> {
81        match self.field(name)? {
82            Field::Atom(value) if value == "true" => Ok(true),
83            Field::Atom(value) if value == "false" => Ok(false),
84            _ => Err(invalid(format!("invalid boolean field {name}"))),
85        }
86    }
87}
88
89impl From<i64> for Field {
90    fn from(value: i64) -> Self {
91        Self::Atom(value.to_string())
92    }
93}
94
95impl From<bool> for Field {
96    fn from(value: bool) -> Self {
97        Self::Atom(value.to_string())
98    }
99}
100
101impl From<Node> for Field {
102    fn from(value: Node) -> Self {
103        Self::Node(value)
104    }
105}
106
107impl fmt::Display for Node {
108    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(output, "{{{}", self.kind)?;
110        for (name, value) in &self.fields {
111            write!(output, " :{name} {value}")?;
112        }
113        output.write_str("}")
114    }
115}
116
117impl fmt::Display for Field {
118    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::Null => output.write_str("<>"),
121            Self::Atom(value) => output.write_str(value),
122            Self::String(value) => write_token(output, value),
123            Self::StringValue(value) => {
124                output.write_str("\"")?;
125                if !value.is_empty() {
126                    write_token(output, value)?;
127                }
128                output.write_str("\"")
129            }
130            Self::Node(node) => node.fmt(output),
131            Self::List(values) => {
132                output.write_str("(")?;
133                for (index, value) in values.iter().enumerate() {
134                    if index != 0 {
135                        output.write_str(" ")?;
136                    }
137                    value.fmt(output)?;
138                }
139                output.write_str(")")
140            }
141            Self::Datum { length, bytes } => {
142                write!(output, "{length} [")?;
143                for byte in bytes {
144                    // PostgreSQL promotes native C char, whose signedness is platform-specific.
145                    write!(output, " {}", std::ffi::c_char::from_ne_bytes([*byte]))?;
146                }
147                output.write_str(" ]")
148            }
149        }
150    }
151}
152
153fn write_token(output: &mut fmt::Formatter<'_>, value: &str) -> fmt::Result {
154    if value.is_empty() {
155        return output.write_str("\"\"");
156    }
157    let mut bytes = value.bytes();
158    let first = bytes.next().expect("nonempty token");
159    if matches!(first, b'<' | b'"')
160        || first.is_ascii_digit()
161        || (matches!(first, b'+' | b'-')
162            && bytes
163                .next()
164                .is_some_and(|byte| byte.is_ascii_digit() || byte == b'.'))
165    {
166        output.write_str("\\")?;
167    }
168    for character in value.chars() {
169        if matches!(character, ' ' | '\n' | '\t' | '(' | ')' | '{' | '}' | '\\') {
170            output.write_str("\\")?;
171        }
172        write!(output, "{character}")?;
173    }
174    Ok(())
175}
176
177pub(super) fn invalid(message: impl Into<String>) -> SQLError {
178    SQLError::Routine {
179        sqlstate: "XX000".into(),
180        message: message.into(),
181    }
182}
183
184#[cfg(test)]
185mod tests;