Skip to main content

pgevolve_core/catalog/
rows.rs

1//! Row types returned by [`crate::catalog::CatalogQuerier`].
2//!
3//! The trait deliberately does not depend on `tokio-postgres` or any other
4//! Postgres driver; the binary's adapter performs the I/O and constructs
5//! [`Row`] values from whatever rows the driver returns. Tests construct
6//! [`Row`] values directly from a literal map.
7
8use std::collections::BTreeMap;
9
10use crate::catalog::CatalogQuery;
11use crate::catalog::error::CatalogError;
12
13/// A SQL value variant supported by the catalog reader.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum Value {
16    /// SQL `NULL`.
17    Null,
18    /// `boolean`.
19    Bool(bool),
20    /// `smallint`.
21    SmallInt(i16),
22    /// `integer` / `bigint` / any oid we don't unpack further.
23    Integer(i64),
24    /// Single-character "char" (used by `pg_constraint.contype`, etc.).
25    Char(char),
26    /// Text-like (`text`, `name`, `varchar`).
27    Text(String),
28    /// `text[]` / `name[]`.
29    TextArray(Vec<String>),
30    /// `int2[]` / `int4[]` / `int8[]`.
31    IntegerArray(Vec<i64>),
32    /// Bytes (used for `pg_node_tree` payloads we don't decode in IR).
33    Bytes(Vec<u8>),
34}
35
36/// A single row returned by a catalog query, keyed by column name.
37#[derive(Debug, Clone, Default)]
38pub struct Row {
39    cols: BTreeMap<String, Value>,
40}
41
42impl Row {
43    /// Construct an empty row.
44    #[must_use]
45    pub const fn new() -> Self {
46        Self {
47            cols: BTreeMap::new(),
48        }
49    }
50
51    /// Insert a column.
52    #[must_use]
53    pub fn with(mut self, key: impl Into<String>, value: Value) -> Self {
54        self.cols.insert(key.into(), value);
55        self
56    }
57
58    /// Insert a column in place.
59    pub fn insert(&mut self, key: impl Into<String>, value: Value) {
60        self.cols.insert(key.into(), value);
61    }
62
63    /// Borrow a column value by name.
64    pub fn get_value(&self, query: CatalogQuery, key: &str) -> Result<&Value, CatalogError> {
65        self.cols
66            .get(key)
67            .ok_or_else(|| CatalogError::MissingColumn {
68                query,
69                column: key.to_string(),
70            })
71    }
72
73    /// Whether the column is absent or NULL.
74    pub fn is_null(&self, key: &str) -> bool {
75        matches!(self.cols.get(key), None | Some(Value::Null))
76    }
77
78    /// Read a non-null integer column (any width).
79    pub fn get_int(&self, query: CatalogQuery, key: &str) -> Result<i64, CatalogError> {
80        match self.get_value(query, key)? {
81            Value::Integer(i) => Ok(*i),
82            Value::SmallInt(i) => Ok(i64::from(*i)),
83            other => Err(CatalogError::BadColumnType {
84                query,
85                column: key.to_string(),
86                message: format!("expected integer, got {other:?}"),
87            }),
88        }
89    }
90
91    /// Read an optional integer column.
92    pub fn get_opt_int(&self, query: CatalogQuery, key: &str) -> Result<Option<i64>, CatalogError> {
93        if self.is_null(key) {
94            return Ok(None);
95        }
96        self.get_int(query, key).map(Some)
97    }
98
99    /// Read a non-null text column.
100    pub fn get_text(&self, query: CatalogQuery, key: &str) -> Result<String, CatalogError> {
101        match self.get_value(query, key)? {
102            Value::Text(s) => Ok(s.clone()),
103            other => Err(CatalogError::BadColumnType {
104                query,
105                column: key.to_string(),
106                message: format!("expected text, got {other:?}"),
107            }),
108        }
109    }
110
111    /// Read an optional text column.
112    pub fn get_opt_text(
113        &self,
114        query: CatalogQuery,
115        key: &str,
116    ) -> Result<Option<String>, CatalogError> {
117        if self.is_null(key) {
118            return Ok(None);
119        }
120        self.get_text(query, key).map(Some)
121    }
122
123    /// Read a non-null bool column.
124    pub fn get_bool(&self, query: CatalogQuery, key: &str) -> Result<bool, CatalogError> {
125        match self.get_value(query, key)? {
126            Value::Bool(b) => Ok(*b),
127            other => Err(CatalogError::BadColumnType {
128                query,
129                column: key.to_string(),
130                message: format!("expected boolean, got {other:?}"),
131            }),
132        }
133    }
134
135    /// Read a non-null char column (e.g., `pg_constraint.contype`).
136    pub fn get_char(&self, query: CatalogQuery, key: &str) -> Result<char, CatalogError> {
137        match self.get_value(query, key)? {
138            Value::Char(c) => Ok(*c),
139            Value::Text(s) if s.chars().count() == 1 => {
140                // SAFETY: count() == 1 guarantees next() yields Some.
141                Ok(s.chars()
142                    .next()
143                    .unwrap_or_else(|| unreachable!("count==1 has one char")))
144            }
145            other => Err(CatalogError::BadColumnType {
146                query,
147                column: key.to_string(),
148                message: format!("expected single-char, got {other:?}"),
149            }),
150        }
151    }
152
153    /// Read a non-null `int2[]`/`int4[]`/`int8[]` column.
154    pub fn get_int_array(&self, query: CatalogQuery, key: &str) -> Result<Vec<i64>, CatalogError> {
155        match self.get_value(query, key)? {
156            Value::IntegerArray(v) => Ok(v.clone()),
157            other => Err(CatalogError::BadColumnType {
158                query,
159                column: key.to_string(),
160                message: format!("expected integer array, got {other:?}"),
161            }),
162        }
163    }
164
165    /// Read a non-null `text[]`/`name[]` column.
166    pub fn get_text_array(
167        &self,
168        query: CatalogQuery,
169        key: &str,
170    ) -> Result<Vec<String>, CatalogError> {
171        match self.get_value(query, key)? {
172            Value::TextArray(v) => Ok(v.clone()),
173            other => Err(CatalogError::BadColumnType {
174                query,
175                column: key.to_string(),
176                message: format!("expected text array, got {other:?}"),
177            }),
178        }
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use crate::catalog::CatalogQuery;
186
187    #[test]
188    fn missing_column_errors() {
189        let r = Row::new();
190        let err = r.get_int(CatalogQuery::Schemas, "oid").unwrap_err();
191        assert!(matches!(err, CatalogError::MissingColumn { .. }));
192    }
193
194    #[test]
195    fn integer_widening_works() {
196        let r = Row::new()
197            .with("a", Value::SmallInt(7))
198            .with("b", Value::Integer(42));
199        assert_eq!(r.get_int(CatalogQuery::Schemas, "a").unwrap(), 7);
200        assert_eq!(r.get_int(CatalogQuery::Schemas, "b").unwrap(), 42);
201    }
202
203    #[test]
204    fn null_is_treated_as_absent() {
205        let r = Row::new().with("c", Value::Null);
206        assert!(r.is_null("c"));
207        assert!(
208            r.get_opt_text(CatalogQuery::Schemas, "c")
209                .unwrap()
210                .is_none()
211        );
212    }
213}