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