Skip to main content

wasi_pg_client/query/
row.rs

1//! Row representation and column metadata.
2//!
3//! This module defines [`FieldDescription`] (metadata for a single column)
4//! and [`Row`] (a single result row with typed accessors).
5
6use std::sync::Arc;
7
8use crate::protocol::Oid;
9use crate::types::{Format, FromSql};
10
11use crate::error::{PgError, Result};
12
13// ---------------------------------------------------------------------------
14// FieldDescription
15// ---------------------------------------------------------------------------
16
17/// Metadata describing a single column in a query result.
18#[derive(Debug, Clone, PartialEq, Eq)]
19#[non_exhaustive]
20pub struct FieldDescription {
21    /// Column name.
22    name: String,
23    /// OID of the table the column belongs to (0 if not a table column).
24    table_oid: Oid,
25    /// Column number within the table (0 if not a table column).
26    column_id: i16,
27    /// OID of the column's data type.
28    type_oid: Oid,
29    /// Type size (negative for variable-length types).
30    type_size: i16,
31    /// Type modifier.
32    type_modifier: i32,
33    /// Format code used for this column (text = 0, binary = 1).
34    format: i16,
35}
36
37impl FieldDescription {
38    /// Create a new field description.
39    pub fn new(
40        name: String,
41        table_oid: Oid,
42        column_id: i16,
43        type_oid: Oid,
44        type_size: i16,
45        type_modifier: i32,
46        format: i16,
47    ) -> Self {
48        Self {
49            name,
50            table_oid,
51            column_id,
52            type_oid,
53            type_size,
54            type_modifier,
55            format,
56        }
57    }
58
59    /// Column name.
60    pub fn name(&self) -> &str {
61        &self.name
62    }
63
64    /// Table OID.
65    pub fn table_oid(&self) -> Oid {
66        self.table_oid
67    }
68
69    /// Column ID within the table.
70    pub fn column_id(&self) -> i16 {
71        self.column_id
72    }
73
74    /// Data type OID.
75    pub fn type_oid(&self) -> Oid {
76        self.type_oid
77    }
78
79    /// Type size.
80    pub fn type_size(&self) -> i16 {
81        self.type_size
82    }
83
84    /// Type modifier.
85    pub fn type_modifier(&self) -> i32 {
86        self.type_modifier
87    }
88
89    /// Format code.
90    pub fn format(&self) -> i16 {
91        self.format
92    }
93}
94
95// ---------------------------------------------------------------------------
96// Row
97// ---------------------------------------------------------------------------
98
99/// A single row from a query result.
100#[derive(Debug, Clone)]
101#[non_exhaustive]
102pub struct Row {
103    columns: Arc<Vec<FieldDescription>>,
104    values: Vec<Option<Vec<u8>>>,
105}
106
107impl Row {
108    /// Create a new row from column descriptions and raw values.
109    pub(crate) fn new(columns: Arc<Vec<FieldDescription>>, values: Vec<Option<Vec<u8>>>) -> Self {
110        Self { columns, values }
111    }
112
113    /// Number of columns in the row.
114    pub fn len(&self) -> usize {
115        self.values.len()
116    }
117
118    /// Returns `true` if the row has no columns.
119    pub fn is_empty(&self) -> bool {
120        self.values.is_empty()
121    }
122
123    /// Returns the column descriptions.
124    pub fn columns(&self) -> &[FieldDescription] {
125        &self.columns
126    }
127
128    /// Returns `true` if the value at `index` is SQL NULL.
129    pub fn is_null(&self, index: usize) -> bool {
130        match self.values.get(index) {
131            Some(value) => value.is_none(),
132            None => true,
133        }
134    }
135
136    /// Returns the raw bytes for the column at `index`, or `None` if NULL.
137    pub fn get_raw(&self, index: usize) -> Option<&[u8]> {
138        self.values.get(index).and_then(|v| v.as_deref())
139    }
140
141    /// Decode the column at `index` as type `T`.
142    ///
143    /// # Type inference
144    ///
145    /// The type `T` must implement `FromSql` for the column's PostgreSQL
146    /// type OID. If the types don't match, a `TypeConversion` error is
147    /// returned.
148    ///
149    /// # NULL handling
150    ///
151    /// If the column value is SQL NULL, this method returns
152    /// `Err(PgError::UnexpectedNull)`. To handle NULL values, use
153    /// `Option<T>` as the type parameter:
154    ///
155    /// ```rust,ignore
156    /// let val: Option<i32> = row.get(0)?; // NULL → None
157    /// ```
158    ///
159    /// # Errors
160    ///
161    /// - `PgError::ColumnIndexOutOfBounds` — index exceeds column count
162    /// - `PgError::UnexpectedNull` — column is NULL and `T` is not `Option`
163    /// - `PgError::TypeConversion` — type mismatch between PG type and `T`
164    #[must_use = "column access errors should be checked"]
165    pub fn get<T: FromSql>(&self, index: usize) -> Result<T> {
166        let raw = self.get_raw(index);
167        let field = self
168            .columns
169            .get(index)
170            .ok_or_else(|| PgError::ColumnIndexOutOfBounds {
171                index,
172                count: self.columns.len(),
173            })?;
174        let ty = crate::types::Type::from_oid(field.type_oid).unwrap_or_else(|| {
175            crate::types::Type::new(
176                "unknown".into(),
177                0,
178                crate::types::Kind::Pseudo,
179                "pg_catalog".into(),
180            )
181        });
182        let format = if field.format() == 1 {
183            Format::Binary
184        } else {
185            Format::Text
186        };
187        T::from_sql(&ty, raw, format).map_err(PgError::TypeConversion)
188    }
189
190    /// Decode a column by name as type `T`.
191    ///
192    /// Column name lookup is O(n) where n is the number of columns.
193    /// For performance-critical code, prefer index-based access.
194    ///
195    /// # Errors
196    ///
197    /// - `PgError::ColumnNotFound` — no column with the given name
198    /// - `PgError::UnexpectedNull` — column is NULL and `T` is not `Option`
199    /// - `PgError::TypeConversion` — type mismatch between PG type and `T`
200    #[must_use = "column access errors should be checked"]
201    pub fn get_by_name<T: FromSql>(&self, name: &str) -> Result<T> {
202        let index = self
203            .columns
204            .iter()
205            .position(|c| c.name() == name)
206            .ok_or_else(|| PgError::ColumnNotFound {
207                name: name.to_string(),
208            })?;
209        self.get(index)
210    }
211
212    /// Get the name of a column by index.
213    ///
214    /// Returns `None` if the index is out of bounds.
215    pub fn column_name(&self, index: usize) -> Option<&str> {
216        self.columns.get(index).map(|c| c.name())
217    }
218
219    /// Get the index of a column by name.
220    ///
221    /// Column name lookup is O(n) where n is the number of columns.
222    /// For performance-critical code, prefer index-based access.
223    pub fn column_index(&self, name: &str) -> Option<usize> {
224        self.columns.iter().position(|c| c.name() == name)
225    }
226}
227
228// ---------------------------------------------------------------------------
229// Tests
230// ---------------------------------------------------------------------------
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn test_row_get_raw() {
238        let cols = Arc::new(vec![FieldDescription::new(
239            "id".into(),
240            0,
241            0,
242            crate::types::INT4_OID,
243            4,
244            -1,
245            0,
246        )]);
247        let row = Row::new(cols.clone(), vec![Some(vec![b'1', b'2', b'3'])]);
248        assert_eq!(row.get_raw(0), Some(b"123".as_slice()));
249        assert!(!row.is_null(0));
250    }
251
252    #[test]
253    fn test_row_null() {
254        let cols = Arc::new(vec![FieldDescription::new(
255            "name".into(),
256            0,
257            0,
258            crate::types::TEXT_OID,
259            -1,
260            -1,
261            0,
262        )]);
263        let row = Row::new(cols, vec![None]);
264        assert!(row.is_null(0));
265        assert_eq!(row.get_raw(0), None);
266    }
267
268    #[test]
269    fn test_row_get_i32() {
270        let cols = Arc::new(vec![FieldDescription::new(
271            "id".into(),
272            0,
273            0,
274            crate::types::INT4_OID,
275            4,
276            -1,
277            0,
278        )]);
279        let row = Row::new(cols, vec![Some(b"42".to_vec())]);
280        let val: i32 = row.get(0).unwrap();
281        assert_eq!(val, 42);
282    }
283
284    #[test]
285    fn test_row_get_by_name() {
286        let cols = Arc::new(vec![
287            FieldDescription::new("id".into(), 0, 0, crate::types::INT4_OID, 4, -1, 0),
288            FieldDescription::new("name".into(), 0, 0, crate::types::TEXT_OID, -1, -1, 0),
289        ]);
290        let row = Row::new(cols, vec![Some(b"1".to_vec()), Some(b"alice".to_vec())]);
291        let id: i32 = row.get_by_name("id").unwrap();
292        assert_eq!(id, 1);
293        let name: String = row.get_by_name("name").unwrap();
294        assert_eq!(name, "alice");
295    }
296
297    #[test]
298    fn test_row_get_by_name_missing() {
299        let cols = Arc::new(vec![]);
300        let row = Row::new(cols, vec![]);
301        assert!(row.get_by_name::<i32>("missing").is_err());
302    }
303}