Skip to main content

sentinel_driver/
row.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use bytes::Bytes;
5
6use crate::error::{Error, Result};
7use crate::protocol::backend::{DataRowColumns, FieldDescription};
8use crate::types::FromSql;
9
10/// Shared column metadata for a result set.
11///
12/// Created once from RowDescription, shared across all rows via `Arc`.
13#[derive(Debug, Clone)]
14pub struct RowDescription {
15    fields: Vec<FieldDescription>,
16    name_index: HashMap<String, usize>,
17}
18
19impl RowDescription {
20    pub fn new(fields: Vec<FieldDescription>) -> Self {
21        let name_index = fields
22            .iter()
23            .enumerate()
24            .map(|(i, f)| (f.name.clone(), i))
25            .collect();
26
27        Self { fields, name_index }
28    }
29
30    /// Number of columns.
31    pub fn len(&self) -> usize {
32        self.fields.len()
33    }
34
35    pub fn is_empty(&self) -> bool {
36        self.fields.is_empty()
37    }
38
39    /// Get field description by index.
40    pub fn field(&self, idx: usize) -> Option<&FieldDescription> {
41        self.fields.get(idx)
42    }
43
44    /// Get column index by name.
45    pub fn column_index(&self, name: &str) -> Option<usize> {
46        self.name_index.get(name).copied()
47    }
48
49    /// Iterator over field descriptions.
50    pub fn fields(&self) -> &[FieldDescription] {
51        &self.fields
52    }
53}
54
55/// A single row from a query result.
56///
57/// Provides zero-copy column access — data is decoded on demand from the
58/// underlying `Bytes` buffer.
59#[derive(Debug)]
60pub struct Row {
61    columns: DataRowColumns,
62    description: Arc<RowDescription>,
63}
64
65impl Row {
66    pub fn new(columns: DataRowColumns, description: Arc<RowDescription>) -> Self {
67        Self {
68            columns,
69            description,
70        }
71    }
72
73    /// Get a typed column value by index.
74    ///
75    /// # Panics
76    ///
77    /// Panics if the column index is out of bounds or the value cannot be decoded.
78    /// Use [`try_get`](Self::try_get) for a non-panicking version.
79    pub fn get<T: FromSql>(&self, idx: usize) -> T {
80        self.try_get(idx)
81            .unwrap_or_else(|e| panic!("error getting column {idx}: {e}"))
82    }
83
84    /// Get a typed column value by name.
85    ///
86    /// # Panics
87    ///
88    /// Panics if the column name doesn't exist or the value cannot be decoded.
89    pub fn get_by_name<T: FromSql>(&self, name: &str) -> T {
90        self.try_get_by_name(name)
91            .unwrap_or_else(|e| panic!("error getting column '{name}': {e}"))
92    }
93
94    /// Try to get a typed column value by index.
95    pub fn try_get<T: FromSql>(&self, idx: usize) -> Result<T> {
96        if idx >= self.columns.len() {
97            return Err(Error::ColumnIndex {
98                index: idx,
99                count: self.columns.len(),
100            });
101        }
102
103        let raw = self.columns.get(idx);
104        T::from_sql_nullable(raw.as_deref())
105    }
106
107    /// Try to get a typed column value by name.
108    pub fn try_get_by_name<T: FromSql>(&self, name: &str) -> Result<T> {
109        let idx = self
110            .description
111            .column_index(name)
112            .ok_or_else(|| Error::ColumnNotFound(name.to_string()))?;
113        self.try_get(idx)
114    }
115
116    /// Get raw bytes for a column. Returns `None` for NULL.
117    pub fn get_raw(&self, idx: usize) -> Option<Bytes> {
118        self.columns.get(idx)
119    }
120
121    /// Check if a column is NULL.
122    pub fn is_null(&self, idx: usize) -> bool {
123        self.columns.is_null(idx)
124    }
125
126    /// Number of columns.
127    pub fn len(&self) -> usize {
128        self.columns.len()
129    }
130
131    pub fn is_empty(&self) -> bool {
132        self.columns.is_empty()
133    }
134
135    /// Get the row description (column metadata).
136    pub fn description(&self) -> &RowDescription {
137        &self.description
138    }
139}
140
141/// Parse the command tag from CommandComplete to extract affected row count.
142///
143/// Tags look like: "INSERT 0 5", "UPDATE 3", "DELETE 1", "SELECT 10"
144pub fn parse_command_tag(tag: &str) -> CommandResult {
145    let parts: Vec<&str> = tag.split_whitespace().collect();
146    let command = parts.first().copied().unwrap_or("");
147
148    let rows_affected = match command {
149        "INSERT" => parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
150        "SELECT" | "UPDATE" | "DELETE" | "COPY" | "MERGE" | "MOVE" | "FETCH" => {
151            parts.last().and_then(|s| s.parse().ok()).unwrap_or(0)
152        }
153        _ => 0,
154    };
155
156    CommandResult {
157        command: command.to_string(),
158        rows_affected,
159    }
160}
161
162/// Result of a command execution (non-SELECT queries).
163#[derive(Debug, Clone)]
164pub struct CommandResult {
165    pub command: String,
166    pub rows_affected: u64,
167}
168
169/// A message returned from the simple query protocol.
170///
171/// Simple queries can return a mix of row data and command completions
172/// (e.g., a multi-statement query like `"SELECT 1; INSERT INTO ..."`).
173#[derive(Debug, Clone)]
174pub enum SimpleQueryMessage {
175    /// A row of text-format data from a SELECT or RETURNING clause.
176    Row(SimpleQueryRow),
177    /// A command completed (INSERT, UPDATE, DELETE, etc.).
178    CommandComplete(u64),
179}
180
181/// A single row from the simple query protocol.
182///
183/// All column values are in PostgreSQL text format. NULL values are
184/// represented as `None`.
185#[derive(Debug, Clone)]
186pub struct SimpleQueryRow {
187    columns: Vec<Option<String>>,
188}
189
190impl SimpleQueryRow {
191    pub fn new(columns: Vec<Option<String>>) -> Self {
192        Self { columns }
193    }
194
195    /// Get a column value by index. Returns `None` for NULL.
196    pub fn get(&self, idx: usize) -> Option<&str> {
197        self.columns.get(idx).and_then(|c| c.as_deref())
198    }
199
200    /// Get a column value by index, returning an error if the index is
201    /// out of bounds or the value is NULL.
202    pub fn try_get(&self, idx: usize) -> Result<&str> {
203        if idx >= self.columns.len() {
204            return Err(Error::ColumnIndex {
205                index: idx,
206                count: self.columns.len(),
207            });
208        }
209        self.columns[idx]
210            .as_deref()
211            .ok_or_else(|| Error::Decode("unexpected NULL in simple query row".into()))
212    }
213
214    /// Number of columns in this row.
215    pub fn len(&self) -> usize {
216        self.columns.len()
217    }
218
219    /// Returns `true` if this row has no columns.
220    pub fn is_empty(&self) -> bool {
221        self.columns.is_empty()
222    }
223}